Merge branch 'main' into implicit-ancestry

Antonio Scandurra created

Change summary

Cargo.lock                                       |  10 
Cargo.toml                                       |   1 
assets/settings/default.json                     |   7 
crates/collab/Cargo.toml                         |   2 
crates/collab_ui/src/contact_list.rs             |   7 
crates/context_menu/src/context_menu.rs          |   7 
crates/copilot_button/Cargo.toml                 |   1 
crates/copilot_button/src/copilot_button.rs      | 121 ++++
crates/editor/Cargo.toml                         |   1 
crates/editor/src/editor.rs                      |  62 ++
crates/editor/src/editor_tests.rs                |  91 ++++
crates/editor/src/multi_buffer.rs                |  11 
crates/feedback/Cargo.toml                       |  18 
crates/feedback/src/feedback_editor.rs           |   2 
crates/gpui/src/app.rs                           | 162 +++---
crates/gpui/src/app/window.rs                    |  24 
crates/gpui/src/elements/keystroke_label.rs      |   2 
crates/gpui/src/keymap_matcher/keymap_context.rs |   5 
crates/picker/src/picker.rs                      |   7 
crates/project/Cargo.toml                        |   2 
crates/project_panel/src/project_panel.rs        |   7 
crates/settings/Cargo.toml                       |   2 
crates/settings/src/settings.rs                  | 377 +++++++++++------
crates/settings/src/settings_file.rs             |  42 +
crates/terminal_view/src/terminal_view.rs        |  35 
crates/vim/src/vim.rs                            |   4 
crates/workspace/Cargo.toml                      |   1 
crates/workspace/src/pane.rs                     |   5 
crates/workspace/src/workspace.rs                |  53 ++
crates/zed/Cargo.toml                            |   2 
crates/zed/src/main.rs                           |   5 
crates/zed/src/menus.rs                          |   2 
crates/zed/src/zed.rs                            |  60 --
33 files changed, 746 insertions(+), 392 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1189,7 +1189,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.10.0"
+version = "0.11.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1362,6 +1362,7 @@ name = "copilot_button"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "assets",
  "context_menu",
  "copilot",
  "editor",
@@ -1984,6 +1985,7 @@ dependencies = [
  "futures 0.3.25",
  "fuzzy",
  "git",
+ "glob",
  "gpui",
  "indoc",
  "itertools",
@@ -2155,6 +2157,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "settings",
+ "smallvec",
  "sysinfo",
  "theme",
  "tree-sitter-markdown",
@@ -5965,8 +5968,10 @@ dependencies = [
  "collections",
  "fs",
  "futures 0.3.25",
+ "glob",
  "gpui",
  "json_comments",
+ "lazy_static",
  "postage",
  "pretty_assertions",
  "schemars",
@@ -8454,6 +8459,7 @@ name = "workspace"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "assets",
  "async-recursion 1.0.0",
  "bincode",
  "call",
@@ -8540,7 +8546,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.85.0"
+version = "0.86.0"
 dependencies = [
  "activity_indicator",
  "anyhow",

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" }

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

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.10.0"
+version = "0.11.0"
 publish = false
 
 [[bin]]

crates/collab_ui/src/contact_list.rs 🔗

@@ -1306,10 +1306,9 @@ impl View for ContactList {
         "ContactList"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut cx = Self::default_keymap_context();
-        cx.add_identifier("menu");
-        cx
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
+        keymap.add_identifier("menu");
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

crates/context_menu/src/context_menu.rs 🔗

@@ -140,10 +140,9 @@ impl View for ContextMenu {
         "ContextMenu"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut cx = Self::default_keymap_context();
-        cx.add_identifier("menu");
-        cx
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
+        keymap.add_identifier("menu");
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

crates/copilot_button/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/copilot_button.rs"
 doctest = false
 
 [dependencies]
+assets = { path = "../assets" }
 copilot = { path = "../copilot" }
 editor = { path = "../editor" }
 context_menu = { path = "../context_menu" }

crates/copilot_button/src/copilot_button.rs 🔗

@@ -1,18 +1,19 @@
+use anyhow::Result;
 use context_menu::{ContextMenu, ContextMenuItem};
 use copilot::{Copilot, SignOut, Status};
-use editor::Editor;
+use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
     elements::*,
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, Entity, MouseState, Subscription, View, ViewContext,
-    ViewHandle, WindowContext,
+    AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
+    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use settings::{settings_file::SettingsFile, Settings};
-use std::sync::Arc;
-use util::ResultExt;
+use std::{path::Path, sync::Arc};
+use util::{paths, ResultExt};
 use workspace::{
-    item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, Toast,
-    Workspace,
+    create_and_open_local_file, item::ItemHandle,
+    notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
 };
 
 const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
@@ -24,6 +25,7 @@ pub struct CopilotButton {
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
     language: Option<Arc<str>>,
+    path: Option<Arc<Path>>,
 }
 
 impl Entity for CopilotButton {
@@ -51,7 +53,7 @@ impl View for CopilotButton {
 
         let enabled = self
             .editor_enabled
-            .unwrap_or(settings.show_copilot_suggestions(None));
+            .unwrap_or(settings.show_copilot_suggestions(None, None));
 
         Stack::new()
             .with_child(
@@ -161,6 +163,7 @@ impl CopilotButton {
             editor_subscription: None,
             editor_enabled: None,
             language: None,
+            path: None,
         }
     }
 
@@ -187,10 +190,10 @@ impl CopilotButton {
     pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
         let settings = cx.global::<Settings>();
 
-        let mut menu_options = Vec::with_capacity(6);
+        let mut menu_options = Vec::with_capacity(8);
 
         if let Some(language) = self.language.clone() {
-            let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
+            let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
             menu_options.push(ContextMenuItem::handler(
                 format!(
                     "{} Suggestions for {}",
@@ -201,7 +204,31 @@ impl CopilotButton {
             ));
         }
 
-        let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
+        if let Some(path) = self.path.as_ref() {
+            let path_enabled = settings.copilot_enabled_for_path(path);
+            let path = path.clone();
+            menu_options.push(ContextMenuItem::handler(
+                format!(
+                    "{} Suggestions for This Path",
+                    if path_enabled { "Hide" } else { "Show" }
+                ),
+                move |cx| {
+                    if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
+                        let workspace = workspace.downgrade();
+                        cx.spawn(|_, cx| {
+                            configure_disabled_globs(
+                                workspace,
+                                path_enabled.then_some(path.clone()),
+                                cx,
+                            )
+                        })
+                        .detach_and_log_err(cx);
+                    }
+                },
+            ));
+        }
+
+        let globally_enabled = cx.global::<Settings>().features.copilot;
         menu_options.push(ContextMenuItem::handler(
             if globally_enabled {
                 "Hide Suggestions for All Files"
@@ -247,10 +274,14 @@ impl CopilotButton {
         let language_name = snapshot
             .language_at(suggestion_anchor)
             .map(|language| language.name());
+        let path = snapshot
+            .file_at(suggestion_anchor)
+            .map(|file| file.path().clone());
 
-        self.language = language_name.clone();
-
-        self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
+        self.editor_enabled =
+            Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref()));
+        self.language = language_name;
+        self.path = path;
 
         cx.notify()
     }
@@ -271,8 +302,62 @@ impl StatusItemView for CopilotButton {
     }
 }
 
+async fn configure_disabled_globs(
+    workspace: WeakViewHandle<Workspace>,
+    path_to_disable: Option<Arc<Path>>,
+    mut cx: AsyncAppContext,
+) -> Result<()> {
+    let settings_editor = workspace
+        .update(&mut cx, |_, cx| {
+            create_and_open_local_file(&paths::SETTINGS, cx, || {
+                Settings::initial_user_settings_content(&assets::Assets)
+                    .as_ref()
+                    .into()
+            })
+        })?
+        .await?
+        .downcast::<Editor>()
+        .unwrap();
+
+    settings_editor.downgrade().update(&mut cx, |item, cx| {
+        let text = item.buffer().read(cx).snapshot(cx).text();
+
+        let edits = SettingsFile::update_unsaved(&text, cx, |file| {
+            let copilot = file.copilot.get_or_insert_with(Default::default);
+            let globs = copilot.disabled_globs.get_or_insert_with(|| {
+                cx.global::<Settings>()
+                    .copilot
+                    .disabled_globs
+                    .clone()
+                    .iter()
+                    .map(|glob| glob.as_str().to_string())
+                    .collect::<Vec<_>>()
+            });
+
+            if let Some(path_to_disable) = &path_to_disable {
+                globs.push(path_to_disable.to_string_lossy().into_owned());
+            } else {
+                globs.clear();
+            }
+        });
+
+        if !edits.is_empty() {
+            item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+                selections.select_ranges(edits.iter().map(|e| e.0.clone()));
+            });
+
+            // When *enabling* a path, don't actually perform an edit, just select the range.
+            if path_to_disable.is_some() {
+                item.edit(edits.iter().cloned(), cx);
+            }
+        }
+    })?;
+
+    anyhow::Ok(())
+}
+
 fn toggle_copilot_globally(cx: &mut AppContext) {
-    let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
+    let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
     SettingsFile::update(cx, move |file_contents| {
         file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
     });
@@ -281,7 +366,7 @@ fn toggle_copilot_globally(cx: &mut AppContext) {
 fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
     let show_copilot_suggestions = cx
         .global::<Settings>()
-        .show_copilot_suggestions(Some(&language));
+        .show_copilot_suggestions(Some(&language), None);
 
     SettingsFile::update(cx, move |file_contents| {
         file_contents.languages.insert(
@@ -291,13 +376,13 @@ fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
                 ..Default::default()
             },
         );
-    })
+    });
 }
 
 fn hide_copilot(cx: &mut AppContext) {
     SettingsFile::update(cx, move |file_contents| {
         file_contents.features.copilot = Some(false)
-    })
+    });
 }
 
 fn initiate_sign_in(cx: &mut WindowContext) {

crates/editor/Cargo.toml 🔗

@@ -80,6 +80,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"

crates/editor/src/editor.rs 🔗

@@ -52,8 +52,8 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
-    Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
-    Point, Selection, SelectionGoal, TransactionId,
+    Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
+    OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@@ -1380,6 +1380,10 @@ impl Editor {
         self.buffer.read(cx).language_at(point, cx)
     }
 
+    pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
+        self.buffer.read(cx).read(cx).file_at(point).cloned()
+    }
+
     pub fn active_excerpt(
         &self,
         cx: &AppContext,
@@ -1423,13 +1427,19 @@ impl Editor {
         }
     }
 
-    pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
+    pub fn set_keymap_context_layer<Tag: 'static>(
+        &mut self,
+        context: KeymapContext,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.keymap_context_layers
             .insert(TypeId::of::<Tag>(), context);
+        cx.notify();
     }
 
-    pub fn remove_keymap_context_layer<Tag: 'static>(&mut self) {
+    pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
         self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+        cx.notify();
     }
 
     pub fn set_input_enabled(&mut self, input_enabled: bool) {
@@ -2949,11 +2959,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;
         }
@@ -3104,6 +3110,25 @@ impl Editor {
         }
     }
 
+    fn is_copilot_enabled_at(
+        &self,
+        location: Anchor,
+        snapshot: &MultiBufferSnapshot,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let settings = cx.global::<Settings>();
+
+        let path = snapshot.file_at(location).map(|file| file.path());
+        let language_name = snapshot
+            .language_at(location)
+            .map(|language| language.name());
+        if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) {
+            return false;
+        }
+
+        true
+    }
+
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
         self.display_map.read(cx).has_suggestion()
     }
@@ -6865,6 +6890,9 @@ impl Editor {
                     self.language_at(0, cx)
                         .map(|language| language.name())
                         .as_deref(),
+                    self.file_at(0, cx)
+                        .map(|file| file.path().clone())
+                        .as_deref(),
                 ),
             };
             telemetry.report_clickhouse_event(event, settings.telemetry())
@@ -7137,28 +7165,26 @@ impl View for Editor {
         false
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut context = Self::default_keymap_context();
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",
             EditorMode::AutoHeight { .. } => "auto_height",
             EditorMode::Full => "full",
         };
-        context.add_key("mode", mode);
+        keymap.add_key("mode", mode);
         if self.pending_rename.is_some() {
-            context.add_identifier("renaming");
+            keymap.add_identifier("renaming");
         }
         match self.context_menu.as_ref() {
-            Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
-            Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
+            Some(ContextMenu::Completions(_)) => keymap.add_identifier("showing_completions"),
+            Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
             None => {}
         }
 
         for layer in self.keymap_context_layers.values() {
-            context.extend(layer);
+            keymap.extend(layer);
         }
-
-        context
     }
 
     fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {

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<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

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<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))

crates/feedback/Cargo.toml 🔗

@@ -11,25 +11,27 @@ path = "src/feedback.rs"
 test-support = []
 
 [dependencies]
-anyhow.workspace = true
 client = { path = "../client" }
 editor = { path = "../editor" }
 language = { path = "../language" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+search = { path = "../search" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+
 log.workspace = true
 futures.workspace = true
-gpui = { path = "../gpui" }
+anyhow.workspace = true
+smallvec.workspace = true
 human_bytes = "0.4.1"
 isahc = "1.7"
 lazy_static.workspace = true
 postage.workspace = true
-project = { path = "../project" }
-search = { path = "../search" }
 serde.workspace = true
 serde_derive.workspace = true
-settings = { path = "../settings" }
 sysinfo = "0.27.1"
-theme = { path = "../theme" }
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
-util = { path = "../util" }
-workspace = { path = "../workspace" }

crates/feedback/src/feedback_editor.rs 🔗

@@ -15,6 +15,7 @@ use language::Buffer;
 use postage::prelude::Stream;
 use project::Project;
 use serde::Serialize;
+use smallvec::SmallVec;
 use std::{
     any::TypeId,
     borrow::Cow,
@@ -25,7 +26,6 @@ use util::ResultExt;
 use workspace::{
     item::{Item, ItemEvent, ItemHandle},
     searchable::{SearchableItem, SearchableItemHandle},
-    smallvec::SmallVec,
     Workspace,
 };
 

crates/gpui/src/app.rs 🔗

@@ -84,14 +84,15 @@ pub trait View: Entity + Sized {
         false
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap_matcher::KeymapContext {
-        Self::default_keymap_context()
+    fn update_keymap_context(&self, keymap: &mut keymap_matcher::KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
     }
-    fn default_keymap_context() -> keymap_matcher::KeymapContext {
-        let mut cx = keymap_matcher::KeymapContext::default();
-        cx.add_identifier(Self::ui_name());
-        cx
+
+    fn reset_to_default_keymap_context(keymap: &mut keymap_matcher::KeymapContext) {
+        keymap.clear();
+        keymap.add_identifier(Self::ui_name());
     }
+
     fn debug_json(&self, _: &AppContext) -> serde_json::Value {
         serde_json::Value::Null
     }
@@ -450,6 +451,7 @@ type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> b
 pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
     views: HashMap<(usize, usize), Box<dyn AnyView>>,
+    views_metadata: HashMap<(usize, usize), ViewMetadata>,
     windows: HashMap<usize, Window>,
     globals: HashMap<TypeId, Box<dyn Any>>,
     element_states: HashMap<ElementStateId, Box<dyn Any>>,
@@ -511,6 +513,7 @@ impl AppContext {
         Self {
             models: Default::default(),
             views: Default::default(),
+            views_metadata: Default::default(),
             windows: Default::default(),
             globals: Default::default(),
             element_states: Default::default(),
@@ -735,9 +738,9 @@ impl AppContext {
     }
 
     pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
-        self.views
+        self.views_metadata
             .get(&(window_id, view_id))
-            .map(|view| view.as_any().type_id())
+            .map(|metadata| metadata.type_id)
     }
 
     pub fn active_labeled_tasks<'a>(
@@ -1053,9 +1056,10 @@ impl AppContext {
                 .read_window(window_id, |cx| {
                     if let Some(focused_view_id) = cx.focused_view_id() {
                         for view_id in cx.ancestors(focused_view_id) {
-                            if let Some(view) = cx.views.get(&(window_id, view_id)) {
-                                let view_type = view.as_any().type_id();
-                                if let Some(actions) = cx.actions.get(&view_type) {
+                            if let Some(view_metadata) =
+                                cx.views_metadata.get(&(window_id, view_id))
+                            {
+                                if let Some(actions) = cx.actions.get(&view_metadata.type_id) {
                                     if actions.contains_key(&action_type) {
                                         return true;
                                     }
@@ -1444,6 +1448,7 @@ impl AppContext {
             for (window_id, view_id) in dropped_views {
                 self.subscriptions.remove(view_id);
                 self.observations.remove(view_id);
+                self.views_metadata.remove(&(window_id, view_id));
                 let mut view = self.views.remove(&(window_id, view_id)).unwrap();
                 view.release(self);
                 let change_focus_to = self.windows.get_mut(&window_id).and_then(|window| {
@@ -1775,9 +1780,11 @@ impl AppContext {
         observed_window_id: usize,
         observed_view_id: usize,
     ) {
-        if self
+        let view_key = (observed_window_id, observed_view_id);
+        if let Some((view, mut view_metadata)) = self
             .views
-            .contains_key(&(observed_window_id, observed_view_id))
+            .remove(&view_key)
+            .zip(self.views_metadata.remove(&view_key))
         {
             if let Some(window) = self.windows.get_mut(&observed_window_id) {
                 window
@@ -1787,6 +1794,10 @@ impl AppContext {
                     .insert(observed_view_id);
             }
 
+            view.update_keymap_context(&mut view_metadata.keymap_context, self);
+            self.views.insert(view_key, view);
+            self.views_metadata.insert(view_key, view_metadata);
+
             let mut observations = self.observations.clone();
             observations.emit(observed_view_id, |callback| callback(self));
         }
@@ -2033,6 +2044,11 @@ pub enum ParentId {
     Root,
 }
 
+struct ViewMetadata {
+    type_id: TypeId,
+    keymap_context: KeymapContext,
+}
+
 #[derive(Default, Clone)]
 pub struct WindowInvalidation {
     pub updated: HashSet<usize>,
@@ -2361,7 +2377,7 @@ pub trait AnyView {
         cx: &mut WindowContext,
         view_id: usize,
     ) -> bool;
-    fn keymap_context(&self, cx: &AppContext) -> KeymapContext;
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext);
     fn debug_json(&self, cx: &WindowContext) -> serde_json::Value;
 
     fn text_for_range(&self, range: Range<usize>, cx: &WindowContext) -> Option<String>;
@@ -2433,11 +2449,10 @@ where
             cx.handle().into_any()
         } else {
             let focused_type = cx
-                .views
+                .views_metadata
                 .get(&(cx.window_id, focused_id))
                 .unwrap()
-                .as_any()
-                .type_id();
+                .type_id;
             AnyViewHandle::new(
                 cx.window_id,
                 focused_id,
@@ -2454,11 +2469,10 @@ where
             cx.handle().into_any()
         } else {
             let blurred_type = cx
-                .views
+                .views_metadata
                 .get(&(cx.window_id, blurred_id))
                 .unwrap()
-                .as_any()
-                .type_id();
+                .type_id;
             AnyViewHandle::new(
                 cx.window_id,
                 blurred_id,
@@ -2489,8 +2503,8 @@ where
         View::modifiers_changed(self, event, &mut cx)
     }
 
-    fn keymap_context(&self, cx: &AppContext) -> KeymapContext {
-        View::keymap_context(self, cx)
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
+        View::update_keymap_context(self, keymap, cx)
     }
 
     fn debug_json(&self, cx: &WindowContext) -> serde_json::Value {
@@ -3234,7 +3248,6 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
     /// Return keystrokes that would dispatch the given action on the given view.
     pub(crate) fn keystrokes_for_action(
         &mut self,
-        view: &V,
         view_id: usize,
         action: &dyn Action,
     ) -> Option<SmallVec<[Keystroke; 2]>> {
@@ -3244,21 +3257,13 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> {
         let mut contexts = Vec::new();
         let mut handler_depth = None;
         for (i, view_id) in self.ancestors(view_id).enumerate() {
-            let view = if view_id == self.view_id {
-                Some(view as _)
-            } else {
-                self.views
-                    .get(&(window_id, view_id))
-                    .map(|view| view.as_ref())
-            };
-
-            if let Some(view) = view {
-                if let Some(actions) = self.actions.get(&view.as_any().type_id()) {
+            if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
+                if let Some(actions) = self.actions.get(&view_metadata.type_id) {
                     if actions.contains_key(&action.as_any().type_id()) {
                         handler_depth = Some(i);
                     }
                 }
-                contexts.push(view.keymap_context(self));
+                contexts.push(view_metadata.keymap_context.clone());
             }
         }
 
@@ -5687,8 +5692,8 @@ mod tests {
                 "View"
             }
 
-            fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-                self.keymap_context.clone()
+            fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+                *keymap = self.keymap_context.clone();
             }
         }
 
@@ -5852,52 +5857,51 @@ mod tests {
         });
 
         let view_1_id = view_1.id();
-        view_1.update(cx, |view_1, cx| {
-            // Sanity check
-            let mut new_parents = Default::default();
-            let mut notify_views_if_parents_change = Default::default();
-            let mut layout_cx = LayoutContext::new(
-                cx,
-                &mut new_parents,
-                &mut notify_views_if_parents_change,
-                false,
-            );
-            assert_eq!(
-                layout_cx
-                    .keystrokes_for_action(view_1, view_1_id, &Action1)
-                    .unwrap()
-                    .as_slice(),
-                &[Keystroke::parse("a").unwrap()]
-            );
-            assert_eq!(
-                layout_cx
-                    .keystrokes_for_action(view_1, view_2.id(), &Action2)
-                    .unwrap()
-                    .as_slice(),
-                &[Keystroke::parse("b").unwrap()]
-            );
+        view_1.update(cx, |_, cx| {
+            view_2.update(cx, |_, cx| {
+                // Sanity check
+                let mut new_parents = Default::default();
+                let mut notify_views_if_parents_change = Default::default();
+                let mut layout_cx = LayoutContext::new(
+                    cx,
+                    &mut new_parents,
+                    &mut notify_views_if_parents_change,
+                    false,
+                );
+                assert_eq!(
+                    layout_cx
+                        .keystrokes_for_action(view_1_id, &Action1)
+                        .unwrap()
+                        .as_slice(),
+                    &[Keystroke::parse("a").unwrap()]
+                );
+                assert_eq!(
+                    layout_cx
+                        .keystrokes_for_action(view_2.id(), &Action2)
+                        .unwrap()
+                        .as_slice(),
+                    &[Keystroke::parse("b").unwrap()]
+                );
 
-            // The 'a' keystroke propagates up the view tree from view_2
-            // to view_1. The action, Action1, is handled by view_1.
-            assert_eq!(
-                layout_cx
-                    .keystrokes_for_action(view_1, view_2.id(), &Action1)
-                    .unwrap()
-                    .as_slice(),
-                &[Keystroke::parse("a").unwrap()]
-            );
+                // The 'a' keystroke propagates up the view tree from view_2
+                // to view_1. The action, Action1, is handled by view_1.
+                assert_eq!(
+                    layout_cx
+                        .keystrokes_for_action(view_2.id(), &Action1)
+                        .unwrap()
+                        .as_slice(),
+                    &[Keystroke::parse("a").unwrap()]
+                );
 
-            // Actions that are handled below the current view don't have bindings
-            assert_eq!(
-                layout_cx.keystrokes_for_action(view_1, view_1_id, &Action2),
-                None
-            );
+                // Actions that are handled below the current view don't have bindings
+                assert_eq!(layout_cx.keystrokes_for_action(view_1_id, &Action2), None);
 
-            // Actions that are handled in other branches of the tree should not have a binding
-            assert_eq!(
-                layout_cx.keystrokes_for_action(view_1, view_2.id(), &GlobalAction),
-                None
-            );
+                // Actions that are handled in other branches of the tree should not have a binding
+                assert_eq!(
+                    layout_cx.keystrokes_for_action(view_2.id(), &GlobalAction),
+                    None
+                );
+            });
         });
 
         // Check that global actions do not have a binding, even if a binding does exist in another view

crates/gpui/src/app/window.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     elements::AnyRootElement,
     geometry::rect::RectF,
     json::ToJson,
-    keymap_matcher::{Binding, Keystroke, MatchResult},
+    keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
     platform::{
         self, Appearance, CursorStyle, Event, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent,
         MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
@@ -34,7 +34,7 @@ use std::{
 use util::ResultExt;
 use uuid::Uuid;
 
-use super::Reference;
+use super::{Reference, ViewMetadata};
 
 pub struct Window {
     pub(crate) root_view: Option<AnyViewHandle>,
@@ -364,10 +364,9 @@ impl<'a> WindowContext<'a> {
         let mut contexts = Vec::new();
         let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
         for (depth, view_id) in self.ancestors(view_id).enumerate() {
-            if let Some(view) = self.views.get(&(window_id, view_id)) {
-                contexts.push(view.keymap_context(self));
-                let view_type = view.as_any().type_id();
-                if let Some(actions) = self.actions.get(&view_type) {
+            if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
+                contexts.push(view_metadata.keymap_context.clone());
+                if let Some(actions) = self.actions.get(&view_metadata.type_id) {
                     handler_depths_by_action_type.extend(
                         actions
                             .keys()
@@ -418,9 +417,9 @@ impl<'a> WindowContext<'a> {
             let dispatch_path = self
                 .ancestors(focused_view_id)
                 .filter_map(|view_id| {
-                    self.views
+                    self.views_metadata
                         .get(&(window_id, view_id))
-                        .map(|view| (view_id, view.keymap_context(self)))
+                        .map(|view| (view_id, view.keymap_context.clone()))
                 })
                 .collect();
 
@@ -1149,6 +1148,15 @@ impl<'a> WindowContext<'a> {
         let view_id = post_inc(&mut self.next_entity_id);
         let mut cx = ViewContext::mutable(self, view_id);
         let handle = if let Some(view) = build_view(&mut cx) {
+            let mut keymap_context = KeymapContext::default();
+            view.update_keymap_context(&mut keymap_context, cx.app_context());
+            self.views_metadata.insert(
+                (window_id, view_id),
+                ViewMetadata {
+                    type_id: TypeId::of::<T>(),
+                    keymap_context,
+                },
+            );
             self.views.insert((window_id, view_id), Box::new(view));
             self.window
                 .invalidation

crates/gpui/src/elements/keystroke_label.rs 🔗

@@ -42,7 +42,7 @@ impl<V: View> Element<V> for KeystrokeLabel {
         cx: &mut LayoutContext<V>,
     ) -> (Vector2F, AnyElement<V>) {
         let mut element = if let Some(keystrokes) =
-            cx.keystrokes_for_action(view, self.view_id, self.action.as_ref())
+            cx.keystrokes_for_action(self.view_id, self.action.as_ref())
         {
             Flex::row()
                 .with_children(keystrokes.iter().map(|keystroke| {

crates/gpui/src/keymap_matcher/keymap_context.rs 🔗

@@ -17,6 +17,11 @@ impl KeymapContext {
         }
     }
 
+    pub fn clear(&mut self) {
+        self.set.clear();
+        self.map.clear();
+    }
+
     pub fn extend(&mut self, other: &Self) {
         for v in &other.set {
             self.set.insert(v.clone());

crates/picker/src/picker.rs 🔗

@@ -126,10 +126,9 @@ impl<D: PickerDelegate> View for Picker<D> {
             .into_any_named("picker")
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut cx = Self::default_keymap_context();
-        cx.add_identifier("menu");
-        cx
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
+        keymap.add_identifier("menu");
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {

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

crates/project_panel/src/project_panel.rs 🔗

@@ -1317,10 +1317,9 @@ impl View for ProjectPanel {
         }
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut cx = Self::default_keymap_context();
-        cx.add_identifier("menu");
-        cx
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
+        keymap.add_identifier("menu");
     }
 }
 

crates/settings/Cargo.toml 🔗

@@ -23,7 +23,9 @@ theme = { path = "../theme" }
 staff_mode = { path = "../staff_mode" }
 util = { path = "../util" }
 
+glob.workspace = true
 json_comments = "0.2"
+lazy_static.workspace = true
 postage.workspace = true
 schemars = "0.8"
 serde.workspace = true

crates/settings/src/settings.rs 🔗

@@ -7,6 +7,7 @@ use gpui::{
     font_cache::{FamilyId, FontCache},
     fonts, AssetSource,
 };
+use lazy_static::lazy_static;
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
@@ -18,14 +19,19 @@ use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
-use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
+use std::{
+    borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
+};
 use theme::{Theme, ThemeRegistry};
-use tree_sitter::Query;
+use tree_sitter::{Query, Tree};
 use util::{RangeExt, ResultExt as _};
 
 pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 pub use watched_json::watch_files;
 
+pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
+pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
+
 #[derive(Clone)]
 pub struct Settings {
     pub features: Features,
@@ -47,6 +53,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 +68,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 +134,17 @@ 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)]
+    pub disabled_globs: Option<Vec<String>>,
+}
+
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct GitSettings {
     pub git_gutter: Option<GitGutter>,
@@ -390,6 +385,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 +435,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,
 }
@@ -451,6 +447,13 @@ pub struct FeaturesContent {
 }
 
 impl Settings {
+    pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
+        match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
+            Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+            Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+        }
+    }
+
     /// Fill out the settings corresponding to the default.json file, overrides will be set later
     pub fn defaults(
         assets: impl AssetSource,
@@ -464,7 +467,7 @@ impl Settings {
         }
 
         let defaults: SettingsFileContent = parse_json_with_comments(
-            str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
+            str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
         )
         .unwrap();
 
@@ -506,6 +509,16 @@ impl Settings {
                 show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
             },
             editor_overrides: Default::default(),
+            copilot: CopilotSettings {
+                disabled_globs: defaults
+                    .copilot
+                    .unwrap()
+                    .disabled_globs
+                    .unwrap()
+                    .into_iter()
+                    .map(|s| glob::Pattern::new(&s).unwrap())
+                    .collect(),
+            },
             git: defaults.git.unwrap(),
             git_overrides: Default::default(),
             journal_defaults: defaults.journal,
@@ -576,6 +589,14 @@ impl Settings {
         merge(&mut self.base_keymap, data.base_keymap);
         merge(&mut self.features.copilot, data.features.copilot);
 
+        if let Some(copilot) = data.copilot {
+            if let Some(disabled_globs) = copilot.disabled_globs {
+                self.copilot.disabled_globs = disabled_globs
+                    .into_iter()
+                    .filter_map(|s| glob::Pattern::new(&s).ok())
+                    .collect()
+            }
+        }
         self.editor_overrides = data.editor;
         self.git_overrides = data.git.unwrap_or_default();
         self.journal_overrides = data.journal;
@@ -602,11 +623,34 @@ impl Settings {
         &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 show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
+        if !self.features.copilot {
+            return false;
+        }
+
+        if !self.copilot_enabled_for_language(language) {
+            return false;
+        }
+
+        if let Some(path) = path {
+            if !self.copilot_enabled_for_path(path) {
+                return false;
+            }
+        }
+
+        true
+    }
+
+    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
+        !self
+            .copilot
+            .disabled_globs
+            .iter()
+            .any(|glob| glob.matches_path(path))
+    }
+
+    pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
+        self.language_setting(language, |settings| settings.show_copilot_suggestions)
     }
 
     pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@@ -751,6 +795,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(),
@@ -859,17 +904,8 @@ pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T>
     )?)
 }
 
-fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
-    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
-    const LANGAUGES: &'static str = "languages";
-
-    let mut parser = tree_sitter::Parser::new();
-    parser.set_language(tree_sitter_json::language()).unwrap();
-    let tree = parser.parse(&settings_content, None).unwrap();
-
-    let mut cursor = tree_sitter::QueryCursor::new();
-
-    let query = Query::new(
+lazy_static! {
+    static ref PAIR_QUERY: Query = Query::new(
         tree_sitter_json::language(),
         "
             (pair
@@ -878,14 +914,65 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
         ",
     )
     .unwrap();
+}
+
+fn update_object_in_settings_file<'a>(
+    old_object: &'a serde_json::Map<String, Value>,
+    new_object: &'a serde_json::Map<String, Value>,
+    text: &str,
+    syntax_tree: &Tree,
+    tab_size: usize,
+    key_path: &mut Vec<&'a str>,
+    edits: &mut Vec<(Range<usize>, String)>,
+) {
+    for (key, old_value) in old_object.iter() {
+        key_path.push(key);
+        let new_value = new_object.get(key).unwrap_or(&Value::Null);
+
+        // If the old and new values are both objects, then compare them key by key,
+        // preserving the comments and formatting of the unchanged parts. Otherwise,
+        // replace the old value with the new value.
+        if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
+            (old_value, new_value)
+        {
+            update_object_in_settings_file(
+                old_sub_object,
+                new_sub_object,
+                text,
+                syntax_tree,
+                tab_size,
+                key_path,
+                edits,
+            )
+        } else if old_value != new_value {
+            let (range, replacement) =
+                update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
+            edits.push((range, replacement));
+        }
+
+        key_path.pop();
+    }
+}
+
+fn update_key_in_settings_file(
+    text: &str,
+    syntax_tree: &Tree,
+    key_path: &[&str],
+    tab_size: usize,
+    new_value: impl Serialize,
+) -> (Range<usize>, String) {
+    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
+    const LANGUAGES: &'static str = "languages";
 
-    let has_language_overrides = settings_content.contains(LANGUAGE_OVERRIDES);
+    let mut cursor = tree_sitter::QueryCursor::new();
+
+    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
 
     let mut depth = 0;
     let mut last_value_range = 0..0;
     let mut first_key_start = None;
-    let mut existing_value_range = 0..settings_content.len();
-    let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
+    let mut existing_value_range = 0..text.len();
+    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
     for mat in matches {
         if mat.captures.len() != 2 {
             continue;
@@ -908,10 +995,10 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
 
         first_key_start.get_or_insert_with(|| key_range.start);
 
-        let found_key = settings_content
+        let found_key = text
             .get(key_range.clone())
             .map(|key_text| {
-                if key_path[depth] == LANGAUGES && has_language_overrides {
+                if key_path[depth] == LANGUAGES && has_language_overrides {
                     return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
                 } else {
                     return key_text == format!("\"{}\"", key_path[depth]);
@@ -935,12 +1022,11 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
 
     // We found the exact key we want, insert the new value
     if depth == key_path.len() {
-        let new_val = serde_json::to_string_pretty(new_value)
-            .expect("Could not serialize new json field to string");
-        settings_content.replace_range(existing_value_range, &new_val);
+        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
+        (existing_value_range, new_val)
     } else {
         // We have key paths, construct the sub objects
-        let new_key = if has_language_overrides && key_path[depth] == LANGAUGES {
+        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
             LANGUAGE_OVERRIDES
         } else {
             key_path[depth]
@@ -949,7 +1035,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
         // We don't have the key, construct the nested objects
         let mut new_value = serde_json::to_value(new_value).unwrap();
         for key in key_path[(depth + 1)..].iter().rev() {
-            if has_language_overrides && key == &LANGAUGES {
+            if has_language_overrides && key == &LANGUAGES {
                 new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
             } else {
                 new_value = serde_json::json!({ key.to_string(): new_value });
@@ -959,7 +1045,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
         if let Some(first_key_start) = first_key_start {
             let mut row = 0;
             let mut column = 0;
-            for (ix, char) in settings_content.char_indices() {
+            for (ix, char) in text.char_indices() {
                 if ix == first_key_start {
                     break;
                 }
@@ -974,37 +1060,29 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
             if row > 0 {
                 // depth is 0 based, but division needs to be 1 based.
                 let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
-                let content = format!(r#""{new_key}": {new_val},"#);
-                settings_content.insert_str(first_key_start, &content);
-
-                settings_content.insert_str(
-                    first_key_start + content.len(),
-                    &format!("\n{:width$}", ' ', width = column),
-                )
+                let space = ' ';
+                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
+                (first_key_start..first_key_start, content)
             } else {
                 let new_val = serde_json::to_string(&new_value).unwrap();
                 let mut content = format!(r#""{new_key}": {new_val},"#);
                 content.push(' ');
-                settings_content.insert_str(first_key_start, &content);
+                (first_key_start..first_key_start, content)
             }
         } else {
             new_value = serde_json::json!({ new_key.to_string(): new_value });
             let indent_prefix_len = 4 * depth;
-            let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
-
-            settings_content.replace_range(existing_value_range, &new_val);
+            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
             if depth == 0 {
-                settings_content.push('\n');
+                new_val.push('\n');
             }
+
+            (existing_value_range, new_val)
         }
     }
 }
 
-fn to_pretty_json(
-    value: &serde_json::Value,
-    indent_size: usize,
-    indent_prefix_len: usize,
-) -> String {
+fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
     const SPACES: [u8; 32] = [b' '; 32];
 
     debug_assert!(indent_size <= SPACES.len());
@@ -1031,13 +1109,16 @@ fn to_pretty_json(
     adjusted_text
 }
 
-pub fn update_settings_file(
-    mut text: String,
+/// Update the settings file with the given callback.
+///
+/// Returns a new JSON string and the offset where the first edit occurred.
+fn update_settings_file(
+    text: &str,
     mut old_file_content: SettingsFileContent,
+    tab_size: NonZeroU32,
     update: impl FnOnce(&mut SettingsFileContent),
-) -> String {
+) -> Vec<(Range<usize>, String)> {
     let mut new_file_content = old_file_content.clone();
-
     update(&mut new_file_content);
 
     if new_file_content.languages.len() != old_file_content.languages.len() {
@@ -1055,51 +1136,25 @@ pub fn update_settings_file(
         }
     }
 
+    let mut parser = tree_sitter::Parser::new();
+    parser.set_language(tree_sitter_json::language()).unwrap();
+    let tree = parser.parse(text, None).unwrap();
+
     let old_object = to_json_object(old_file_content);
     let new_object = to_json_object(new_file_content);
-
-    fn apply_changes_to_json_text(
-        old_object: &serde_json::Map<String, Value>,
-        new_object: &serde_json::Map<String, Value>,
-        current_key_path: Vec<&str>,
-        json_text: &mut String,
-    ) {
-        for (key, old_value) in old_object.iter() {
-            // We know that these two are from the same shape of object, so we can just unwrap
-            let new_value = new_object.get(key).unwrap();
-
-            if old_value != new_value {
-                match new_value {
-                    Value::Bool(_) | Value::Number(_) | Value::String(_) => {
-                        let mut key_path = current_key_path.clone();
-                        key_path.push(key);
-                        write_settings_key(json_text, &key_path, &new_value);
-                    }
-                    Value::Object(new_sub_object) => {
-                        let mut key_path = current_key_path.clone();
-                        key_path.push(key);
-                        if let Value::Object(old_sub_object) = old_value {
-                            apply_changes_to_json_text(
-                                old_sub_object,
-                                new_sub_object,
-                                key_path,
-                                json_text,
-                            );
-                        } else {
-                            unimplemented!("This function doesn't support changing values from simple values to objects yet");
-                        }
-                    }
-                    Value::Null | Value::Array(_) => {
-                        unimplemented!("We only support objects and simple values");
-                    }
-                }
-            }
-        }
-    }
-
-    apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
-
-    text
+    let mut key_path = Vec::new();
+    let mut edits = Vec::new();
+    update_object_in_settings_file(
+        &old_object,
+        &new_object,
+        &text,
+        &tree,
+        tab_size.get() as usize,
+        &mut key_path,
+        &mut edits,
+    );
+    edits.sort_unstable_by_key(|e| e.0.start);
+    return edits;
 }
 
 fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
@@ -1115,15 +1170,18 @@ mod tests {
     use super::*;
     use unindent::Unindent;
 
-    fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
-        old_json: S1,
+    fn assert_new_settings(
+        old_json: String,
         update: fn(&mut SettingsFileContent),
-        expected_new_json: S2,
+        expected_new_json: String,
     ) {
-        let old_json = old_json.into();
         let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
-        let new_json = update_settings_file(old_json, old_content, update);
-        pretty_assertions::assert_eq!(new_json, expected_new_json.into());
+        let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
+        let mut new_json = old_json;
+        for (range, replacement) in edits.into_iter().rev() {
+            new_json.replace_range(range, &replacement);
+        }
+        pretty_assertions::assert_eq!(new_json, expected_new_json);
     }
 
     #[test]
@@ -1164,6 +1222,63 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_update_copilot_globs() {
+        assert_new_settings(
+            r#"
+                {
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings.copilot = Some(CopilotSettingsContent {
+                    disabled_globs: Some(vec![]),
+                });
+            },
+            r#"
+                {
+                    "copilot": {
+                        "disabled_globs": []
+                    }
+                }
+            "#
+            .unindent(),
+        );
+
+        assert_new_settings(
+            r#"
+                {
+                    "copilot": {
+                        "disabled_globs": [
+                            "**/*.json"
+                        ]
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings
+                    .copilot
+                    .get_or_insert(Default::default())
+                    .disabled_globs
+                    .as_mut()
+                    .unwrap()
+                    .push(".env".into());
+            },
+            r#"
+                {
+                    "copilot": {
+                        "disabled_globs": [
+                            "**/*.json",
+                            ".env"
+                        ]
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
+
     #[test]
     fn test_update_copilot() {
         assert_new_settings(
@@ -1347,7 +1462,7 @@ mod tests {
     #[test]
     fn test_update_telemetry_setting() {
         assert_new_settings(
-            "{}",
+            "{}".into(),
             |settings| settings.telemetry.set_diagnostics(true),
             r#"
                 {
@@ -1363,7 +1478,7 @@ mod tests {
     #[test]
     fn test_update_object_empty_doc() {
         assert_new_settings(
-            "",
+            "".into(),
             |settings| settings.telemetry.set_diagnostics(true),
             r#"
                 {
@@ -1416,7 +1531,7 @@ mod tests {
     #[test]
     fn write_key_no_document() {
         assert_new_settings(
-            "",
+            "".to_string(),
             |settings| settings.theme = Some("summerfruit-light".to_string()),
             r#"
                 {
@@ -1430,16 +1545,16 @@ mod tests {
     #[test]
     fn test_write_theme_into_single_line_settings_without_theme() {
         assert_new_settings(
-            r#"{ "a": "", "ok": true }"#,
+            r#"{ "a": "", "ok": true }"#.to_string(),
             |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
+            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
         );
     }
 
     #[test]
     fn test_write_theme_pre_object_whitespace() {
         assert_new_settings(
-            r#"          { "a": "", "ok": true }"#,
+            r#"          { "a": "", "ok": true }"#.to_string(),
             |settings| settings.theme = Some("summerfruit-light".to_string()),
             r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
         );

crates/settings/src/settings_file.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
+use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
 use anyhow::Result;
 use assets::Assets;
 use fs::Fs;
-use gpui::{AppContext, AssetSource};
-use std::{io::ErrorKind, path::Path, sync::Arc};
+use gpui::AppContext;
+use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
 
 // TODO: Switch SettingsFile to open a worktree and buffer for synchronization
 //       And instant updates in the Zed editor
@@ -33,14 +33,7 @@ impl SettingsFile {
             Err(err) => {
                 if let Some(e) = err.downcast_ref::<std::io::Error>() {
                     if e.kind() == ErrorKind::NotFound {
-                        return Ok(std::str::from_utf8(
-                            Assets
-                                .load("settings/initial_user_settings.json")
-                                .unwrap()
-                                .as_ref(),
-                        )
-                        .unwrap()
-                        .to_string());
+                        return Ok(Settings::initial_user_settings_content(&Assets).to_string());
                     }
                 }
                 return Err(err);
@@ -48,28 +41,39 @@ impl SettingsFile {
         }
     }
 
+    pub fn update_unsaved(
+        text: &str,
+        cx: &AppContext,
+        update: impl FnOnce(&mut SettingsFileContent),
+    ) -> Vec<(Range<usize>, String)> {
+        let this = cx.global::<SettingsFile>();
+        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
+        let current_file_content = this.settings_file_content.current();
+        update_settings_file(&text, current_file_content, tab_size, update)
+    }
+
     pub fn update(
         cx: &mut AppContext,
         update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
     ) {
         let this = cx.global::<SettingsFile>();
-
+        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
         let current_file_content = this.settings_file_content.current();
-
         let fs = this.fs.clone();
         let path = this.path.clone();
 
         cx.background()
             .spawn(async move {
                 let old_text = SettingsFile::load_settings(path, &fs).await?;
-
-                let new_text = update_settings_file(old_text, current_file_content, update);
-
+                let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
+                let mut new_text = old_text;
+                for (range, replacement) in edits.into_iter().rev() {
+                    new_text.replace_range(range, &replacement);
+                }
                 fs.atomic_write(path.to_path_buf(), new_text).await?;
-
-                Ok(()) as Result<()>
+                anyhow::Ok(())
             })
-            .detach_and_log_err(cx);
+            .detach_and_log_err(cx)
     }
 }
 

crates/terminal_view/src/terminal_view.rs 🔗

@@ -446,11 +446,11 @@ impl View for TerminalView {
         });
     }
 
-    fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
-        let mut context = Self::default_keymap_context();
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
 
         let mode = self.terminal.read(cx).last_content.mode;
-        context.add_key(
+        keymap.add_key(
             "screen",
             if mode.contains(TermMode::ALT_SCREEN) {
                 "alt"
@@ -460,40 +460,40 @@ impl View for TerminalView {
         );
 
         if mode.contains(TermMode::APP_CURSOR) {
-            context.add_identifier("DECCKM");
+            keymap.add_identifier("DECCKM");
         }
         if mode.contains(TermMode::APP_KEYPAD) {
-            context.add_identifier("DECPAM");
+            keymap.add_identifier("DECPAM");
         } else {
-            context.add_identifier("DECPNM");
+            keymap.add_identifier("DECPNM");
         }
         if mode.contains(TermMode::SHOW_CURSOR) {
-            context.add_identifier("DECTCEM");
+            keymap.add_identifier("DECTCEM");
         }
         if mode.contains(TermMode::LINE_WRAP) {
-            context.add_identifier("DECAWM");
+            keymap.add_identifier("DECAWM");
         }
         if mode.contains(TermMode::ORIGIN) {
-            context.add_identifier("DECOM");
+            keymap.add_identifier("DECOM");
         }
         if mode.contains(TermMode::INSERT) {
-            context.add_identifier("IRM");
+            keymap.add_identifier("IRM");
         }
         //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
         if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
-            context.add_identifier("LNM");
+            keymap.add_identifier("LNM");
         }
         if mode.contains(TermMode::FOCUS_IN_OUT) {
-            context.add_identifier("report_focus");
+            keymap.add_identifier("report_focus");
         }
         if mode.contains(TermMode::ALTERNATE_SCROLL) {
-            context.add_identifier("alternate_scroll");
+            keymap.add_identifier("alternate_scroll");
         }
         if mode.contains(TermMode::BRACKETED_PASTE) {
-            context.add_identifier("bracketed_paste");
+            keymap.add_identifier("bracketed_paste");
         }
         if mode.intersects(TermMode::MOUSE_MODE) {
-            context.add_identifier("any_mouse_reporting");
+            keymap.add_identifier("any_mouse_reporting");
         }
         {
             let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
@@ -505,7 +505,7 @@ impl View for TerminalView {
             } else {
                 "off"
             };
-            context.add_key("mouse_reporting", mouse_reporting);
+            keymap.add_key("mouse_reporting", mouse_reporting);
         }
         {
             let format = if mode.contains(TermMode::SGR_MOUSE) {
@@ -515,9 +515,8 @@ impl View for TerminalView {
             } else {
                 "normal"
             };
-            context.add_key("mouse_format", format);
+            keymap.add_key("mouse_format", format);
         }
-        context
     }
 }
 

crates/vim/src/vim.rs 🔗

@@ -309,7 +309,7 @@ impl Vim {
                 editor.set_input_enabled(!state.vim_controlled());
                 editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
                 let context_layer = state.keymap_context_layer();
-                editor.set_keymap_context_layer::<Self>(context_layer);
+                editor.set_keymap_context_layer::<Self>(context_layer, cx);
             } else {
                 Self::unhook_vim_settings(editor, cx);
             }
@@ -321,7 +321,7 @@ impl Vim {
         editor.set_clip_at_line_ends(false, cx);
         editor.set_input_enabled(true);
         editor.selections.line_mode = false;
-        editor.remove_keymap_context_layer::<Self>();
+        editor.remove_keymap_context_layer::<Self>(cx);
     }
 }
 

crates/workspace/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 ]
 
 [dependencies]
+assets = { path = "../assets" }
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }

crates/workspace/src/pane.rs 🔗

@@ -1841,12 +1841,11 @@ impl View for Pane {
         });
     }
 
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut keymap = Self::default_keymap_context();
+    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+        Self::reset_to_default_keymap_context(keymap);
         if self.docked.is_some() {
             keymap.add_identifier("docked");
         }
-        keymap
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -14,9 +14,8 @@ pub mod sidebar;
 mod status_bar;
 mod toolbar;
 
-pub use smallvec;
-
 use anyhow::{anyhow, Context, Result};
+use assets::Assets;
 use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
@@ -38,7 +37,6 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     impl_actions,
-    keymap_matcher::KeymapContext,
     platform::{
         CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
         WindowOptions,
@@ -48,13 +46,14 @@ use gpui::{
     WindowContext,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
-use language::LanguageRegistry;
+use language::{LanguageRegistry, Rope};
 use std::{
     any::TypeId,
     borrow::Cow,
     cmp, env,
     future::Future,
     path::{Path, PathBuf},
+    str,
     sync::Arc,
     time::Duration,
 };
@@ -83,7 +82,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::ResultExt;
+use util::{paths, ResultExt};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -127,6 +126,8 @@ actions!(
     ]
 );
 
+actions!(zed, [OpenSettings]);
+
 #[derive(Clone, PartialEq)]
 pub struct OpenPaths {
     pub paths: Vec<PathBuf>,
@@ -266,6 +267,17 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         .detach();
     });
 
+    cx.add_action(
+        move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+            create_and_open_local_file(&paths::SETTINGS, cx, || {
+                Settings::initial_user_settings_content(&Assets)
+                    .as_ref()
+                    .into()
+            })
+            .detach_and_log_err(cx);
+        },
+    );
+
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
@@ -2770,10 +2782,6 @@ impl View for Workspace {
             cx.focus(&self.active_pane);
         }
     }
-
-    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        Self::default_keymap_context()
-    }
 }
 
 impl ViewId {
@@ -2929,6 +2937,33 @@ pub fn open_new(
     })
 }
 
+pub fn create_and_open_local_file(
+    path: &'static Path,
+    cx: &mut ViewContext<Workspace>,
+    default_content: impl 'static + Send + FnOnce() -> Rope,
+) -> Task<Result<Box<dyn ItemHandle>>> {
+    cx.spawn(|workspace, mut cx| async move {
+        let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
+        if !fs.is_file(path).await {
+            fs.create_file(path, Default::default()).await?;
+            fs.save(path, &default_content(), Default::default())
+                .await?;
+        }
+
+        let mut items = workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.with_local_workspace(cx, |workspace, cx| {
+                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
+                })
+            })?
+            .await?
+            .await;
+
+        let item = items.pop().flatten();
+        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
+    })
+}
+
 pub fn join_remote_project(
     project_id: u64,
     follow_user_id: u64,

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.85.0"
+version = "0.86.0"
 publish = false
 
 [lib]

crates/zed/src/main.rs 🔗

@@ -52,9 +52,10 @@ use staff_mode::StaffMode;
 use theme::ThemeRegistry;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{
-    self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace,
+    dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
+    Workspace,
 };
-use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
+use zed::{self, build_window_options, initialize_workspace, languages, menus};
 
 fn main() {
     let http = http::client();

crates/zed/src/menus.rs 🔗

@@ -12,7 +12,7 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::submenu(Menu {
                     name: "Preferences",
                     items: vec![
-                        MenuItem::action("Open Settings", super::OpenSettings),
+                        MenuItem::action("Open Settings", workspace::OpenSettings),
                         MenuItem::action("Open Key Bindings", super::OpenKeymap),
                         MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
                         MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),

crates/zed/src/zed.rs 🔗

@@ -21,22 +21,24 @@ use gpui::{
     geometry::vector::vec2f,
     impl_actions,
     platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
-    AppContext, AssetSource, ViewContext,
+    AppContext, ViewContext,
 };
-use language::Rope;
 pub use lsp;
 pub use project;
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
-use settings::Settings;
-use std::{borrow::Cow, env, path::Path, str, sync::Arc};
+use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
+use std::{borrow::Cow, str, sync::Arc};
 use terminal_view::terminal_button::TerminalButton;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
 pub use workspace;
-use workspace::{open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, Workspace};
+use workspace::{
+    create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
+    Workspace,
+};
 
 #[derive(Deserialize, Clone, PartialEq)]
 pub struct OpenBrowser {
@@ -57,7 +59,6 @@ actions!(
         ToggleFullScreen,
         Quit,
         DebugElements,
-        OpenSettings,
         OpenLog,
         OpenLicenses,
         OpenTelemetryLog,
@@ -148,20 +149,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         })
         .detach_and_log_err(cx);
     });
-    cx.add_action(
-        move |workspace: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
-            open_config_file(workspace, &paths::SETTINGS, cx, || {
-                str::from_utf8(
-                    Assets
-                        .load("settings/initial_user_settings.json")
-                        .unwrap()
-                        .as_ref(),
-                )
-                .unwrap()
-                .into()
-            });
-        },
-    );
     cx.add_action(
         move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
             open_log_file(workspace, cx);
@@ -184,8 +171,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         },
     );
     cx.add_action(
-        move |workspace: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
-            open_config_file(workspace, &paths::KEYMAP, cx, Default::default);
+        move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
+            create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
         },
     );
     cx.add_action(
@@ -205,7 +192,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
               cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
                 workspace,
-                "settings/default.json",
+                DEFAULT_SETTINGS_ASSET_PATH,
                 "Default Settings",
                 "JSON",
                 cx,
@@ -450,33 +437,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
     cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
 }
 
-fn open_config_file(
-    workspace: &mut Workspace,
-    path: &'static Path,
-    cx: &mut ViewContext<Workspace>,
-    default_content: impl 'static + Send + FnOnce() -> Rope,
-) {
-    let fs = workspace.app_state().fs.clone();
-    cx.spawn(|workspace, mut cx| async move {
-        if !fs.is_file(path).await {
-            fs.create_file(path, Default::default()).await?;
-            fs.save(path, &default_content(), Default::default())
-                .await?;
-        }
-
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.with_local_workspace(cx, |workspace, cx| {
-                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
-                })
-            })?
-            .await?
-            .await;
-        Ok::<_, anyhow::Error>(())
-    })
-    .detach_and_log_err(cx)
-}
-
 fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
     const MAX_LINES: usize = 1000;