Detailed changes
@@ -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",
@@ -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" }
@@ -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
@@ -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"
@@ -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::<Settings>()
- .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<Self>,
+ ) -> bool {
+ let settings = cx.global::<Settings>();
+
+ 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()
}
@@ -6387,6 +6387,97 @@ async fn test_copilot_multibuffer(
});
}
+#[gpui::test]
+async fn test_copilot_disabled_globs(
+ deterministic: Arc<Deterministic>,
+ 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::<copilot::request::GetCompletions, _, _>(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<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -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<dyn File>> {
+ 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<Language>> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_at(offset))
@@ -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
@@ -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"
@@ -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<CopilotSettings> for bool {
- fn from(value: CopilotSettings) -> Self {
- match value {
- CopilotSettings::On => true,
- CopilotSettings::Off => false,
- }
- }
-}
-
-impl CopilotSettings {
- pub fn is_on(&self) -> bool {
- <CopilotSettings as Into<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<glob::Pattern>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct CopilotSettingsContent {
+ #[serde(default)]
+ disabled_globs: Vec<String>,
+}
+
+impl From<CopilotSettingsContent> 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<GitGutter>,
@@ -390,6 +391,8 @@ pub struct SettingsFileContent {
#[serde(default)]
pub buffer_font_features: Option<fonts::Features>,
#[serde(default)]
+ pub copilot: Option<CopilotSettingsContent>,
+ #[serde(default)]
pub active_pane_magnification: Option<f32>,
#[serde(default)]
pub cursor_blink: Option<bool>,
@@ -438,8 +441,7 @@ pub struct LspSettings {
pub initialization_options: Option<Value>,
}
-#[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(),