From 89446c7fd41f3733b657e4f0a57438d323770331 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 29 May 2023 13:48:27 -0700 Subject: [PATCH] Start work on respecting project-specific settings --- 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(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index de9104a6848d504eb78d28ab45da896d771dca29..88b0aebd17127765535db8bf8966dd630e95b851 100644 --- a/crates/copilot/src/copilot.rs +++ b/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 { diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 17d27ca41f66caf08117518d29b7f6f627febe4c..686b3d2dfb3dc222433166f3aeed20f5d869be33 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/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!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 366e47ddc640fa6c9ee205debdc7b837b205ebe8..f8d6f0bc406a497f13e36f320ed461aff1ddec60 100644 --- a/crates/editor/src/display_map.rs +++ b/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)] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 40e7c89cb298e34510002550349f7faff6f0fa19..8da746075e25f7130b739b82d40e33ceb2ef8130 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1231,6 +1231,10 @@ mod tests { unimplemented!() } + fn worktree_id(&self) -> usize { + 0 + } + fn is_deleted(&self) -> bool { unimplemented!() } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4650dff38f58088eab5fc1638080680dc86dd84e..597feb832b07c80ed33bdf578377c720c16a393e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/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)) { @@ -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 { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 93b50cf597cfd3e0ed18ac8e317fa5847740f1b1..ddec085e2a622ca84190c5acec9c891512a80b2f 100644 --- a/crates/language/src/buffer.rs +++ b/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(&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(&self, position: D) -> Option { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c98297c03648f7db9c307a592b4f7bf2dcfe279d..70aaca03c408ab0a9995fee88f54144f4ab997d9 100644 --- a/crates/language/src/language_settings.rs +++ b/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::(cx); } -pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { - settings::get::(cx).language(language) +pub fn language_settings<'a>( + language: Option<&str>, + file: Option<&dyn File>, + cx: &'a AppContext, +) -> &'a LanguageSettings { + settings::get_local::( + file.map(|f| (f.worktree_id(), f.path().as_ref())), + cx, + ) + .language(language) } pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5ee64438963977fb6f265ed4d5fe32444bd96b3f..0a8b5f8a2e52f6a33e14bd5bc4a01ebe3dba7601 100644 --- a/crates/project/src/lsp_command.rs +++ b/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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 92210a75a8ea2067aafe3e36d68449c3af2e5142..c76587ff56328fabdab93abf77965e547f11979b 100644 --- a/crates/project/src/project.rs +++ b/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_path: Arc, language: Arc, cx: &mut ModelContext, ) { - 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>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers + let language_server_lookup_info: HashSet<(ModelHandle, Arc)> = 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, + worktree: ModelHandle, language: Arc, cx: &mut ModelContext, ) { + 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, ) -> Task>> { - 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, + changes: &UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + 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::(|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, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 34b63fd5bd9fcaff67f9736ac79f6302045fd8f0..577b22d7309e3afeba8ae1f5a5ccb376c5aeeffb 100644 --- a/crates/project/src/project_tests.rs +++ b/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, + 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, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index dc3c172775721c5142aedab6fc4f0f5d66a1c913..7432eb8d40b5ce7da02b3fb06c007aaa74de1730 100644 --- a/crates/project/src/worktree.rs +++ b/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) -> Option { + 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) -> 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, @@ -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, diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index cca2909da22dba93842304b87c600e2b05a99ae4..4c98dca51a4e661126843b28615e1cdb8a53accc 100644 --- a/crates/settings/src/settings_file.rs +++ b/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(cx: &mut AppContext) { @@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { cx.global::().get(None) } +pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T { + cx.global::().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()), diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 71b3cc635f4e03465d94cb498567c21bd36bd76a..2560d0b752da72881b8ba373772159e07541c74b 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -89,14 +89,14 @@ pub struct SettingsStore { setting_values: HashMap>, default_deserialized_settings: Option, user_deserialized_settings: Option, - local_deserialized_settings: BTreeMap, serde_json::Value>, + local_deserialized_settings: BTreeMap<(usize, Arc), serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } #[derive(Debug)] struct SettingValue { global_value: Option, - local_values: Vec<(Arc, T)>, + local_values: Vec<(usize, Arc, T)>, } trait AnySettingValue { @@ -109,9 +109,9 @@ trait AnySettingValue { custom: &[DeserializedSetting], cx: &AppContext, ) -> Result>; - 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); - fn set_local_value(&mut self, path: Arc, value: Box); + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); 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(&self, path: Option<&Path>) -> &T { + pub fn get(&self, path: Option<(usize, &Path)>) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) @@ -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, 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::::new(); - let mut paths_stack = Vec::>::new(); + let mut paths_stack = Vec::>::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::>(), + ) + .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 AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { T::KEY @@ -546,10 +569,10 @@ impl AnySettingValue for SettingValue { 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 AnySettingValue for SettingValue { self.global_value = Some(*value.downcast().unwrap()); } - fn set_local_value(&mut self, path: Arc, value: Box) { + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box) { 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::(Some(Path::new("/root1/something"))), + store.get::(Some((1, Path::new("/root1/something")))), &UserSettings { name: "John Doe".to_string(), age: 31, @@ -914,7 +943,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root1/subdir/something"))), + store.get::(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::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &UserSettings { name: "John Doe".to_string(), age: 42, @@ -930,7 +959,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 767e3bf4dbb3847064c7a32d217596840457c080..2842dfa8a4f2592830ce3d092e2bfec4ebd2e35c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -905,7 +905,10 @@ mod tests { cx: &mut TestAppContext, ) -> (ModelHandle, ViewHandle) { 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)); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f998fc319fcb7bcdfc6be70c66b482ec247a93b0..e3397a1557cfeed520e4c56fa38c40e4539d71a9 100644 --- a/crates/util/src/paths.rs +++ b/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 { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index bd5f2b4c021e7d8239d76ea29d8ef88ddcf8015b..d66602ee04cb58da09d8b037020cb1fb483937fa 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/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(),