Start work on respecting project-specific settings

Max Brunsfeld created

Change summary

crates/copilot/src/copilot.rs               |   5 
crates/copilot_button/src/copilot_button.rs |   2 
crates/editor/src/display_map.rs            |   2 
crates/editor/src/items.rs                  |   4 
crates/editor/src/multi_buffer.rs           |  20 ++
crates/language/src/buffer.rs               |  17 ++
crates/language/src/language_settings.rs    |  13 +
crates/project/src/lsp_command.rs           |   3 
crates/project/src/project.rs               | 130 ++++++++++++++++++----
crates/project/src/project_tests.rs         |  56 +++++++++
crates/project/src/worktree.rs              |  45 ++++++-
crates/settings/src/settings_file.rs        |  13 ++
crates/settings/src/settings_store.rs       |  91 ++++++++++-----
crates/terminal_view/src/terminal_view.rs   |   5 
crates/util/src/paths.rs                    |   1 
crates/zed/src/languages/yaml.rs            |   2 
16 files changed, 326 insertions(+), 83 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -787,6 +787,7 @@ impl Copilot {
         let position = position.to_point_utf16(buffer);
         let settings = language_settings(
             buffer.language_at(position).map(|l| l.name()).as_deref(),
+            buffer.file().map(|f| f.as_ref()),
             cx,
         );
         let tab_size = settings.tab_size;
@@ -1175,6 +1176,10 @@ mod tests {
         fn to_proto(&self) -> rpc::proto::File {
             unimplemented!()
         }
+
+        fn worktree_id(&self) -> usize {
+            0
+        }
     }
 
     impl language::LocalFile for File {

crates/copilot_button/src/copilot_button.rs 🔗

@@ -198,7 +198,7 @@ impl CopilotButton {
         if let Some(language) = self.language.clone() {
             let fs = fs.clone();
             let language_enabled =
-                language_settings::language_settings(Some(language.as_ref()), cx)
+                language_settings::language_settings(Some(language.as_ref()), None, cx)
                     .show_copilot_suggestions;
             menu_options.push(ContextMenuItem::handler(
                 format!(

crates/editor/src/display_map.rs 🔗

@@ -277,7 +277,7 @@ impl DisplayMap {
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).language())
             .map(|language| language.name());
-        language_settings(language_name.as_deref(), cx).tab_size
+        language_settings(language_name.as_deref(), None, cx).tab_size
     }
 
     #[cfg(test)]

crates/editor/src/items.rs 🔗

@@ -1231,6 +1231,10 @@ mod tests {
             unimplemented!()
         }
 
+        fn worktree_id(&self) -> usize {
+            0
+        }
+
         fn is_deleted(&self) -> bool {
             unimplemented!()
         }

crates/editor/src/multi_buffer.rs 🔗

@@ -1377,8 +1377,14 @@ impl MultiBuffer {
         point: T,
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
-        let language = self.language_at(point, cx);
-        language_settings(language.map(|l| l.name()).as_deref(), cx)
+        let mut language = None;
+        let mut file = None;
+        if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
+            let buffer = buffer.read(cx);
+            language = buffer.language_at(offset).map(|l| l.name());
+            file = buffer.file().map(|f| f.as_ref());
+        }
+        language_settings(language.as_deref(), file, cx)
     }
 
     pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
@@ -2785,9 +2791,13 @@ impl MultiBufferSnapshot {
         point: T,
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
-        self.point_to_buffer_offset(point)
-            .map(|(buffer, offset)| buffer.settings_at(offset, cx))
-            .unwrap_or_else(|| language_settings(None, cx))
+        let mut language = None;
+        let mut file = None;
+        if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
+            language = buffer.language_at(offset).map(|l| l.name());
+            file = buffer.file().map(|f| f.as_ref());
+        }
+        language_settings(language.as_deref(), file, cx)
     }
 
     pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {

crates/language/src/buffer.rs 🔗

@@ -216,6 +216,11 @@ pub trait File: Send + Sync {
     /// of its worktree, then this method will return the name of the worktree itself.
     fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
 
+    /// Returns the id of the worktree to which this file belongs.
+    ///
+    /// This is needed for looking up project-specific settings.
+    fn worktree_id(&self) -> usize;
+
     fn is_deleted(&self) -> bool;
 
     fn as_any(&self) -> &dyn Any;
@@ -1803,7 +1808,11 @@ impl BufferSnapshot {
 
     pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
         let language_name = self.language_at(position).map(|language| language.name());
-        let settings = language_settings(language_name.as_deref(), cx);
+        let settings = language_settings(
+            language_name.as_deref(),
+            self.file().map(|f| f.as_ref()),
+            cx,
+        );
         if settings.hard_tabs {
             IndentSize::tab()
         } else {
@@ -2128,7 +2137,11 @@ impl BufferSnapshot {
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
         let language = self.language_at(position);
-        language_settings(language.map(|l| l.name()).as_deref(), cx)
+        language_settings(
+            language.map(|l| l.name()).as_deref(),
+            self.file.as_ref().map(AsRef::as_ref),
+            cx,
+        )
     }
 
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {

crates/language/src/language_settings.rs 🔗

@@ -1,3 +1,4 @@
+use crate::File;
 use anyhow::Result;
 use collections::HashMap;
 use globset::GlobMatcher;
@@ -13,8 +14,16 @@ pub fn init(cx: &mut AppContext) {
     settings::register::<AllLanguageSettings>(cx);
 }
 
-pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
-    settings::get::<AllLanguageSettings>(cx).language(language)
+pub fn language_settings<'a>(
+    language: Option<&str>,
+    file: Option<&dyn File>,
+    cx: &'a AppContext,
+) -> &'a LanguageSettings {
+    settings::get_local::<AllLanguageSettings>(
+        file.map(|f| (f.worktree_id(), f.path().as_ref())),
+        cx,
+    )
+    .language(language)
 }
 
 pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {

crates/project/src/lsp_command.rs 🔗

@@ -1717,7 +1717,8 @@ impl LspCommand for OnTypeFormatting {
 
         let tab_size = buffer.read_with(&cx, |buffer, cx| {
             let language_name = buffer.language().map(|language| language.name());
-            language_settings(language_name.as_deref(), cx).tab_size
+            let file = buffer.file().map(|f| f.as_ref());
+            language_settings(language_name.as_deref(), file, cx).tab_size
         });
 
         Ok(Self {

crates/project/src/project.rs 🔗

@@ -71,7 +71,10 @@ use std::{
     time::{Duration, Instant, SystemTime},
 };
 use terminals::Terminals;
-use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
+use util::{
+    debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
+    ResultExt, TryFutureExt as _,
+};
 
 pub use fs::*;
 pub use worktree::*;
@@ -697,12 +700,7 @@ impl Project {
                         .language(Some(&language.name()))
                         .enable_language_server
                     {
-                        let worktree = file.worktree.read(cx);
-                        language_servers_to_start.push((
-                            worktree.id(),
-                            worktree.as_local().unwrap().abs_path().clone(),
-                            language.clone(),
-                        ));
+                        language_servers_to_start.push((file.worktree.clone(), language.clone()));
                     }
                 }
             }
@@ -732,8 +730,9 @@ impl Project {
         }
 
         // Start all the newly-enabled language servers.
-        for (worktree_id, worktree_path, language) in language_servers_to_start {
-            self.start_language_servers(worktree_id, worktree_path, language, cx);
+        for (worktree, language) in language_servers_to_start {
+            let worktree_path = worktree.read(cx).abs_path();
+            self.start_language_servers(&worktree, worktree_path, language, cx);
         }
 
         if !self.copilot_enabled && Copilot::global(cx).is_some() {
@@ -2320,25 +2319,34 @@ impl Project {
         });
 
         if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
-            if let Some(worktree) = file.worktree.read(cx).as_local() {
-                let worktree_id = worktree.id();
-                let worktree_abs_path = worktree.abs_path().clone();
-                self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
+            let worktree = file.worktree.clone();
+            if let Some(tree) = worktree.read(cx).as_local() {
+                self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
             }
         }
     }
 
     fn start_language_servers(
         &mut self,
-        worktree_id: WorktreeId,
+        worktree: &ModelHandle<Worktree>,
         worktree_path: Arc<Path>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        if !language_settings(Some(&language.name()), cx).enable_language_server {
+        if !language_settings(
+            Some(&language.name()),
+            worktree
+                .update(cx, |tree, cx| tree.root_file(cx))
+                .as_ref()
+                .map(|f| f as _),
+            cx,
+        )
+        .enable_language_server
+        {
             return;
         }
 
+        let worktree_id = worktree.read(cx).id();
         for adapter in language.lsp_adapters() {
             let key = (worktree_id, adapter.name.clone());
             if self.language_server_ids.contains_key(&key) {
@@ -2747,23 +2755,22 @@ impl Project {
         buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
+        let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = buffers
             .into_iter()
             .filter_map(|buffer| {
                 let buffer = buffer.read(cx);
                 let file = File::from_dyn(buffer.file())?;
-                let worktree = file.worktree.read(cx).as_local()?;
                 let full_path = file.full_path(cx);
                 let language = self
                     .languages
                     .language_for_file(&full_path, Some(buffer.as_rope()))
                     .now_or_never()?
                     .ok()?;
-                Some((worktree.id(), worktree.abs_path().clone(), language))
+                Some((file.worktree.clone(), language))
             })
             .collect();
-        for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
-            self.restart_language_servers(worktree_id, worktree_abs_path, language, cx);
+        for (worktree, language) in language_server_lookup_info {
+            self.restart_language_servers(worktree, language, cx);
         }
 
         None
@@ -2772,11 +2779,13 @@ impl Project {
     // TODO This will break in the case where the adapter's root paths and worktrees are not equal
     fn restart_language_servers(
         &mut self,
-        worktree_id: WorktreeId,
-        fallback_path: Arc<Path>,
+        worktree: ModelHandle<Worktree>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
+        let worktree_id = worktree.read(cx).id();
+        let fallback_path = worktree.read(cx).abs_path();
+
         let mut stops = Vec::new();
         for adapter in language.lsp_adapters() {
             stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
@@ -2806,7 +2815,7 @@ impl Project {
                     .map(|path_buf| Arc::from(path_buf.as_path()))
                     .unwrap_or(fallback_path);
 
-                this.start_language_servers(worktree_id, root_path, language.clone(), cx);
+                this.start_language_servers(&worktree, root_path, language.clone(), cx);
 
                 // Lookup new server ids and set them for each of the orphaned worktrees
                 for adapter in language.lsp_adapters() {
@@ -3430,7 +3439,12 @@ impl Project {
                 for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
                     let settings = buffer.read_with(&cx, |buffer, cx| {
                         let language_name = buffer.language().map(|language| language.name());
-                        language_settings(language_name.as_deref(), cx).clone()
+                        language_settings(
+                            language_name.as_deref(),
+                            buffer.file().map(|f| f.as_ref()),
+                            cx,
+                        )
+                        .clone()
                     });
 
                     let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@@ -4439,11 +4453,15 @@ impl Project {
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Transaction>>> {
-        let tab_size = buffer.read_with(cx, |buffer, cx| {
-            let language_name = buffer.language().map(|language| language.name());
-            language_settings(language_name.as_deref(), cx).tab_size
+        let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
+            let position = position.to_point_utf16(buffer);
+            let language_name = buffer.language_at(position).map(|l| l.name());
+            let file = buffer.file().map(|f| f.as_ref());
+            (
+                position,
+                language_settings(language_name.as_deref(), file, cx).tab_size,
+            )
         });
-        let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(
             buffer.clone(),
             OnTypeFormatting {
@@ -4849,6 +4867,7 @@ impl Project {
                 worktree::Event::UpdatedEntries(changes) => {
                     this.update_local_worktree_buffers(&worktree, changes, cx);
                     this.update_local_worktree_language_servers(&worktree, changes, cx);
+                    this.update_local_worktree_settings(&worktree, changes, cx);
                 }
                 worktree::Event::UpdatedGitRepositories(updated_repos) => {
                     this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
@@ -5155,6 +5174,61 @@ impl Project {
         .detach();
     }
 
+    pub fn update_local_worktree_settings(
+        &mut self,
+        worktree: &ModelHandle<Worktree>,
+        changes: &UpdatedEntriesSet,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let worktree_id = worktree.id();
+        let worktree = worktree.read(cx).as_local().unwrap();
+
+        let mut settings_contents = Vec::new();
+        for (path, _, change) in changes.iter() {
+            if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
+                let settings_dir = Arc::from(
+                    path.ancestors()
+                        .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
+                        .unwrap(),
+                );
+                let fs = self.fs.clone();
+                let removed = *change == PathChange::Removed;
+                let abs_path = worktree.absolutize(path);
+                settings_contents.push(async move {
+                    anyhow::Ok((
+                        settings_dir,
+                        (!removed).then_some(fs.load(&abs_path).await?),
+                    ))
+                });
+            }
+        }
+
+        if settings_contents.is_empty() {
+            return;
+        }
+
+        cx.spawn_weak(move |_, mut cx| async move {
+            let settings_contents = futures::future::join_all(settings_contents).await;
+            cx.update(|cx| {
+                cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                    for entry in settings_contents {
+                        if let Some((directory, file_content)) = entry.log_err() {
+                            store
+                                .set_local_settings(
+                                    worktree_id,
+                                    directory,
+                                    file_content.as_ref().map(String::as_str),
+                                    cx,
+                                )
+                                .log_err();
+                        }
+                    }
+                });
+            });
+        })
+        .detach();
+    }
+
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;

crates/project/src/project_tests.rs 🔗

@@ -63,6 +63,62 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_managing_project_specific_settings(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            ".zed": {
+                "settings.json": r#"{ "tab_size": 8 }"#
+            },
+            "a": {
+                "a.rs": "fn a() {\n    A\n}"
+            },
+            "b": {
+                ".zed": {
+                    "settings.json": r#"{ "tab_size": 2 }"#
+                },
+                "b.rs": "fn b() {\n  B\n}"
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+
+    deterministic.run_until_parked();
+    cx.read(|cx| {
+        let tree = worktree.read(cx);
+
+        let settings_a = language_settings(
+            None,
+            Some(&File::for_entry(
+                tree.entry_for_path("a/a.rs").unwrap().clone(),
+                worktree.clone(),
+            )),
+            cx,
+        );
+        let settings_b = language_settings(
+            None,
+            Some(&File::for_entry(
+                tree.entry_for_path("b/b.rs").unwrap().clone(),
+                worktree.clone(),
+            )),
+            cx,
+        );
+
+        assert_eq!(settings_a.tab_size.get(), 8);
+        assert_eq!(settings_b.tab_size.get(), 2);
+    });
+}
+
 #[gpui::test]
 async fn test_managing_language_servers(
     deterministic: Arc<Deterministic>,

crates/project/src/worktree.rs 🔗

@@ -677,6 +677,18 @@ impl Worktree {
             Worktree::Remote(worktree) => worktree.abs_path.clone(),
         }
     }
+
+    pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<File> {
+        let entry = self.entry_for_path("")?;
+        Some(File {
+            worktree: cx.handle(),
+            path: entry.path.clone(),
+            mtime: entry.mtime,
+            entry_id: entry.id,
+            is_local: self.is_local(),
+            is_deleted: false,
+        })
+    }
 }
 
 impl LocalWorktree {
@@ -684,14 +696,6 @@ impl LocalWorktree {
         path.starts_with(&self.abs_path)
     }
 
-    fn absolutize(&self, path: &Path) -> PathBuf {
-        if path.file_name().is_some() {
-            self.abs_path.join(path)
-        } else {
-            self.abs_path.to_path_buf()
-        }
-    }
-
     pub(crate) fn load_buffer(
         &mut self,
         id: u64,
@@ -1544,6 +1548,14 @@ impl Snapshot {
         &self.abs_path
     }
 
+    pub fn absolutize(&self, path: &Path) -> PathBuf {
+        if path.file_name().is_some() {
+            self.abs_path.join(path)
+        } else {
+            self.abs_path.to_path_buf()
+        }
+    }
+
     pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
         self.entries_by_id.get(&entry_id, &()).is_some()
     }
@@ -2383,6 +2395,10 @@ impl language::File for File {
             .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
     }
 
+    fn worktree_id(&self) -> usize {
+        self.worktree.id()
+    }
+
     fn is_deleted(&self) -> bool {
         self.is_deleted
     }
@@ -2447,6 +2463,17 @@ impl language::LocalFile for File {
 }
 
 impl File {
+    pub fn for_entry(entry: Entry, worktree: ModelHandle<Worktree>) -> Self {
+        Self {
+            worktree,
+            path: entry.path.clone(),
+            mtime: entry.mtime,
+            entry_id: entry.id,
+            is_local: true,
+            is_deleted: false,
+        }
+    }
+
     pub fn from_proto(
         proto: rpc::proto::File,
         worktree: ModelHandle<Worktree>,
@@ -2507,7 +2534,7 @@ pub enum EntryKind {
     File(CharBag),
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub enum PathChange {
     /// A filesystem entry was was created.
     Added,

crates/settings/src/settings_file.rs 🔗

@@ -4,7 +4,14 @@ use assets::Assets;
 use fs::Fs;
 use futures::{channel::mpsc, StreamExt};
 use gpui::{executor::Background, AppContext, AssetSource};
-use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use std::{
+    borrow::Cow,
+    io::ErrorKind,
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+    time::Duration,
+};
 use util::{paths, ResultExt};
 
 pub fn register<T: Setting>(cx: &mut AppContext) {
@@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
     cx.global::<SettingsStore>().get(None)
 }
 
+pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
+    cx.global::<SettingsStore>().get(location)
+}
+
 pub fn default_settings() -> Cow<'static, str> {
     match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
         Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),

crates/settings/src/settings_store.rs 🔗

@@ -89,14 +89,14 @@ pub struct SettingsStore {
     setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
     default_deserialized_settings: Option<serde_json::Value>,
     user_deserialized_settings: Option<serde_json::Value>,
-    local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
+    local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
     tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
 }
 
 #[derive(Debug)]
 struct SettingValue<T> {
     global_value: Option<T>,
-    local_values: Vec<(Arc<Path>, T)>,
+    local_values: Vec<(usize, Arc<Path>, T)>,
 }
 
 trait AnySettingValue {
@@ -109,9 +109,9 @@ trait AnySettingValue {
         custom: &[DeserializedSetting],
         cx: &AppContext,
     ) -> Result<Box<dyn Any>>;
-    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
     fn set_global_value(&mut self, value: Box<dyn Any>);
-    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
     fn json_schema(
         &self,
         generator: &mut SchemaGenerator,
@@ -165,7 +165,7 @@ impl SettingsStore {
     ///
     /// Panics if the given setting type has not been registered, or if there is no
     /// value for this setting.
-    pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
+    pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
         self.setting_values
             .get(&TypeId::of::<T>())
             .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -343,17 +343,19 @@ impl SettingsStore {
     /// Add or remove a set of local settings via a JSON string.
     pub fn set_local_settings(
         &mut self,
+        root_id: usize,
         path: Arc<Path>,
         settings_content: Option<&str>,
         cx: &AppContext,
     ) -> Result<()> {
         if let Some(content) = settings_content {
             self.local_deserialized_settings
-                .insert(path.clone(), parse_json_with_comments(content)?);
+                .insert((root_id, path.clone()), parse_json_with_comments(content)?);
         } else {
-            self.local_deserialized_settings.remove(&path);
+            self.local_deserialized_settings
+                .remove(&(root_id, path.clone()));
         }
-        self.recompute_values(Some(&path), cx)?;
+        self.recompute_values(Some((root_id, &path)), cx)?;
         Ok(())
     }
 
@@ -436,12 +438,12 @@ impl SettingsStore {
 
     fn recompute_values(
         &mut self,
-        changed_local_path: Option<&Path>,
+        changed_local_path: Option<(usize, &Path)>,
         cx: &AppContext,
     ) -> Result<()> {
         // Reload the global and local values for every setting.
         let mut user_settings_stack = Vec::<DeserializedSetting>::new();
-        let mut paths_stack = Vec::<Option<&Path>>::new();
+        let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
         for setting_value in self.setting_values.values_mut() {
             if let Some(default_settings) = &self.default_deserialized_settings {
                 let default_settings = setting_value.deserialize_setting(default_settings)?;
@@ -469,11 +471,11 @@ impl SettingsStore {
                 }
 
                 // Reload the local values for the setting.
-                for (path, local_settings) in &self.local_deserialized_settings {
+                for ((root_id, path), local_settings) in &self.local_deserialized_settings {
                     // Build a stack of all of the local values for that setting.
-                    while let Some(prev_path) = paths_stack.last() {
-                        if let Some(prev_path) = prev_path {
-                            if !path.starts_with(prev_path) {
+                    while let Some(prev_entry) = paths_stack.last() {
+                        if let Some((prev_root_id, prev_path)) = prev_entry {
+                            if root_id != prev_root_id || !path.starts_with(prev_path) {
                                 paths_stack.pop();
                                 user_settings_stack.pop();
                                 continue;
@@ -485,14 +487,17 @@ impl SettingsStore {
                     if let Some(local_settings) =
                         setting_value.deserialize_setting(&local_settings).log_err()
                     {
-                        paths_stack.push(Some(path.as_ref()));
+                        paths_stack.push(Some((*root_id, path.as_ref())));
                         user_settings_stack.push(local_settings);
 
                         // If a local settings file changed, then avoid recomputing local
                         // settings for any path outside of that directory.
-                        if changed_local_path.map_or(false, |changed_local_path| {
-                            !path.starts_with(changed_local_path)
-                        }) {
+                        if changed_local_path.map_or(
+                            false,
+                            |(changed_root_id, changed_local_path)| {
+                                *root_id != changed_root_id || !path.starts_with(changed_local_path)
+                            },
+                        ) {
                             continue;
                         }
 
@@ -500,7 +505,7 @@ impl SettingsStore {
                             .load_setting(&default_settings, &user_settings_stack, cx)
                             .log_err()
                         {
-                            setting_value.set_local_value(path.clone(), value);
+                            setting_value.set_local_value(*root_id, path.clone(), value);
                         }
                     }
                 }
@@ -510,6 +515,24 @@ impl SettingsStore {
     }
 }
 
+impl Debug for SettingsStore {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SettingsStore")
+            .field(
+                "types",
+                &self
+                    .setting_values
+                    .values()
+                    .map(|value| value.setting_type_name())
+                    .collect::<Vec<_>>(),
+            )
+            .field("default_settings", &self.default_deserialized_settings)
+            .field("user_settings", &self.user_deserialized_settings)
+            .field("local_settings", &self.local_deserialized_settings)
+            .finish_non_exhaustive()
+    }
+}
+
 impl<T: Setting> AnySettingValue for SettingValue<T> {
     fn key(&self) -> Option<&'static str> {
         T::KEY
@@ -546,10 +569,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
         Ok(DeserializedSetting(Box::new(value)))
     }
 
-    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
-        if let Some(path) = path {
-            for (settings_path, value) in self.local_values.iter().rev() {
-                if path.starts_with(&settings_path) {
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
+        if let Some((root_id, path)) = path {
+            for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
+                if root_id == *settings_root_id && path.starts_with(&settings_path) {
                     return value;
                 }
             }
@@ -563,11 +586,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
         self.global_value = Some(*value.downcast().unwrap());
     }
 
-    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
         let value = *value.downcast().unwrap();
-        match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
-            Ok(ix) => self.local_values[ix].1 = value,
-            Err(ix) => self.local_values.insert(ix, (path, value)),
+        match self
+            .local_values
+            .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
+        {
+            Ok(ix) => self.local_values[ix].2 = value,
+            Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
         }
     }
 
@@ -884,6 +910,7 @@ mod tests {
 
         store
             .set_local_settings(
+                1,
                 Path::new("/root1").into(),
                 Some(r#"{ "user": { "staff": true } }"#),
                 cx,
@@ -891,6 +918,7 @@ mod tests {
             .unwrap();
         store
             .set_local_settings(
+                1,
                 Path::new("/root1/subdir").into(),
                 Some(r#"{ "user": { "name": "Jane Doe" } }"#),
                 cx,
@@ -899,6 +927,7 @@ mod tests {
 
         store
             .set_local_settings(
+                1,
                 Path::new("/root2").into(),
                 Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
                 cx,
@@ -906,7 +935,7 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root1/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 31,
@@ -914,7 +943,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
             &UserSettings {
                 name: "Jane Doe".to_string(),
                 age: 31,
@@ -922,7 +951,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root2/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 42,
@@ -930,7 +959,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
+            store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
             &MultiKeySettings {
                 key1: "a".to_string(),
                 key2: "b".to_string(),

crates/terminal_view/src/terminal_view.rs 🔗

@@ -905,7 +905,10 @@ mod tests {
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
         let params = cx.update(AppState::test);
-        cx.update(|cx| theme::init((), cx));
+        cx.update(|cx| {
+            theme::init((), cx);
+            language::init(cx);
+        });
 
         let project = Project::test(params.fs.clone(), [], cx).await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));

crates/util/src/paths.rs 🔗

@@ -15,6 +15,7 @@ lazy_static::lazy_static! {
     pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
     pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
     pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
+    pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
 }
 
 pub mod legacy {

crates/zed/src/languages/yaml.rs 🔗

@@ -107,7 +107,7 @@ impl LspAdapter for YamlLspAdapter {
                     "keyOrdering": false
                 },
                 "[yaml]": {
-                    "editor.tabSize": language_settings(Some("YAML"), cx).tab_size,
+                    "editor.tabSize": language_settings(Some("YAML"), None, cx).tab_size,
                 }
             }))
             .boxed(),