From ebe57254e013cb7f990e297bbf66e90c7cda4aea Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 14 Apr 2023 17:46:53 -0400 Subject: [PATCH 01/34] Add tab tooltips --- crates/diagnostics/src/diagnostics.rs | 5 ++ crates/editor/src/items.rs | 17 +++++ crates/feedback/src/feedback_editor.rs | 5 ++ crates/gpui/src/elements/tooltip.rs | 13 ++-- crates/search/src/project_search.rs | 5 ++ crates/terminal_view/src/terminal_view.rs | 5 ++ crates/welcome/src/welcome.rs | 6 +- crates/workspace/src/item.rs | 8 +++ crates/workspace/src/pane.rs | 86 ++++++++++++++--------- crates/workspace/src/shared_screen.rs | 8 ++- 10 files changed, 119 insertions(+), 39 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 0ac282d199..d94d82487a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -24,6 +24,7 @@ use settings::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + borrow::Cow, cmp::Ordering, ops::Range, path::PathBuf, @@ -531,6 +532,10 @@ impl Item for ProjectDiagnosticsEditor { .update(cx, |editor, cx| editor.navigate(data, cx)) } + fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + Some("Project Diagnostics".into()) + } + fn is_dirty(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).is_dirty(cx) } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0f2a98dfe9..630b900e27 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -514,6 +514,23 @@ impl Item for Editor { } } + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + let file_path = self + .buffer() + .read(cx) + .as_singleton()? + .read(cx) + .file() + .and_then(|f| f.as_local())? + .abs_path(cx); + + let file_path = util::paths::compact(&file_path) + .to_string_lossy() + .to_string(); + + Some(file_path.into()) + } + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { match path_for_buffer(&self.buffer, detail, true, cx)? { Cow::Borrowed(path) => Some(path.to_string_lossy()), diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 15334138db..c45c29c989 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -1,5 +1,6 @@ use std::{ any::TypeId, + borrow::Cow, ops::{Range, RangeInclusive}, sync::Arc, }; @@ -248,6 +249,10 @@ impl Entity for FeedbackEditor { } impl Item for FeedbackEditor { + fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + Some("Send Feedback".into()) + } + fn tab_content(&self, _: Option, style: &theme::Tab, _: &AppContext) -> ElementBox { Flex::row() .with_child( diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 562f12295c..2f26ee116d 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -39,7 +39,7 @@ pub struct TooltipStyle { pub container: ContainerStyle, pub text: TextStyle, keystroke: KeystrokeStyle, - pub max_text_width: f32, + pub max_text_width: Option, } #[derive(Clone, Deserialize, Default)] @@ -140,9 +140,14 @@ impl Tooltip { ) -> impl Element { Flex::row() .with_child({ - let text = Text::new(text, style.text) - .constrained() - .with_max_width(style.max_text_width); + let text = if let Some(max_text_width) = style.max_text_width { + Text::new(text, style.text) + .constrained() + .with_max_width(max_text_width) + } else { + Text::new(text, style.text).constrained() + }; + if measure { text.flex(1., false).boxed() } else { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c7660aa336..b57561a4cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -22,6 +22,7 @@ use settings::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + borrow::Cow, mem, ops::Range, path::PathBuf, @@ -225,6 +226,10 @@ impl View for ProjectSearchView { } impl Item for ProjectSearchView { + fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { + Some(self.query_editor.read(cx).text(cx).into()) + } + fn act_as_type<'a>( &'a self, type_id: TypeId, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3af147b9ff..99f15c126e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -3,6 +3,7 @@ pub mod terminal_button; pub mod terminal_element; use std::{ + borrow::Cow, ops::RangeInclusive, path::{Path, PathBuf}, time::Duration, @@ -543,6 +544,10 @@ impl View for TerminalView { } impl Item for TerminalView { + fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { + Some(self.terminal().read(cx).title().into()) + } + fn tab_content( &self, _detail: Option, diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 28b0e43c57..114f3eb88e 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,6 +1,6 @@ mod base_keymap_picker; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use db::kvp::KEY_VALUE_STORE; use gpui::{ @@ -198,6 +198,10 @@ impl WelcomePage { } impl Item for WelcomePage { + fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + Some("Welcome to Zed!".into()) + } + fn tab_content( &self, _detail: Option, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f7ffe64f97..347a0022cc 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -44,6 +44,9 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } + fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + None + } fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { None } @@ -162,6 +165,7 @@ pub trait ItemHandle: 'static + fmt::Debug { cx: &mut AppContext, handler: Box, ) -> gpui::Subscription; + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -248,6 +252,10 @@ impl ItemHandle for ViewHandle { }) } + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + self.read(cx).tab_tooltip_text(cx) + } + fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option> { self.read(cx).tab_description(detail, cx) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a54aed96f4..c394ae4631 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1386,6 +1386,9 @@ impl Pane { let detail = detail.clone(); let theme = cx.global::().theme.clone(); + let mut tooltip_theme = theme.tooltip.clone(); + tooltip_theme.max_text_width = None; + let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); move |mouse_state, cx| { let tab_style = @@ -1393,39 +1396,56 @@ impl Pane { let hovered = mouse_state.hovered(); enum Tab {} - MouseEventHandler::::new(ix, cx, |_, cx| { - Self::render_tab( - &item, - pane.clone(), - ix == 0, - detail, - hovered, - tab_style, - cx, - ) - }) - .on_down(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ActivateItem(ix)); - }) - .on_click(MouseButton::Middle, { - let item = item.clone(); - let pane = pane.clone(); - move |_, cx: &mut EventContext| { - cx.dispatch_action(CloseItemById { - item_id: item.id(), - pane: pane.clone(), - }) - } - }) - .on_down(MouseButton::Right, move |e, cx| { - let item = item.clone(); - cx.dispatch_action(DeployTabContextMenu { - position: e.position, - item_id: item.id(), - pane: pane.clone(), - }); - }) - .boxed() + let mouse_event_handler = + MouseEventHandler::::new(ix, cx, |_, cx| { + Self::render_tab( + &item, + pane.clone(), + ix == 0, + detail, + hovered, + tab_style, + cx, + ) + }) + .on_down(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ActivateItem(ix)); + }) + .on_click(MouseButton::Middle, { + let item = item.clone(); + let pane = pane.clone(); + move |_, cx: &mut EventContext| { + cx.dispatch_action(CloseItemById { + item_id: item.id(), + pane: pane.clone(), + }) + } + }) + .on_down( + MouseButton::Right, + move |e, cx| { + let item = item.clone(); + cx.dispatch_action(DeployTabContextMenu { + position: e.position, + item_id: item.id(), + pane: pane.clone(), + }); + }, + ); + + if let Some(tab_tooltip_text) = tab_tooltip_text { + return mouse_event_handler + .with_tooltip::( + ix, + tab_tooltip_text, + None, + tooltip_theme, + cx, + ) + .boxed(); + } + + mouse_event_handler.boxed() } }); diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 7f7bf3b25f..a1dcb89e44 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -13,7 +13,10 @@ use gpui::{ }; use settings::Settings; use smallvec::SmallVec; -use std::sync::{Arc, Weak}; +use std::{ + borrow::Cow, + sync::{Arc, Weak}, +}; pub enum Event { Close, @@ -92,6 +95,9 @@ impl View for SharedScreen { } impl Item for SharedScreen { + fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + Some(format!("{}'s screen", self.user.github_login).into()) + } fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_ref() { nav_history.push::<()>(None, cx); From 1dcd4717b12fb6a67d24cb8918f5550351aff33a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 16 Apr 2023 12:28:27 -0700 Subject: [PATCH 02/34] Select language based on a file's first content line in addition to its path --- crates/language/src/buffer_tests.rs | 12 ++-- crates/language/src/language.rs | 58 +++++++++++++++++-- crates/project/src/project.rs | 42 +++++++------- .../zed/src/languages/javascript/config.toml | 1 + crates/zed/src/languages/python/config.toml | 1 + crates/zed/src/languages/ruby/config.toml | 1 + 6 files changed, 85 insertions(+), 30 deletions(-) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 8d99b9bad7..4675e4e9dc 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -81,14 +81,14 @@ fn test_select_language() { // matching file extension assert_eq!( registry - .language_for_path("zed/lib.rs") + .language_for_file("zed/lib.rs", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( registry - .language_for_path("zed/lib.mk") + .language_for_file("zed/lib.mk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -97,7 +97,7 @@ fn test_select_language() { // matching filename assert_eq!( registry - .language_for_path("zed/Makefile") + .language_for_file("zed/Makefile", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -106,21 +106,21 @@ fn test_select_language() { // matching suffix that is not the full file extension or filename assert_eq!( registry - .language_for_path("zed/cars") + .language_for_file("zed/cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_path("zed/a.cars") + .language_for_file("zed/a.cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_path("zed/sumk") + .language_for_file("zed/sumk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 60bb2cfddf..81aa1de7bd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,6 +262,8 @@ pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, pub brackets: BracketPairConfig, + #[serde(default, deserialize_with = "deserialize_regex")] + pub first_line_pattern: Option, #[serde(default = "auto_indent_using_last_non_empty_line_default")] pub auto_indent_using_last_non_empty_line: bool, #[serde(default, deserialize_with = "deserialize_regex")] @@ -334,6 +336,7 @@ impl Default for LanguageConfig { path_suffixes: Default::default(), brackets: Default::default(), auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(), + first_line_pattern: Default::default(), increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), autoclose_before: Default::default(), @@ -660,19 +663,30 @@ impl LanguageRegistry { }) } - pub fn language_for_path( + pub fn language_for_file( self: &Arc, path: impl AsRef, + content: Option<&Rope>, ) -> UnwrapFuture>>> { let path = path.as_ref(); let filename = path.file_name().and_then(|name| name.to_str()); let extension = path.extension().and_then(|name| name.to_str()); let path_suffixes = [extension, filename]; self.get_or_load_language(|config| { - config + let path_matches = config .path_suffixes .iter() - .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) + .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))); + let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or( + false, + |(content, pattern)| { + let end = content.clip_point(Point::new(0, 256), Bias::Left); + let end = content.point_to_offset(end); + let text = content.chunks_in_range(0..end).collect::(); + pattern.is_match(&text) + }, + ); + path_matches || content_matches }) } @@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { #[cfg(test)] mod tests { + use super::*; use gpui::TestAppContext; - use super::*; + #[gpui::test(iterations = 10)] + async fn test_first_line_pattern(cx: &mut TestAppContext) { + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.background()); + let languages = Arc::new(languages); + languages.register( + "/javascript", + LanguageConfig { + name: "JavaScript".into(), + path_suffixes: vec!["js".into()], + first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + ..Default::default() + }, + tree_sitter_javascript::language(), + None, + |_| Default::default(), + ); + + languages + .language_for_file("the/script", None) + .await + .unwrap_err(); + languages + .language_for_file("the/script", Some(&"nothing".into())) + .await + .unwrap_err(); + assert_eq!( + languages + .language_for_file("the/script", Some(&"#!/bin/env node".into())) + .await + .unwrap() + .name() + .as_ref(), + "JavaScript" + ); + } #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9192c7a411..e080cca964 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1997,17 +1997,19 @@ impl Project { fn detect_language_for_buffer( &mut self, - buffer: &ModelHandle, + buffer_handle: &ModelHandle, cx: &mut ModelContext, ) -> Option<()> { // If the buffer has a language, set it and start the language server if we haven't already. - let full_path = buffer.read(cx).file()?.full_path(cx); + let buffer = buffer_handle.read(cx); + let full_path = buffer.file()?.full_path(cx); + let content = buffer.as_rope(); let new_language = self .languages - .language_for_path(&full_path) + .language_for_file(&full_path, Some(content)) .now_or_never()? .ok()?; - self.set_language_for_buffer(buffer, new_language, cx); + self.set_language_for_buffer(buffer_handle, new_language, cx); None } @@ -2418,26 +2420,23 @@ impl Project { buffers: impl IntoIterator>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, PathBuf)> = buffers + let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers .into_iter() .filter_map(|buffer| { - let file = File::from_dyn(buffer.read(cx).file())?; + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; let worktree = file.worktree.read(cx).as_local()?; - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); let full_path = file.full_path(cx); - Some((worktree_id, worktree_abs_path, full_path)) + let language = self + .languages + .language_for_file(&full_path, Some(buffer.as_rope())) + .now_or_never()? + .ok()?; + Some((worktree.id(), worktree.abs_path().clone(), language)) }) .collect(); - for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { - if let Some(language) = self - .languages - .language_for_path(&full_path) - .now_or_never() - .and_then(|language| language.ok()) - { - self.restart_language_server(worktree_id, worktree_abs_path, language, cx); - } + for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { + self.restart_language_server(worktree_id, worktree_abs_path, language, cx); } None @@ -3471,7 +3470,7 @@ impl Project { let adapter_language = adapter_language.clone(); let language = this .languages - .language_for_path(&project_path.path) + .language_for_file(&project_path.path, None) .unwrap_or_else(move |_| adapter_language); let language_server_name = adapter.name.clone(); Some(async move { @@ -5900,7 +5899,10 @@ impl Project { worktree_id, path: PathBuf::from(serialized_symbol.path).into(), }; - let language = languages.language_for_path(&path.path).await.log_err(); + let language = languages + .language_for_file(&path.path, None) + .await + .log_err(); Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 7c49ac9513..c23ddcd6e7 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,5 +1,6 @@ name = "JavaScript" path_suffixes = ["js", "jsx", "mjs"] +first_line_pattern = '^#!.*\bnode\b' line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index e733676d89..80609de0ba 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,5 +1,6 @@ name = "Python" path_suffixes = ["py", "pyi"] +first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml index 329e080740..a0b26bff92 100644 --- a/crates/zed/src/languages/ruby/config.toml +++ b/crates/zed/src/languages/ruby/config.toml @@ -1,5 +1,6 @@ name = "Ruby" path_suffixes = ["rb", "Gemfile"] +first_line_pattern = '^#!.*\bruby\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [ From 9afd804062fbfa777fda7840f90c04df93caf055 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 18 Apr 2023 14:03:02 -0400 Subject: [PATCH 03/34] Remove unnecessary lifetimes from `tab_tooltip_text` --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/items.rs | 2 +- crates/feedback/src/feedback_editor.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/welcome/src/welcome.rs | 2 +- crates/workspace/src/item.rs | 6 +++--- crates/workspace/src/shared_screen.rs | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d94d82487a..75a95586be 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -532,7 +532,7 @@ impl Item for ProjectDiagnosticsEditor { .update(cx, |editor, cx| editor.navigate(data, cx)) } - fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { Some("Project Diagnostics".into()) } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 630b900e27..64147effff 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -514,7 +514,7 @@ impl Item for Editor { } } - fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { let file_path = self .buffer() .read(cx) diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index c45c29c989..f2d6c54aa7 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -249,7 +249,7 @@ impl Entity for FeedbackEditor { } impl Item for FeedbackEditor { - fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { Some("Send Feedback".into()) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b57561a4cc..906a0becfd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -226,7 +226,7 @@ impl View for ProjectSearchView { } impl Item for ProjectSearchView { - fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { Some(self.query_editor.read(cx).text(cx).into()) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 99f15c126e..e420a3d5e0 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -544,7 +544,7 @@ impl View for TerminalView { } impl Item for TerminalView { - fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { Some(self.terminal().read(cx).title().into()) } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 114f3eb88e..80e27666f5 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -198,7 +198,7 @@ impl WelcomePage { } impl Item for WelcomePage { - fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { Some("Welcome to Zed!".into()) } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 347a0022cc..4cf47c7713 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -44,7 +44,7 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } - fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { None } fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { @@ -165,7 +165,7 @@ pub trait ItemHandle: 'static + fmt::Debug { cx: &mut AppContext, handler: Box, ) -> gpui::Subscription; - fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; + fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option>; fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -252,7 +252,7 @@ impl ItemHandle for ViewHandle { }) } - fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { self.read(cx).tab_tooltip_text(cx) } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index a1dcb89e44..a397c3a719 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -95,7 +95,7 @@ impl View for SharedScreen { } impl Item for SharedScreen { - fn tab_tooltip_text<'a>(&'a self, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { Some(format!("{}'s screen", self.user.github_login).into()) } fn deactivated(&mut self, cx: &mut ViewContext) { From 304eddbbe4a83482fb3b9e4ab37065a50485e5da Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 18 Apr 2023 14:15:56 -0400 Subject: [PATCH 04/34] Remove unnecessary lifetimes from `tab_description` --- crates/editor/src/items.rs | 2 +- crates/workspace/src/item.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 64147effff..c5e0e8993e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -531,7 +531,7 @@ impl Item for Editor { Some(file_path.into()) } - fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { match path_for_buffer(&self.buffer, detail, true, cx)? { Cow::Borrowed(path) => Some(path.to_string_lossy()), Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4cf47c7713..6c41507478 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -47,7 +47,7 @@ pub trait Item: View { fn tab_tooltip_text(&self, _: &AppContext) -> Option> { None } - fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { + fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { None } fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) @@ -165,8 +165,8 @@ pub trait ItemHandle: 'static + fmt::Debug { cx: &mut AppContext, handler: Box, ) -> gpui::Subscription; - fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option>; - fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option>; fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; @@ -252,11 +252,11 @@ impl ItemHandle for ViewHandle { }) } - fn tab_tooltip_text<'a>(&'a self, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { self.read(cx).tab_tooltip_text(cx) } - fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option> { + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { self.read(cx).tab_description(detail, cx) } @@ -913,7 +913,7 @@ pub(crate) mod test { } impl Item for TestItem { - fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option> { + fn tab_description(&self, detail: usize, _: &AppContext) -> Option> { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; Some(description.into()) From ae0647c3a994200142ea4ed2f353bdff8e3f6ffb Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 18 Apr 2023 14:29:08 -0400 Subject: [PATCH 05/34] Update predictive color Co-Authored-By: Mikayla Maki --- styles/src/themes/common/syntax.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/themes/common/syntax.ts b/styles/src/themes/common/syntax.ts index dcfd93628e..39fedbf5c2 100644 --- a/styles/src/themes/common/syntax.ts +++ b/styles/src/themes/common/syntax.ts @@ -139,7 +139,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { primary: colorScheme.ramps.neutral(1).hex(), comment: colorScheme.ramps.neutral(0.71).hex(), punctuation: colorScheme.ramps.neutral(0.86).hex(), - predictive: colorScheme.ramps.neutral(0.57).hex(), + predictive: colorScheme.ramps.violet(0.38).hex(), emphasis: colorScheme.ramps.blue(0.5).hex(), string: colorScheme.ramps.orange(0.5).hex(), function: colorScheme.ramps.yellow(0.5).hex(), From 957ab65422e7dd6a20dd92592c04b467781c6612 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 18 Apr 2023 14:47:52 -0400 Subject: [PATCH 06/34] Mix neutral and blue to make a predictive color that is unique --- styles/src/themes/common/syntax.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/styles/src/themes/common/syntax.ts b/styles/src/themes/common/syntax.ts index 39fedbf5c2..50c3e4e3b7 100644 --- a/styles/src/themes/common/syntax.ts +++ b/styles/src/themes/common/syntax.ts @@ -1,6 +1,7 @@ import deepmerge from "deepmerge" import { FontWeight, fontWeights } from "../../common" import { ColorScheme } from "./colorScheme" +import chroma from "chroma-js" export interface SyntaxHighlightStyle { color: string @@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { [key: string]: Omit } = {} + const light = colorScheme.isLight + // then spread the default to each style for (const key of Object.keys({} as Syntax)) { syntax[key as keyof Syntax] = { @@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { } } + // Mix the neutral and blue colors to get a + // predictive color distinct from any other color in the theme + const predictive = chroma.mix( + colorScheme.ramps.neutral(0.4).hex(), + colorScheme.ramps.blue(0.4).hex(), + 0.45, + "lch" + ).hex() + const color = { primary: colorScheme.ramps.neutral(1).hex(), comment: colorScheme.ramps.neutral(0.71).hex(), punctuation: colorScheme.ramps.neutral(0.86).hex(), - predictive: colorScheme.ramps.violet(0.38).hex(), + predictive: predictive, emphasis: colorScheme.ramps.blue(0.5).hex(), string: colorScheme.ramps.orange(0.5).hex(), function: colorScheme.ramps.yellow(0.5).hex(), From 721baf5746d176fe9c473876637ae8d36d9351b5 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 18 Apr 2023 14:56:39 -0400 Subject: [PATCH 07/34] Re-render toolbar items when updating their knowledge of pane focus --- crates/workspace/src/toolbar.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 7531ab9f92..bffca3e3c8 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -70,6 +70,7 @@ impl View for Toolbar { for (item, position) in &self.items { match *position { ToolbarItemLocation::Hidden => {} + ToolbarItemLocation::PrimaryLeft { flex } => { let left_item = ChildView::new(item.as_any(), cx) .aligned() @@ -81,6 +82,7 @@ impl View for Toolbar { primary_left_items.push(left_item.boxed()); } } + ToolbarItemLocation::PrimaryRight { flex } => { let right_item = ChildView::new(item.as_any(), cx) .aligned() @@ -93,6 +95,7 @@ impl View for Toolbar { primary_right_items.push(right_item.boxed()); } } + ToolbarItemLocation::Secondary => { secondary_item = Some( ChildView::new(item.as_any(), cx) @@ -300,7 +303,10 @@ impl ToolbarItemViewHandle for ViewHandle { } fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut AppContext) { - self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx)); + self.update(cx, |this, cx| { + this.pane_focus_update(pane_focused, cx); + cx.notify(); + }); } } From bd7d50f339d17abccce34c7a64dd033f9c0b51ac Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 18 Apr 2023 16:13:18 -0700 Subject: [PATCH 08/34] Fix 'invalid insertion' panic when following Wait for the necessary buffer operations to arrive before attempting to set selections and scroll top. --- crates/collab/src/tests/integration_tests.rs | 52 ++-- crates/editor/src/items.rs | 284 ++++++++++--------- crates/editor/src/multi_buffer.rs | 38 ++- crates/language/src/buffer.rs | 6 +- crates/project/src/lsp_command.rs | 8 +- crates/text/src/text.rs | 8 +- 6 files changed, 229 insertions(+), 167 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index dda8035874..eb10640d59 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5862,10 +5862,17 @@ async fn test_basic_following( // Client A updates their selections in those editors editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([0..1])) + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); }); editor_a2.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([2..3])) + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); }); // When client B starts following client A, all visible view states are replicated to client B. @@ -5878,6 +5885,27 @@ async fn test_basic_following( .await .unwrap(); + cx_c.foreground().run_until_parked(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + cx_c.foreground().run_until_parked(); let active_call_c = cx_c.read(ActiveCall::global); let project_c = client_c.build_remote_project(project_id, cx_c).await; @@ -6033,26 +6061,6 @@ async fn test_basic_following( }); } - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - cx_b.read(|cx| editor_b2.project_path(cx)), - Some((worktree_id, "2.txt").into()) - ); - assert_eq!( - editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![2..3] - ); - assert_eq!( - editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![0..1] - ); - // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a1, cx) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c5e0e8993e..a9f82e0ccb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -3,12 +3,12 @@ use crate::{ movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -72,11 +72,11 @@ impl FollowableItem for Editor { let editor = pane.read_with(&cx, |pane, cx| { let mut editors = pane.items_of_type::(); editors.find(|editor| { - editor.remote_id(&client, cx) == Some(remote_id) - || state.singleton - && buffers.len() == 1 - && editor.read(cx).buffer.read(cx).as_singleton().as_ref() - == Some(&buffers[0]) + let ids_match = editor.remote_id(&client, cx) == Some(remote_id); + let singleton_buffer_matches = state.singleton + && buffers.first() + == editor.read(cx).buffer.read(cx).as_singleton().as_ref(); + ids_match || singleton_buffer_matches }) }); @@ -115,46 +115,29 @@ impl FollowableItem for Editor { multibuffer }); - cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx)) + cx.add_view(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); + editor.remote_id = Some(remote_id); + editor + }) }) }); - editor.update(&mut cx, |editor, cx| { - editor.remote_id = Some(remote_id); - let buffer = editor.buffer.read(cx).read(cx); - let selections = state - .selections - .into_iter() - .map(|selection| { - deserialize_selection(&buffer, selection) - .ok_or_else(|| anyhow!("invalid selection")) - }) - .collect::>>()?; - let pending_selection = state - .pending_selection - .map(|selection| deserialize_selection(&buffer, selection)) - .flatten(); - let scroll_top_anchor = state - .scroll_top_anchor - .and_then(|anchor| deserialize_anchor(&buffer, anchor)); - drop(buffer); - - if !selections.is_empty() || pending_selection.is_some() { - editor.set_selections_from_remote(selections, pending_selection, cx); - } - - if let Some(scroll_top_anchor) = scroll_top_anchor { - editor.set_scroll_anchor_remote( - ScrollAnchor { - top_anchor: scroll_top_anchor, - offset: vec2f(state.scroll_x, state.scroll_y), - }, - cx, - ); - } - - anyhow::Ok(()) - })?; + update_editor_from_message( + editor.clone(), + project, + proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }, + &mut cx, + ) + .await?; Ok(editor) })) @@ -299,96 +282,9 @@ impl FollowableItem for Editor { cx: &mut ViewContext, ) -> Task> { let update_view::Variant::Editor(message) = message; - let multibuffer = self.buffer.read(cx); - let multibuffer = multibuffer.read(cx); - - let buffer_ids = message - .inserted_excerpts - .iter() - .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) - .collect::>(); - - let mut removals = message - .deleted_excerpts - .into_iter() - .map(ExcerptId::from_proto) - .collect::>(); - removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); - - let selections = message - .selections - .into_iter() - .filter_map(|selection| deserialize_selection(&multibuffer, selection)) - .collect::>(); - let pending_selection = message - .pending_selection - .and_then(|selection| deserialize_selection(&multibuffer, selection)); - - let scroll_top_anchor = message - .scroll_top_anchor - .and_then(|anchor| deserialize_anchor(&multibuffer, anchor)); - drop(multibuffer); - - let buffers = project.update(cx, |project, cx| { - buffer_ids - .into_iter() - .map(|id| project.open_buffer_by_id(id, cx)) - .collect::>() - }); - let project = project.clone(); cx.spawn(|this, mut cx| async move { - let _buffers = try_join_all(buffers).await?; - this.update(&mut cx, |this, cx| { - this.buffer.update(cx, |multibuffer, cx| { - let mut insertions = message.inserted_excerpts.into_iter().peekable(); - while let Some(insertion) = insertions.next() { - let Some(excerpt) = insertion.excerpt else { continue }; - let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; - let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; - - let adjacent_excerpts = iter::from_fn(|| { - let insertion = insertions.peek()?; - if insertion.previous_excerpt_id.is_none() - && insertion.excerpt.as_ref()?.buffer_id == buffer_id - { - insertions.next()?.excerpt - } else { - None - } - }); - - multibuffer.insert_excerpts_with_ids_after( - ExcerptId::from_proto(previous_excerpt_id), - buffer, - [excerpt] - .into_iter() - .chain(adjacent_excerpts) - .filter_map(|excerpt| { - Some(( - ExcerptId::from_proto(excerpt.id), - deserialize_excerpt_range(excerpt)?, - )) - }), - cx, - ); - } - - multibuffer.remove_excerpts(removals, cx); - }); - - if !selections.is_empty() || pending_selection.is_some() { - this.set_selections_from_remote(selections, pending_selection, cx); - this.request_autoscroll_remotely(Autoscroll::newest(), cx); - } else if let Some(anchor) = scroll_top_anchor { - this.set_scroll_anchor_remote(ScrollAnchor { - top_anchor: anchor, - offset: vec2f(message.scroll_x, message.scroll_y) - }, cx); - } - }); - Ok(()) + update_editor_from_message(this, project, message, &mut cx).await }) } @@ -402,6 +298,128 @@ impl FollowableItem for Editor { } } +async fn update_editor_from_message( + this: ViewHandle, + project: ModelHandle, + message: proto::update_view::Editor, + cx: &mut AsyncAppContext, +) -> Result<()> { + // Open all of the buffers of which excerpts were added to the editor. + let inserted_excerpt_buffer_ids = message + .inserted_excerpts + .iter() + .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) + .collect::>(); + let inserted_excerpt_buffers = project.update(cx, |project, cx| { + inserted_excerpt_buffer_ids + .into_iter() + .map(|id| project.open_buffer_by_id(id, cx)) + .collect::>() + }); + let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?; + + // Update the editor's excerpts. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |multibuffer, cx| { + let mut removed_excerpt_ids = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removed_excerpt_ids.sort_by({ + let multibuffer = multibuffer.read(cx); + move |a, b| a.cmp(&b, &multibuffer) + }); + + let mut insertions = message.inserted_excerpts.into_iter().peekable(); + while let Some(insertion) = insertions.next() { + let Some(excerpt) = insertion.excerpt else { continue }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let buffer_id = excerpt.buffer_id; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + + let adjacent_excerpts = iter::from_fn(|| { + let insertion = insertions.peek()?; + if insertion.previous_excerpt_id.is_none() + && insertion.excerpt.as_ref()?.buffer_id == buffer_id + { + insertions.next()?.excerpt + } else { + None + } + }); + + multibuffer.insert_excerpts_with_ids_after( + ExcerptId::from_proto(previous_excerpt_id), + buffer, + [excerpt] + .into_iter() + .chain(adjacent_excerpts) + .filter_map(|excerpt| { + Some(( + ExcerptId::from_proto(excerpt.id), + deserialize_excerpt_range(excerpt)?, + )) + }), + cx, + ); + } + + multibuffer.remove_excerpts(removed_excerpt_ids, cx); + }); + }); + + // Deserialize the editor state. + let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| { + let buffer = editor.buffer.read(cx).read(cx); + let selections = message + .selections + .into_iter() + .filter_map(|selection| deserialize_selection(&buffer, selection)) + .collect::>(); + let pending_selection = message + .pending_selection + .and_then(|selection| deserialize_selection(&buffer, selection)); + let scroll_top_anchor = message + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&buffer, anchor)); + anyhow::Ok((selections, pending_selection, scroll_top_anchor)) + })?; + + // Wait until the buffer has received all of the operations referenced by + // the editor's new state. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |buffer, cx| { + buffer.wait_for_anchors( + selections + .iter() + .chain(pending_selection.as_ref()) + .flat_map(|selection| [selection.start, selection.end]) + .chain(scroll_top_anchor), + cx, + ) + }) + }) + .await?; + + // Update the editor's state. + this.update(cx, |editor, cx| { + if !selections.is_empty() || pending_selection.is_some() { + editor.set_selections_from_remote(selections, pending_selection, cx); + editor.request_autoscroll_remotely(Autoscroll::newest(), cx); + } else if let Some(scroll_top_anchor) = scroll_top_anchor { + editor.set_scroll_anchor_remote( + ScrollAnchor { + top_anchor: scroll_top_anchor, + offset: vec2f(message.scroll_x, message.scroll_y), + }, + cx, + ); + } + }); + Ok(()) +} + fn serialize_excerpt( buffer_id: u64, id: &ExcerptId, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bec9d5967c..f8a56557ab 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1,6 +1,7 @@ mod anchor; pub use anchor::{Anchor, AnchorRangeExt}; +use anyhow::{anyhow, Result}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; @@ -16,7 +17,9 @@ use language::{ use std::{ borrow::Cow, cell::{Ref, RefCell}, - cmp, fmt, io, + cmp, fmt, + future::Future, + io, iter::{self, FromIterator}, mem, ops::{Range, RangeBounds, Sub}, @@ -1238,6 +1241,39 @@ impl MultiBuffer { cx.notify(); } + pub fn wait_for_anchors<'a>( + &self, + anchors: impl 'a + Iterator, + cx: &mut ModelContext, + ) -> impl 'static + Future> { + let borrow = self.buffers.borrow(); + let mut error = None; + let mut futures = Vec::new(); + for anchor in anchors { + if let Some(buffer_id) = anchor.buffer_id { + if let Some(buffer) = borrow.get(&buffer_id) { + buffer.buffer.update(cx, |buffer, _| { + futures.push(buffer.wait_for_anchors([anchor.text_anchor])) + }); + } else { + error = Some(anyhow!( + "buffer {buffer_id} is not part of this multi-buffer" + )); + break; + } + } + } + async move { + if let Some(error) = error { + Err(error)?; + } + for future in futures { + future.await?; + } + Ok(()) + } + } + pub fn text_anchor_for_position( &self, position: T, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fa8368f20b..fb16d41640 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1313,10 +1313,10 @@ impl Buffer { self.text.wait_for_edits(edit_ids) } - pub fn wait_for_anchors<'a>( + pub fn wait_for_anchors( &mut self, - anchors: impl IntoIterator, - ) -> impl Future> { + anchors: impl IntoIterator, + ) -> impl 'static + Future> { self.text.wait_for_anchors(anchors) } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d9fafceab0..fb69df8766 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -572,7 +572,7 @@ async fn location_links_from_proto( .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("missing origin end"))?; buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) .await?; Some(Location { buffer, @@ -597,7 +597,7 @@ async fn location_links_from_proto( .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("missing target end"))?; buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) .await?; let target = Location { buffer, @@ -868,7 +868,7 @@ impl LspCommand for GetReferences { .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("missing target end"))?; target_buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) .await?; locations.push(Location { buffer: target_buffer, @@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights { .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("missing target end"))?; buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) .await?; let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b857ec5d5e..3df83fa305 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1331,15 +1331,15 @@ impl Buffer { } } - pub fn wait_for_anchors<'a>( + pub fn wait_for_anchors( &mut self, - anchors: impl IntoIterator, + anchors: impl IntoIterator, ) -> impl 'static + Future> { let mut futures = Vec::new(); for anchor in anchors { if !self.version.observed(anchor.timestamp) - && *anchor != Anchor::MAX - && *anchor != Anchor::MIN + && anchor != Anchor::MAX + && anchor != Anchor::MIN { let (tx, rx) = oneshot::channel(); self.edit_id_resolvers From 7cc868bc8cbc5779bfde96777dd827e096d2f7e9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 18 Apr 2023 17:05:26 -0700 Subject: [PATCH 09/34] When opening projects, only reuse the current window if it is empty --- crates/recent_projects/src/recent_projects.rs | 2 +- crates/workspace/src/workspace.rs | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 58369f4aa5..115f5792eb 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -167,7 +167,7 @@ impl PickerDelegate for RecentProjectsView { fn confirm(&mut self, cx: &mut ViewContext) { if let Some(selected_match) = &self.matches.get(self.selected_index()) { let workspace_location = &self.workspace_locations[selected_match.candidate_id]; - cx.dispatch_global_action(OpenPaths { + cx.dispatch_action(OpenPaths { paths: workspace_location.paths().as_ref().clone(), }); cx.emit(Event::Dismissed); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 739153fc78..a7dfe78221 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -302,14 +302,26 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { let app_state = app_state.upgrade()?; let window_id = cx.window_id(); let action = action.clone(); - let close = workspace.prepare_to_close(false, cx); + let is_remote = workspace.project.read(cx).is_remote(); + let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some(); + let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx)); + let close_task = if is_remote || has_worktree || has_dirty_items { + None + } else { + Some(workspace.prepare_to_close(false, cx)) + }; Some(cx.spawn_weak(|_, mut cx| async move { - let can_close = close.await?; - if can_close { - cx.update(|cx| open_paths(&action.paths, &app_state, Some(window_id), cx)) - .await; - } + let window_id_to_replace = if let Some(close_task) = close_task { + if !close_task.await? { + return Ok(()); + } + Some(window_id) + } else { + None + }; + cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx)) + .await; Ok(()) })) } From c5e56a5e45a66208e1ef643f152c697bfd08f76f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 18 Apr 2023 18:41:33 -0700 Subject: [PATCH 10/34] Fail the randomized test build after reporting the error to zed.dev --- script/randomized-test-ci | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/randomized-test-ci b/script/randomized-test-ci index 7816ebfbbf..4d3f85aef4 100755 --- a/script/randomized-test-ci +++ b/script/randomized-test-ci @@ -56,6 +56,8 @@ async function main() { headers: {"Content-Type": "application/json"}, body: JSON.stringify(body) }) + + process.exit(1) } function randomU64() { From f9c60b98c040a49aa5c528e405187bd0c1dc3c3f Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Wed, 19 Apr 2023 19:57:23 +0300 Subject: [PATCH 11/34] Add newline above and improve newline below Add a new action for inserting a new line above the current line. @ForLoveOfCats also helped fix a bug among other things. When two collaborators had their cursors at the end of a line, and one collaborator performed a newline below action, the second collaborator's cursor would be dragged to the new line. This is also fixing that. Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- Cargo.lock | 1 + assets/keymaps/default.json | 1 + crates/collab/Cargo.toml | 1 + crates/collab/src/tests/integration_tests.rs | 104 +++++++++++++++++- crates/editor/src/editor.rs | 93 ++++++++++++++-- crates/editor/src/editor_tests.rs | 49 +++++++++ crates/editor/src/test/editor_test_context.rs | 32 ++++-- 7 files changed, 264 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 365f383b48..6f05512b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,6 +1213,7 @@ dependencies = [ "git", "gpui", "hyper", + "indoc", "language", "lazy_static", "lipsum", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 14e3056e6a..f570f20bc5 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -163,6 +163,7 @@ "context": "Editor && mode == full", "bindings": { "enter": "editor::Newline", + "cmd-shift-enter": "editor::NewlineAbove", "cmd-enter": "editor::NewlineBelow", "alt-z": "editor::ToggleSoftWrap", "cmd-f": [ diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 2891fe3010..58fc602c94 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -55,6 +55,7 @@ toml = "0.5.8" tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } +indoc = "1.0.4" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index eb10640d59..092fdddb96 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6,8 +6,9 @@ use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; use collections::HashSet; use editor::{ - ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, - Rename, ToOffset, ToggleCodeActions, Undo, + test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, + ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, + Undo, }; use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; @@ -15,6 +16,7 @@ use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext, ViewHandle, }; +use indoc::indoc; use language::{ tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, OffsetRangeExt, Point, Rope, @@ -3042,6 +3044,104 @@ async fn test_editing_while_guest_opens_buffer( buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text)); } +#[gpui::test] +async fn test_newline_above_or_below_does_not_move_guest_cursor( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs + .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + + // Open a buffer as client A + let buffer_a = project_a + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let (_, window_a) = cx_a.add_window(|_| EmptyView); + let editor_a = cx_a.add_view(&window_a, |cx| { + Editor::for_buffer(buffer_a, Some(project_a), cx) + }); + let mut editor_cx_a = EditorTestContext { + cx: cx_a, + window_id: window_a.id(), + editor: editor_a, + }; + + // Open a buffer as client B + let buffer_b = project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + let (_, window_b) = cx_b.add_window(|_| EmptyView); + let editor_b = cx_b.add_view(&window_b, |cx| { + Editor::for_buffer(buffer_b, Some(project_b), cx) + }); + let mut editor_cx_b = EditorTestContext { + cx: cx_b, + window_id: window_b.id(), + editor: editor_b, + }; + + // Test newline above + editor_cx_a.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); + deterministic.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + ˇ + Some text + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + "}); + + // Test newline below + editor_cx_a.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_b.set_selections_state(indoc! {" + + Some textˇ + "}); + editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); + deterministic.run_until_parked(); + editor_cx_a.assert_editor_state(indoc! {" + + Some text + ˇ + "}); + editor_cx_b.assert_editor_state(indoc! {" + + Some textˇ + + "}); +} + #[gpui::test(iterations = 10)] async fn test_leaving_worktree_while_opening_buffer( deterministic: Arc, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3f3723bc1..48940c3075 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -184,6 +184,7 @@ actions!( Backspace, Delete, Newline, + NewlineAbove, NewlineBelow, GoToDiagnostic, GoToPrevDiagnostic, @@ -301,6 +302,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::select); cx.add_action(Editor::cancel); cx.add_action(Editor::newline); + cx.add_action(Editor::newline_above); cx.add_action(Editor::newline_below); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); @@ -2118,7 +2120,7 @@ impl Editor { }); } - pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext) { + pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext) { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -2130,19 +2132,17 @@ impl Editor { let cursor = selection.head(); let row = cursor.row; - let end_of_line = snapshot - .clip_point(Point::new(row, snapshot.line_len(row)), Bias::Left) - .to_point(&snapshot); + let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); let newline = "\n".to_string(); - edits.push((end_of_line..end_of_line, newline)); + edits.push((start_of_line..start_of_line, newline)); - rows_inserted += 1; rows.push(row + rows_inserted); + rows_inserted += 1; } self.transact(cx, |editor, cx| { - editor.edit_with_autoindent(edits, cx); + editor.edit(edits, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { let mut index = 0; @@ -2157,6 +2157,85 @@ impl Editor { (clipped, SelectionGoal::None) }); }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + let mut rows_inserted = 0; + + for selection in self.selections.all_adjusted(cx) { + let cursor = selection.head(); + let row = cursor.row; + + let point = Point::new(row + 1, 0); + let start_of_line = snapshot.clip_point(point, Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows_inserted += 1; + rows.push(row + rows_inserted); + } + + self.transact(cx, |editor, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9cf3c4a81e..ce293ed064 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1467,6 +1467,55 @@ fn test_newline_with_old_selections(cx: &mut gpui::AppContext) { }); } +#[gpui::test] +async fn test_newline_above(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx); + cx.update(|cx| { + cx.update_global::(|settings, _| { + settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); + }); + }); + + let language = Arc::new( + Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + ) + .with_indents_query(r#"(_ "(" ")" @end) @indent"#) + .unwrap(), + ); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + cx.set_state(indoc! {" + const a: ˇA = ( + (ˇ + «const_functionˇ»(ˇ), + so«mˇ»et«hˇ»ing_ˇelse,ˇ + )ˇ + ˇ);ˇ + "}); + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); + cx.assert_editor_state(indoc! {" + ˇ + const a: A = ( + ˇ + ( + ˇ + ˇ + const_function(), + ˇ + ˇ + ˇ + ˇ + something_else, + ˇ + ) + ˇ + ˇ + ); + "}); +} + #[gpui::test] async fn test_newline_below(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 9e66fea8df..ccd0e936a4 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -167,7 +167,7 @@ impl<'a> EditorTestContext<'a> { /// /// See the `util::test::marked_text_ranges` function for more information. pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { - let _state_context = self.add_assertion_context(format!( + let state_context = self.add_assertion_context(format!( "Initial Editor State: \"{}\"", marked_text.escape_debug().to_string() )); @@ -178,7 +178,22 @@ impl<'a> EditorTestContext<'a> { s.select_ranges(selection_ranges) }) }); - _state_context + state_context + } + + /// Only change the editor's selections + pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { + let state_context = self.add_assertion_context(format!( + "Initial Editor State: \"{}\"", + marked_text.escape_debug().to_string() + )); + let (_, selection_ranges) = marked_text_ranges(marked_text, true); + self.editor.update(self.cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(selection_ranges) + }) + }); + state_context } /// Make an assertion about the editor's text and the ranges and directions @@ -189,10 +204,11 @@ impl<'a> EditorTestContext<'a> { pub fn assert_editor_state(&mut self, marked_text: &str) { let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); let buffer_text = self.buffer_text(); - assert_eq!( - buffer_text, unmarked_text, - "Unmarked text doesn't match buffer text" - ); + + if buffer_text != unmarked_text { + panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); + } + self.assert_selections(expected_selections, marked_text.to_string()) } @@ -254,10 +270,10 @@ impl<'a> EditorTestContext<'a> { panic!( indoc! {" {}Editor has unexpected selections. - + Expected selections: {} - + Actual selections: {} "}, From b3b8f8532de232d2f6380c6f68e75907c71dabab Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Wed, 19 Apr 2023 20:52:01 +0300 Subject: [PATCH 12/34] Assert the editor and unmarked texts are the same Co-Authored-By: Max Brunsfeld --- crates/editor/src/test/editor_test_context.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index ccd0e936a4..dfdcdad19b 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -187,8 +187,9 @@ impl<'a> EditorTestContext<'a> { "Initial Editor State: \"{}\"", marked_text.escape_debug().to_string() )); - let (_, selection_ranges) = marked_text_ranges(marked_text, true); + let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); self.editor.update(self.cx, |editor, cx| { + assert_eq!(editor.text(cx), unmarked_text); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges(selection_ranges) }) From 1fd07b6fcfc455c40ce30c474690924eed89198f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 20:07:05 +0200 Subject: [PATCH 13/34] Clarify copilot settings Co-Authored-By: Nathan Sobo --- assets/settings/default.json | 13 ++- crates/copilot/src/copilot.rs | 4 +- crates/copilot_button/src/copilot_button.rs | 28 ++--- crates/editor/src/editor.rs | 5 +- crates/settings/src/settings.rs | 111 ++++++++------------ 5 files changed, 73 insertions(+), 88 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 0f663e1b81..1ecfbf03a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,6 +1,11 @@ { // The name of the Zed theme to use for the UI "theme": "One Dark", + // Features that can be globally enabled or disabled + "features": { + // Show Copilot icon in status bar + "copilot": true + }, // The name of a font to use for rendering text in the editor "buffer_font_family": "Zed Mono", // The OpenType features to enable for text in the editor. @@ -13,11 +18,6 @@ // The factor to grow the active pane by. Defaults to 1.0 // which gives the same size as all other panes. "active_pane_magnification": 1.0, - // Enable / disable copilot integration. - "enable_copilot_integration": true, - // Controls whether copilot provides suggestion immediately - // or waits for a `copilot::Toggle` - "copilot": "on", // Whether to enable vim modes and key bindings "vim_mode": false, // Whether to show the informational hover box when moving the mouse @@ -30,6 +30,9 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Controls whether copilot provides suggestion immediately + // or waits for a `copilot::Toggle` + "show_copilot_suggestions": true, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 43fcf55e95..aa086775b3 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -172,7 +172,7 @@ impl Copilot { let http = http.clone(); let node_runtime = node_runtime.clone(); move |this, cx| { - if cx.global::().enable_copilot_integration { + if cx.global::().features.copilot { if matches!(this.server, CopilotServer::Disabled) { let start_task = cx .spawn({ @@ -194,7 +194,7 @@ impl Copilot { }) .detach(); - if cx.global::().enable_copilot_integration { + if cx.global::().features.copilot { let start_task = cx .spawn({ let http = http.clone(); diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 8721829241..b326998616 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -50,15 +50,16 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CopilotButton::deploy_copilot_menu); cx.add_action( |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { - let language = action.language.to_owned(); - - let current_langauge = cx.global::().copilot_on(Some(&language)); + let language = action.language.clone(); + let show_copilot_suggestions = cx + .global::() + .show_copilot_suggestions(Some(&language)); SettingsFile::update(cx, move |file_contents| { file_contents.languages.insert( - language.to_owned(), + language, settings::EditorSettings { - copilot: Some((!current_langauge).into()), + show_copilot_suggestions: Some((!show_copilot_suggestions).into()), ..Default::default() }, ); @@ -67,10 +68,9 @@ pub fn init(cx: &mut AppContext) { ); cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| { - let copilot_on = cx.global::().copilot_on(None); - + let show_copilot_suggestions = cx.global::().show_copilot_suggestions(None); SettingsFile::update(cx, move |file_contents| { - file_contents.editor.copilot = Some((!copilot_on).into()) + file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }) }); } @@ -94,7 +94,7 @@ impl View for CopilotButton { fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { let settings = cx.global::(); - if !settings.enable_copilot_integration { + if !settings.features.copilot { return Empty::new().boxed(); } @@ -105,7 +105,9 @@ impl View for CopilotButton { }; let status = copilot.read(cx).status(); - let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + let enabled = self + .editor_enabled + .unwrap_or(settings.show_copilot_suggestions(None)); let view_id = cx.view_id(); @@ -248,7 +250,7 @@ impl CopilotButton { let mut menu_options = Vec::with_capacity(6); if let Some(language) = &self.language { - let language_enabled = settings.copilot_on(Some(language.as_ref())); + let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref())); menu_options.push(ContextMenuItem::item( format!( @@ -266,7 +268,7 @@ impl CopilotButton { )); } - let globally_enabled = cx.global::().copilot_on(None); + let globally_enabled = cx.global::().show_copilot_suggestions(None); menu_options.push(ContextMenuItem::item( if globally_enabled { "Disable Copilot Globally" @@ -319,7 +321,7 @@ impl CopilotButton { self.language = language_name.clone(); - self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); + self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref())); cx.notify() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3f3723bc1..2d39416858 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2816,7 +2816,10 @@ impl Editor { let snapshot = self.buffer.read(cx).snapshot(cx); let cursor = self.selections.newest_anchor().head(); let language_name = snapshot.language_at(cursor).map(|language| language.name()); - if !cx.global::().copilot_on(language_name.as_deref()) { + if !cx + .global::() + .show_copilot_suggestions(language_name.as_deref()) + { self.hide_copilot_suggestion(cx); return None; } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index feb4017018..6942a6e57b 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -28,11 +28,11 @@ pub use watched_json::watch_files; #[derive(Clone)] pub struct Settings { + pub features: Features, pub buffer_font_family_name: String, pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, - pub enable_copilot_integration: bool, pub buffer_font_size: f32, pub active_pane_magnification: f32, pub cursor_blink: bool, @@ -177,43 +177,7 @@ pub struct EditorSettings { pub ensure_final_newline_on_save: Option, pub formatter: Option, pub enable_language_server: Option, - #[schemars(skip)] - pub copilot: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum OnOff { - On, - Off, -} - -impl OnOff { - pub fn as_bool(&self) -> bool { - match self { - OnOff::On => true, - OnOff::Off => false, - } - } - - pub fn from_bool(value: bool) -> OnOff { - match value { - true => OnOff::On, - false => OnOff::Off, - } - } -} - -impl From for bool { - fn from(value: OnOff) -> bool { - value.as_bool() - } -} - -impl From for OnOff { - fn from(value: bool) -> OnOff { - OnOff::from_bool(value) - } + pub show_copilot_suggestions: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -437,8 +401,7 @@ pub struct SettingsFileContent { #[serde(default)] pub base_keymap: Option, #[serde(default)] - #[schemars(skip)] - pub enable_copilot_integration: Option, + pub features: FeaturesContent, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -447,6 +410,18 @@ pub struct LspSettings { pub initialization_options: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct Features { + pub copilot: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct FeaturesContent { + pub copilot: Option, +} + impl Settings { /// Fill out the settings corresponding to the default.json file, overrides will be set later pub fn defaults( @@ -500,7 +475,7 @@ impl Settings { format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), - copilot: required(defaults.editor.copilot), + show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), }, editor_overrides: Default::default(), git: defaults.git.unwrap(), @@ -517,7 +492,9 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), base_keymap: Default::default(), - enable_copilot_integration: defaults.enable_copilot_integration.unwrap(), + features: Features { + copilot: defaults.features.copilot.unwrap(), + }, } } @@ -569,10 +546,7 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); - merge( - &mut self.enable_copilot_integration, - data.enable_copilot_integration, - ); + merge(&mut self.features.copilot, data.features.copilot); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); @@ -596,12 +570,15 @@ impl Settings { self } - pub fn copilot_on(&self, language: Option<&str>) -> bool { - if self.enable_copilot_integration { - self.language_setting(language, |settings| settings.copilot.map(Into::into)) - } else { - false - } + pub fn features(&self) -> &Features { + &self.features + } + + pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool { + self.features.copilot + && self.language_setting(language, |settings| { + settings.show_copilot_suggestions.map(Into::into) + }) } pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { @@ -740,7 +717,7 @@ impl Settings { format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), - copilot: Some(OnOff::On), + show_copilot_suggestions: Some(true), }, editor_overrides: Default::default(), journal_defaults: Default::default(), @@ -760,7 +737,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: true, base_keymap: Default::default(), - enable_copilot_integration: true, + features: Features { copilot: true }, } } @@ -1125,7 +1102,7 @@ mod tests { { "language_overrides": { "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } @@ -1135,7 +1112,7 @@ mod tests { settings.languages.insert( "Rust".into(), EditorSettings { - copilot: Some(OnOff::On), + show_copilot_suggestions: Some(true), ..Default::default() }, ); @@ -1144,10 +1121,10 @@ mod tests { { "language_overrides": { "Rust": { - "copilot": "on" + "show_copilot_suggestions": true }, "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } @@ -1163,21 +1140,21 @@ mod tests { { "languages": { "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } "# .unindent(), |settings| { - settings.editor.copilot = Some(OnOff::On); + settings.editor.show_copilot_suggestions = Some(true); }, r#" { - "copilot": "on", + "show_copilot_suggestions": true, "languages": { "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } @@ -1187,13 +1164,13 @@ mod tests { } #[test] - fn test_update_langauge_copilot() { + fn test_update_language_copilot() { assert_new_settings( r#" { "languages": { "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } @@ -1203,7 +1180,7 @@ mod tests { settings.languages.insert( "Rust".into(), EditorSettings { - copilot: Some(OnOff::On), + show_copilot_suggestions: Some(true), ..Default::default() }, ); @@ -1212,10 +1189,10 @@ mod tests { { "languages": { "Rust": { - "copilot": "on" + "show_copilot_suggestions": true }, "JSON": { - "copilot": "off" + "show_copilot_suggestions": false } } } From 4a9989fe384106ec102feacb582f8ac145c9283c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 20:10:57 +0200 Subject: [PATCH 14/34] Clear all suggestions from `Editor` when disabling Copilot Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d39416858..45365b8381 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1265,7 +1265,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::(Self::on_settings_changed), + cx.observe_global::(Self::settings_changed), ], }; this.end_selection(cx); @@ -2808,7 +2808,7 @@ impl Editor { fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { let copilot = Copilot::global(cx)?; if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { - self.hide_copilot_suggestion(cx); + self.clear_copilot_suggestions(cx); return None; } self.update_visible_copilot_suggestion(cx); @@ -2820,7 +2820,7 @@ impl Editor { .global::() .show_copilot_suggestions(language_name.as_deref()) { - self.hide_copilot_suggestion(cx); + self.clear_copilot_suggestions(cx); return None; } @@ -2941,6 +2941,11 @@ impl Editor { } } + fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) { + self.copilot_state = Default::default(); + self.hide_copilot_suggestion(cx); + } + pub fn render_code_actions_indicator( &self, style: &EditorStyle, @@ -6494,7 +6499,7 @@ impl Editor { cx.notify(); } - fn on_settings_changed(&mut self, cx: &mut ViewContext) { + fn settings_changed(&mut self, cx: &mut ViewContext) { self.refresh_copilot_suggestions(cx); } From 54a78d702492482f9dc9789890d087cf6f1475cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 20:18:06 +0200 Subject: [PATCH 15/34] Clarify Copilot context menu Co-Authored-By: Nathan Sobo --- crates/copilot_button/src/copilot_button.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index b326998616..8d535cc96b 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -254,12 +254,8 @@ impl CopilotButton { menu_options.push(ContextMenuItem::item( format!( - "{} Copilot for {}", - if language_enabled { - "Disable" - } else { - "Enable" - }, + "{} Suggestions for {}", + if language_enabled { "Hide" } else { "Show" }, language ), ToggleCopilotForLanguage { @@ -271,9 +267,9 @@ impl CopilotButton { let globally_enabled = cx.global::().show_copilot_suggestions(None); menu_options.push(ContextMenuItem::item( if globally_enabled { - "Disable Copilot Globally" + "Hide Suggestions for All Files" } else { - "Enable Copilot Globally" + "Show Suggestions for All Files" }, ToggleCopilotGlobally, )); From 8610f3acf30f66ba73b0c6f0078f061bd4c1ced4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 20:25:59 +0200 Subject: [PATCH 16/34] Introduce a button to disable copilot integration Co-Authored-By: Nathan Sobo --- crates/copilot/src/sign_in.rs | 16 +++++++++++++++- styles/src/styleTree/copilot.ts | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index a65be325ce..43b6709636 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -5,7 +5,7 @@ use gpui::{ platform::{WindowBounds, WindowKind, WindowOptions}, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, }; -use settings::Settings; +use settings::{settings_file::SettingsFile, Settings}; use theme::ui::modal; #[derive(PartialEq, Eq, Debug, Clone)] @@ -199,6 +199,20 @@ impl CopilotCodeVerification { }, ) .boxed(), + theme::ui::cta_button_with_click( + "Disable Copilot Integration", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + move |_, cx| { + SettingsFile::update(cx, move |settings| { + settings.features.copilot = Some(false); + }); + } + }, + ) + .boxed(), ]) .align_children_center() .boxed() diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 9fa86cd741..b1dd17b3b3 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -113,7 +113,7 @@ export default function copilot(colorScheme: ColorScheme) { }, dimensions: { width: 280, - height: 280, + height: 320, }, }, From ea1c3fa7a0fb0beebe4e954bc975652728b4bb5d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 18 Apr 2023 14:53:01 -0700 Subject: [PATCH 17/34] Only fire completion cycling requests if specifically asked for by the user --- assets/keymaps/default.json | 2 +- crates/copilot/src/copilot.rs | 5 ++- crates/editor/src/editor.rs | 72 +++++++++++++++++++++++------------ 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 14e3056e6a..77094e53e3 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -177,7 +177,7 @@ "focus": false } ], - "alt-\\": "copilot::NextSuggestion", + "alt-\\": "copilot::Suggest", "alt-]": "copilot::NextSuggestion", "alt-[": "copilot::PreviousSuggestion" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index aa086775b3..084d7fcf7a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -29,7 +29,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; actions!(copilot_auth, [SignIn, SignOut]); const COPILOT_NAMESPACE: &'static str = "copilot"; -actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]); +actions!( + copilot, + [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] +); pub fn init(http: Arc, node_runtime: Arc, cx: &mut AppContext) { // Disable Copilot for stable releases. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45365b8381..dfc609fe8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -395,6 +395,7 @@ pub fn init(cx: &mut AppContext) { 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::copilot_suggest); hover_popover::init(cx); link_go_to_definition::init(cx); @@ -1016,6 +1017,7 @@ pub struct CopilotState { pending_refresh: Task>, completions: Vec, active_completion_index: usize, + cycled: bool, } impl Default for CopilotState { @@ -1025,6 +1027,7 @@ impl Default for CopilotState { pending_refresh: Task::ready(Some(())), completions: Default::default(), active_completion_index: 0, + cycled: false, } } } @@ -2026,13 +2029,13 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); if had_active_copilot_suggestion { - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, 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); + this.refresh_copilot_suggestions(false, cx); } }); } @@ -2114,7 +2117,7 @@ impl Editor { .collect(); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, cx); }); } @@ -2512,7 +2515,7 @@ impl Editor { }); } - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, cx); }); let project = self.project.clone()?; @@ -2805,7 +2808,11 @@ impl Editor { None } - fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { + fn refresh_copilot_suggestions( + &mut self, + cycling: bool, + cx: &mut ViewContext, + ) -> Option<()> { let copilot = Copilot::global(cx)?; if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { self.clear_copilot_suggestions(cx); @@ -2828,17 +2835,24 @@ impl Editor { 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), - copilot.completions_cycling(&buffer, buffer_position, cx), - ) - }); - let (completion, completions_cycling) = futures::join!(completion, completions_cycling); let mut completions = Vec::new(); - completions.extend(completion.log_err().into_iter().flatten()); - completions.extend(completions_cycling.log_err().into_iter().flatten()); + + let completions_iter = if cycling { + copilot + .update(&mut cx, |copilot, cx| { + copilot.completions(&buffer, buffer_position, cx) + }) + .await + } else { + copilot + .update(&mut cx, |copilot, cx| { + copilot.completions_cycling(&buffer, buffer_position, cx) + }) + .await + }; + completions.extend(completions_iter.log_err().into_iter().flatten()); + this.upgrade(&cx)?.update(&mut cx, |this, cx| { if !completions.is_empty() { this.copilot_state.completions.clear(); @@ -2847,6 +2861,7 @@ impl Editor { for completion in completions { this.copilot_state.push_completion(completion); } + this.copilot_state.cycled = cycling; this.update_visible_copilot_suggestion(cx); } }); @@ -2857,9 +2872,18 @@ impl Editor { Some(()) } - fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext) { if !self.has_active_copilot_suggestion(cx) { - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(false, cx); + return; + } + + self.update_visible_copilot_suggestion(cx); + } + + fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + if !self.has_active_copilot_suggestion(cx) || !self.copilot_state.cycled { + self.refresh_copilot_suggestions(true, cx); return; } @@ -2873,8 +2897,8 @@ impl Editor { _: &copilot::PreviousSuggestion, cx: &mut ViewContext, ) { - if !self.has_active_copilot_suggestion(cx) { - self.refresh_copilot_suggestions(cx); + if !self.has_active_copilot_suggestion(cx) || !self.copilot_state.cycled { + self.refresh_copilot_suggestions(true, cx); return; } @@ -3231,7 +3255,7 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.insert("", cx); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, cx); }); } @@ -3247,7 +3271,7 @@ impl Editor { }) }); this.insert("", cx); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, cx); }); } @@ -3343,7 +3367,7 @@ 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.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(false, cx); }); } @@ -4023,7 +4047,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(false, cx); cx.emit(Event::Edited); } } @@ -4038,7 +4062,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(false, cx); cx.emit(Event::Edited); } } @@ -6500,7 +6524,7 @@ impl Editor { } fn settings_changed(&mut self, cx: &mut ViewContext) { - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(false, cx); } pub fn set_searchable(&mut self, searchable: bool) { From 70ff4ca48f5455f7c73d984804054ba1af42d895 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 12:58:21 -0700 Subject: [PATCH 18/34] WIP: lower our usage of the copilot API by seperating out the cycling completion Restore copilot setting visibility co-authored-by: antonio --- crates/editor/src/editor.rs | 143 ++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 49 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dfc609fe8c..710d1c6958 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1015,15 +1015,17 @@ impl CodeActionsMenu { pub struct CopilotState { excerpt_id: Option, pending_refresh: Task>, + pending_cycling_refresh: Task>, + cycled: bool, completions: Vec, active_completion_index: usize, - cycled: bool, } impl Default for CopilotState { fn default() -> Self { Self { excerpt_id: None, + pending_cycling_refresh: Task::ready(Some(())), pending_refresh: Task::ready(Some(())), completions: Default::default(), active_completion_index: 0, @@ -1071,6 +1073,22 @@ impl CopilotState { } } + fn cycle_completions(&mut self, direction: Direction) { + match direction { + Direction::Prev => { + self.active_completion_index = if self.active_completion_index == 0 { + self.completions.len() - 1 + } else { + self.active_completion_index - 1 + }; + } + Direction::Next => { + self.active_completion_index = + (self.active_completion_index + 1) % self.completions.len(); + } + } + } + fn push_completion(&mut self, new_completion: copilot::Completion) { for completion in &self.completions { if *completion == new_completion { @@ -2029,13 +2047,13 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); if had_active_copilot_suggestion { - this.refresh_copilot_suggestions(false, cx); + 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(false, cx); + this.refresh_copilot_suggestions(cx); } }); } @@ -2117,7 +2135,7 @@ impl Editor { .collect(); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); - this.refresh_copilot_suggestions(false, cx); + this.refresh_copilot_suggestions(cx); }); } @@ -2515,7 +2533,7 @@ impl Editor { }); } - this.refresh_copilot_suggestions(false, cx); + this.refresh_copilot_suggestions(cx); }); let project = self.project.clone()?; @@ -2808,11 +2826,7 @@ impl Editor { None } - fn refresh_copilot_suggestions( - &mut self, - cycling: bool, - cx: &mut ViewContext, - ) -> Option<()> { + fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { let copilot = Copilot::global(cx)?; if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { self.clear_copilot_suggestions(cx); @@ -2838,30 +2852,31 @@ impl Editor { let mut completions = Vec::new(); - let completions_iter = if cycling { - copilot - .update(&mut cx, |copilot, cx| { - copilot.completions(&buffer, buffer_position, cx) - }) - .await - } else { - copilot - .update(&mut cx, |copilot, cx| { - copilot.completions_cycling(&buffer, buffer_position, cx) - }) - .await - }; + // let completions_iter = if cycling { + let completions_iter = copilot + .update(&mut cx, |copilot, cx| { + copilot.completions(&buffer, buffer_position, cx) + }) + .await; + // } else { + // copilot + // .update(&mut cx, |copilot, cx| { + // copilot.completions_cycling(&buffer, buffer_position, cx) + // }) + // .await + // }; completions.extend(completions_iter.log_err().into_iter().flatten()); this.upgrade(&cx)?.update(&mut cx, |this, cx| { if !completions.is_empty() { + this.copilot_state.cycled = false; + this.copilot_state.pending_cycling_refresh = Task::ready(None); this.copilot_state.completions.clear(); this.copilot_state.active_completion_index = 0; this.copilot_state.excerpt_id = Some(cursor.excerpt_id); for completion in completions { this.copilot_state.push_completion(completion); } - this.copilot_state.cycled = cycling; this.update_visible_copilot_suggestion(cx); } }); @@ -2872,9 +2887,49 @@ impl Editor { Some(()) } + fn cycle_suggestions( + &mut self, + direction: Direction, + cx: &mut ViewContext, + ) -> Option<()> { + let copilot = Copilot::global(cx)?; + if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { + return None; + } + + if self.copilot_state.cycled { + self.copilot_state.cycle_completions(direction); + self.update_visible_copilot_suggestion(cx); + } else { + let cursor = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move { + let completions = copilot + .update(&mut cx, |copilot, cx| { + copilot.completions_cycling(&buffer, buffer_position, cx) + }) + .await; + + this.upgrade(&cx)?.update(&mut cx, |this, cx| { + this.copilot_state.cycled = true; + for completion in completions.log_err().into_iter().flatten() { + this.copilot_state.push_completion(completion); + } + this.copilot_state.cycle_completions(direction); + this.update_visible_copilot_suggestion(cx); + }); + + Some(()) + }); + } + + Some(()) + } + fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext) { if !self.has_active_copilot_suggestion(cx) { - self.refresh_copilot_suggestions(false, cx); + self.refresh_copilot_suggestions(cx); return; } @@ -2882,14 +2937,11 @@ impl Editor { } fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { - if !self.has_active_copilot_suggestion(cx) || !self.copilot_state.cycled { - self.refresh_copilot_suggestions(true, cx); - return; + if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Next, cx); + } else { + self.refresh_copilot_suggestions(cx); } - - self.copilot_state.active_completion_index = - (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); - self.update_visible_copilot_suggestion(cx); } fn previous_copilot_suggestion( @@ -2897,18 +2949,11 @@ impl Editor { _: &copilot::PreviousSuggestion, cx: &mut ViewContext, ) { - if !self.has_active_copilot_suggestion(cx) || !self.copilot_state.cycled { - self.refresh_copilot_suggestions(true, cx); - return; + if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Prev, cx); + } else { + self.refresh_copilot_suggestions(cx); } - - self.copilot_state.active_completion_index = - if self.copilot_state.active_completion_index == 0 { - self.copilot_state.completions.len() - 1 - } else { - self.copilot_state.active_completion_index - 1 - }; - self.update_visible_copilot_suggestion(cx); } fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { @@ -3255,7 +3300,7 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.insert("", cx); - this.refresh_copilot_suggestions(false, cx); + this.refresh_copilot_suggestions(cx); }); } @@ -3271,7 +3316,7 @@ impl Editor { }) }); this.insert("", cx); - this.refresh_copilot_suggestions(false, cx); + this.refresh_copilot_suggestions(cx); }); } @@ -3367,7 +3412,7 @@ 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.refresh_copilot_suggestions(false, cx); + this.refresh_copilot_suggestions(cx); }); } @@ -4047,7 +4092,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(false, cx); + self.refresh_copilot_suggestions(cx); cx.emit(Event::Edited); } } @@ -4062,7 +4107,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(false, cx); + self.refresh_copilot_suggestions(cx); cx.emit(Event::Edited); } } @@ -6524,7 +6569,7 @@ impl Editor { } fn settings_changed(&mut self, cx: &mut ViewContext) { - self.refresh_copilot_suggestions(false, cx); + self.refresh_copilot_suggestions(cx); } pub fn set_searchable(&mut self, searchable: bool) { From 745e5e3a09059b13d6e534b5e55f62b33b97122e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 13:54:43 -0700 Subject: [PATCH 19/34] Add italic styles to copilot suggestions --- styles/src/styleTree/editor.ts | 4 +--- styles/src/themes/common/syntax.ts | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 0c2435ed5a..84ef51406e 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) { activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), // Inline autocomplete suggestions, Co-pilot suggestions, etc. - suggestion: { - color: syntax.predictive.color, - }, + suggestion: syntax.predictive, codeActions: { indicator: { color: foreground(layer, "variant"), diff --git a/styles/src/themes/common/syntax.ts b/styles/src/themes/common/syntax.ts index 50c3e4e3b7..925ed7e5c1 100644 --- a/styles/src/themes/common/syntax.ts +++ b/styles/src/themes/common/syntax.ts @@ -181,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { }, predictive: { color: color.predictive, + italic: true, }, emphasis: { color: color.emphasis, From 2882e0fa5b7ac604d8f2f0abae27ecbecb04c674 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 16:39:55 -0700 Subject: [PATCH 20/34] Remove new CTA in copilot sign in UI Add a trim_end to copilot suggestions --- crates/copilot/src/sign_in.rs | 14 -------------- crates/editor/src/editor.rs | 4 ++-- styles/src/styleTree/copilot.ts | 2 +- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 43b6709636..4378c90c88 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -199,20 +199,6 @@ impl CopilotCodeVerification { }, ) .boxed(), - theme::ui::cta_button_with_click( - "Disable Copilot Integration", - style.auth.content_width, - &style.auth.cta_button, - cx, - { - move |_, cx| { - SettingsFile::update(cx, move |settings| { - settings.features.copilot = Some(false); - }); - } - }, - ) - .boxed(), ]) .align_children_center() .boxed() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 710d1c6958..cba98c73c5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2995,11 +2995,11 @@ impl Editor { .copilot_state .text_for_active_completion(cursor, &snapshot) { - self.display_map.update(cx, |map, cx| { + self.display_map.update(cx, move |map, cx| { map.replace_suggestion( Some(Suggestion { position: cursor, - text: text.into(), + text: text.trim_end().into(), }), cx, ) diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index b1dd17b3b3..9fa86cd741 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -113,7 +113,7 @@ export default function copilot(colorScheme: ColorScheme) { }, dimensions: { width: 280, - height: 320, + height: 280, }, }, From 9b8a3e4de5d75c31aa92beb4e1fb3625d72f1619 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 16:50:31 -0700 Subject: [PATCH 21/34] Fixed panic in new cycling code Made cycling fire without debounce --- crates/copilot/src/sign_in.rs | 2 +- crates/editor/src/editor.rs | 47 ++++++++++++++++------------------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 4378c90c88..a65be325ce 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -5,7 +5,7 @@ use gpui::{ platform::{WindowBounds, WindowKind, WindowOptions}, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, }; -use settings::{settings_file::SettingsFile, Settings}; +use settings::Settings; use theme::ui::modal; #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cba98c73c5..de47fbd4ed 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1083,8 +1083,12 @@ impl CopilotState { }; } Direction::Next => { - self.active_completion_index = - (self.active_completion_index + 1) % self.completions.len(); + if self.completions.len() == 0 { + self.active_completion_index = 0 + } else { + self.active_completion_index = + (self.active_completion_index + 1) % self.completions.len(); + } } } } @@ -2850,22 +2854,15 @@ impl Editor { self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await; - let mut completions = Vec::new(); - - // let completions_iter = if cycling { - let completions_iter = copilot + let completions = copilot .update(&mut cx, |copilot, cx| { copilot.completions(&buffer, buffer_position, cx) }) - .await; - // } else { - // copilot - // .update(&mut cx, |copilot, cx| { - // copilot.completions_cycling(&buffer, buffer_position, cx) - // }) - // .await - // }; - completions.extend(completions_iter.log_err().into_iter().flatten()); + .await + .log_err() + .into_iter() + .flatten() + .collect_vec(); this.upgrade(&cx)?.update(&mut cx, |this, cx| { if !completions.is_empty() { @@ -2937,11 +2934,11 @@ impl Editor { } fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { - if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Next, cx); - } else { - self.refresh_copilot_suggestions(cx); - } + // if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Next, cx); + // } else { + // self.refresh_copilot_suggestions(cx); + // } } fn previous_copilot_suggestion( @@ -2949,11 +2946,11 @@ impl Editor { _: &copilot::PreviousSuggestion, cx: &mut ViewContext, ) { - if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Prev, cx); - } else { - self.refresh_copilot_suggestions(cx); - } + // if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Prev, cx); + // } else { + // self.refresh_copilot_suggestions(cx); + // } } fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { From f16b96cafcd24e98f5e86bb9baa3b495342e0487 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 17:27:44 -0700 Subject: [PATCH 22/34] add copilot menu --- crates/copilot/src/copilot.rs | 4 +- crates/copilot/src/sign_in.rs | 47 +++++++- crates/copilot_button/src/copilot_button.rs | 124 ++++++++++++++------ 3 files changed, 130 insertions(+), 45 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 084d7fcf7a..1967c3cd14 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -202,7 +202,9 @@ impl Copilot { .spawn({ let http = http.clone(); let node_runtime = node_runtime.clone(); - move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + move |this, cx| async { + Self::start_language_server(http, node_runtime, this, cx).await + } }) .shared(); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index a65be325ce..dc09ddf3f2 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -2,12 +2,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ elements::*, geometry::rect::RectF, + impl_internal_actions, platform::{WindowBounds, WindowKind, WindowOptions}, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, }; use settings::Settings; use theme::ui::modal; +#[derive(PartialEq, Eq, Debug, Clone)] +struct ClickedConnect; + +impl_internal_actions!(copilot_verification, [ClickedConnect]); + #[derive(PartialEq, Eq, Debug, Clone)] struct CopyUserCode; @@ -56,6 +62,12 @@ pub fn init(cx: &mut AppContext) { } }) .detach(); + + cx.add_action( + |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| { + code_verification.connect_clicked = true; + }, + ); } fn create_copilot_auth_window( @@ -81,11 +93,15 @@ fn create_copilot_auth_window( pub struct CopilotCodeVerification { status: Status, + connect_clicked: bool, } impl CopilotCodeVerification { pub fn new(status: Status) -> Self { - Self { status } + Self { + status, + connect_clicked: false, + } } pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { @@ -143,6 +159,7 @@ impl CopilotCodeVerification { } fn render_prompting_modal( + connect_clicked: bool, data: &PromptUserDeviceFlow, style: &theme::Copilot, cx: &mut gpui::RenderContext, @@ -189,13 +206,20 @@ impl CopilotCodeVerification { .with_style(style.auth.prompting.hint.container.clone()) .boxed(), theme::ui::cta_button_with_click( - "Connect to GitHub", + if connect_clicked { + "Waiting for connection..." + } else { + "Connect to GitHub" + }, style.auth.content_width, &style.auth.cta_button, cx, { let verification_uri = data.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) + move |_, cx| { + cx.platform().open_url(&verification_uri); + cx.dispatch_action(ClickedConnect) + } }, ) .boxed(), @@ -343,9 +367,20 @@ impl View for CopilotCodeVerification { match &self.status { Status::SigningIn { prompt: Some(prompt), - } => Self::render_prompting_modal(&prompt, &style.copilot, cx), - Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), - Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), + } => Self::render_prompting_modal( + self.connect_clicked, + &prompt, + &style.copilot, + cx, + ), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal(&style.copilot, cx) + } + Status::Authorized => { + self.connect_clicked = false; + Self::render_enabled_modal(&style.copilot, cx) + } _ => Empty::new().boxed(), }, ]) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 8d535cc96b..b7e93536b9 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338; #[derive(Clone, PartialEq)] pub struct DeployCopilotMenu; +#[derive(Clone, PartialEq)] +pub struct DeployCopilotStartMenu; + +#[derive(Clone, PartialEq)] +pub struct HideCopilot; + +#[derive(Clone, PartialEq)] +pub struct InitiateSignIn; + #[derive(Clone, PartialEq)] pub struct ToggleCopilotForLanguage { language: Arc, @@ -40,6 +49,9 @@ impl_internal_actions!( copilot, [ DeployCopilotMenu, + DeployCopilotStartMenu, + HideCopilot, + InitiateSignIn, DeployCopilotModal, ToggleCopilotForLanguage, ToggleCopilotGlobally, @@ -48,6 +60,7 @@ impl_internal_actions!( pub fn init(cx: &mut AppContext) { cx.add_action(CopilotButton::deploy_copilot_menu); + cx.add_action(CopilotButton::deploy_copilot_start_menu); cx.add_action( |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { let language = action.language.clone(); @@ -73,6 +86,58 @@ pub fn init(cx: &mut AppContext) { file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }) }); + + cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| { + SettingsFile::update(cx, move |file_contents| { + file_contents.features.copilot = Some(false) + }) + }); + + cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| { + let Some(copilot) = Copilot::global(cx) else { + return; + }; + let status = copilot.read(cx).status(); + + match status { + Status::Starting { task } => { + cx.dispatch_action(workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot is starting...", + )); + let window_id = cx.window_id(); + let task = task.to_owned(); + cx.spawn(|handle, mut cx| async move { + task.await; + cx.update(|cx| { + if let Some(copilot) = Copilot::global(cx) { + let status = copilot.read(cx).status(); + match status { + Status::Authorized => cx.dispatch_action_at( + window_id, + handle.id(), + workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot has started!", + ), + ), + _ => { + cx.dispatch_action_at( + window_id, + handle.id(), + DismissToast::new(COPILOT_STARTING_TOAST_ID), + ); + cx.dispatch_action_at(window_id, handle.id(), SignIn) + } + } + } + }) + }) + .detach(); + } + _ => cx.dispatch_action(SignIn), + } + }) } pub struct CopilotButton { @@ -109,8 +174,6 @@ impl View for CopilotButton { .editor_enabled .unwrap_or(settings.show_copilot_suggestions(None)); - let view_id = cx.view_id(); - Stack::new() .with_child( MouseEventHandler::::new(0, cx, { @@ -157,48 +220,13 @@ impl View for CopilotButton { let status = status.clone(); move |_, cx| match status { Status::Authorized => cx.dispatch_action(DeployCopilotMenu), - Status::Starting { ref task } => { - cx.dispatch_action(workspace::Toast::new( - COPILOT_STARTING_TOAST_ID, - "Copilot is starting...", - )); - let window_id = cx.window_id(); - let task = task.to_owned(); - cx.spawn(|mut cx| async move { - task.await; - cx.update(|cx| { - if let Some(copilot) = Copilot::global(cx) { - let status = copilot.read(cx).status(); - match status { - Status::Authorized => cx.dispatch_action_at( - window_id, - view_id, - workspace::Toast::new( - COPILOT_STARTING_TOAST_ID, - "Copilot has started!", - ), - ), - _ => { - cx.dispatch_action_at( - window_id, - view_id, - DismissToast::new(COPILOT_STARTING_TOAST_ID), - ); - cx.dispatch_global_action(SignIn) - } - } - } - }) - }) - .detach(); - } Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action( COPILOT_ERROR_TOAST_ID, format!("Copilot can't be started: {}", e), "Reinstall Copilot", Reinstall, )), - _ => cx.dispatch_action(SignIn), + _ => cx.dispatch_action(DeployCopilotStartMenu), } }) .with_tooltip::( @@ -244,6 +272,26 @@ impl CopilotButton { } } + pub fn deploy_copilot_start_menu( + &mut self, + _: &DeployCopilotStartMenu, + cx: &mut ViewContext, + ) { + let mut menu_options = Vec::with_capacity(2); + + menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn)); + menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot)); + + self.popup_menu.update(cx, |menu, cx| { + menu.show( + Default::default(), + AnchorCorner::BottomRight, + menu_options, + cx, + ); + }); + } + pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { let settings = cx.global::(); From 26ab774b7faac4cf7129a65bfdabb73c7207e68c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 17:34:09 -0700 Subject: [PATCH 23/34] Removed debounce on suggestion cycling code --- crates/editor/src/editor.rs | 52 +++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index de47fbd4ed..308f0079d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2051,13 +2051,13 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); if had_active_copilot_suggestion { - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, 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); + this.refresh_copilot_suggestions(true, cx); } }); } @@ -2139,7 +2139,7 @@ impl Editor { .collect(); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, cx); }); } @@ -2537,7 +2537,7 @@ impl Editor { }); } - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, cx); }); let project = self.project.clone()?; @@ -2830,7 +2830,11 @@ impl Editor { None } - fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { + fn refresh_copilot_suggestions( + &mut self, + debounce: bool, + cx: &mut ViewContext, + ) -> Option<()> { let copilot = Copilot::global(cx)?; if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { self.clear_copilot_suggestions(cx); @@ -2852,7 +2856,9 @@ impl Editor { 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; + if debounce { + cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await; + } let completions = copilot .update(&mut cx, |copilot, cx| { @@ -2926,7 +2932,7 @@ impl Editor { fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext) { if !self.has_active_copilot_suggestion(cx) { - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(false, cx); return; } @@ -2934,11 +2940,11 @@ impl Editor { } fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { - // if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Next, cx); - // } else { - // self.refresh_copilot_suggestions(cx); - // } + if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Next, cx); + } else { + self.refresh_copilot_suggestions(false, cx); + } } fn previous_copilot_suggestion( @@ -2946,11 +2952,11 @@ impl Editor { _: &copilot::PreviousSuggestion, cx: &mut ViewContext, ) { - // if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Prev, cx); - // } else { - // self.refresh_copilot_suggestions(cx); - // } + if self.has_active_copilot_suggestion(cx) { + self.cycle_suggestions(Direction::Prev, cx); + } else { + self.refresh_copilot_suggestions(false, cx); + } } fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { @@ -3297,7 +3303,7 @@ impl Editor { this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.insert("", cx); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, cx); }); } @@ -3313,7 +3319,7 @@ impl Editor { }) }); this.insert("", cx); - this.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, cx); }); } @@ -3409,7 +3415,7 @@ 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.refresh_copilot_suggestions(cx); + this.refresh_copilot_suggestions(true, cx); }); } @@ -4089,7 +4095,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(true, cx); cx.emit(Event::Edited); } } @@ -4104,7 +4110,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(true, cx); cx.emit(Event::Edited); } } @@ -6566,7 +6572,7 @@ impl Editor { } fn settings_changed(&mut self, cx: &mut ViewContext) { - self.refresh_copilot_suggestions(cx); + self.refresh_copilot_suggestions(true, cx); } pub fn set_searchable(&mut self, searchable: bool) { From ce8442a3d84a3716e4f37b7f933a1e55c43c1c93 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Apr 2023 17:42:19 -0700 Subject: [PATCH 24/34] Fix underflow potential --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f6fc89de2f..742541293e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1079,7 +1079,7 @@ impl CopilotState { match direction { Direction::Prev => { self.active_completion_index = if self.active_completion_index == 0 { - self.completions.len() - 1 + self.completions.len().saturating_sub(1) } else { self.active_completion_index - 1 }; From 672cf6b8c789a9882effab185ad120e6adff42ca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 12:19:24 +0200 Subject: [PATCH 25/34] Relay buffer change events to Copilot --- Cargo.lock | 1 + crates/copilot/src/copilot.rs | 338 ++++++++++++++++++++++++---------- crates/copilot/src/request.rs | 3 - crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 24 +++ 5 files changed, 268 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f05512b76..bb931853fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4687,6 +4687,7 @@ dependencies = [ "client", "clock", "collections", + "copilot", "ctor", "db", "env_logger", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 1967c3cd14..57abd08939 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -6,8 +6,13 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use collections::HashMap; use futures::{future::Shared, Future, FutureExt, TryFutureExt}; -use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; -use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16}; +use gpui::{ + actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, +}; +use language::{ + point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, + ToPointUtf16, +}; use log::{debug, error}; use lsp::LanguageServer; use node_runtime::NodeRuntime; @@ -105,7 +110,7 @@ enum CopilotServer { Started { server: Arc, status: SignInStatus, - subscriptions_by_buffer_id: HashMap, + registered_buffers: HashMap, }, } @@ -141,6 +146,66 @@ impl Status { } } +struct RegisteredBuffer { + uri: lsp::Url, + snapshot: Option<(i32, BufferSnapshot)>, + _subscriptions: [gpui::Subscription; 2], +} + +impl RegisteredBuffer { + fn report_changes( + &mut self, + buffer: &ModelHandle, + server: &LanguageServer, + cx: &AppContext, + ) -> Result<(i32, BufferSnapshot)> { + let buffer = buffer.read(cx); + let (version, prev_snapshot) = self + .snapshot + .as_ref() + .ok_or_else(|| anyhow!("expected at least one snapshot"))?; + let next_snapshot = buffer.snapshot(); + + let content_changes = buffer + .edits_since::<(PointUtf16, usize)>(prev_snapshot.version()) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = next_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + point_to_lsp(edit_start), + point_to_lsp(edit_end), + )), + range_length: None, + text: new_text, + } + }) + .collect::>(); + + if content_changes.is_empty() { + Ok((*version, prev_snapshot.clone())) + } else { + let next_version = version + 1; + self.snapshot = Some((next_version, next_snapshot.clone())); + + server.notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + self.uri.clone(), + next_version, + ), + content_changes, + }, + )?; + + Ok((next_version, next_snapshot)) + } + } +} + #[derive(Debug, PartialEq, Eq)] pub struct Completion { pub range: Range, @@ -151,6 +216,7 @@ pub struct Copilot { http: Arc, node_runtime: Arc, server: CopilotServer, + buffers: HashMap>, } impl Entity for Copilot { @@ -212,12 +278,14 @@ impl Copilot { http, node_runtime, server: CopilotServer::Starting { task: start_task }, + buffers: Default::default(), } } else { Self { http, node_runtime, server: CopilotServer::Disabled, + buffers: Default::default(), } } } @@ -233,8 +301,9 @@ impl Copilot { server: CopilotServer::Started { server: Arc::new(server), status: SignInStatus::Authorized, - subscriptions_by_buffer_id: Default::default(), + registered_buffers: Default::default(), }, + buffers: Default::default(), }); (this, fake_server) } @@ -297,7 +366,7 @@ impl Copilot { this.server = CopilotServer::Started { server, status: SignInStatus::SignedOut, - subscriptions_by_buffer_id: Default::default(), + registered_buffers: Default::default(), }; this.update_sign_in_status(status, cx); } @@ -396,10 +465,8 @@ impl Copilot { } fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { - if let CopilotServer::Started { server, status, .. } = &mut self.server { - *status = SignInStatus::SignedOut; - cx.notify(); - + self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); + if let CopilotServer::Started { server, .. } = &self.server { let server = server.clone(); cx.background().spawn(async move { server @@ -433,6 +500,108 @@ impl Copilot { cx.foreground().spawn(start_task) } + pub fn register_buffer(&mut self, buffer: &ModelHandle, cx: &mut ModelContext) { + let buffer_id = buffer.id(); + self.buffers.insert(buffer_id, buffer.downgrade()); + + if let CopilotServer::Started { + server, + status, + registered_buffers, + .. + } = &mut self.server + { + if !matches!(status, SignInStatus::Authorized { .. }) { + return; + } + + let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap(); + registered_buffers.entry(buffer.id()).or_insert_with(|| { + let snapshot = buffer.read(cx).snapshot(); + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem { + uri: uri.clone(), + language_id: id_for_language(buffer.read(cx).language()), + version: 0, + text: snapshot.text(), + }, + }, + ) + .log_err(); + + RegisteredBuffer { + uri, + snapshot: Some((0, snapshot)), + _subscriptions: [ + cx.subscribe(buffer, |this, buffer, event, cx| { + this.handle_buffer_event(buffer, event, cx).log_err(); + }), + cx.observe_release(buffer, move |this, _buffer, _cx| { + this.buffers.remove(&buffer_id); + this.unregister_buffer(buffer_id); + }), + ], + } + }); + } + } + + fn handle_buffer_event( + &mut self, + buffer: ModelHandle, + event: &language::Event, + cx: &mut ModelContext, + ) -> Result<()> { + if let CopilotServer::Started { + server, + registered_buffers, + .. + } = &mut self.server + { + if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) { + match event { + language::Event::Edited => { + registered_buffer.report_changes(&buffer, server, cx)?; + } + language::Event::Saved => { + server.notify::( + lsp::DidSaveTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + registered_buffer.uri.clone(), + ), + text: None, + }, + )?; + } + _ => {} + } + } + } + + Ok(()) + } + + fn unregister_buffer(&mut self, buffer_id: usize) { + if let CopilotServer::Started { + server, + registered_buffers, + .. + } = &mut self.server + { + if let Some(buffer) = registered_buffers.remove(&buffer_id) { + server + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer.uri), + }, + ) + .log_err(); + } + } + } + pub fn completions( &mut self, buffer: &ModelHandle, @@ -464,16 +633,14 @@ impl Copilot { cx: &mut ModelContext, ) -> Task>> where - R: lsp::request::Request< - Params = request::GetCompletionsParams, - Result = request::GetCompletionsResult, - >, + R: 'static + + lsp::request::Request< + Params = request::GetCompletionsParams, + Result = request::GetCompletionsResult, + >, T: ToPointUtf16, { - let buffer_id = buffer.id(); - let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap(); - let snapshot = buffer.read(cx).snapshot(); - let server = match &mut self.server { + let (server, registered_buffer) = match &mut self.server { CopilotServer::Starting { .. } => { return Task::ready(Err(anyhow!("copilot is still starting"))) } @@ -487,56 +654,28 @@ impl Copilot { CopilotServer::Started { server, status, - subscriptions_by_buffer_id, + registered_buffers, + .. } => { if matches!(status, SignInStatus::Authorized { .. }) { - subscriptions_by_buffer_id - .entry(buffer_id) - .or_insert_with(|| { - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem { - uri: uri.clone(), - language_id: id_for_language( - buffer.read(cx).language(), - ), - version: 0, - text: snapshot.text(), - }, - }, - ) - .log_err(); - - let uri = uri.clone(); - cx.observe_release(buffer, move |this, _, _| { - if let CopilotServer::Started { - server, - subscriptions_by_buffer_id, - .. - } = &mut this.server - { - server - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - uri.clone(), - ), - }, - ) - .log_err(); - subscriptions_by_buffer_id.remove(&buffer_id); - } - }) - }); - - server.clone() + if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) { + (server.clone(), registered_buffer) + } else { + return Task::ready(Err(anyhow!( + "requested completions for an unregistered buffer" + ))); + } } else { return Task::ready(Err(anyhow!("must sign in before using copilot"))); } } }; + let (version, snapshot) = match registered_buffer.report_changes(buffer, &server, cx) { + Ok((version, snapshot)) => (version, snapshot), + Err(error) => return Task::ready(Err(error)), + }; + let uri = registered_buffer.uri.clone(); let settings = cx.global::(); let position = position.to_point_utf16(&snapshot); let language = snapshot.language_at(position); @@ -544,39 +683,23 @@ impl Copilot { let language_name = language_name.as_deref(); let tab_size = settings.tab_size(language_name); let hard_tabs = settings.hard_tabs(language_name); - let language_id = id_for_language(language); - - let path; - let relative_path; - if let Some(file) = snapshot.file() { - if let Some(file) = file.as_local() { - path = file.abs_path(cx); - } else { - path = file.full_path(cx); - } - relative_path = file.path().to_path_buf(); - } else { - path = PathBuf::new(); - relative_path = PathBuf::new(); - } - + let relative_path = snapshot + .file() + .map(|file| file.path().to_path_buf()) + .unwrap_or_default(); + let request = server.request::(request::GetCompletionsParams { + doc: request::GetCompletionsDocument { + uri, + tab_size: tab_size.into(), + indent_size: 1, + insert_spaces: !hard_tabs, + relative_path: relative_path.to_string_lossy().into(), + position: point_to_lsp(position), + version: version.try_into().unwrap(), + }, + }); cx.background().spawn(async move { - let result = server - .request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - source: snapshot.text(), - tab_size: tab_size.into(), - indent_size: 1, - insert_spaces: !hard_tabs, - uri, - path: path.to_string_lossy().into(), - relative_path: relative_path.to_string_lossy().into(), - language_id, - position: point_to_lsp(position), - version: 0, - }, - }) - .await?; + let result = request.await?; let completions = result .completions .into_iter() @@ -616,14 +739,37 @@ impl Copilot { lsp_status: request::SignInStatus, cx: &mut ModelContext, ) { + self.buffers.retain(|_, buffer| buffer.is_upgradable(cx)); + if let CopilotServer::Started { status, .. } = &mut self.server { - *status = match lsp_status { + match lsp_status { request::SignInStatus::Ok { .. } | request::SignInStatus::MaybeOk { .. } - | request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized, - request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized, - request::SignInStatus::NotSignedIn => SignInStatus::SignedOut, - }; + | request::SignInStatus::AlreadySignedIn { .. } => { + *status = SignInStatus::Authorized; + + for buffer in self.buffers.values().cloned().collect::>() { + if let Some(buffer) = buffer.upgrade(cx) { + self.register_buffer(&buffer, cx); + } + } + } + request::SignInStatus::NotAuthorized { .. } => { + *status = SignInStatus::Unauthorized; + + for buffer_id in self.buffers.keys().copied().collect::>() { + self.unregister_buffer(buffer_id); + } + } + request::SignInStatus::NotSignedIn => { + *status = SignInStatus::SignedOut; + + for buffer_id in self.buffers.keys().copied().collect::>() { + self.unregister_buffer(buffer_id); + } + } + } + cx.notify(); } } diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 415f160ea3..08173c413a 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -99,14 +99,11 @@ pub struct GetCompletionsParams { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionsDocument { - pub source: String, pub tab_size: u32, pub indent_size: u32, pub insert_spaces: bool, pub uri: lsp::Url, - pub path: String, pub relative_path: String, - pub language_id: String, pub position: lsp::Position, pub version: usize, } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f5c144a3ad..e30ab56e45 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ [dependencies] text = { path = "../text" } +copilot = { path = "../copilot" } client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d126cb4994..d5b7ac3f3f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use copilot::Copilot; use futures::{ channel::mpsc::{self, UnboundedReceiver}, future::{try_join_all, Shared}, @@ -129,6 +130,7 @@ pub struct Project { _maintain_buffer_languages: Task<()>, _maintain_workspace_config: Task<()>, terminals: Terminals, + copilot_enabled: bool, } enum BufferMessage { @@ -472,6 +474,7 @@ impl Project { terminals: Terminals { local_handles: Vec::new(), }, + copilot_enabled: Copilot::global(cx).is_some(), } }) } @@ -559,6 +562,7 @@ impl Project { terminals: Terminals { local_handles: Vec::new(), }, + copilot_enabled: Copilot::global(cx).is_some(), }; for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); @@ -664,6 +668,15 @@ impl Project { self.start_language_server(worktree_id, worktree_path, language, cx); } + if !self.copilot_enabled && Copilot::global(cx).is_some() { + self.copilot_enabled = true; + for buffer in self.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade(cx) { + self.register_buffer_with_copilot(&buffer, cx); + } + } + } + cx.notify(); } @@ -1616,6 +1629,7 @@ impl Project { self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_server(buffer, cx); + self.register_buffer_with_copilot(buffer, cx); cx.observe_release(buffer, |this, buffer, cx| { if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { @@ -1731,6 +1745,16 @@ impl Project { }); } + fn register_buffer_with_copilot( + &self, + buffer_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(copilot) = Copilot::global(cx) { + copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx)); + } + } + async fn send_buffer_messages( this: WeakModelHandle, rx: UnboundedReceiver, From 34bcf6f07263825da81ed762611856a002d2617d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 14:27:26 +0200 Subject: [PATCH 26/34] Reopen file in Copilot language server when language or URI changes --- crates/copilot/src/copilot.rs | 85 +++++++++++++++++++++---------- crates/editor/src/editor.rs | 1 + crates/editor/src/multi_buffer.rs | 2 + crates/language/src/buffer.rs | 2 + 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 57abd08939..ebe139c1cf 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,6 +21,7 @@ use settings::Settings; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ ffi::OsString, + mem, ops::Range, path::{Path, PathBuf}, sync::Arc, @@ -148,7 +149,9 @@ impl Status { struct RegisteredBuffer { uri: lsp::Url, - snapshot: Option<(i32, BufferSnapshot)>, + language_id: String, + snapshot: BufferSnapshot, + snapshot_version: i32, _subscriptions: [gpui::Subscription; 2], } @@ -158,20 +161,15 @@ impl RegisteredBuffer { buffer: &ModelHandle, server: &LanguageServer, cx: &AppContext, - ) -> Result<(i32, BufferSnapshot)> { + ) -> Result<()> { let buffer = buffer.read(cx); - let (version, prev_snapshot) = self - .snapshot - .as_ref() - .ok_or_else(|| anyhow!("expected at least one snapshot"))?; - let next_snapshot = buffer.snapshot(); - + let new_snapshot = buffer.snapshot(); let content_changes = buffer - .edits_since::<(PointUtf16, usize)>(prev_snapshot.version()) + .edits_since::<(PointUtf16, usize)>(self.snapshot.version()) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = next_snapshot + let new_text = new_snapshot .text_for_range(edit.new.start.1..edit.new.end.1) .collect(); lsp::TextDocumentContentChangeEvent { @@ -185,24 +183,21 @@ impl RegisteredBuffer { }) .collect::>(); - if content_changes.is_empty() { - Ok((*version, prev_snapshot.clone())) - } else { - let next_version = version + 1; - self.snapshot = Some((next_version, next_snapshot.clone())); - + if !content_changes.is_empty() { + self.snapshot_version += 1; + self.snapshot = new_snapshot; server.notify::( lsp::DidChangeTextDocumentParams { text_document: lsp::VersionedTextDocumentIdentifier::new( self.uri.clone(), - next_version, + self.snapshot_version, ), content_changes, }, )?; - - Ok((next_version, next_snapshot)) } + + Ok(()) } } @@ -515,15 +510,16 @@ impl Copilot { return; } - let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap(); registered_buffers.entry(buffer.id()).or_insert_with(|| { + let uri: lsp::Url = uri_for_buffer(buffer, cx); + let language_id = id_for_language(buffer.read(cx).language()); let snapshot = buffer.read(cx).snapshot(); server .notify::( lsp::DidOpenTextDocumentParams { text_document: lsp::TextDocumentItem { uri: uri.clone(), - language_id: id_for_language(buffer.read(cx).language()), + language_id: language_id.clone(), version: 0, text: snapshot.text(), }, @@ -533,7 +529,9 @@ impl Copilot { RegisteredBuffer { uri, - snapshot: Some((0, snapshot)), + language_id, + snapshot, + snapshot_version: 0, _subscriptions: [ cx.subscribe(buffer, |this, buffer, event, cx| { this.handle_buffer_event(buffer, event, cx).log_err(); @@ -575,6 +573,31 @@ impl Copilot { }, )?; } + language::Event::FileHandleChanged | language::Event::LanguageChanged => { + let new_language_id = id_for_language(buffer.read(cx).language()); + let new_uri = uri_for_buffer(&buffer, cx); + if new_uri != registered_buffer.uri + || new_language_id != registered_buffer.language_id + { + let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); + registered_buffer.language_id = new_language_id; + server.notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), + }, + )?; + server.notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + registered_buffer.uri.clone(), + registered_buffer.language_id.clone(), + registered_buffer.snapshot_version, + registered_buffer.snapshot.text(), + ), + }, + )?; + } + } _ => {} } } @@ -659,6 +682,10 @@ impl Copilot { } => { if matches!(status, SignInStatus::Authorized { .. }) { if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) { + if let Err(error) = registered_buffer.report_changes(buffer, &server, cx) { + return Task::ready(Err(error)); + } + (server.clone(), registered_buffer) } else { return Task::ready(Err(anyhow!( @@ -671,11 +698,9 @@ impl Copilot { } }; - let (version, snapshot) = match registered_buffer.report_changes(buffer, &server, cx) { - Ok((version, snapshot)) => (version, snapshot), - Err(error) => return Task::ready(Err(error)), - }; let uri = registered_buffer.uri.clone(); + let snapshot = registered_buffer.snapshot.clone(); + let version = registered_buffer.snapshot_version; let settings = cx.global::(); let position = position.to_point_utf16(&snapshot); let language = snapshot.language_at(position); @@ -784,6 +809,14 @@ fn id_for_language(language: Option<&Arc>) -> String { } } +fn uri_for_buffer(buffer: &ModelHandle, cx: &AppContext) -> lsp::Url { + if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { + lsp::Url::from_file_path(file.abs_path(cx)).unwrap() + } else { + format!("buffer://{}", buffer.id()).parse().unwrap() + } +} + async fn clear_copilot_dir() { remove_matching(&paths::COPILOT_DIR, |_| true).await } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 742541293e..83b92cc06c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6643,6 +6643,7 @@ impl Editor { multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } + multi_buffer::Event::LanguageChanged => {} } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index f8a56557ab..824c108e46 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -64,6 +64,7 @@ pub enum Event { }, Edited, Reloaded, + LanguageChanged, Reparsed, Saved, FileHandleChanged, @@ -1302,6 +1303,7 @@ impl MultiBuffer { language::Event::Saved => Event::Saved, language::Event::FileHandleChanged => Event::FileHandleChanged, language::Event::Reloaded => Event::Reloaded, + language::Event::LanguageChanged => Event::LanguageChanged, language::Event::Reparsed => Event::Reparsed, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, language::Event::Closed => Event::Closed, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fb16d41640..7325ca9af5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -187,6 +187,7 @@ pub enum Event { Saved, FileHandleChanged, Reloaded, + LanguageChanged, Reparsed, DiagnosticsUpdated, Closed, @@ -536,6 +537,7 @@ impl Buffer { self.syntax_map.lock().clear(); self.language = language; self.reparse(cx); + cx.emit(Event::LanguageChanged); } pub fn set_language_registry(&mut self, language_registry: Arc) { From b9a7b70e52870aa121bd3551abe5ef38d06bf4c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 14:30:23 +0200 Subject: [PATCH 27/34] Register unknown buffer on the fly if completions are requested for it --- crates/copilot/src/copilot.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ebe139c1cf..71843402a6 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -663,6 +663,7 @@ impl Copilot { >, T: ToPointUtf16, { + self.register_buffer(buffer, cx); let (server, registered_buffer) = match &mut self.server { CopilotServer::Starting { .. } => { return Task::ready(Err(anyhow!("copilot is still starting"))) @@ -681,17 +682,11 @@ impl Copilot { .. } => { if matches!(status, SignInStatus::Authorized { .. }) { - if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) { - if let Err(error) = registered_buffer.report_changes(buffer, &server, cx) { - return Task::ready(Err(error)); - } - - (server.clone(), registered_buffer) - } else { - return Task::ready(Err(anyhow!( - "requested completions for an unregistered buffer" - ))); + let registered_buffer = registered_buffers.get_mut(&buffer.id()).unwrap(); + if let Err(error) = registered_buffer.report_changes(buffer, &server, cx) { + return Task::ready(Err(error)); } + (server.clone(), registered_buffer) } else { return Task::ready(Err(anyhow!("must sign in before using copilot"))); } From 4c3d6c854afd284630105f75d7e5d7081923e7d1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 19 Apr 2023 15:45:55 +0200 Subject: [PATCH 28/34] Send editor information to copilot --- crates/copilot/src/copilot.rs | 13 +++++++++++++ crates/copilot/src/request.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 71843402a6..4b67bc1650 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -350,6 +350,19 @@ impl Copilot { ) .detach(); + server + .request::(request::SetEditorInfoParams { + editor_info: request::EditorInfo { + name: "zed".into(), + version: env!("CARGO_PKG_VERSION").into(), + }, + editor_plugin_info: request::EditorPluginInfo { + name: "zed-copilot".into(), + version: "0.0.1".into(), + }, + }) + .await?; + anyhow::Ok((server, status)) }; diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 08173c413a..0d43bb7deb 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -166,3 +166,32 @@ impl lsp::notification::Notification for StatusNotification { type Params = StatusNotificationParams; const METHOD: &'static str = "statusNotification"; } + +pub enum SetEditorInfo {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetEditorInfoParams { + pub editor_info: EditorInfo, + pub editor_plugin_info: EditorPluginInfo, +} + +impl lsp::request::Request for SetEditorInfo { + type Params = SetEditorInfoParams; + type Result = String; + const METHOD: &'static str = "setEditorInfo"; +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorInfo { + pub name: String, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorPluginInfo { + pub name: String, + pub version: String, +} From 5d571673025491a800336c0d398c1140f50886e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 20 Apr 2023 09:59:19 +0200 Subject: [PATCH 29/34] Make it easier to access a running/authenticated copilot server --- crates/copilot/src/copilot.rs | 223 +++++++++++++++++----------------- 1 file changed, 113 insertions(+), 110 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4b67bc1650..28bc7977c1 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -104,15 +104,38 @@ pub fn init(http: Arc, node_runtime: Arc, cx: &mut enum CopilotServer { Disabled, - Starting { - task: Shared>, - }, + Starting { task: Shared> }, Error(Arc), - Started { - server: Arc, - status: SignInStatus, - registered_buffers: HashMap, - }, + Running(RunningCopilotServer), +} + +impl CopilotServer { + fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> { + let server = self.as_running()?; + if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) { + Ok(server) + } else { + Err(anyhow!("must sign in before using copilot")) + } + } + + fn as_running(&mut self) -> Result<&mut RunningCopilotServer> { + match self { + CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")), + CopilotServer::Disabled => Err(anyhow!("copilot is disabled")), + CopilotServer::Error(error) => Err(anyhow!( + "copilot was not started because of an error: {}", + error + )), + CopilotServer::Running(server) => Ok(server), + } + } +} + +struct RunningCopilotServer { + lsp: Arc, + sign_in_status: SignInStatus, + registered_buffers: HashMap, } #[derive(Clone, Debug)] @@ -293,11 +316,11 @@ impl Copilot { 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, + server: CopilotServer::Running(RunningCopilotServer { + lsp: Arc::new(server), + sign_in_status: SignInStatus::Authorized, registered_buffers: Default::default(), - }, + }), buffers: Default::default(), }); (this, fake_server) @@ -371,11 +394,11 @@ impl Copilot { cx.notify(); match server { Ok((server, status)) => { - this.server = CopilotServer::Started { - server, - status: SignInStatus::SignedOut, + this.server = CopilotServer::Running(RunningCopilotServer { + lsp: server, + sign_in_status: SignInStatus::SignedOut, registered_buffers: Default::default(), - }; + }); this.update_sign_in_status(status, cx); } Err(error) => { @@ -388,8 +411,8 @@ impl Copilot { } fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { - if let CopilotServer::Started { server, status, .. } = &mut self.server { - let task = match status { + if let CopilotServer::Running(server) = &mut self.server { + let task = match &server.sign_in_status { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { Task::ready(Ok(())).shared() } @@ -398,11 +421,11 @@ impl Copilot { task.clone() } SignInStatus::SignedOut => { - let server = server.clone(); + let lsp = server.lsp.clone(); let task = cx .spawn(|this, mut cx| async move { let sign_in = async { - let sign_in = server + let sign_in = lsp .request::( request::SignInInitiateParams {}, ) @@ -413,8 +436,10 @@ impl Copilot { } request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { this.update(&mut cx, |this, cx| { - if let CopilotServer::Started { status, .. } = - &mut this.server + if let CopilotServer::Running(RunningCopilotServer { + sign_in_status: status, + .. + }) = &mut this.server { if let SignInStatus::SigningIn { prompt: prompt_flow, @@ -426,7 +451,7 @@ impl Copilot { } } }); - let response = server + let response = lsp .request::( request::SignInConfirmParams { user_code: flow.user_code, @@ -454,7 +479,7 @@ impl Copilot { }) }) .shared(); - *status = SignInStatus::SigningIn { + server.sign_in_status = SignInStatus::SigningIn { prompt: None, task: task.clone(), }; @@ -474,7 +499,7 @@ impl Copilot { fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); - if let CopilotServer::Started { server, .. } = &self.server { + if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server { let server = server.clone(); cx.background().spawn(async move { server @@ -512,12 +537,12 @@ impl Copilot { let buffer_id = buffer.id(); self.buffers.insert(buffer_id, buffer.downgrade()); - if let CopilotServer::Started { - server, - status, + if let CopilotServer::Running(RunningCopilotServer { + lsp: server, + sign_in_status: status, registered_buffers, .. - } = &mut self.server + }) = &mut self.server { if !matches!(status, SignInStatus::Authorized { .. }) { return; @@ -565,26 +590,23 @@ impl Copilot { event: &language::Event, cx: &mut ModelContext, ) -> Result<()> { - if let CopilotServer::Started { - server, - registered_buffers, - .. - } = &mut self.server - { - if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) { + if let Ok(server) = self.server.as_running() { + if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) { match event { language::Event::Edited => { - registered_buffer.report_changes(&buffer, server, cx)?; + registered_buffer.report_changes(&buffer, &server.lsp, cx)?; } language::Event::Saved => { - server.notify::( - lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - registered_buffer.uri.clone(), - ), - text: None, - }, - )?; + server + .lsp + .notify::( + lsp::DidSaveTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + registered_buffer.uri.clone(), + ), + text: None, + }, + )?; } language::Event::FileHandleChanged | language::Event::LanguageChanged => { let new_language_id = id_for_language(buffer.read(cx).language()); @@ -594,21 +616,25 @@ impl Copilot { { let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); registered_buffer.language_id = new_language_id; - server.notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(old_uri), - }, - )?; - server.notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - registered_buffer.uri.clone(), - registered_buffer.language_id.clone(), - registered_buffer.snapshot_version, - registered_buffer.snapshot.text(), - ), - }, - )?; + server + .lsp + .notify::( + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), + }, + )?; + server + .lsp + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + registered_buffer.uri.clone(), + registered_buffer.language_id.clone(), + registered_buffer.snapshot_version, + registered_buffer.snapshot.text(), + ), + }, + )?; } } _ => {} @@ -620,14 +646,10 @@ impl Copilot { } fn unregister_buffer(&mut self, buffer_id: usize) { - if let CopilotServer::Started { - server, - registered_buffers, - .. - } = &mut self.server - { - if let Some(buffer) = registered_buffers.remove(&buffer_id) { + if let Ok(server) = self.server.as_running() { + if let Some(buffer) = server.registered_buffers.remove(&buffer_id) { server + .lsp .notify::( lsp::DidCloseTextDocumentParams { text_document: lsp::TextDocumentIdentifier::new(buffer.uri), @@ -677,34 +699,15 @@ impl Copilot { T: ToPointUtf16, { self.register_buffer(buffer, cx); - let (server, registered_buffer) = match &mut self.server { - CopilotServer::Starting { .. } => { - return Task::ready(Err(anyhow!("copilot is still starting"))) - } - CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))), - CopilotServer::Error(error) => { - return Task::ready(Err(anyhow!( - "copilot was not started because of an error: {}", - error - ))) - } - CopilotServer::Started { - server, - status, - registered_buffers, - .. - } => { - if matches!(status, SignInStatus::Authorized { .. }) { - let registered_buffer = registered_buffers.get_mut(&buffer.id()).unwrap(); - if let Err(error) = registered_buffer.report_changes(buffer, &server, cx) { - return Task::ready(Err(error)); - } - (server.clone(), registered_buffer) - } else { - return Task::ready(Err(anyhow!("must sign in before using copilot"))); - } - } + + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), }; + let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap(); + if let Err(error) = registered_buffer.report_changes(buffer, &server.lsp, cx) { + return Task::ready(Err(error)); + } let uri = registered_buffer.uri.clone(); let snapshot = registered_buffer.snapshot.clone(); @@ -720,7 +723,7 @@ impl Copilot { .file() .map(|file| file.path().to_path_buf()) .unwrap_or_default(); - let request = server.request::(request::GetCompletionsParams { + let request = server.lsp.request::(request::GetCompletionsParams { doc: request::GetCompletionsDocument { uri, tab_size: tab_size.into(), @@ -742,6 +745,7 @@ impl Copilot { let end = snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); Completion { + uuid: completion.uuid, range: snapshot.anchor_before(start)..snapshot.anchor_after(end), text: completion.text, } @@ -756,14 +760,16 @@ impl Copilot { CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, CopilotServer::Disabled => Status::Disabled, CopilotServer::Error(error) => Status::Error(error.clone()), - CopilotServer::Started { status, .. } => match status { - SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, - SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { - prompt: prompt.clone(), - }, - SignInStatus::SignedOut => Status::SignedOut, - }, + CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => { + match sign_in_status { + SignInStatus::Authorized { .. } => Status::Authorized, + SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { + prompt: prompt.clone(), + }, + SignInStatus::SignedOut => Status::SignedOut, + } + } } } @@ -774,13 +780,12 @@ impl Copilot { ) { self.buffers.retain(|_, buffer| buffer.is_upgradable(cx)); - if let CopilotServer::Started { status, .. } = &mut self.server { + if let Ok(server) = self.server.as_running() { match lsp_status { request::SignInStatus::Ok { .. } | request::SignInStatus::MaybeOk { .. } | request::SignInStatus::AlreadySignedIn { .. } => { - *status = SignInStatus::Authorized; - + server.sign_in_status = SignInStatus::Authorized; for buffer in self.buffers.values().cloned().collect::>() { if let Some(buffer) = buffer.upgrade(cx) { self.register_buffer(&buffer, cx); @@ -788,15 +793,13 @@ impl Copilot { } } request::SignInStatus::NotAuthorized { .. } => { - *status = SignInStatus::Unauthorized; - + server.sign_in_status = SignInStatus::Unauthorized; for buffer_id in self.buffers.keys().copied().collect::>() { self.unregister_buffer(buffer_id); } } request::SignInStatus::NotSignedIn => { - *status = SignInStatus::SignedOut; - + server.sign_in_status = SignInStatus::SignedOut; for buffer_id in self.buffers.keys().copied().collect::>() { self.unregister_buffer(buffer_id); } From 4d207981ae0ca73800afc6c4634b6c996d421be9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 20 Apr 2023 10:12:13 +0200 Subject: [PATCH 30/34] Notify LSP when Copilot suggestions are accepted/rejected --- crates/copilot/src/copilot.rs | 48 ++++++++++++++++++++++- crates/copilot/src/request.rs | 28 +++++++++++++ crates/editor/src/editor.rs | 74 +++++++++++++++++++++++------------ 3 files changed, 123 insertions(+), 27 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 28bc7977c1..6b32cc2603 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -224,8 +224,9 @@ impl RegisteredBuffer { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub struct Completion { + uuid: String, pub range: Range, pub text: String, } @@ -684,6 +685,51 @@ impl Copilot { self.request_completions::(buffer, position, cx) } + pub fn accept_completion( + &mut self, + completion: &Completion, + cx: &mut ModelContext, + ) -> Task> { + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + let request = + server + .lsp + .request::(request::NotifyAcceptedParams { + uuid: completion.uuid.clone(), + }); + cx.background().spawn(async move { + request.await?; + Ok(()) + }) + } + + pub fn discard_completions( + &mut self, + completions: &[Completion], + cx: &mut ModelContext, + ) -> Task> { + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + let request = + server + .lsp + .request::(request::NotifyRejectedParams { + uuids: completions + .iter() + .map(|completion| completion.uuid.clone()) + .collect(), + }); + cx.background().spawn(async move { + request.await?; + Ok(()) + }) + } + fn request_completions( &mut self, buffer: &ModelHandle, diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 0d43bb7deb..43b5109d02 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -195,3 +195,31 @@ pub struct EditorPluginInfo { pub name: String, pub version: String, } + +pub enum NotifyAccepted {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotifyAcceptedParams { + pub uuid: String, +} + +impl lsp::request::Request for NotifyAccepted { + type Params = NotifyAcceptedParams; + type Result = String; + const METHOD: &'static str = "notifyAccepted"; +} + +pub enum NotifyRejected {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotifyRejectedParams { + pub uuids: Vec, +} + +impl lsp::request::Request for NotifyRejected { + type Params = NotifyRejectedParams; + type Result = String; + const METHOD: &'static str = "notifyRejected"; +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 83b92cc06c..59f8ab6acb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind}; use language::{ AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, - Point, Rope, Selection, SelectionGoal, TransactionId, + Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, @@ -1037,6 +1037,10 @@ impl Default for CopilotState { } impl CopilotState { + fn active_completion(&self) -> Option<&copilot::Completion> { + self.completions.get(self.active_completion_index) + } + fn text_for_active_completion( &self, cursor: Anchor, @@ -1044,7 +1048,7 @@ impl CopilotState { ) -> Option<&str> { use language::ToOffset as _; - let completion = self.completions.get(self.active_completion_index)?; + let completion = self.active_completion()?; let excerpt_id = self.excerpt_id?; let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; if excerpt_id != cursor.excerpt_id @@ -1097,7 +1101,7 @@ impl CopilotState { fn push_completion(&mut self, new_completion: copilot::Completion) { for completion in &self.completions { - if *completion == new_completion { + if completion.text == new_completion.text && completion.range == new_completion.range { return; } } @@ -1496,7 +1500,7 @@ impl Editor { self.refresh_code_actions(cx); self.refresh_document_highlights(cx); refresh_matching_bracket_highlights(self, cx); - self.hide_copilot_suggestion(cx); + self.discard_copilot_suggestion(cx); } self.blink_manager.update(cx, BlinkManager::pause_blinking); @@ -1870,7 +1874,7 @@ impl Editor { return; } - if self.hide_copilot_suggestion(cx).is_some() { + if self.discard_copilot_suggestion(cx) { return; } @@ -2969,7 +2973,7 @@ impl Editor { Some(()) } - fn cycle_suggestions( + fn cycle_copilot_suggestions( &mut self, direction: Direction, cx: &mut ViewContext, @@ -3020,7 +3024,7 @@ impl Editor { fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Next, cx); + self.cycle_copilot_suggestions(Direction::Next, cx); } else { self.refresh_copilot_suggestions(false, cx); } @@ -3032,15 +3036,45 @@ impl Editor { cx: &mut ViewContext, ) { if self.has_active_copilot_suggestion(cx) { - self.cycle_suggestions(Direction::Prev, cx); + self.cycle_copilot_suggestions(Direction::Prev, cx); } else { self.refresh_copilot_suggestions(false, cx); } } fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { - if let Some(text) = self.hide_copilot_suggestion(cx) { - self.insert_with_autoindent_mode(&text.to_string(), None, cx); + if let Some(suggestion) = self + .display_map + .update(cx, |map, cx| map.replace_suggestion::(None, cx)) + { + if let Some((copilot, completion)) = + Copilot::global(cx).zip(self.copilot_state.active_completion()) + { + copilot + .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) + .detach_and_log_err(cx); + } + self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); + cx.notify(); + true + } else { + false + } + } + + fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + if self.has_active_copilot_suggestion(cx) { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| { + copilot.discard_completions(&self.copilot_state.completions, cx) + }) + .detach_and_log_err(cx); + } + + self.display_map + .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + cx.notify(); true } else { false @@ -3051,18 +3085,6 @@ impl Editor { self.display_map.read(cx).has_suggestion() } - fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext) -> Option { - if self.has_active_copilot_suggestion(cx) { - let old_suggestion = self - .display_map - .update(cx, |map, cx| map.replace_suggestion::(None, cx)); - cx.notify(); - old_suggestion.map(|suggestion| suggestion.text) - } else { - None - } - } - fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext) { let snapshot = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest_anchor(); @@ -3072,7 +3094,7 @@ impl Editor { || !self.completion_tasks.is_empty() || selection.start != selection.end { - self.hide_copilot_suggestion(cx); + self.discard_copilot_suggestion(cx); } else if let Some(text) = self .copilot_state .text_for_active_completion(cursor, &snapshot) @@ -3088,13 +3110,13 @@ impl Editor { }); cx.notify(); } else { - self.hide_copilot_suggestion(cx); + self.discard_copilot_suggestion(cx); } } fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) { self.copilot_state = Default::default(); - self.hide_copilot_suggestion(cx); + self.discard_copilot_suggestion(cx); } pub fn render_code_actions_indicator( @@ -3212,7 +3234,7 @@ impl Editor { self.completion_tasks.clear(); } self.context_menu = Some(menu); - self.hide_copilot_suggestion(cx); + self.discard_copilot_suggestion(cx); cx.notify(); } From 4151bd39da112ee95fa5e8744a0e034128744103 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 20 Apr 2023 10:51:50 +0200 Subject: [PATCH 31/34] Add buffer management test to Copilot --- Cargo.lock | 3 + crates/copilot/Cargo.toml | 5 +- crates/copilot/src/copilot.rs | 223 ++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bb931853fc..d7c0249798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,14 +1341,17 @@ dependencies = [ "anyhow", "async-compression", "async-tar", + "clock", "collections", "context_menu", + "fs", "futures 0.3.25", "gpui", "language", "log", "lsp", "node_runtime", + "rpc", "serde", "serde_derive", "settings", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 9d68edd6b5..bfafdbc0ca 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -38,10 +38,13 @@ smol = "1.2.5" futures = "0.3" [dev-dependencies] +clock = { path = "../clock" } collections = { path = "../collections", features = ["test-support"] } +fs = { path = "../fs", 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"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6b32cc2603..a558307a70 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -945,3 +945,226 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { } } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{executor::Deterministic, TestAppContext}; + + #[gpui::test(iterations = 10)] + async fn test_buffer_management(deterministic: Arc, cx: &mut TestAppContext) { + deterministic.forbid_parking(); + let (copilot, mut lsp) = Copilot::fake(cx); + + let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx)); + let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); + copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 0, + "Hello".into() + ), + } + ); + + let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx)); + let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); + copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + } + ); + + buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1), + content_changes: vec![lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + lsp::Position::new(0, 5), + lsp::Position::new(0, 5) + )), + range_length: None, + text: " world".into(), + }], + } + ); + + // Ensure updates to the file are reflected in the LSP. + buffer_1 + .update(cx, |buffer, cx| { + buffer.file_updated( + Arc::new(File { + abs_path: "/root/child/buffer-1".into(), + path: Path::new("child/buffer-1").into(), + }), + cx, + ) + }) + .await; + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), + } + ); + let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 1, + "Hello world".into() + ), + } + ); + + // Ensure all previously-registered buffers are closed when signing out. + lsp.handle_request::(|_, _| async { + Ok(request::SignOutResult {}) + }); + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .await + .unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), + } + ); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), + } + ); + + // Ensure all previously-registered buffers are re-opened when signing in. + lsp.handle_request::(|_, _| async { + Ok(request::SignInInitiateResult::AlreadySignedIn { + user: "user-1".into(), + }) + }); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .await + .unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + } + ); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 0, + "Hello world".into() + ), + } + ); + + // Dropping a buffer causes it to be closed on the LSP side as well. + cx.update(|_| drop(buffer_2)); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri), + } + ); + } + + struct File { + abs_path: PathBuf, + path: Arc, + } + + impl language::File for File { + fn as_local(&self) -> Option<&dyn language::LocalFile> { + Some(self) + } + + fn mtime(&self) -> std::time::SystemTime { + todo!() + } + + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &AppContext) -> PathBuf { + todo!() + } + + fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { + todo!() + } + + fn is_deleted(&self) -> bool { + todo!() + } + + fn as_any(&self) -> &dyn std::any::Any { + todo!() + } + + fn to_proto(&self) -> rpc::proto::File { + todo!() + } + } + + impl language::LocalFile for File { + fn abs_path(&self, _: &AppContext) -> PathBuf { + self.abs_path.clone() + } + + fn load(&self, _: &AppContext) -> Task> { + todo!() + } + + fn buffer_reloaded( + &self, + _: u64, + _: &clock::Global, + _: language::RopeFingerprint, + _: ::fs::LineEnding, + _: std::time::SystemTime, + _: &mut AppContext, + ) { + todo!() + } + } +} From df71a9cfaeb6c68f90db3d8d3c28c391f859be6e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 20 Apr 2023 11:57:37 +0200 Subject: [PATCH 32/34] Move buffer change reporting to a background task --- crates/copilot/src/copilot.rs | 163 +++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 60 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index a558307a70..c3ec63c43c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use collections::HashMap; -use futures::{future::Shared, Future, FutureExt, TryFutureExt}; +use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, }; @@ -171,56 +171,97 @@ impl Status { } struct RegisteredBuffer { + id: usize, uri: lsp::Url, language_id: String, snapshot: BufferSnapshot, snapshot_version: i32, _subscriptions: [gpui::Subscription; 2], + pending_buffer_change: Task>, } impl RegisteredBuffer { fn report_changes( &mut self, buffer: &ModelHandle, - server: &LanguageServer, - cx: &AppContext, - ) -> Result<()> { - let buffer = buffer.read(cx); - let new_snapshot = buffer.snapshot(); - let content_changes = buffer - .edits_since::<(PointUtf16, usize)>(self.snapshot.version()) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = new_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - point_to_lsp(edit_start), - point_to_lsp(edit_end), - )), - range_length: None, - text: new_text, - } - }) - .collect::>(); + cx: &mut ModelContext, + ) -> oneshot::Receiver<(i32, BufferSnapshot)> { + let id = self.id; + let (done_tx, done_rx) = oneshot::channel(); - if !content_changes.is_empty() { - self.snapshot_version += 1; - self.snapshot = new_snapshot; - server.notify::( - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - self.uri.clone(), - self.snapshot_version, - ), - content_changes, - }, - )?; + if buffer.read(cx).version() == self.snapshot.version { + let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); + } else { + let buffer = buffer.downgrade(); + let prev_pending_change = + mem::replace(&mut self.pending_buffer_change, Task::ready(None)); + self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move { + prev_pending_change.await; + + let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| { + let server = copilot.server.as_authenticated().log_err()?; + let buffer = server.registered_buffers.get_mut(&id)?; + Some(buffer.snapshot.version.clone()) + })?; + let new_snapshot = buffer + .upgrade(&cx)? + .read_with(&cx, |buffer, _| buffer.snapshot()); + + let content_changes = cx + .background() + .spawn({ + let new_snapshot = new_snapshot.clone(); + async move { + new_snapshot + .edits_since::<(PointUtf16, usize)>(&old_version) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = new_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + point_to_lsp(edit_start), + point_to_lsp(edit_end), + )), + range_length: None, + text: new_text, + } + }) + .collect::>() + } + }) + .await; + + copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| { + let server = copilot.server.as_authenticated().log_err()?; + let buffer = server.registered_buffers.get_mut(&id)?; + if !content_changes.is_empty() { + buffer.snapshot_version += 1; + buffer.snapshot = new_snapshot; + server + .lsp + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + buffer.uri.clone(), + buffer.snapshot_version, + ), + content_changes, + }, + ) + .log_err(); + } + let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone())); + Some(()) + })?; + + Some(()) + }); } - Ok(()) + done_rx } } @@ -567,10 +608,12 @@ impl Copilot { .log_err(); RegisteredBuffer { + id: buffer_id, uri, language_id, snapshot, snapshot_version: 0, + pending_buffer_change: Task::ready(Some(())), _subscriptions: [ cx.subscribe(buffer, |this, buffer, event, cx| { this.handle_buffer_event(buffer, event, cx).log_err(); @@ -595,7 +638,7 @@ impl Copilot { if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) { match event { language::Event::Edited => { - registered_buffer.report_changes(&buffer, &server.lsp, cx)?; + let _ = registered_buffer.report_changes(&buffer, cx); } language::Event::Saved => { server @@ -750,38 +793,38 @@ impl Copilot { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; + let lsp = server.lsp.clone(); let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap(); - if let Err(error) = registered_buffer.report_changes(buffer, &server.lsp, cx) { - return Task::ready(Err(error)); - } - + let snapshot = registered_buffer.report_changes(buffer, cx); + let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); - let snapshot = registered_buffer.snapshot.clone(); - let version = registered_buffer.snapshot_version; let settings = cx.global::(); - let position = position.to_point_utf16(&snapshot); - let language = snapshot.language_at(position); + let position = position.to_point_utf16(buffer); + let language = buffer.language_at(position); let language_name = language.map(|language| language.name()); let language_name = language_name.as_deref(); let tab_size = settings.tab_size(language_name); let hard_tabs = settings.hard_tabs(language_name); - let relative_path = snapshot + let relative_path = buffer .file() .map(|file| file.path().to_path_buf()) .unwrap_or_default(); - let request = server.lsp.request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - uri, - tab_size: tab_size.into(), - indent_size: 1, - insert_spaces: !hard_tabs, - relative_path: relative_path.to_string_lossy().into(), - position: point_to_lsp(position), - version: version.try_into().unwrap(), - }, - }); - cx.background().spawn(async move { - let result = request.await?; + + cx.foreground().spawn(async move { + let (version, snapshot) = snapshot.await?; + let result = lsp + .request::(request::GetCompletionsParams { + doc: request::GetCompletionsDocument { + uri, + tab_size: tab_size.into(), + indent_size: 1, + insert_spaces: !hard_tabs, + relative_path: relative_path.to_string_lossy().into(), + position: point_to_lsp(position), + version: version.try_into().unwrap(), + }, + }) + .await?; let completions = result .completions .into_iter() From bed76462e21ddb2c631bfaaef31672da43071223 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 20 Apr 2023 14:06:43 +0300 Subject: [PATCH 33/34] Define tab_size equal to 2 in default settings --- assets/settings/default.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 1ecfbf03a1..09f987cd9a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -231,6 +231,9 @@ }, "YAML": { "tab_size": 2 + }, + "JSON": { + "tab_size": 2 } }, // LSP Specific settings. From eca93c124a488b4e538946cd2d313bd571aa2b86 Mon Sep 17 00:00:00 2001 From: Petros Amoiridis Date: Thu, 20 Apr 2023 14:08:30 +0300 Subject: [PATCH 34/34] Apply the tab_size change to keymaps and settings --- assets/keymaps/default.json | 1090 ++++++++++---------- assets/keymaps/vim.json | 626 +++++------ assets/settings/default.json | 504 ++++----- assets/settings/initial_user_settings.json | 2 +- 4 files changed, 1111 insertions(+), 1111 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 5c835663f0..b89940a751 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -1,548 +1,548 @@ [ - // Standard macOS bindings - { - "bindings": { - "up": "menu::SelectPrev", - "pageup": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "ctrl-p": "menu::SelectPrev", - "down": "menu::SelectNext", - "pagedown": "menu::SelectLast", - "shift-pagedown": "menu::SelectFirst", - "ctrl-n": "menu::SelectNext", - "cmd-up": "menu::SelectFirst", - "cmd-down": "menu::SelectLast", - "enter": "menu::Confirm", - "escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "cmd-{": "pane::ActivatePrevItem", - "cmd-}": "pane::ActivateNextItem", - "alt-cmd-left": "pane::ActivatePrevItem", - "alt-cmd-right": "pane::ActivateNextItem", - "cmd-w": "pane::CloseActiveItem", - "alt-cmd-t": "pane::CloseInactiveItems", - "cmd-k u": "pane::CloseCleanItems", - "cmd-k cmd-w": "pane::CloseAllItems", - "cmd-shift-w": "workspace::CloseWindow", - "cmd-s": "workspace::Save", - "cmd-shift-s": "workspace::SaveAs", - "cmd-=": "zed::IncreaseBufferFontSize", - "cmd--": "zed::DecreaseBufferFontSize", - "cmd-0": "zed::ResetBufferFontSize", - "cmd-,": "zed::OpenSettings", - "cmd-q": "zed::Quit", - "cmd-h": "zed::Hide", - "alt-cmd-h": "zed::HideOthers", - "cmd-m": "zed::Minimize", - "ctrl-cmd-f": "zed::ToggleFullScreen", - "cmd-n": "workspace::NewFile", - "cmd-shift-n": "workspace::NewWindow", - "cmd-o": "workspace::Open", - "alt-cmd-o": "projects::OpenRecent", - "ctrl-`": "workspace::NewTerminal" - } - }, - { - "context": "Editor", - "bindings": { - "escape": "editor::Cancel", - "backspace": "editor::Backspace", - "shift-backspace": "editor::Backspace", - "ctrl-h": "editor::Backspace", - "delete": "editor::Delete", - "ctrl-d": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::TabPrev", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-t": "editor::Transpose", - "cmd-backspace": "editor::DeleteToBeginningOfLine", - "cmd-delete": "editor::DeleteToEndOfLine", - "alt-backspace": "editor::DeleteToPreviousWordStart", - "alt-delete": "editor::DeleteToNextWordEnd", - "alt-h": "editor::DeleteToPreviousWordStart", - "alt-d": "editor::DeleteToNextWordEnd", - "cmd-x": "editor::Cut", - "cmd-c": "editor::Copy", - "cmd-v": "editor::Paste", - "cmd-z": "editor::Undo", - "cmd-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "pageup": "editor::PageUp", - "shift-pageup": "editor::MovePageUp", - "home": "editor::MoveToBeginningOfLine", - "down": "editor::MoveDown", - "pagedown": "editor::PageDown", - "shift-pagedown": "editor::MovePageDown", - "end": "editor::MoveToEndOfLine", - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-p": "editor::MoveUp", - "ctrl-n": "editor::MoveDown", - "ctrl-b": "editor::MoveLeft", - "ctrl-f": "editor::MoveRight", - "ctrl-l": "editor::NextScreen", - "alt-left": "editor::MoveToPreviousWordStart", - "alt-b": "editor::MoveToPreviousWordStart", - "alt-right": "editor::MoveToNextWordEnd", - "alt-f": "editor::MoveToNextWordEnd", - "cmd-left": "editor::MoveToBeginningOfLine", - "ctrl-a": "editor::MoveToBeginningOfLine", - "cmd-right": "editor::MoveToEndOfLine", - "ctrl-e": "editor::MoveToEndOfLine", - "cmd-up": "editor::MoveToBeginning", - "cmd-down": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "ctrl-shift-p": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "ctrl-shift-n": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "ctrl-shift-b": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-f": "editor::SelectRight", - "alt-shift-left": "editor::SelectToPreviousWordStart", - "alt-shift-b": "editor::SelectToPreviousWordStart", - "alt-shift-right": "editor::SelectToNextWordEnd", - "alt-shift-f": "editor::SelectToNextWordEnd", - "cmd-shift-up": "editor::SelectToBeginning", - "cmd-shift-down": "editor::SelectToEnd", - "cmd-a": "editor::SelectAll", - "cmd-l": "editor::SelectLine", - "cmd-shift-i": "editor::Format", - "cmd-shift-left": [ - "editor::SelectToBeginningOfLine", - { - "stop_at_soft_wraps": true - } - ], - "shift-home": [ - "editor::SelectToBeginningOfLine", - { - "stop_at_soft_wraps": true - } - ], - "ctrl-shift-a": [ - "editor::SelectToBeginningOfLine", - { - "stop_at_soft_wraps": true - } - ], - "cmd-shift-right": [ - "editor::SelectToEndOfLine", - { - "stop_at_soft_wraps": true - } - ], - "shift-end": [ - "editor::SelectToEndOfLine", - { - "stop_at_soft_wraps": true - } - ], - "ctrl-shift-e": [ - "editor::SelectToEndOfLine", - { - "stop_at_soft_wraps": true - } - ], - "ctrl-v": [ - "editor::MovePageDown", - { - "center_cursor": true - } - ], - "alt-v": [ - "editor::MovePageUp", - { - "center_cursor": true - } - ], - "ctrl-cmd-space": "editor::ShowCharacterPalette" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "enter": "editor::Newline", - "cmd-shift-enter": "editor::NewlineAbove", - "cmd-enter": "editor::NewlineBelow", - "alt-z": "editor::ToggleSoftWrap", - "cmd-f": [ - "buffer_search::Deploy", - { - "focus": true - } - ], - "cmd-e": [ - "buffer_search::Deploy", - { - "focus": false - } - ], - "alt-\\": "copilot::Suggest", - "alt-]": "copilot::NextSuggestion", - "alt-[": "copilot::PreviousSuggestion" - } - }, - { - "context": "Editor && mode == auto_height", - "bindings": { - "alt-enter": "editor::Newline", - "cmd-alt-enter": "editor::NewlineBelow" - } - }, - { - "context": "BufferSearchBar > Editor", - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPrevMatch" - } - }, - { - "context": "Pane", - "bindings": { - "cmd-f": "project_search::ToggleFocus", - "cmd-g": "search::SelectNextMatch", - "cmd-shift-g": "search::SelectPrevMatch", - "alt-cmd-c": "search::ToggleCaseSensitive", - "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "bindings": { - "cmd-[": "editor::Outdent", - "cmd-]": "editor::Indent", - "cmd-alt-up": "editor::AddSelectionAbove", - "cmd-ctrl-p": "editor::AddSelectionAbove", - "cmd-alt-down": "editor::AddSelectionBelow", - "cmd-ctrl-n": "editor::AddSelectionBelow", - "cmd-d": [ - "editor::SelectNext", - { - "replace_newest": false - } - ], - "cmd-k cmd-d": [ - "editor::SelectNext", - { - "replace_newest": true - } - ], - "cmd-k cmd-i": "editor::Hover", - "cmd-/": [ - "editor::ToggleComments", - { - "advance_downwards": false - } - ], - "alt-up": "editor::SelectLargerSyntaxNode", - "alt-down": "editor::SelectSmallerSyntaxNode", - "cmd-u": "editor::UndoSelection", - "cmd-shift-u": "editor::RedoSelection", - "f8": "editor::GoToDiagnostic", - "shift-f8": "editor::GoToPrevDiagnostic", - "f2": "editor::Rename", - "f12": "editor::GoToDefinition", - "cmd-f12": "editor::GoToTypeDefinition", - "alt-shift-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", - "alt-cmd-[": "editor::Fold", - "alt-cmd-]": "editor::UnfoldLines", - "ctrl-space": "editor::ShowCompletions", - "cmd-.": "editor::ToggleCodeActions", - "alt-cmd-r": "editor::RevealInFinder" - } - }, - { - "context": "Editor && mode == full", - "bindings": { - "cmd-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Pane", - "bindings": { - "ctrl-1": [ - "pane::ActivateItem", - 0 - ], - "ctrl-2": [ - "pane::ActivateItem", - 1 - ], - "ctrl-3": [ - "pane::ActivateItem", - 2 - ], - "ctrl-4": [ - "pane::ActivateItem", - 3 - ], - "ctrl-5": [ - "pane::ActivateItem", - 4 - ], - "ctrl-6": [ - "pane::ActivateItem", - 5 - ], - "ctrl-7": [ - "pane::ActivateItem", - 6 - ], - "ctrl-8": [ - "pane::ActivateItem", - 7 - ], - "ctrl-9": [ - "pane::ActivateItem", - 8 - ], - "ctrl-0": "pane::ActivateLastItem", - "ctrl--": "pane::GoBack", - "ctrl-_": "pane::GoForward", - "cmd-shift-t": "pane::ReopenClosedItem", - "cmd-shift-f": "project_search::ToggleFocus" - } - }, - { - "context": "Workspace", - "bindings": { - "cmd-1": [ - "workspace::ActivatePane", - 0 - ], - "cmd-2": [ - "workspace::ActivatePane", - 1 - ], - "cmd-3": [ - "workspace::ActivatePane", - 2 - ], - "cmd-4": [ - "workspace::ActivatePane", - 3 - ], - "cmd-5": [ - "workspace::ActivatePane", - 4 - ], - "cmd-6": [ - "workspace::ActivatePane", - 5 - ], - "cmd-7": [ - "workspace::ActivatePane", - 6 - ], - "cmd-8": [ - "workspace::ActivatePane", - 7 - ], - "cmd-9": [ - "workspace::ActivatePane", - 8 - ], - "cmd-b": "workspace::ToggleLeftSidebar", - "cmd-shift-f": "workspace::NewSearch", - "cmd-k cmd-t": "theme_selector::Toggle", - "cmd-k cmd-s": "zed::OpenKeymap", - "cmd-t": "project_symbols::Toggle", - "cmd-p": "file_finder::Toggle", - "cmd-shift-p": "command_palette::Toggle", - "cmd-shift-m": "diagnostics::Deploy", - "cmd-shift-e": "project_panel::ToggleFocus", - "cmd-alt-s": "workspace::SaveAll", - "cmd-k m": "language_selector::Toggle" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "bindings": { - "ctrl-shift-k": "editor::DeleteLine", - "cmd-shift-d": "editor::DuplicateLine", - "cmd-shift-l": "editor::SplitSelectionIntoLines", - "ctrl-cmd-up": "editor::MoveLineUp", - "ctrl-cmd-down": "editor::MoveLineDown", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-b": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-alt-f": "editor::MoveToNextSubwordEnd", - "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", - "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", - "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" - } - }, - { - "bindings": { - "cmd-k cmd-left": "workspace::ActivatePreviousPane", - "cmd-k cmd-right": "workspace::ActivateNextPane" - } - }, - // Bindings from Atom - { - "context": "Pane", - "bindings": { - "cmd-k up": "pane::SplitUp", - "cmd-k down": "pane::SplitDown", - "cmd-k left": "pane::SplitLeft", - "cmd-k right": "pane::SplitRight" - } - }, - // Bindings that should be unified with bindings for more general actions - { - "context": "Editor && renaming", - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "bindings": { - "enter": "editor::ConfirmCompletion", - "tab": "editor::ConfirmCompletion" - } - }, - { - "context": "Editor && showing_code_actions", - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - // Custom bindings - { - "bindings": { - "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", - "cmd-shift-c": "collab::ToggleContactsMenu", - "cmd-alt-i": "zed::DebugElements" - } - }, - { - "context": "Editor", - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "cmd-f8": "editor::GoToHunk", - "cmd-shift-f8": "editor::GoToPrevHunk" - } - }, - { - "context": "ProjectSearchBar", - "bindings": { - "cmd-enter": "project_search::SearchInNew" - } - }, - { - "context": "Workspace", - "bindings": { - "shift-escape": "dock::FocusDock" - } - }, - { - "bindings": { - "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight", - "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom", - "cmd-shift-k cmd-shift-up": "dock::ExpandDock" - } - }, - { - "context": "Pane", - "bindings": { - "cmd-escape": "dock::AddTabToDock" - } - }, - { - "context": "Pane && docked", - "bindings": { - "shift-escape": "dock::HideDock", - "cmd-escape": "dock::RemoveTabFromDock" - } - }, - { - "context": "ProjectPanel", - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry", - "cmd-x": "project_panel::Cut", - "cmd-c": "project_panel::Copy", - "cmd-v": "project_panel::Paste", - "cmd-alt-c": "project_panel::CopyPath", - "alt-cmd-shift-c": "project_panel::CopyRelativePath", - "f2": "project_panel::Rename", - "backspace": "project_panel::Delete", - "alt-cmd-r": "project_panel::RevealInFinder" - } - }, - { - "context": "Terminal", - "bindings": { - "ctrl-cmd-space": "terminal::ShowCharacterPalette", - "cmd-c": "terminal::Copy", - "cmd-v": "terminal::Paste", - "cmd-k": "terminal::Clear", - // Some nice conveniences - "cmd-backspace": [ - "terminal::SendText", - "\u0015" - ], - "cmd-right": [ - "terminal::SendText", - "\u0005" - ], - "cmd-left": [ - "terminal::SendText", - "\u0001" - ], - // Terminal.app compatability - "alt-left": [ - "terminal::SendText", - "\u001bb" - ], - "alt-right": [ - "terminal::SendText", - "\u001bf" - ], - // There are conflicting bindings for these keys in the global context. - // these bindings override them, remove at your own risk: - "up": [ - "terminal::SendKeystroke", - "up" - ], - "pageup": [ - "terminal::SendKeystroke", - "pageup" - ], - "down": [ - "terminal::SendKeystroke", - "down" - ], - "pagedown": [ - "terminal::SendKeystroke", - "pagedown" - ], - "escape": [ - "terminal::SendKeystroke", - "escape" - ], - "enter": [ - "terminal::SendKeystroke", - "enter" - ], - "ctrl-c": [ - "terminal::SendKeystroke", - "ctrl-c" - ] - } + // Standard macOS bindings + { + "bindings": { + "up": "menu::SelectPrev", + "pageup": "menu::SelectFirst", + "shift-pageup": "menu::SelectFirst", + "ctrl-p": "menu::SelectPrev", + "down": "menu::SelectNext", + "pagedown": "menu::SelectLast", + "shift-pagedown": "menu::SelectFirst", + "ctrl-n": "menu::SelectNext", + "cmd-up": "menu::SelectFirst", + "cmd-down": "menu::SelectLast", + "enter": "menu::Confirm", + "escape": "menu::Cancel", + "ctrl-c": "menu::Cancel", + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", + "cmd-w": "pane::CloseActiveItem", + "alt-cmd-t": "pane::CloseInactiveItems", + "cmd-k u": "pane::CloseCleanItems", + "cmd-k cmd-w": "pane::CloseAllItems", + "cmd-shift-w": "workspace::CloseWindow", + "cmd-s": "workspace::Save", + "cmd-shift-s": "workspace::SaveAs", + "cmd-=": "zed::IncreaseBufferFontSize", + "cmd--": "zed::DecreaseBufferFontSize", + "cmd-0": "zed::ResetBufferFontSize", + "cmd-,": "zed::OpenSettings", + "cmd-q": "zed::Quit", + "cmd-h": "zed::Hide", + "alt-cmd-h": "zed::HideOthers", + "cmd-m": "zed::Minimize", + "ctrl-cmd-f": "zed::ToggleFullScreen", + "cmd-n": "workspace::NewFile", + "cmd-shift-n": "workspace::NewWindow", + "cmd-o": "workspace::Open", + "alt-cmd-o": "projects::OpenRecent", + "ctrl-`": "workspace::NewTerminal" } + }, + { + "context": "Editor", + "bindings": { + "escape": "editor::Cancel", + "backspace": "editor::Backspace", + "shift-backspace": "editor::Backspace", + "ctrl-h": "editor::Backspace", + "delete": "editor::Delete", + "ctrl-d": "editor::Delete", + "tab": "editor::Tab", + "shift-tab": "editor::TabPrev", + "ctrl-k": "editor::CutToEndOfLine", + "ctrl-t": "editor::Transpose", + "cmd-backspace": "editor::DeleteToBeginningOfLine", + "cmd-delete": "editor::DeleteToEndOfLine", + "alt-backspace": "editor::DeleteToPreviousWordStart", + "alt-delete": "editor::DeleteToNextWordEnd", + "alt-h": "editor::DeleteToPreviousWordStart", + "alt-d": "editor::DeleteToNextWordEnd", + "cmd-x": "editor::Cut", + "cmd-c": "editor::Copy", + "cmd-v": "editor::Paste", + "cmd-z": "editor::Undo", + "cmd-shift-z": "editor::Redo", + "up": "editor::MoveUp", + "pageup": "editor::PageUp", + "shift-pageup": "editor::MovePageUp", + "home": "editor::MoveToBeginningOfLine", + "down": "editor::MoveDown", + "pagedown": "editor::PageDown", + "shift-pagedown": "editor::MovePageDown", + "end": "editor::MoveToEndOfLine", + "left": "editor::MoveLeft", + "right": "editor::MoveRight", + "ctrl-p": "editor::MoveUp", + "ctrl-n": "editor::MoveDown", + "ctrl-b": "editor::MoveLeft", + "ctrl-f": "editor::MoveRight", + "ctrl-l": "editor::NextScreen", + "alt-left": "editor::MoveToPreviousWordStart", + "alt-b": "editor::MoveToPreviousWordStart", + "alt-right": "editor::MoveToNextWordEnd", + "alt-f": "editor::MoveToNextWordEnd", + "cmd-left": "editor::MoveToBeginningOfLine", + "ctrl-a": "editor::MoveToBeginningOfLine", + "cmd-right": "editor::MoveToEndOfLine", + "ctrl-e": "editor::MoveToEndOfLine", + "cmd-up": "editor::MoveToBeginning", + "cmd-down": "editor::MoveToEnd", + "shift-up": "editor::SelectUp", + "ctrl-shift-p": "editor::SelectUp", + "shift-down": "editor::SelectDown", + "ctrl-shift-n": "editor::SelectDown", + "shift-left": "editor::SelectLeft", + "ctrl-shift-b": "editor::SelectLeft", + "shift-right": "editor::SelectRight", + "ctrl-shift-f": "editor::SelectRight", + "alt-shift-left": "editor::SelectToPreviousWordStart", + "alt-shift-b": "editor::SelectToPreviousWordStart", + "alt-shift-right": "editor::SelectToNextWordEnd", + "alt-shift-f": "editor::SelectToNextWordEnd", + "cmd-shift-up": "editor::SelectToBeginning", + "cmd-shift-down": "editor::SelectToEnd", + "cmd-a": "editor::SelectAll", + "cmd-l": "editor::SelectLine", + "cmd-shift-i": "editor::Format", + "cmd-shift-left": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "shift-home": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-shift-a": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "cmd-shift-right": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "shift-end": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-shift-e": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-v": [ + "editor::MovePageDown", + { + "center_cursor": true + } + ], + "alt-v": [ + "editor::MovePageUp", + { + "center_cursor": true + } + ], + "ctrl-cmd-space": "editor::ShowCharacterPalette" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "enter": "editor::Newline", + "cmd-shift-enter": "editor::NewlineAbove", + "cmd-enter": "editor::NewlineBelow", + "alt-z": "editor::ToggleSoftWrap", + "cmd-f": [ + "buffer_search::Deploy", + { + "focus": true + } + ], + "cmd-e": [ + "buffer_search::Deploy", + { + "focus": false + } + ], + "alt-\\": "copilot::Suggest", + "alt-]": "copilot::NextSuggestion", + "alt-[": "copilot::PreviousSuggestion" + } + }, + { + "context": "Editor && mode == auto_height", + "bindings": { + "alt-enter": "editor::Newline", + "cmd-alt-enter": "editor::NewlineBelow" + } + }, + { + "context": "BufferSearchBar > Editor", + "bindings": { + "escape": "buffer_search::Dismiss", + "tab": "buffer_search::FocusEditor", + "enter": "search::SelectNextMatch", + "shift-enter": "search::SelectPrevMatch" + } + }, + { + "context": "Pane", + "bindings": { + "cmd-f": "project_search::ToggleFocus", + "cmd-g": "search::SelectNextMatch", + "cmd-shift-g": "search::SelectPrevMatch", + "alt-cmd-c": "search::ToggleCaseSensitive", + "alt-cmd-w": "search::ToggleWholeWord", + "alt-cmd-r": "search::ToggleRegex" + } + }, + // Bindings from VS Code + { + "context": "Editor", + "bindings": { + "cmd-[": "editor::Outdent", + "cmd-]": "editor::Indent", + "cmd-alt-up": "editor::AddSelectionAbove", + "cmd-ctrl-p": "editor::AddSelectionAbove", + "cmd-alt-down": "editor::AddSelectionBelow", + "cmd-ctrl-n": "editor::AddSelectionBelow", + "cmd-d": [ + "editor::SelectNext", + { + "replace_newest": false + } + ], + "cmd-k cmd-d": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "cmd-k cmd-i": "editor::Hover", + "cmd-/": [ + "editor::ToggleComments", + { + "advance_downwards": false + } + ], + "alt-up": "editor::SelectLargerSyntaxNode", + "alt-down": "editor::SelectSmallerSyntaxNode", + "cmd-u": "editor::UndoSelection", + "cmd-shift-u": "editor::RedoSelection", + "f8": "editor::GoToDiagnostic", + "shift-f8": "editor::GoToPrevDiagnostic", + "f2": "editor::Rename", + "f12": "editor::GoToDefinition", + "cmd-f12": "editor::GoToTypeDefinition", + "alt-shift-f12": "editor::FindAllReferences", + "ctrl-m": "editor::MoveToEnclosingBracket", + "alt-cmd-[": "editor::Fold", + "alt-cmd-]": "editor::UnfoldLines", + "ctrl-space": "editor::ShowCompletions", + "cmd-.": "editor::ToggleCodeActions", + "alt-cmd-r": "editor::RevealInFinder" + } + }, + { + "context": "Editor && mode == full", + "bindings": { + "cmd-shift-o": "outline::Toggle", + "ctrl-g": "go_to_line::Toggle" + } + }, + { + "context": "Pane", + "bindings": { + "ctrl-1": [ + "pane::ActivateItem", + 0 + ], + "ctrl-2": [ + "pane::ActivateItem", + 1 + ], + "ctrl-3": [ + "pane::ActivateItem", + 2 + ], + "ctrl-4": [ + "pane::ActivateItem", + 3 + ], + "ctrl-5": [ + "pane::ActivateItem", + 4 + ], + "ctrl-6": [ + "pane::ActivateItem", + 5 + ], + "ctrl-7": [ + "pane::ActivateItem", + 6 + ], + "ctrl-8": [ + "pane::ActivateItem", + 7 + ], + "ctrl-9": [ + "pane::ActivateItem", + 8 + ], + "ctrl-0": "pane::ActivateLastItem", + "ctrl--": "pane::GoBack", + "ctrl-_": "pane::GoForward", + "cmd-shift-t": "pane::ReopenClosedItem", + "cmd-shift-f": "project_search::ToggleFocus" + } + }, + { + "context": "Workspace", + "bindings": { + "cmd-1": [ + "workspace::ActivatePane", + 0 + ], + "cmd-2": [ + "workspace::ActivatePane", + 1 + ], + "cmd-3": [ + "workspace::ActivatePane", + 2 + ], + "cmd-4": [ + "workspace::ActivatePane", + 3 + ], + "cmd-5": [ + "workspace::ActivatePane", + 4 + ], + "cmd-6": [ + "workspace::ActivatePane", + 5 + ], + "cmd-7": [ + "workspace::ActivatePane", + 6 + ], + "cmd-8": [ + "workspace::ActivatePane", + 7 + ], + "cmd-9": [ + "workspace::ActivatePane", + 8 + ], + "cmd-b": "workspace::ToggleLeftSidebar", + "cmd-shift-f": "workspace::NewSearch", + "cmd-k cmd-t": "theme_selector::Toggle", + "cmd-k cmd-s": "zed::OpenKeymap", + "cmd-t": "project_symbols::Toggle", + "cmd-p": "file_finder::Toggle", + "cmd-shift-p": "command_palette::Toggle", + "cmd-shift-m": "diagnostics::Deploy", + "cmd-shift-e": "project_panel::ToggleFocus", + "cmd-alt-s": "workspace::SaveAll", + "cmd-k m": "language_selector::Toggle" + } + }, + // Bindings from Sublime Text + { + "context": "Editor", + "bindings": { + "ctrl-shift-k": "editor::DeleteLine", + "cmd-shift-d": "editor::DuplicateLine", + "cmd-shift-l": "editor::SplitSelectionIntoLines", + "ctrl-cmd-up": "editor::MoveLineUp", + "ctrl-cmd-down": "editor::MoveLineDown", + "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-b": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-right": "editor::MoveToNextSubwordEnd", + "ctrl-alt-f": "editor::MoveToNextSubwordEnd", + "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", + "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd" + } + }, + { + "bindings": { + "cmd-k cmd-left": "workspace::ActivatePreviousPane", + "cmd-k cmd-right": "workspace::ActivateNextPane" + } + }, + // Bindings from Atom + { + "context": "Pane", + "bindings": { + "cmd-k up": "pane::SplitUp", + "cmd-k down": "pane::SplitDown", + "cmd-k left": "pane::SplitLeft", + "cmd-k right": "pane::SplitRight" + } + }, + // Bindings that should be unified with bindings for more general actions + { + "context": "Editor && renaming", + "bindings": { + "enter": "editor::ConfirmRename" + } + }, + { + "context": "Editor && showing_completions", + "bindings": { + "enter": "editor::ConfirmCompletion", + "tab": "editor::ConfirmCompletion" + } + }, + { + "context": "Editor && showing_code_actions", + "bindings": { + "enter": "editor::ConfirmCodeAction" + } + }, + // Custom bindings + { + "bindings": { + "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", + "cmd-shift-c": "collab::ToggleContactsMenu", + "cmd-alt-i": "zed::DebugElements" + } + }, + { + "context": "Editor", + "bindings": { + "alt-enter": "editor::OpenExcerpts", + "cmd-f8": "editor::GoToHunk", + "cmd-shift-f8": "editor::GoToPrevHunk" + } + }, + { + "context": "ProjectSearchBar", + "bindings": { + "cmd-enter": "project_search::SearchInNew" + } + }, + { + "context": "Workspace", + "bindings": { + "shift-escape": "dock::FocusDock" + } + }, + { + "bindings": { + "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight", + "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom", + "cmd-shift-k cmd-shift-up": "dock::ExpandDock" + } + }, + { + "context": "Pane", + "bindings": { + "cmd-escape": "dock::AddTabToDock" + } + }, + { + "context": "Pane && docked", + "bindings": { + "shift-escape": "dock::HideDock", + "cmd-escape": "dock::RemoveTabFromDock" + } + }, + { + "context": "ProjectPanel", + "bindings": { + "left": "project_panel::CollapseSelectedEntry", + "right": "project_panel::ExpandSelectedEntry", + "cmd-x": "project_panel::Cut", + "cmd-c": "project_panel::Copy", + "cmd-v": "project_panel::Paste", + "cmd-alt-c": "project_panel::CopyPath", + "alt-cmd-shift-c": "project_panel::CopyRelativePath", + "f2": "project_panel::Rename", + "backspace": "project_panel::Delete", + "alt-cmd-r": "project_panel::RevealInFinder" + } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-cmd-space": "terminal::ShowCharacterPalette", + "cmd-c": "terminal::Copy", + "cmd-v": "terminal::Paste", + "cmd-k": "terminal::Clear", + // Some nice conveniences + "cmd-backspace": [ + "terminal::SendText", + "\u0015" + ], + "cmd-right": [ + "terminal::SendText", + "\u0005" + ], + "cmd-left": [ + "terminal::SendText", + "\u0001" + ], + // Terminal.app compatability + "alt-left": [ + "terminal::SendText", + "\u001bb" + ], + "alt-right": [ + "terminal::SendText", + "\u001bf" + ], + // There are conflicting bindings for these keys in the global context. + // these bindings override them, remove at your own risk: + "up": [ + "terminal::SendKeystroke", + "up" + ], + "pageup": [ + "terminal::SendKeystroke", + "pageup" + ], + "down": [ + "terminal::SendKeystroke", + "down" + ], + "pagedown": [ + "terminal::SendKeystroke", + "pagedown" + ], + "escape": [ + "terminal::SendKeystroke", + "escape" + ], + "enter": [ + "terminal::SendKeystroke", + "enter" + ], + "ctrl-c": [ + "terminal::SendKeystroke", + "ctrl-c" + ] + } + } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 332e3a7414..47c5f8c458 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1,325 +1,325 @@ [ - { - "context": "Editor && VimControl && !VimWaiting", - "bindings": { - "g": [ - "vim::PushOperator", - { - "Namespace": "G" - } - ], - "i": [ - "vim::PushOperator", - { - "Object": { - "around": false - } - } - ], - "a": [ - "vim::PushOperator", - { - "Object": { - "around": true - } - } - ], - "h": "vim::Left", - "backspace": "vim::Backspace", - "j": "vim::Down", - "enter": "vim::NextLineStart", - "k": "vim::Up", - "l": "vim::Right", - "$": "vim::EndOfLine", - "shift-g": "vim::EndOfDocument", - "w": "vim::NextWordStart", - "shift-w": [ - "vim::NextWordStart", - { - "ignorePunctuation": true - } - ], - "e": "vim::NextWordEnd", - "shift-e": [ - "vim::NextWordEnd", - { - "ignorePunctuation": true - } - ], - "b": "vim::PreviousWordStart", - "shift-b": [ - "vim::PreviousWordStart", - { - "ignorePunctuation": true - } - ], - "%": "vim::Matching", - "ctrl-y": [ - "vim::Scroll", - "LineUp" - ], - "f": [ - "vim::PushOperator", - { - "FindForward": { - "before": false - } - } - ], - "t": [ - "vim::PushOperator", - { - "FindForward": { - "before": true - } - } - ], - "shift-f": [ - "vim::PushOperator", - { - "FindBackward": { - "after": false - } - } - ], - "shift-t": [ - "vim::PushOperator", - { - "FindBackward": { - "after": true - } - } - ], - "escape": "editor::Cancel", - "0": "vim::StartOfLine", // When no number operator present, use start of line motion - "1": [ - "vim::Number", - 1 - ], - "2": [ - "vim::Number", - 2 - ], - "3": [ - "vim::Number", - 3 - ], - "4": [ - "vim::Number", - 4 - ], - "5": [ - "vim::Number", - 5 - ], - "6": [ - "vim::Number", - 6 - ], - "7": [ - "vim::Number", - 7 - ], - "8": [ - "vim::Number", - 8 - ], - "9": [ - "vim::Number", - 9 - ] + { + "context": "Editor && VimControl && !VimWaiting", + "bindings": { + "g": [ + "vim::PushOperator", + { + "Namespace": "G" } - }, - { - "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", - "bindings": { - "c": [ - "vim::PushOperator", - "Change" - ], - "shift-c": "vim::ChangeToEndOfLine", - "d": [ - "vim::PushOperator", - "Delete" - ], - "shift-d": "vim::DeleteToEndOfLine", - "y": [ - "vim::PushOperator", - "Yank" - ], - "z": [ - "vim::PushOperator", - { - "Namespace": "Z" - } - ], - "i": [ - "vim::SwitchMode", - "Insert" - ], - "shift-i": "vim::InsertFirstNonWhitespace", - "a": "vim::InsertAfter", - "shift-a": "vim::InsertEndOfLine", - "x": "vim::DeleteRight", - "shift-x": "vim::DeleteLeft", - "^": "vim::FirstNonWhitespace", - "o": "vim::InsertLineBelow", - "shift-o": "vim::InsertLineAbove", - "v": [ - "vim::SwitchMode", - { - "Visual": { - "line": false - } - } - ], - "shift-v": [ - "vim::SwitchMode", - { - "Visual": { - "line": true - } - } - ], - "p": "vim::Paste", - "u": "editor::Undo", - "ctrl-r": "editor::Redo", - "ctrl-o": "pane::GoBack", - "/": [ - "buffer_search::Deploy", - { - "focus": true - } - ], - "ctrl-f": [ - "vim::Scroll", - "PageDown" - ], - "ctrl-b": [ - "vim::Scroll", - "PageUp" - ], - "ctrl-d": [ - "vim::Scroll", - "HalfPageDown" - ], - "ctrl-u": [ - "vim::Scroll", - "HalfPageUp" - ], - "ctrl-e": [ - "vim::Scroll", - "LineDown" - ], - "r": [ - "vim::PushOperator", - "Replace" - ] + ], + "i": [ + "vim::PushOperator", + { + "Object": { + "around": false + } } - }, - { - "context": "Editor && vim_operator == n", - "bindings": { - "0": [ - "vim::Number", - 0 - ] + ], + "a": [ + "vim::PushOperator", + { + "Object": { + "around": true + } } - }, - { - "context": "Editor && vim_operator == g", - "bindings": { - "g": "vim::StartOfDocument", - "h": "editor::Hover", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "d": "editor::GoToDefinition" + ], + "h": "vim::Left", + "backspace": "vim::Backspace", + "j": "vim::Down", + "enter": "vim::NextLineStart", + "k": "vim::Up", + "l": "vim::Right", + "$": "vim::EndOfLine", + "shift-g": "vim::EndOfDocument", + "w": "vim::NextWordStart", + "shift-w": [ + "vim::NextWordStart", + { + "ignorePunctuation": true } - }, - { - "context": "Editor && vim_operator == c", - "bindings": { - "c": "vim::CurrentLine" + ], + "e": "vim::NextWordEnd", + "shift-e": [ + "vim::NextWordEnd", + { + "ignorePunctuation": true } - }, - { - "context": "Editor && vim_operator == d", - "bindings": { - "d": "vim::CurrentLine" + ], + "b": "vim::PreviousWordStart", + "shift-b": [ + "vim::PreviousWordStart", + { + "ignorePunctuation": true } - }, - { - "context": "Editor && vim_operator == y", - "bindings": { - "y": "vim::CurrentLine" + ], + "%": "vim::Matching", + "ctrl-y": [ + "vim::Scroll", + "LineUp" + ], + "f": [ + "vim::PushOperator", + { + "FindForward": { + "before": false + } } - }, - { - "context": "Editor && vim_operator == z", - "bindings": { - "t": "editor::ScrollCursorTop", - "z": "editor::ScrollCursorCenter", - "b": "editor::ScrollCursorBottom", - "escape": [ - "vim::SwitchMode", - "Normal" - ] + ], + "t": [ + "vim::PushOperator", + { + "FindForward": { + "before": true + } } - }, - { - "context": "Editor && VimObject", - "bindings": { - "w": "vim::Word", - "shift-w": [ - "vim::Word", - { - "ignorePunctuation": true - } - ], - "s": "vim::Sentence", - "'": "vim::Quotes", - "`": "vim::BackQuotes", - "\"": "vim::DoubleQuotes", - "(": "vim::Parentheses", - ")": "vim::Parentheses", - "[": "vim::SquareBrackets", - "]": "vim::SquareBrackets", - "{": "vim::CurlyBrackets", - "}": "vim::CurlyBrackets", - "<": "vim::AngleBrackets", - ">": "vim::AngleBrackets" + ], + "shift-f": [ + "vim::PushOperator", + { + "FindBackward": { + "after": false + } } - }, - { - "context": "Editor && vim_mode == visual && !VimWaiting", - "bindings": { - "u": "editor::Undo", - "c": "vim::VisualChange", - "d": "vim::VisualDelete", - "x": "vim::VisualDelete", - "y": "vim::VisualYank", - "p": "vim::VisualPaste", - "r": [ - "vim::PushOperator", - "Replace" - ] - } - }, - { - "context": "Editor && vim_mode == insert", - "bindings": { - "escape": "vim::NormalBefore", - "ctrl-c": "vim::NormalBefore" - } - }, - { - "context": "Editor && VimWaiting", - "bindings": { - "tab": "vim::Tab", - "enter": "vim::Enter", - "escape": "editor::Cancel" + ], + "shift-t": [ + "vim::PushOperator", + { + "FindBackward": { + "after": true + } } + ], + "escape": "editor::Cancel", + "0": "vim::StartOfLine", // When no number operator present, use start of line motion + "1": [ + "vim::Number", + 1 + ], + "2": [ + "vim::Number", + 2 + ], + "3": [ + "vim::Number", + 3 + ], + "4": [ + "vim::Number", + 4 + ], + "5": [ + "vim::Number", + 5 + ], + "6": [ + "vim::Number", + 6 + ], + "7": [ + "vim::Number", + 7 + ], + "8": [ + "vim::Number", + 8 + ], + "9": [ + "vim::Number", + 9 + ] } -] \ No newline at end of file + }, + { + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "bindings": { + "c": [ + "vim::PushOperator", + "Change" + ], + "shift-c": "vim::ChangeToEndOfLine", + "d": [ + "vim::PushOperator", + "Delete" + ], + "shift-d": "vim::DeleteToEndOfLine", + "y": [ + "vim::PushOperator", + "Yank" + ], + "z": [ + "vim::PushOperator", + { + "Namespace": "Z" + } + ], + "i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-i": "vim::InsertFirstNonWhitespace", + "a": "vim::InsertAfter", + "shift-a": "vim::InsertEndOfLine", + "x": "vim::DeleteRight", + "shift-x": "vim::DeleteLeft", + "^": "vim::FirstNonWhitespace", + "o": "vim::InsertLineBelow", + "shift-o": "vim::InsertLineAbove", + "v": [ + "vim::SwitchMode", + { + "Visual": { + "line": false + } + } + ], + "shift-v": [ + "vim::SwitchMode", + { + "Visual": { + "line": true + } + } + ], + "p": "vim::Paste", + "u": "editor::Undo", + "ctrl-r": "editor::Redo", + "ctrl-o": "pane::GoBack", + "/": [ + "buffer_search::Deploy", + { + "focus": true + } + ], + "ctrl-f": [ + "vim::Scroll", + "PageDown" + ], + "ctrl-b": [ + "vim::Scroll", + "PageUp" + ], + "ctrl-d": [ + "vim::Scroll", + "HalfPageDown" + ], + "ctrl-u": [ + "vim::Scroll", + "HalfPageUp" + ], + "ctrl-e": [ + "vim::Scroll", + "LineDown" + ], + "r": [ + "vim::PushOperator", + "Replace" + ] + } + }, + { + "context": "Editor && vim_operator == n", + "bindings": { + "0": [ + "vim::Number", + 0 + ] + } + }, + { + "context": "Editor && vim_operator == g", + "bindings": { + "g": "vim::StartOfDocument", + "h": "editor::Hover", + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "d": "editor::GoToDefinition" + } + }, + { + "context": "Editor && vim_operator == c", + "bindings": { + "c": "vim::CurrentLine" + } + }, + { + "context": "Editor && vim_operator == d", + "bindings": { + "d": "vim::CurrentLine" + } + }, + { + "context": "Editor && vim_operator == y", + "bindings": { + "y": "vim::CurrentLine" + } + }, + { + "context": "Editor && vim_operator == z", + "bindings": { + "t": "editor::ScrollCursorTop", + "z": "editor::ScrollCursorCenter", + "b": "editor::ScrollCursorBottom", + "escape": [ + "vim::SwitchMode", + "Normal" + ] + } + }, + { + "context": "Editor && VimObject", + "bindings": { + "w": "vim::Word", + "shift-w": [ + "vim::Word", + { + "ignorePunctuation": true + } + ], + "s": "vim::Sentence", + "'": "vim::Quotes", + "`": "vim::BackQuotes", + "\"": "vim::DoubleQuotes", + "(": "vim::Parentheses", + ")": "vim::Parentheses", + "[": "vim::SquareBrackets", + "]": "vim::SquareBrackets", + "{": "vim::CurlyBrackets", + "}": "vim::CurlyBrackets", + "<": "vim::AngleBrackets", + ">": "vim::AngleBrackets" + } + }, + { + "context": "Editor && vim_mode == visual && !VimWaiting", + "bindings": { + "u": "editor::Undo", + "c": "vim::VisualChange", + "d": "vim::VisualDelete", + "x": "vim::VisualDelete", + "y": "vim::VisualYank", + "p": "vim::VisualPaste", + "r": [ + "vim::PushOperator", + "Replace" + ] + } + }, + { + "context": "Editor && vim_mode == insert", + "bindings": { + "escape": "vim::NormalBefore", + "ctrl-c": "vim::NormalBefore" + } + }, + { + "context": "Editor && VimWaiting", + "bindings": { + "tab": "vim::Tab", + "enter": "vim::Enter", + "escape": "editor::Cancel" + } + } +] diff --git a/assets/settings/default.json b/assets/settings/default.json index 09f987cd9a..f3ddba1e42 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,257 +1,257 @@ { - // The name of the Zed theme to use for the UI - "theme": "One Dark", - // Features that can be globally enabled or disabled - "features": { - // Show Copilot icon in status bar - "copilot": true - }, - // The name of a font to use for rendering text in the editor - "buffer_font_family": "Zed Mono", - // The OpenType features to enable for text in the editor. - "buffer_font_features": { - // Disable ligatures: - // "calt": false - }, - // The default font size for text in the editor - "buffer_font_size": 15, - // The factor to grow the active pane by. Defaults to 1.0 - // which gives the same size as all other panes. - "active_pane_magnification": 1.0, - // Whether to enable vim modes and key bindings - "vim_mode": false, - // Whether to show the informational hover box when moving the mouse - // over symbols in the editor. - "hover_popover_enabled": true, - // Whether to confirm before quitting Zed. - "confirm_quit": false, - // Whether the cursor blinks in the editor. - "cursor_blink": true, - // Whether to pop the completions menu while typing in an editor without - // explicitly requesting it. - "show_completions_on_input": true, - // Controls whether copilot provides suggestion immediately - // or waits for a `copilot::Toggle` - "show_copilot_suggestions": true, - // Whether the screen sharing icon is shown in the os status bar. - "show_call_status_icon": true, - // Whether to use language servers to provide code intelligence. - "enable_language_server": true, - // When to automatically save edited buffers. This setting can - // take four values. - // - // 1. Never automatically save: - // "autosave": "off", - // 2. Save when changing focus away from the Zed window: - // "autosave": "on_window_change", - // 3. Save when changing focus away from a specific buffer: - // "autosave": "on_focus_change", - // 4. Save when idle for a certain amount of time: - // "autosave": { "after_delay": {"milliseconds": 500} }, - "autosave": "off", - // Where to place the dock by default. This setting can take three - // values: - // - // 1. Position the dock attached to the bottom of the workspace - // "default_dock_anchor": "bottom" - // 2. Position the dock to the right of the workspace like a side panel - // "default_dock_anchor": "right" - // 3. Position the dock full screen over the entire workspace" - // "default_dock_anchor": "expanded" - "default_dock_anchor": "bottom", - // Whether or not to remove any trailing whitespace from lines of a buffer - // before saving it. - "remove_trailing_whitespace_on_save": true, - // Whether or not to ensure there's a single newline at the end of a buffer - // when saving it. - "ensure_final_newline_on_save": true, - // Whether or not to perform a buffer format before saving - "format_on_save": "on", - // How to perform a buffer format. This setting can take two values: - // - // 1. Format code using the current language server: - // "format_on_save": "language_server" - // 2. Format code using an external command: - // "format_on_save": { - // "external": { - // "command": "prettier", - // "arguments": ["--stdin-filepath", "{buffer_path}"] - // } + // The name of the Zed theme to use for the UI + "theme": "One Dark", + // Features that can be globally enabled or disabled + "features": { + // Show Copilot icon in status bar + "copilot": true + }, + // The name of a font to use for rendering text in the editor + "buffer_font_family": "Zed Mono", + // The OpenType features to enable for text in the editor. + "buffer_font_features": { + // Disable ligatures: + // "calt": false + }, + // The default font size for text in the editor + "buffer_font_size": 15, + // The factor to grow the active pane by. Defaults to 1.0 + // which gives the same size as all other panes. + "active_pane_magnification": 1.0, + // Whether to enable vim modes and key bindings + "vim_mode": false, + // Whether to show the informational hover box when moving the mouse + // over symbols in the editor. + "hover_popover_enabled": true, + // Whether to confirm before quitting Zed. + "confirm_quit": false, + // Whether the cursor blinks in the editor. + "cursor_blink": true, + // Whether to pop the completions menu while typing in an editor without + // explicitly requesting it. + "show_completions_on_input": true, + // Controls whether copilot provides suggestion immediately + // or waits for a `copilot::Toggle` + "show_copilot_suggestions": true, + // Whether the screen sharing icon is shown in the os status bar. + "show_call_status_icon": true, + // Whether to use language servers to provide code intelligence. + "enable_language_server": true, + // When to automatically save edited buffers. This setting can + // take four values. + // + // 1. Never automatically save: + // "autosave": "off", + // 2. Save when changing focus away from the Zed window: + // "autosave": "on_window_change", + // 3. Save when changing focus away from a specific buffer: + // "autosave": "on_focus_change", + // 4. Save when idle for a certain amount of time: + // "autosave": { "after_delay": {"milliseconds": 500} }, + "autosave": "off", + // Where to place the dock by default. This setting can take three + // values: + // + // 1. Position the dock attached to the bottom of the workspace + // "default_dock_anchor": "bottom" + // 2. Position the dock to the right of the workspace like a side panel + // "default_dock_anchor": "right" + // 3. Position the dock full screen over the entire workspace" + // "default_dock_anchor": "expanded" + "default_dock_anchor": "bottom", + // Whether or not to remove any trailing whitespace from lines of a buffer + // before saving it. + "remove_trailing_whitespace_on_save": true, + // Whether or not to ensure there's a single newline at the end of a buffer + // when saving it. + "ensure_final_newline_on_save": true, + // Whether or not to perform a buffer format before saving + "format_on_save": "on", + // How to perform a buffer format. This setting can take two values: + // + // 1. Format code using the current language server: + // "format_on_save": "language_server" + // 2. Format code using an external command: + // "format_on_save": { + // "external": { + // "command": "prettier", + // "arguments": ["--stdin-filepath", "{buffer_path}"] + // } + // } + "formatter": "language_server", + // How to soft-wrap long lines of text. This setting can take + // three values: + // + // 1. Do not soft wrap. + // "soft_wrap": "none", + // 2. Soft wrap lines that overflow the editor: + // "soft_wrap": "editor_width", + // 3. Soft wrap lines at the preferred line length + // "soft_wrap": "preferred_line_length", + "soft_wrap": "none", + // The column at which to soft-wrap lines, for buffers where soft-wrap + // is enabled. + "preferred_line_length": 80, + // Whether to indent lines using tab characters, as opposed to multiple + // spaces. + "hard_tabs": false, + // How many columns a tab should occupy. + "tab_size": 4, + // Control what info is collected by Zed. + "telemetry": { + // Send debug info like crash reports. + "diagnostics": true, + // Send anonymized usage data like what languages you're using Zed with. + "metrics": true + }, + // Automatically update Zed + "auto_update": true, + // Git gutter behavior configuration. + "git": { + // Control whether the git gutter is shown. May take 2 values: + // 1. Show the gutter + // "git_gutter": "tracked_files" + // 2. Hide the gutter + // "git_gutter": "hide" + "git_gutter": "tracked_files" + }, + // Settings specific to journaling + "journal": { + // The path of the directory where journal entries are stored + "path": "~", + // What format to display the hours in + // May take 2 values: + // 1. hour12 + // 2. hour24 + "hour_format": "hour12" + }, + // Settings specific to the terminal + "terminal": { + // What shell to use when opening a terminal. May take 3 values: + // 1. Use the system's default terminal configuration in /etc/passwd + // "shell": "system" + // 2. A program: + // "shell": { + // "program": "sh" + // } + // 3. A program with arguments: + // "shell": { + // "with_arguments": { + // "program": "/bin/bash", + // "arguments": ["--login"] + // } // } - "formatter": "language_server", - // How to soft-wrap long lines of text. This setting can take - // three values: + "shell": "system", + // What working directory to use when launching the terminal. + // May take 4 values: + // 1. Use the current file's project directory. Will Fallback to the + // first project directory strategy if unsuccessful + // "working_directory": "current_project_directory" + // 2. Use the first project in this workspace's directory + // "working_directory": "first_project_directory" + // 3. Always use this platform's home directory (if we can find it) + // "working_directory": "always_home" + // 4. Always use a specific directory. This value will be shell expanded. + // If this path is not a valid directory the terminal will default to + // this platform's home directory (if we can find it) + // "working_directory": { + // "always": { + // "directory": "~/zed/projects/" + // } + // } // - // 1. Do not soft wrap. - // "soft_wrap": "none", - // 2. Soft wrap lines that overflow the editor: - // "soft_wrap": "editor_width", - // 3. Soft wrap lines at the preferred line length - // "soft_wrap": "preferred_line_length", - "soft_wrap": "none", - // The column at which to soft-wrap lines, for buffers where soft-wrap - // is enabled. - "preferred_line_length": 80, - // Whether to indent lines using tab characters, as opposed to multiple - // spaces. - "hard_tabs": false, - // How many columns a tab should occupy. - "tab_size": 4, - // Control what info is collected by Zed. - "telemetry": { - // Send debug info like crash reports. - "diagnostics": true, - // Send anonymized usage data like what languages you're using Zed with. - "metrics": true - }, - // Automatically update Zed - "auto_update": true, - // Git gutter behavior configuration. - "git": { - // Control whether the git gutter is shown. May take 2 values: - // 1. Show the gutter - // "git_gutter": "tracked_files" - // 2. Hide the gutter - // "git_gutter": "hide" - "git_gutter": "tracked_files" - }, - // Settings specific to journaling - "journal": { - // The path of the directory where journal entries are stored - "path": "~", - // What format to display the hours in - // May take 2 values: - // 1. hour12 - // 2. hour24 - "hour_format": "hour12" - }, - // Settings specific to the terminal - "terminal": { - // What shell to use when opening a terminal. May take 3 values: - // 1. Use the system's default terminal configuration in /etc/passwd - // "shell": "system" - // 2. A program: - // "shell": { - // "program": "sh" - // } - // 3. A program with arguments: - // "shell": { - // "with_arguments": { - // "program": "/bin/bash", - // "arguments": ["--login"] - // } - // } - "shell": "system", - // What working directory to use when launching the terminal. - // May take 4 values: - // 1. Use the current file's project directory. Will Fallback to the - // first project directory strategy if unsuccessful - // "working_directory": "current_project_directory" - // 2. Use the first project in this workspace's directory - // "working_directory": "first_project_directory" - // 3. Always use this platform's home directory (if we can find it) - // "working_directory": "always_home" - // 4. Always use a specific directory. This value will be shell expanded. - // If this path is not a valid directory the terminal will default to - // this platform's home directory (if we can find it) - // "working_directory": { - // "always": { - // "directory": "~/zed/projects/" - // } - // } - // - // - "working_directory": "current_project_directory", - // Set the cursor blinking behavior in the terminal. - // May take 4 values: - // 1. Never blink the cursor, ignoring the terminal mode - // "blinking": "off", - // 2. Default the cursor blink to off, but allow the terminal to - // set blinking - // "blinking": "terminal_controlled", - // 3. Always blink the cursor, ignoring the terminal mode - // "blinking": "on", - "blinking": "terminal_controlled", - // Set whether Alternate Scroll mode (code: ?1007) is active by default. - // Alternate Scroll mode converts mouse scroll events into up / down key - // presses when in the alternate screen (e.g. when running applications - // like vim or less). The terminal can still set and unset this mode. - // May take 2 values: - // 1. Default alternate scroll mode to on - // "alternate_scroll": "on", - // 2. Default alternate scroll mode to off - // "alternate_scroll": "off", - "alternate_scroll": "off", - // Set whether the option key behaves as the meta key. - // May take 2 values: - // 1. Rely on default platform handling of option key, on macOS - // this means generating certain unicode characters - // "option_to_meta": false, - // 2. Make the option keys behave as a 'meta' key, e.g. for emacs - // "option_to_meta": true, - "option_as_meta": false, - // Whether or not selecting text in the terminal will automatically - // copy to the system clipboard. - "copy_on_select": false, - // Any key-value pairs added to this list will be added to the terminal's - // enviroment. Use `:` to seperate multiple values. - "env": { - // "KEY": "value1:value2" - } - // Set the terminal's font size. If this option is not included, - // the terminal will default to matching the buffer's font size. - // "font_size": "15" - // Set the terminal's font family. If this option is not included, - // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" - }, - // Different settings for specific languages. - "languages": { - "Plain Text": { - "soft_wrap": "preferred_line_length" - }, - "Elixir": { - "tab_size": 2 - }, - "Go": { - "tab_size": 4, - "hard_tabs": true - }, - "Markdown": { - "soft_wrap": "preferred_line_length" - }, - "JavaScript": { - "tab_size": 2 - }, - "TypeScript": { - "tab_size": 2 - }, - "TSX": { - "tab_size": 2 - }, - "YAML": { - "tab_size": 2 - }, - "JSON": { - "tab_size": 2 - } - }, - // LSP Specific settings. - "lsp": { - // Specify the LSP name as a key here. - // As of 8/10/22, supported LSPs are: - // pyright - // gopls - // rust-analyzer - // typescript-language-server - // vscode-json-languageserver - // "rust-analyzer": { - // //These initialization options are merged into Zed's defaults - // "initialization_options": { - // "checkOnSave": { - // "command": "clippy" - // } - // } - // } + // + "working_directory": "current_project_directory", + // Set the cursor blinking behavior in the terminal. + // May take 4 values: + // 1. Never blink the cursor, ignoring the terminal mode + // "blinking": "off", + // 2. Default the cursor blink to off, but allow the terminal to + // set blinking + // "blinking": "terminal_controlled", + // 3. Always blink the cursor, ignoring the terminal mode + // "blinking": "on", + "blinking": "terminal_controlled", + // Set whether Alternate Scroll mode (code: ?1007) is active by default. + // Alternate Scroll mode converts mouse scroll events into up / down key + // presses when in the alternate screen (e.g. when running applications + // like vim or less). The terminal can still set and unset this mode. + // May take 2 values: + // 1. Default alternate scroll mode to on + // "alternate_scroll": "on", + // 2. Default alternate scroll mode to off + // "alternate_scroll": "off", + "alternate_scroll": "off", + // Set whether the option key behaves as the meta key. + // May take 2 values: + // 1. Rely on default platform handling of option key, on macOS + // this means generating certain unicode characters + // "option_to_meta": false, + // 2. Make the option keys behave as a 'meta' key, e.g. for emacs + // "option_to_meta": true, + "option_as_meta": false, + // Whether or not selecting text in the terminal will automatically + // copy to the system clipboard. + "copy_on_select": false, + // Any key-value pairs added to this list will be added to the terminal's + // enviroment. Use `:` to seperate multiple values. + "env": { + // "KEY": "value1:value2" } + // Set the terminal's font size. If this option is not included, + // the terminal will default to matching the buffer's font size. + // "font_size": "15" + // Set the terminal's font family. If this option is not included, + // the terminal will default to matching the buffer's font family. + // "font_family": "Zed Mono" + }, + // Different settings for specific languages. + "languages": { + "Plain Text": { + "soft_wrap": "preferred_line_length" + }, + "Elixir": { + "tab_size": 2 + }, + "Go": { + "tab_size": 4, + "hard_tabs": true + }, + "Markdown": { + "soft_wrap": "preferred_line_length" + }, + "JavaScript": { + "tab_size": 2 + }, + "TypeScript": { + "tab_size": 2 + }, + "TSX": { + "tab_size": 2 + }, + "YAML": { + "tab_size": 2 + }, + "JSON": { + "tab_size": 2 + } + }, + // LSP Specific settings. + "lsp": { + // Specify the LSP name as a key here. + // As of 8/10/22, supported LSPs are: + // pyright + // gopls + // rust-analyzer + // typescript-language-server + // vscode-json-languageserver + // "rust-analyzer": { + // //These initialization options are merged into Zed's defaults + // "initialization_options": { + // "checkOnSave": { + // "command": "clippy" + // } + // } + // } + } } diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 4c90b8a002..dc79fd7911 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -7,5 +7,5 @@ // custom settings, run the `open default settings` command // from the command palette or from `Zed` application menu. { - "buffer_font_size": 15 + "buffer_font_size": 15 }