Merge branch 'main' into randomized-tests-operation-script

This commit is contained in:
Max Brunsfeld 2023-04-05 17:10:20 -07:00
commit 2d63ed3ca4
42 changed files with 1183 additions and 1960 deletions

View file

@ -17,6 +17,7 @@ db = { path = "../db" }
gpui = { path = "../gpui" }
util = { path = "../util" }
rpc = { path = "../rpc" }
staff_mode = { path = "../staff_mode" }
sum_tree = { path = "../sum_tree" }
anyhow = "1.0.38"
async-recursion = "0.3"

View file

@ -6,9 +6,10 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings;
use staff_mode::StaffMode;
use std::sync::{Arc, Weak};
use util::http::HttpClient;
use util::{StaffMode, TryFutureExt as _};
use util::TryFutureExt as _;
#[derive(Default, Debug)]
pub struct User {

View file

@ -8,6 +8,16 @@ publish = false
path = "src/copilot.rs"
doctest = false
[features]
test-support = [
"collections/test-support",
"gpui/test-support",
"language/test-support",
"lsp/test-support",
"settings/test-support",
"util/test-support",
]
[dependencies]
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
@ -18,7 +28,6 @@ theme = { path = "../theme" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
client = { path = "../client" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
anyhow = "1.0"
@ -29,10 +38,10 @@ smol = "1.2.5"
futures = "0.3"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -1,10 +1,9 @@
mod request;
pub mod request;
mod sign_in;
use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::Client;
use collections::HashMap;
use futures::{future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{
@ -25,40 +24,31 @@ use std::{
sync::Arc,
};
use util::{
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
paths, ResultExt,
};
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot";
actions!(
copilot,
[NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
);
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]);
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
// Disable Copilot for stable releases.
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
});
return;
}
let copilot = cx.add_model({
let node_runtime = node_runtime.clone();
move |cx| Copilot::start(http, node_runtime, cx)
});
cx.set_global(copilot.clone());
cx.add_global_action(|_: &SignIn, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
});
cx.add_global_action(|_: &SignOut, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.detach_and_log_err(cx);
});
cx.add_global_action(|_: &Reinstall, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.reinstall(cx))
.detach();
});
cx.observe(&copilot, |handle, cx| {
let status = handle.read(cx).status();
@ -82,6 +72,28 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
.detach();
sign_in::init(cx);
cx.add_global_action(|_: &SignIn, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
});
cx.add_global_action(|_: &SignOut, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.detach_and_log_err(cx);
}
});
cx.add_global_action(|_: &Reinstall, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.reinstall(cx))
.detach();
}
});
}
enum CopilotServer {
@ -99,12 +111,8 @@ enum CopilotServer {
#[derive(Clone, Debug)]
enum SignInStatus {
Authorized {
_user: String,
},
Unauthorized {
_user: String,
},
Authorized,
Unauthorized,
SigningIn {
prompt: Option<request::PromptUserDeviceFlow>,
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
@ -150,13 +158,6 @@ impl Entity for Copilot {
}
impl Copilot {
pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
match self.server {
CopilotServer::Starting { ref task } => Some(task.clone()),
_ => None,
}
}
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<Self>>().clone())
@ -219,6 +220,23 @@ impl Copilot {
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
let (server, fake_server) =
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
let this = cx.add_model(|cx| Self {
http: http.clone(),
node_runtime: NodeRuntime::new(http, cx.background().clone()),
server: CopilotServer::Started {
server: Arc::new(server),
status: SignInStatus::Authorized,
subscriptions_by_buffer_id: Default::default(),
},
});
(this, fake_server)
}
fn start_language_server(
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
@ -598,14 +616,10 @@ impl Copilot {
) {
if let CopilotServer::Started { status, .. } = &mut self.server {
*status = match lsp_status {
request::SignInStatus::Ok { user }
| request::SignInStatus::MaybeOk { user }
| request::SignInStatus::AlreadySignedIn { user } => {
SignInStatus::Authorized { _user: user }
}
request::SignInStatus::NotAuthorized { user } => {
SignInStatus::Unauthorized { _user: user }
}
request::SignInStatus::Ok { .. }
| request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
};
cx.notify();

View file

@ -117,7 +117,7 @@ pub struct GetCompletionsResult {
pub completions: Vec<Completion>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub text: String,

View file

@ -23,28 +23,17 @@ pub fn init(cx: &mut MutableAppContext) {
match &status {
crate::Status::SigningIn { prompt } => {
if let Some(code_verification) = code_verification.as_ref() {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
cx.activate_window(code_verification.window_id());
if let Some(code_verification_handle) = code_verification.as_mut() {
if cx.has_window(code_verification_handle.window_id()) {
code_verification_handle.update(cx, |code_verification_view, cx| {
code_verification_view.set_status(status, cx)
});
cx.activate_window(code_verification_handle.window_id());
} else {
create_copilot_auth_window(cx, &status, &mut code_verification);
}
} else if let Some(_prompt) = prompt {
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(
Default::default(),
window_size,
)),
titlebar: None,
center: true,
focus: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
};
let (_, view) =
cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
code_verification = Some(view);
create_copilot_auth_window(cx, &status, &mut code_verification);
}
}
Status::Authorized | Status::Unauthorized => {
@ -67,6 +56,27 @@ pub fn init(cx: &mut MutableAppContext) {
.detach();
}
fn create_copilot_auth_window(
cx: &mut MutableAppContext,
status: &Status,
code_verification: &mut Option<ViewHandle<CopilotCodeVerification>>,
) {
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
titlebar: None,
center: true,
focus: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
};
let (_, view) = cx.add_window(window_options, |_cx| {
CopilotCodeVerification::new(status.clone())
});
*code_verification = Some(view);
}
pub struct CopilotCodeVerification {
status: Status,
}

View file

@ -228,13 +228,8 @@ impl CopilotButton {
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
let this_handle = cx.handle().downgrade();
cx.observe_global::<Settings, _>(move |cx| {
if let Some(handle) = this_handle.upgrade(cx) {
handle.update(cx, |_, cx| cx.notify())
}
})
.detach();
cx.observe_global::<Settings, _>(move |_, cx| cx.notify())
.detach();
Self {
popup_menu: menu,
@ -249,19 +244,6 @@ impl CopilotButton {
let mut menu_options = Vec::with_capacity(6);
if let Some((_, view_id)) = self.editor_subscription.as_ref() {
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
menu_options.push(ContextMenuItem::item_for_view(
if locally_enabled {
"Pause Copilot for this file"
} else {
"Resume Copilot for this file"
},
*view_id,
copilot::Toggle,
));
}
if let Some(language) = &self.language {
let language_enabled = settings.copilot_on(Some(language.as_ref()));
@ -334,11 +316,7 @@ impl CopilotButton {
self.language = language_name.clone();
if let Some(enabled) = editor.copilot_state.user_enabled {
self.editor_enabled = Some(enabled);
} else {
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
}
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
cx.notify()
}

View file

@ -11,6 +11,7 @@ doctest = false
[features]
test-support = [
"rand",
"copilot/test-support",
"text/test-support",
"language/test-support",
"gpui/test-support",
@ -65,6 +66,7 @@ tree-sitter-javascript = { version = "*", optional = true }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
[dev-dependencies]
copilot = { path = "../copilot", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }

View file

@ -7,7 +7,7 @@ mod wrap_map;
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use fold_map::{FoldMap, FoldOffset};
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
@ -238,19 +238,22 @@ impl DisplayMap {
&self,
new_suggestion: Option<Suggestion<T>>,
cx: &mut ModelContext<Self>,
) where
) -> Option<Suggestion<FoldOffset>>
where
T: ToPoint,
{
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.replace(new_suggestion, snapshot, edits);
let (snapshot, edits, old_suggestion) =
self.suggestion_map.replace(new_suggestion, snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
old_suggestion
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {

View file

@ -79,7 +79,11 @@ impl SuggestionMap {
new_suggestion: Option<Suggestion<T>>,
fold_snapshot: FoldSnapshot,
fold_edits: Vec<FoldEdit>,
) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
) -> (
SuggestionSnapshot,
Vec<SuggestionEdit>,
Option<Suggestion<FoldOffset>>,
)
where
T: ToPoint,
{
@ -99,7 +103,8 @@ impl SuggestionMap {
let mut snapshot = self.0.lock();
let mut patch = Patch::new(edits);
if let Some(suggestion) = snapshot.suggestion.take() {
let old_suggestion = snapshot.suggestion.take();
if let Some(suggestion) = &old_suggestion {
patch = patch.compose([SuggestionEdit {
old: SuggestionOffset(suggestion.position.0)
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
@ -119,7 +124,7 @@ impl SuggestionMap {
snapshot.suggestion = new_suggestion;
snapshot.version += 1;
(snapshot.clone(), patch.into_inner())
(snapshot.clone(), patch.into_inner(), old_suggestion)
}
pub fn sync(
@ -589,7 +594,7 @@ mod tests {
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
let (suggestion_snapshot, _) = suggestion_map.replace(
let (suggestion_snapshot, _, _) = suggestion_map.replace(
Some(Suggestion {
position: 3,
text: "123\n456".into(),
@ -854,7 +859,9 @@ mod tests {
};
log::info!("replacing suggestion with {:?}", new_suggestion);
self.replace(new_suggestion, fold_snapshot, Default::default())
let (snapshot, edits, _) =
self.replace(new_suggestion, fold_snapshot, Default::default());
(snapshot, edits)
}
}
}

View file

@ -53,7 +53,7 @@ pub use language::{char_kind, CharKind};
use language::{
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
Point, Selection, SelectionGoal, TransactionId,
Point, Rope, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@ -95,6 +95,7 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@ -391,7 +392,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::toggle_copilot_suggestions);
hover_popover::init(cx);
link_go_to_definition::init(cx);
@ -510,7 +510,7 @@ pub struct Editor {
hover_state: HoverState,
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
pub copilot_state: CopilotState,
copilot_state: CopilotState,
_subscriptions: Vec<Subscription>,
}
@ -1013,7 +1013,6 @@ pub struct CopilotState {
pending_refresh: Task<Option<()>>,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
pub user_enabled: Option<bool>,
}
impl Default for CopilotState {
@ -1023,7 +1022,6 @@ impl Default for CopilotState {
pending_refresh: Task::ready(Some(())),
completions: Default::default(),
active_completion_index: 0,
user_enabled: None,
}
}
}
@ -1039,6 +1037,11 @@ impl CopilotState {
let completion = self.completions.get(self.active_completion_index)?;
let excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
if !completion.range.start.is_valid(completion_buffer)
|| !completion.range.end.is_valid(completion_buffer)
{
return None;
}
let mut completion_range = completion.range.to_offset(&completion_buffer);
let prefix_len = Self::common_prefix(
@ -1258,6 +1261,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global::<Settings, _>(Self::on_settings_changed),
],
};
this.end_selection(cx);
@ -1461,7 +1465,7 @@ impl Editor {
self.refresh_code_actions(cx);
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.refresh_copilot_suggestions(cx);
self.hide_copilot_suggestion(cx);
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@ -1835,7 +1839,7 @@ impl Editor {
return;
}
if self.clear_copilot_suggestions(cx) {
if self.hide_copilot_suggestion(cx).is_some() {
return;
}
@ -2014,8 +2018,18 @@ impl Editor {
}
drop(snapshot);
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
this.trigger_completion_on_input(&text, cx);
if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(cx);
if !this.has_active_copilot_suggestion(cx) {
this.trigger_completion_on_input(&text, cx);
}
} else {
this.trigger_completion_on_input(&text, cx);
this.refresh_copilot_suggestions(cx);
}
});
}
@ -2096,6 +2110,7 @@ impl Editor {
.collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
this.refresh_copilot_suggestions(cx);
});
}
@ -2188,9 +2203,7 @@ impl Editor {
}
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
if !cx.global::<Settings>().show_completions_on_input
|| self.has_active_copilot_suggestion(cx)
{
if !cx.global::<Settings>().show_completions_on_input {
return;
}
@ -2377,8 +2390,8 @@ impl Editor {
this.completion_tasks.retain(|(id, _)| *id > menu.id);
if this.focused && !menu.matches.is_empty() {
this.show_context_menu(ContextMenu::Completions(menu), cx);
} else {
this.hide_context_menu(cx);
} else if this.hide_context_menu(cx).is_none() {
this.update_visible_copilot_suggestion(cx);
}
});
}
@ -2481,6 +2494,8 @@ impl Editor {
);
});
}
this.refresh_copilot_suggestions(cx);
});
let project = self.project.clone()?;
@ -2775,50 +2790,24 @@ impl Editor {
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full {
return None;
}
let settings = cx.global::<Settings>();
if !self
.copilot_state
.user_enabled
.unwrap_or_else(|| settings.copilot_on(None))
{
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
self.hide_copilot_suggestion(cx);
return None;
}
self.update_visible_copilot_suggestion(cx);
let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor();
if !self.copilot_state.user_enabled.is_some() {
let language_name = snapshot
.language_at(selection.start)
.map(|language| language.name());
let copilot_enabled = settings.copilot_on(language_name.as_deref());
if !copilot_enabled {
return None;
}
}
let cursor = if selection.start == selection.end {
selection.start.bias_left(&snapshot)
} else {
return None;
};
self.refresh_active_copilot_suggestion(cx);
if !copilot.read(cx).status().is_authorized() {
let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name());
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
self.hide_copilot_suggestion(cx);
return None;
}
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
(
copilot.completions(&buffer, buffer_position, cx),
@ -2838,7 +2827,7 @@ impl Editor {
for completion in completions {
this.copilot_state.push_completion(completion);
}
this.refresh_active_copilot_suggestion(cx);
this.update_visible_copilot_suggestion(cx);
}
});
@ -2849,21 +2838,14 @@ impl Editor {
}
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
// Auto re-enable copilot if you're asking for a suggestion
if self.copilot_state.user_enabled == Some(false) {
cx.notify();
self.copilot_state.user_enabled = Some(true);
}
if self.copilot_state.completions.is_empty() {
if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx);
return;
}
self.copilot_state.active_completion_index =
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
self.refresh_active_copilot_suggestion(cx);
self.update_visible_copilot_suggestion(cx);
}
fn previous_copilot_suggestion(
@ -2871,13 +2853,7 @@ impl Editor {
_: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>,
) {
// Auto re-enable copilot if you're asking for a suggestion
if self.copilot_state.user_enabled == Some(false) {
cx.notify();
self.copilot_state.user_enabled = Some(true);
}
if self.copilot_state.completions.is_empty() {
if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx);
return;
}
@ -2888,45 +2864,44 @@ impl Editor {
} else {
self.copilot_state.active_completion_index - 1
};
self.refresh_active_copilot_suggestion(cx);
self.update_visible_copilot_suggestion(cx);
}
fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext<Self>) {
self.copilot_state.user_enabled = match self.copilot_state.user_enabled {
Some(enabled) => Some(!enabled),
None => {
let selection = self.selections.newest_anchor().start;
let language_name = self
.snapshot(cx)
.language_at(selection)
.map(|language| language.name());
let copilot_enabled = cx.global::<Settings>().copilot_on(language_name.as_deref());
Some(!copilot_enabled)
}
};
// We know this can't be None, as we just set it to Some above
if self.copilot_state.user_enabled == Some(true) {
self.refresh_copilot_suggestions(cx);
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(text) = self.hide_copilot_suggestion(cx) {
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
true
} else {
self.clear_copilot_suggestions(cx);
false
}
cx.notify();
}
fn refresh_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
self.display_map.read(cx).has_suggestion()
}
if self.context_menu.is_some() {
self.display_map
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
if self.has_active_copilot_suggestion(cx) {
let old_suggestion = self
.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
old_suggestion.map(|suggestion| suggestion.text)
} else {
None
}
}
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor();
let cursor = selection.head();
if self.context_menu.is_some()
|| !self.completion_tasks.is_empty()
|| selection.start != selection.end
{
self.hide_copilot_suggestion(cx);
} else if let Some(text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
@ -2941,44 +2916,11 @@ impl Editor {
)
});
cx.notify();
} else if self.has_active_copilot_suggestion(cx) {
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
}
}
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
if let Some(text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
self.clear_copilot_suggestions(cx);
true
} else {
false
self.hide_copilot_suggestion(cx);
}
}
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> bool {
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
let was_empty = self.copilot_state.completions.is_empty();
self.copilot_state.completions.clear();
self.copilot_state.active_completion_index = 0;
self.copilot_state.pending_refresh = Task::ready(None);
self.copilot_state.excerpt_id = None;
cx.notify();
!was_empty
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
self.display_map.read(cx).has_suggestion()
}
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
@ -3094,7 +3036,7 @@ impl Editor {
self.completion_tasks.clear();
}
self.context_menu = Some(menu);
self.refresh_active_copilot_suggestion(cx);
self.hide_copilot_suggestion(cx);
cx.notify();
}
@ -3102,7 +3044,9 @@ impl Editor {
cx.notify();
self.completion_tasks.clear();
let context_menu = self.context_menu.take();
self.refresh_active_copilot_suggestion(cx);
if context_menu.is_some() {
self.update_visible_copilot_suggestion(cx);
}
context_menu
}
@ -3262,6 +3206,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
this.refresh_copilot_suggestions(cx);
});
}
@ -3277,6 +3222,7 @@ impl Editor {
})
});
this.insert("", cx);
this.refresh_copilot_suggestions(cx);
});
}
@ -3289,10 +3235,6 @@ impl Editor {
}
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
if self.accept_copilot_suggestion(cx) {
return;
}
if self.move_to_next_snippet_tabstop(cx) {
return;
}
@ -3322,8 +3264,8 @@ impl Editor {
// If the selection is empty and the cursor is in the leading whitespace before the
// suggested indentation, then auto-indent the line.
let cursor = selection.head();
let current_indent = snapshot.indent_size_for_line(cursor.row);
if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
let current_indent = snapshot.indent_size_for_line(cursor.row);
if cursor.column < suggested_indent.len
&& cursor.column <= current_indent.len
&& current_indent.len <= suggested_indent.len
@ -3342,6 +3284,16 @@ impl Editor {
}
}
// Accept copilot suggestion if there is only one selection and the cursor is not
// in the leading whitespace.
if self.selections.count() == 1
&& cursor.column >= current_indent.len
&& self.has_active_copilot_suggestion(cx)
{
self.accept_copilot_suggestion(cx);
return;
}
// Otherwise, insert a hard or soft tab.
let settings = cx.global::<Settings>();
let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
@ -3365,7 +3317,8 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections))
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.refresh_copilot_suggestions(cx);
});
}
@ -4045,6 +3998,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(cx);
cx.emit(Event::Edited);
}
}
@ -4059,6 +4013,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_copilot_suggestions(cx);
cx.emit(Event::Edited);
}
}
@ -6466,7 +6421,9 @@ impl Editor {
multi_buffer::Event::Edited => {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
self.refresh_copilot_suggestions(cx);
if self.has_active_copilot_suggestion(cx) {
self.update_visible_copilot_suggestion(cx);
}
cx.emit(Event::BufferEdited);
}
multi_buffer::Event::ExcerptsAdded {
@ -6497,6 +6454,10 @@ impl Editor {
cx.notify();
}
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(cx);
}
pub fn set_searchable(&mut self, searchable: bool) {
self.searchable = searchable;
}

View file

@ -16,7 +16,7 @@ use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegist
use parking_lot::Mutex;
use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use unindent::Unindent;
use util::{
assert_set_eq,
@ -4585,81 +4585,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("editor.closeˇ");
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
// Handle completion request passing a marked string specifying where the completion
// should be triggered from using '|' character, what range should be replaced, and what completions
// should be returned using '<' and '>' to delimit the range
async fn handle_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
marked_string: &str,
completions: Vec<&'static str>,
) {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let (_, mut marked_ranges) = marked_text_ranges_by(
marked_string,
vec![complete_from_marker.clone(), replace_range_marker.clone()],
);
let complete_from_position =
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
let replace_range =
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.position,
complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array(
completions
.iter()
.map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: replace_range,
new_text: completion_text.to_string(),
})),
..Default::default()
})
.collect(),
)))
}
})
.next()
.await;
}
async fn handle_resolve_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
edits: Option<Vec<(&'static str, &'static str)>>,
) {
let edits = edits.map(|edits| {
edits
.iter()
.map(|(marked_string, new_text)| {
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
lsp::TextEdit::new(replace_range, new_text.to_string())
})
.collect::<Vec<_>>()
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let edits = edits.clone();
async move {
Ok(lsp::CompletionItem {
additional_text_edits: edits,
..Default::default()
})
}
})
.next()
.await;
}
}
#[gpui::test]
@ -5956,6 +5881,288 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test(iterations = 10)]
async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
oneˇ
two
three
"});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec!["completion_a", "completion_b"],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
..Default::default()
}],
vec![],
);
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_copilot_suggestion(cx));
// Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
editor
.confirm_completion(&Default::default(), cx)
.unwrap()
.detach();
assert!(!editor.context_menu_visible());
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
cx.set_state(indoc! {"
oneˇ
two
three
"});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.simulate_keystroke(".");
let _ = handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec!["completion_a", "completion_b"],
);
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_copilot_suggestion(cx));
// When hiding the context menu, the Copilot suggestion becomes visible.
editor.hide_context_menu(cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Ensure existing completion is interpolated when inserting again.
cx.simulate_keystroke("c");
deterministic.run_until_parked();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
// After debouncing, new Copilot completions should be requested.
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "one.copilot2".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
..Default::default()
}],
vec![],
);
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should remove the active Copilot suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// After canceling, tabbing shouldn't insert the previously shown suggestion.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
cx.update_editor(|editor, cx| {
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// Tabbing when there is an active suggestion inserts it.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// Hide suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
});
// If an edit occurs outside of this editor but no suggestion is being shown,
// we won't make it visible.
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
cx.update_editor(|editor, cx| {
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
});
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, cx| {
editor.set_text("fn foo() {\n \n}", cx);
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: " let x = 4;".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
editor.tab(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Tabbing again accepts the suggestion.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
});
}
#[gpui::test]
async fn test_copilot_completion_invalidation(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
one
twˇ
three
"});
handle_copilot_completion_request(
&copilot_lsp,
vec![copilot::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\nt\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
// Deleting across the original suggestion range invalidates it.
editor.backspace(&Default::default(), cx);
assert!(!editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one\nthree\n");
assert_eq!(editor.text(cx), "one\nthree\n");
// Undoing the deletion restores the suggestion.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_copilot_suggestion(cx));
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@ -5971,3 +6178,106 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
marked_text
);
}
/// Handle completion request passing a marked string specifying where the completion
/// should be triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range
fn handle_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
marked_string: &str,
completions: Vec<&'static str>,
) -> impl Future<Output = ()> {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let (_, mut marked_ranges) = marked_text_ranges_by(
marked_string,
vec![complete_from_marker.clone(), replace_range_marker.clone()],
);
let complete_from_position =
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
let replace_range =
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.position,
complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array(
completions
.iter()
.map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: replace_range,
new_text: completion_text.to_string(),
})),
..Default::default()
})
.collect(),
)))
}
});
async move {
request.next().await;
}
}
fn handle_resolve_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
edits: Option<Vec<(&'static str, &'static str)>>,
) -> impl Future<Output = ()> {
let edits = edits.map(|edits| {
edits
.iter()
.map(|(marked_string, new_text)| {
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
lsp::TextEdit::new(replace_range, new_text.to_string())
})
.collect::<Vec<_>>()
});
let mut request =
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let edits = edits.clone();
async move {
Ok(lsp::CompletionItem {
additional_text_edits: edits,
..Default::default()
})
}
});
async move {
request.next().await;
}
}
fn handle_copilot_completion_request(
lsp: &lsp::FakeLanguageServer,
completions: Vec<copilot::request::Completion>,
completions_cycling: Vec<copilot::request::Completion>,
) {
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
let completions = completions.clone();
async move {
Ok(copilot::request::GetCompletionsResult {
completions: completions.clone(),
})
}
});
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
let completions_cycling = completions_cycling.clone();
async move {
Ok(copilot::request::GetCompletionsResult {
completions: completions_cycling.clone(),
})
}
});
}

View file

@ -3952,6 +3952,19 @@ impl<'a, T: View> ViewContext<'a, T> {
})
}
pub fn observe_global<G, F>(&mut self, mut callback: F) -> Subscription
where
G: Any,
F: 'static + FnMut(&mut T, &mut ViewContext<T>),
{
let observer = self.weak_handle();
self.app.observe_global::<G, _>(move |cx| {
if let Some(observer) = observer.upgrade(cx) {
observer.update(cx, |observer, cx| callback(observer, cx));
}
})
}
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),

View file

@ -227,6 +227,7 @@ pub enum Lifecycle<T: Element> {
paint: T::PaintState,
},
}
pub struct ElementBox(ElementRc);
#[derive(Clone)]

View file

@ -65,8 +65,15 @@ impl platform::Screen for Screen {
// This approach is similar to that which winit takes
// https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99
let device_description = self.native_screen.deviceDescription();
let key = ns_string("NSScreenNumber");
let device_id_obj = device_description.objectForKey_(key);
if device_id_obj.is_null() {
// Under some circumstances, especially display re-arrangements or display locking, we seem to get a null pointer
// to the device id. See: https://linear.app/zed-industries/issue/Z-257/lock-screen-crash-with-multiple-monitors
return None;
}
let mut device_id: u32 = 0;
CFNumberGetValue(
device_id_obj as CFNumberRef,

View file

@ -335,6 +335,7 @@ impl LanguageServer {
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
workspace_folders: Some(true),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {

View file

@ -20,6 +20,7 @@ fs = { path = "../fs" }
anyhow = "1.0.38"
futures = "0.3"
theme = { path = "../theme" }
staff_mode = { path = "../staff_mode" }
util = { path = "../util" }
json_comments = "0.2"
postage = { workspace = true }

View file

@ -177,6 +177,7 @@ pub struct EditorSettings {
pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>,
#[schemars(skip)]
pub copilot: Option<OnOff>,
}
@ -436,6 +437,7 @@ pub struct SettingsFileContent {
#[serde(default)]
pub base_keymap: Option<BaseKeymap>,
#[serde(default)]
#[schemars(skip)]
pub enable_copilot_integration: Option<bool>,
}
@ -779,6 +781,7 @@ pub fn settings_file_json_schema(
settings.option_add_null_type = false;
});
let generator = SchemaGenerator::new(settings);
let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
// Create a schema for a theme name.
@ -791,6 +794,7 @@ pub fn settings_file_json_schema(
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
assert!(root_schema.definitions.contains_key("EditorSettings"));
let languages_object_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
object: Some(Box::new(ObjectValidation {

View file

@ -0,0 +1,12 @@
[package]
name = "staff_mode"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/staff_mode.rs"
[dependencies]
gpui = { path = "../gpui" }
anyhow = "1.0.38"

View file

@ -0,0 +1,42 @@
use gpui::MutableAppContext;
#[derive(Debug, Default)]
pub struct StaffMode(pub bool);
impl std::ops::Deref for StaffMode {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Despite what the type system requires me to tell you, the init function will only be called a once
/// as soon as we know that the staff mode is enabled.
pub fn staff_mode<F: FnMut(&mut MutableAppContext) + 'static>(
cx: &mut MutableAppContext,
mut init: F,
) {
if **cx.default_global::<StaffMode>() {
init(cx)
} else {
let mut once = Some(());
cx.observe_global::<StaffMode, _>(move |cx| {
if **cx.global::<StaffMode>() && once.take().is_some() {
init(cx);
}
})
.detach();
}
}
/// Immediately checks and runs the init function if the staff mode is not enabled.
/// This is only included for symettry with staff_mode() above
pub fn not_staff_mode<F: FnOnce(&mut MutableAppContext) + 'static>(
cx: &mut MutableAppContext,
init: F,
) {
if !**cx.default_global::<StaffMode>() {
init(cx)
}
}

View file

@ -1,4 +1,7 @@
use crate::{BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16};
use crate::{
locator::Locator, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint,
ToPointUtf16,
};
use anyhow::Result;
use std::{cmp::Ordering, fmt::Debug, ops::Range};
use sum_tree::Bias;
@ -86,6 +89,20 @@ impl Anchor {
{
content.summary_for_anchor(self)
}
/// Returns true when the [Anchor] is located inside a visible fragment.
pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
if *self == Anchor::MIN || *self == Anchor::MAX {
true
} else {
let fragment_id = buffer.fragment_id_for_anchor(self);
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>();
fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None);
fragment_cursor
.item()
.map_or(false, |fragment| fragment.visible)
}
}
}
pub trait OffsetRangeExt {

View file

@ -15,6 +15,7 @@ gpui = { path = "../gpui" }
picker = { path = "../picker" }
theme = { path = "../theme" }
settings = { path = "../settings" }
staff_mode = { path = "../staff_mode" }
workspace = { path = "../workspace" }
util = { path = "../util" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }

View file

@ -5,9 +5,9 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use settings::{settings_file::SettingsFile, Settings};
use staff_mode::StaffMode;
use std::sync::Arc;
use theme::{Theme, ThemeMeta, ThemeRegistry};
use util::StaffMode;
use workspace::{AppState, Workspace};
pub struct ThemeSelector {

View file

@ -3,8 +3,12 @@ use std::env;
use lazy_static::lazy_static;
lazy_static! {
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string())
} else {
include_str!("../../zed/RELEASE_CHANNEL").to_string()
};
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
"dev" => ReleaseChannel::Dev,
"preview" => ReleaseChannel::Preview,

View file

@ -17,17 +17,6 @@ pub use backtrace::Backtrace;
use futures::Future;
use rand::{seq::SliceRandom, Rng};
#[derive(Debug, Default)]
pub struct StaffMode(pub bool);
impl std::ops::Deref for StaffMode {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[macro_export]
macro_rules! debug_panic {
( $($fmt_arg:tt)* ) => {

View file

@ -191,16 +191,8 @@ impl View for WelcomePage {
impl WelcomePage {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
if let Some(handle) = handle.upgrade(cx) {
handle.update(cx, |_, cx| cx.notify())
}
});
WelcomePage {
_settings_subscription: settings_subscription,
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
}
}
}

View file

@ -1,7 +1,16 @@
use std::ops::Range;
use crate::{ItemHandle, Pane};
use gpui::{
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
View, ViewContext, ViewHandle,
elements::*,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
AnyViewHandle, DebugContext, ElementBox, Entity, LayoutContext, MeasurementContext,
MutableAppContext, PaintContext, RenderContext, SizeConstraint, Subscription, View,
ViewContext, ViewHandle,
};
use settings::Settings;
@ -40,27 +49,33 @@ impl View for StatusBar {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
Flex::row()
.with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_right(theme.item_spacing)
.boxed()
}))
.with_children(self.right_items.iter().rev().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.flex_float()
.boxed()
}))
.contained()
.with_style(theme.container)
.constrained()
.with_height(theme.height)
.boxed()
StatusBarElement {
left: Flex::row()
.with_children(self.left_items.iter().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_right(theme.item_spacing)
.boxed()
}))
.boxed(),
right: Flex::row()
.with_children(self.right_items.iter().rev().map(|i| {
ChildView::new(i.as_any(), cx)
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.boxed()
}))
.boxed(),
}
.contained()
.with_style(theme.container)
.constrained()
.with_height(theme.height)
.boxed()
}
}
@ -131,3 +146,74 @@ impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
val.as_any().clone()
}
}
struct StatusBarElement {
left: ElementBox,
right: ElementBox,
}
impl Element for StatusBarElement {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
mut constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let max_width = constraint.max.x();
constraint.min = vec2f(0., constraint.min.y());
let right_size = self.right.layout(constraint, cx);
let constraint = SizeConstraint::new(
vec2f(0., constraint.min.y()),
vec2f(max_width - right_size.x(), constraint.max.y()),
);
self.left.layout(constraint, cx);
(vec2f(max_width, right_size.y()), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let origin_y = bounds.upper_right().y();
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let left_origin = vec2f(bounds.lower_left().x(), origin_y);
self.left.paint(left_origin, visible_bounds, cx);
let right_origin = vec2f(bounds.upper_right().x() - self.right.size().x(), origin_y);
self.right.paint(right_origin, visible_bounds, cx);
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "StatusBarElement",
"bounds": bounds.to_json()
})
}
}

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.81.0"
version = "0.82.0"
publish = false
[lib]
@ -55,6 +55,7 @@ project_symbols = { path = "../project_symbols" }
recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
staff_mode = { path = "../staff_mode" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
terminal_view = { path = "../terminal_view" }

View file

@ -8,6 +8,7 @@ use node_runtime::NodeRuntime;
use serde_json::json;
use settings::{keymap_file_json_schema, settings_file_json_schema};
use smol::fs;
use staff_mode::StaffMode;
use std::{
any::Any,
ffi::OsString,
@ -17,7 +18,7 @@ use std::{
};
use theme::ThemeRegistry;
use util::http::HttpClient;
use util::{paths, ResultExt, StaffMode};
use util::{paths, ResultExt};
const SERVER_PATH: &'static str =
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";

View file

@ -109,10 +109,6 @@
(false)
] @constant.builtin
(interpolation
"#{" @punctuation.special
"}" @punctuation.special) @embedded
(comment) @comment
; Operators
@ -178,4 +174,8 @@
"}"
"%w("
"%i("
] @punctuation.bracket
] @punctuation.bracket
(interpolation
"#{" @punctuation.special
"}" @punctuation.special) @embedded

View file

@ -106,6 +106,9 @@ impl LspAdapter for YamlLspAdapter {
let settings = cx.global::<Settings>();
Some(
future::ready(serde_json::json!({
"yaml": {
"keyOrdering": false
},
"[yaml]": {
"editor.tabSize": settings.tab_size(Some("YAML"))
}

View file

@ -38,9 +38,9 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
use fs::RealFs;
use settings::watched_json::WatchedJsonFile;
use theme::ThemeRegistry;
#[cfg(debug_assertions)]
use util::StaffMode;
use staff_mode::StaffMode;
use theme::ThemeRegistry;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
@ -161,7 +161,7 @@ fn main() {
terminal_view::init(cx);
theme_testbench::init(cx);
recent_projects::init(cx);
copilot::init(client.clone(), node_runtime, cx);
copilot::init(http.clone(), node_runtime, cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();