From c485fc86a21567d694fd8b25d2f7b450f0d9241c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 27 Apr 2023 18:06:52 -0700 Subject: [PATCH] Add copilot.disabled_globs setting --- Cargo.lock | 2 + Cargo.toml | 1 + assets/settings/default.json | 7 +++ crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 37 +++++++++++-- crates/editor/src/editor_tests.rs | 91 +++++++++++++++++++++++++++++++ crates/editor/src/multi_buffer.rs | 11 +++- crates/project/Cargo.toml | 2 +- crates/settings/Cargo.toml | 1 + crates/settings/src/settings.rs | 57 ++++++++++--------- 10 files changed, 176 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b6ebbe5ed..af780197e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1983,6 +1983,7 @@ dependencies = [ "futures 0.3.25", "fuzzy", "git", + "glob", "gpui", "indoc", "itertools", @@ -5964,6 +5965,7 @@ dependencies = [ "collections", "fs", "futures 0.3.25", + "glob", "gpui", "json_comments", "postage", diff --git a/Cargo.toml b/Cargo.toml index 1ef283d135..15df687d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ async-trait = { version = "0.1" } ctor = { version = "0.1" } env_logger = { version = "0.9" } futures = { version = "0.3" } +glob = { version = "0.3.1" } lazy_static = { version = "1.4.0" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } diff --git a/assets/settings/default.json b/assets/settings/default.json index b12bd00efa..b72dab05c7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -115,6 +115,13 @@ // "git_gutter": "hide" "git_gutter": "tracked_files" }, + "copilot": { + // The set of glob patterns for which copilot should be disabled + // in any matching file. + "disabled_globs": [ + ".env" + ] + }, // Settings specific to journaling "journal": { // The path of the directory where journal entries are stored diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index feb55e1b2f..e8cb323a27 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -79,6 +79,7 @@ workspace = { path = "../workspace", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true +glob.workspace = true rand.workspace = true unindent.workspace = true tree-sitter = "0.20" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eea418b211..9128cdb0ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2925,11 +2925,7 @@ 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::() - .show_copilot_suggestions(language_name.as_deref()) - { + if !self.is_copilot_enabled_at(cursor, &snapshot, cx) { self.clear_copilot_suggestions(cx); return None; } @@ -3080,6 +3076,37 @@ impl Editor { } } + fn is_copilot_enabled_at( + &self, + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) -> bool { + let settings = cx.global::(); + + let language_name = snapshot + .language_at(location) + .map(|language| language.name()); + if !settings.show_copilot_suggestions(language_name.as_deref()) { + return false; + } + + let file = snapshot.file_at(location); + if let Some(file) = file { + let path = file.path(); + if settings + .copilot + .disabled_globs + .iter() + .any(|glob| glob.matches_path(path)) + { + return false; + } + } + + true + } + fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { self.display_map.read(cx).has_suggestion() } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index af4bc1674b..3099bb640d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6387,6 +6387,97 @@ async fn test_copilot_multibuffer( }); } +#[gpui::test] +async fn test_copilot_disabled_globs( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| { + let mut settings = Settings::test(cx); + settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()]; + cx.set_global(settings); + cx.set_global(copilot) + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/test", + json!({ + ".env": "SECRET=something\n", + "README.md": "hello\n" + }), + ) + .await; + let project = Project::test(fs, ["/test".as_ref()], cx).await; + + let private_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/.env", cx) + }) + .await + .unwrap(); + let public_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/test/README.md", cx) + }) + .await + .unwrap(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + private_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + public_buffer.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 0), + primary: None, + }], + cx, + ); + multibuffer + }); + let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); + + let mut copilot_requests = copilot_lsp + .handle_request::(move |_params, _cx| async move { + Ok(copilot::request::GetCompletionsResult { + completions: vec![copilot::request::Completion { + text: "next line".into(), + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), + ..Default::default() + }], + }) + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_err()); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + }); + editor.next_copilot_suggestion(&Default::default(), cx); + }); + + deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); + assert!(copilot_requests.try_next().is_ok()); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bf2f12e82e..39e66d0f93 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -10,9 +10,9 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, - DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, - OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, + DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, + Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, + ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, }; use std::{ borrow::Cow, @@ -2754,6 +2754,11 @@ impl MultiBufferSnapshot { self.trailing_excerpt_update_count } + pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + self.point_to_buffer_offset(point) + .and_then(|(buffer, _)| buffer.file()) + } + pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_at(offset)) diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 56803bb062..46b00fc6ee 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -28,7 +28,6 @@ fs = { path = "../fs" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } git = { path = "../git" } -glob = { version = "0.3.1" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } @@ -43,6 +42,7 @@ anyhow.workspace = true async-trait.workspace = true backtrace = "0.3" futures.workspace = true +glob.workspace = true ignore = "0.4" lazy_static.workspace = true log.workspace = true diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 3a87832e15..00f6cda3a7 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -23,6 +23,7 @@ theme = { path = "../theme" } staff_mode = { path = "../staff_mode" } util = { path = "../util" } +glob.workspace = true json_comments = "0.2" postage.workspace = true schemars = "0.8" diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f2082be6bb..260f9531a8 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -47,6 +47,7 @@ pub struct Settings { pub editor_overrides: EditorSettings, pub git: GitSettings, pub git_overrides: GitSettings, + pub copilot: CopilotSettings, pub journal_defaults: JournalSettings, pub journal_overrides: JournalSettings, pub terminal_defaults: TerminalSettings, @@ -61,29 +62,6 @@ pub struct Settings { pub base_keymap: BaseKeymap, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CopilotSettings { - #[default] - On, - Off, -} - -impl From for bool { - fn from(value: CopilotSettings) -> Self { - match value { - CopilotSettings::On => true, - CopilotSettings::Off => false, - } - } -} - -impl CopilotSettings { - pub fn is_on(&self) -> bool { - >::into(*self) - } -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { #[default] @@ -150,6 +128,29 @@ impl TelemetrySettings { } } +#[derive(Clone, Debug, Default)] +pub struct CopilotSettings { + pub disabled_globs: Vec, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CopilotSettingsContent { + #[serde(default)] + disabled_globs: Vec, +} + +impl From for CopilotSettings { + fn from(value: CopilotSettingsContent) -> Self { + Self { + disabled_globs: value + .disabled_globs + .into_iter() + .filter_map(|p| glob::Pattern::new(&p).ok()) + .collect(), + } + } +} + #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, @@ -390,6 +391,8 @@ pub struct SettingsFileContent { #[serde(default)] pub buffer_font_features: Option, #[serde(default)] + pub copilot: Option, + #[serde(default)] pub active_pane_magnification: Option, #[serde(default)] pub cursor_blink: Option, @@ -438,8 +441,7 @@ pub struct LspSettings { pub initialization_options: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Features { pub copilot: bool, } @@ -506,6 +508,7 @@ impl Settings { show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), }, editor_overrides: Default::default(), + copilot: defaults.copilot.unwrap().into(), git: defaults.git.unwrap(), git_overrides: Default::default(), journal_defaults: defaults.journal, @@ -576,6 +579,9 @@ impl Settings { merge(&mut self.base_keymap, data.base_keymap); merge(&mut self.features.copilot, data.features.copilot); + if let Some(copilot) = data.copilot.map(CopilotSettings::from) { + self.copilot = copilot; + } self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); self.journal_overrides = data.journal; @@ -751,6 +757,7 @@ impl Settings { show_copilot_suggestions: Some(true), }, editor_overrides: Default::default(), + copilot: Default::default(), journal_defaults: Default::default(), journal_overrides: Default::default(), terminal_defaults: Default::default(),