From 3b59fef8a8faf753e44c730b9f36ce030cf38be8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Oct 2024 19:47:10 +0300 Subject: [PATCH 01/12] Draft editorconfig sync --- Cargo.lock | 7 + Cargo.toml | 1 + crates/collab/src/tests/integration_tests.rs | 44 +--- crates/paths/src/paths.rs | 3 + crates/project/src/project_settings.rs | 26 +- crates/settings/Cargo.toml | 1 + crates/settings/src/settings_store.rs | 237 ++++++++++++------- 7 files changed, 188 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b51a98764227..30a36135e7cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3649,6 +3649,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ec4rs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf65d056c7da9c971c2847ce250fd1f0f9659d5718845c3ec0ad95f5668352c" + [[package]] name = "ecdsa" version = "0.14.8" @@ -10301,6 +10307,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "ec4rs", "fs", "futures 0.3.30", "gpui", diff --git a/Cargo.toml b/Cargo.toml index e6d71a291aee9..9ffe48b4a1aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,6 +347,7 @@ ctor = "0.2.6" dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" +ec4rs = "1.1" emojis = "0.6.1" env_logger = "0.11" exec = "0.3.1" diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e124fd6a7e3ff..80cc2500f5f4c 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -34,7 +34,7 @@ use project::{ }; use rand::prelude::*; use serde_json::json; -use settings::{LocalSettingsKind, SettingsStore}; +use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, env, future, mem, @@ -3328,16 +3328,8 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - ( - Path::new("").into(), - LocalSettingsKind::Settings, - r#"{"tab_size":2}"#.to_string() - ), - ( - Path::new("a").into(), - LocalSettingsKind::Settings, - r#"{"tab_size":8}"#.to_string() - ), + (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), ] ) }); @@ -3355,16 +3347,8 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - ( - Path::new("").into(), - LocalSettingsKind::Settings, - r#"{}"#.to_string() - ), - ( - Path::new("a").into(), - LocalSettingsKind::Settings, - r#"{"tab_size":8}"#.to_string() - ), + (Path::new("").into(), r#"{}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), ] ) }); @@ -3392,16 +3376,8 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - ( - Path::new("a").into(), - LocalSettingsKind::Settings, - r#"{"tab_size":8}"#.to_string() - ), - ( - Path::new("b").into(), - LocalSettingsKind::Settings, - r#"{"tab_size":4}"#.to_string() - ), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), ] ) }); @@ -3431,11 +3407,7 @@ async fn test_local_settings( store .local_settings(worktree_b.read(cx).id()) .collect::>(), - &[( - Path::new("a").into(), - LocalSettingsKind::Settings, - r#"{"hard_tabs":true}"#.to_string() - ),] + &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] ) }); } diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 679bab32d2bb0..e29a94b6a0393 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -293,3 +293,6 @@ pub fn local_tasks_file_relative_path() -> &'static Path { pub fn local_vscode_tasks_file_relative_path() -> &'static Path { Path::new(".vscode/tasks.json") } + +/// A default editorconfig file name to use when resolving project settings. +pub const EDITORCONFIG_NAME: &str = ".editorconfig"; diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 3ecf0e300878c..efa107d3b3099 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -5,7 +5,7 @@ use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, M use language::LanguageServerName; use paths::{ local_settings_file_relative_path, local_tasks_file_relative_path, - local_vscode_tasks_file_relative_path, + local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME, }; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use schemars::JsonSchema; @@ -287,14 +287,29 @@ impl SettingsObserver { let store = cx.global::(); for worktree in self.worktree_store.read(cx).worktrees() { let worktree_id = worktree.read(cx).id().to_proto(); - for (path, kind, content) in store.local_settings(worktree.read(cx).id()) { + for (path, content) in store.local_settings(worktree.read(cx).id()) { downstream_client .send(proto::UpdateWorktreeSettings { project_id, worktree_id, path: path.to_string_lossy().into(), content: Some(content), - kind: Some(local_settings_kind_to_proto(kind).into()), + kind: Some( + local_settings_kind_to_proto(LocalSettingsKind::Settings).into(), + ), + }) + .log_err(); + } + for (path, content) in store.local_editorconfig_settings(worktree.read(cx).id()) { + downstream_client + .send(proto::UpdateWorktreeSettings { + project_id, + worktree_id, + path: path.to_string_lossy().into(), + content: Some(content), + kind: Some( + local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(), + ), }) .log_err(); } @@ -453,6 +468,11 @@ impl SettingsObserver { .unwrap(), ); (settings_dir, LocalSettingsKind::Tasks) + } else if path.ends_with(EDITORCONFIG_NAME) { + let Some(settings_dir) = path.parent().map(Arc::from) else { + continue; + }; + (settings_dir, LocalSettingsKind::Editorconfig) } else { continue; }; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index e9f6f6e4899cc..cad4b2b0cf7d3 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "fs/test-support"] [dependencies] anyhow.workspace = true collections.workspace = true +ec4rs.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 60494a6aee2e9..14f4721b49371 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -167,8 +167,8 @@ pub struct SettingsStore { raw_user_settings: serde_json::Value, raw_server_settings: Option, raw_extension_settings: serde_json::Value, - raw_local_settings: - BTreeMap<(WorktreeId, Arc), HashMap>, + raw_local_settings: BTreeMap<(WorktreeId, Arc), serde_json::Value>, + raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), String>, tab_size_callback: Option<( TypeId, Box Option + Send + Sync + 'static>, @@ -226,6 +226,7 @@ impl SettingsStore { raw_server_settings: None, raw_extension_settings: serde_json::json!({}), raw_local_settings: Default::default(), + raw_editorconfig_settings: Default::default(), tab_size_callback: Default::default(), setting_file_updates_tx, _setting_file_updates: cx.spawn(|cx| async move { @@ -567,35 +568,79 @@ impl SettingsStore { settings_content: Option<&str>, cx: &mut AppContext, ) -> std::result::Result<(), InvalidSettingsError> { - debug_assert!( - kind != LocalSettingsKind::Tasks, - "Attempted to submit tasks into the settings store" - ); - - let raw_local_settings = self - .raw_local_settings - .entry((root_id, directory_path.clone())) - .or_default(); - let changed = if settings_content.is_some_and(|content| !content.is_empty()) { - let new_contents = - parse_json_with_comments(settings_content.unwrap()).map_err(|e| { - InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: e.to_string(), + let mut zed_settings_changed = false; + let mut editorconfig_settings_changed = false; + match ( + kind, + settings_content + .map(|content| content.trim()) + .filter(|content| !content.is_empty()), + ) { + (LocalSettingsKind::Tasks, _) => { + anyhow::bail!("Attempted to submit tasks into the settings store"); + } + (LocalSettingsKind::Settings, None) => { + zed_settings_changed = self + .raw_local_settings + .remove(&(root_id, directory_path.clone())) + .is_some() + } + (LocalSettingsKind::Editorconfig, None) => { + editorconfig_settings_changed = self + .raw_editorconfig_settings + .remove(&(root_id, directory_path.clone())) + .is_some() + } + (LocalSettingsKind::Settings, Some(settings_contents)) => { + let new_settings = parse_json_with_comments::(settings_contents) + .map_err(|e| { + InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), + } + })?; + match self + .raw_local_settings + .entry((root_id, directory_path.clone())) + { + btree_map::Entry::Vacant(v) => { + v.insert(new_settings); + zed_settings_changed = true; } - })?; - if Some(&new_contents) == raw_local_settings.get(&kind) { - false - } else { - raw_local_settings.insert(kind, new_contents); - true + btree_map::Entry::Occupied(mut o) => { + if o.get() != &new_settings { + o.insert(new_settings); + zed_settings_changed = true; + } + } + } + } + (LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => { + match self + .raw_editorconfig_settings + .entry((root_id, directory_path.clone())) + { + btree_map::Entry::Vacant(v) => { + v.insert(editorconfig_contents.to_owned()); + editorconfig_settings_changed = true; + } + btree_map::Entry::Occupied(mut o) => { + if o.get() != editorconfig_contents { + o.insert(editorconfig_contents.to_owned()); + editorconfig_settings_changed = true; + } + } + } } - } else { - raw_local_settings.remove(&kind).is_some() }; - if changed { + + if zed_settings_changed { self.recompute_values(Some((root_id, &directory_path)), cx)?; } + if editorconfig_settings_changed { + self.recompute_editorconfig_values((root_id, &directory_path), cx)?; + } + Ok(()) } @@ -605,13 +650,10 @@ impl SettingsStore { cx: &mut AppContext, ) -> Result<()> { let settings: serde_json::Value = serde_json::to_value(content)?; - if settings.is_object() { - self.raw_extension_settings = settings; - self.recompute_values(None, cx)?; - Ok(()) - } else { - Err(anyhow!("settings must be an object")) - } + anyhow::ensure!(settings.is_object(), "settings must be an object"); + self.raw_extension_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) } /// Add or remove a set of local settings via a JSON string. @@ -625,7 +667,7 @@ impl SettingsStore { pub fn local_settings( &self, root_id: WorktreeId, - ) -> impl '_ + Iterator, LocalSettingsKind, String)> { + ) -> impl '_ + Iterator, String)> { self.raw_local_settings .range( (root_id, Path::new("").into()) @@ -634,12 +676,22 @@ impl SettingsStore { Path::new("").into(), ), ) - .flat_map(|((_, path), content)| { - content.iter().filter_map(|(&kind, raw_content)| { - let parsed_content = serde_json::to_string(raw_content).log_err()?; - Some((path.clone(), kind, parsed_content)) - }) - }) + .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + } + + pub fn local_editorconfig_settings( + &self, + root_id: WorktreeId, + ) -> impl '_ + Iterator, String)> { + self.raw_editorconfig_settings + .range( + (root_id, Path::new("").into()) + ..( + WorktreeId::from_usize(root_id.to_usize() + 1), + Path::new("").into(), + ), + ) + .map(|((_, path), content)| (path.clone(), content.clone())) } pub fn json_schema( @@ -819,69 +871,70 @@ impl SettingsStore { paths_stack.clear(); project_settings_stack.clear(); for ((root_id, directory_path), local_settings) in &self.raw_local_settings { - if let Some(local_settings) = local_settings.get(&LocalSettingsKind::Settings) { - // Build a stack of all of the local values for that setting. - while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry { - if root_id != prev_root_id || !directory_path.starts_with(prev_path) { - paths_stack.pop(); - project_settings_stack.pop(); - continue; - } + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !directory_path.starts_with(prev_path) { + paths_stack.pop(); + project_settings_stack.pop(); + continue; } - break; } + break; + } - match setting_value.deserialize_setting(local_settings) { - Ok(local_settings) => { - paths_stack.push(Some((*root_id, directory_path.as_ref()))); - project_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_root_id, changed_local_path)| { - *root_id != changed_root_id - || !directory_path.starts_with(changed_local_path) - }, - ) { - continue; - } - - if let Some(value) = setting_value - .load_setting( - SettingsSources { - default: &default_settings, - extensions: extension_settings.as_ref(), - user: user_settings.as_ref(), - release_channel: release_channel_settings.as_ref(), - server: server_settings.as_ref(), - project: &project_settings_stack.iter().collect::>(), - }, - cx, - ) - .log_err() - { - setting_value.set_local_value( - *root_id, - directory_path.clone(), - value, - ); - } + match setting_value.deserialize_setting(local_settings) { + Ok(local_settings) => { + paths_stack.push(Some((*root_id, directory_path.as_ref()))); + project_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_root_id, changed_local_path)| { + *root_id != changed_root_id + || !directory_path.starts_with(changed_local_path) + }, + ) { + continue; } - Err(error) => { - return Err(InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: error.to_string(), - }); + + if let Some(value) = setting_value + .load_setting( + SettingsSources { + default: &default_settings, + extensions: extension_settings.as_ref(), + user: user_settings.as_ref(), + release_channel: release_channel_settings.as_ref(), + server: server_settings.as_ref(),project: &project_settings_stack.iter().collect::>(), + }, + cx, + ) + .log_err() + { + setting_value.set_local_value(*root_id, directory_path.clone(), value); } } + Err(error) => { + return Err(InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: error.to_string(), + }); + } } } } Ok(()) } + + fn recompute_editorconfig_values( + &self, + directory_of_item_changed: (WorktreeId, &Path), + cx: &mut AppContext, + ) -> Result<()> { + todo!("TODO kb sync editorconfig settings") + } } #[derive(Debug, Clone, PartialEq)] From b846bddefe1cf3c0867a43af7f09b0e5243b0d4f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 15 Oct 2024 13:33:57 +0300 Subject: [PATCH 02/12] Draft editorconfig parsing --- Cargo.lock | 44 +++++- Cargo.toml | 2 +- crates/project/src/project_settings.rs | 2 +- crates/settings/Cargo.toml | 2 +- crates/settings/src/editorconfig.rs | 189 +++++++++++++++++++++++++ crates/settings/src/settings.rs | 1 + crates/settings/src/settings_store.rs | 83 ++++++++--- 7 files changed, 292 insertions(+), 31 deletions(-) create mode 100644 crates/settings/src/editorconfig.rs diff --git a/Cargo.lock b/Cargo.lock index 30a36135e7cf8..5ad05f02196ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3596,6 +3596,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "docs_preprocessor" version = "0.1.0" @@ -3649,12 +3658,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" -[[package]] -name = "ec4rs" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf65d056c7da9c971c2847ce250fd1f0f9659d5718845c3ec0ad95f5668352c" - [[package]] name = "ecdsa" version = "0.14.8" @@ -7679,6 +7682,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -9610,6 +9623,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rust_decimal" version = "1.36.0" @@ -10307,7 +10331,6 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "ec4rs", "fs", "futures 0.3.30", "gpui", @@ -10317,6 +10340,7 @@ dependencies = [ "pretty_assertions", "release_channel", "rust-embed", + "rust-ini", "schemars", "serde", "serde_derive", @@ -12499,6 +12523,12 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 9ffe48b4a1aaa..4aad09bccac35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,7 +347,6 @@ ctor = "0.2.6" dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" -ec4rs = "1.1" emojis = "0.6.1" env_logger = "0.11" exec = "0.3.1" @@ -404,6 +403,7 @@ runtimelib = { version = "0.15", default-features = false, features = [ ] } rustc-demangle = "0.1.23" rust-embed = { version = "8.4", features = ["include-exclude"] } +rust-ini = "0.21" rustls = "0.20.3" rustls-native-certs = "0.8.0" schemars = { version = "0.8", features = ["impl_json_schema"] } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index efa107d3b3099..cab420d12cd9f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -300,7 +300,7 @@ impl SettingsObserver { }) .log_err(); } - for (path, content) in store.local_editorconfig_settings(worktree.read(cx).id()) { + for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) { downstream_client .send(proto::UpdateWorktreeSettings { project_id, diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index cad4b2b0cf7d3..0ea57d2a9f09b 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -18,7 +18,6 @@ test-support = ["gpui/test-support", "fs/test-support"] [dependencies] anyhow.workspace = true collections.workspace = true -ec4rs.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -26,6 +25,7 @@ log.workspace = true paths.workspace = true release_channel.workspace = true rust-embed.workspace = true +rust-ini.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/settings/src/editorconfig.rs b/crates/settings/src/editorconfig.rs new file mode 100644 index 0000000000000..01e3325b1006d --- /dev/null +++ b/crates/settings/src/editorconfig.rs @@ -0,0 +1,189 @@ +use std::{num::NonZeroU32, str::FromStr}; + +use collections::{hash_map, HashMap}; +use gpui::SharedString; + +use anyhow::Context as _; +use ini::Ini; + +#[derive(Debug, Clone)] +pub struct Editorconfig { + main_section: Section, + glob_sections: HashMap, + glob_sections_order: Vec, + is_root: bool, +} + +impl Editorconfig { + pub fn merge_with(&mut self, parent_editorconfig: &Editorconfig) { + debug_assert!( + !self.is_root, + "Should not merge root editorconfig with more properties" + ); + + self.main_section + .merge_with(&parent_editorconfig.main_section); + for (parent_pattern, parent_section) in &parent_editorconfig.glob_sections { + match self.glob_sections.entry(parent_pattern.clone()) { + hash_map::Entry::Vacant(e) => { + self.glob_sections_order.push(parent_pattern.clone()); + e.insert(*parent_section); + } + hash_map::Entry::Occupied(mut e) => { + e.get_mut().merge_with(parent_section); + } + } + } + } + + pub fn is_root(&self) -> bool { + self.is_root + } +} + +#[derive(Debug, Default, Copy, Clone)] +struct Section { + indent_style: Option, + indent_size: Option, + tab_width: Option, + trim_trailing_whitespace: Option, + insert_final_newline: Option, + max_line_length: Option, + // Currently not supported by Zed + // end_of_line: Option, + // charset: Option, + // spelling_language: Option, +} +impl Section { + fn merge_with(&mut self, parent_section: &Section) { + if self.indent_style.is_none() { + self.indent_style = parent_section.indent_style; + } + if self.indent_size.is_none() { + self.indent_size = parent_section.indent_size; + } + if self.tab_width.is_none() { + self.tab_width = parent_section.tab_width; + } + if self.trim_trailing_whitespace.is_none() { + self.trim_trailing_whitespace = parent_section.trim_trailing_whitespace; + } + if self.insert_final_newline.is_none() { + self.insert_final_newline = parent_section.insert_final_newline; + } + if self.max_line_length.is_none() { + self.max_line_length = parent_section.max_line_length; + } + } +} + +#[derive(Debug, Copy, Clone)] +enum IndentStyle { + Tab, + Space, +} + +#[derive(Debug, Copy, Clone)] +enum IndentSize { + Tab, + Value(NonZeroU32), +} + +#[derive(Debug, Copy, Clone)] +enum MaxLineLength { + Off, + Value(NonZeroU32), +} + +impl FromStr for Editorconfig { + type Err = anyhow::Error; + + // TODO kb be more lenient and allow (omit) partially incorrect fields? + fn from_str(s: &str) -> Result { + let editorconfig_ini = + Ini::load_from_str(s).context("parsing editorconfig string as ini")?; + let mut is_root = false; + let mut main_section = Section::default(); + let mut glob_sections = HashMap::default(); + let mut glob_sections_order = Vec::new(); + for (ini_section, ini_properties) in editorconfig_ini { + let (has_section, section_to_fill) = match ini_section { + Some(ini_section) => { + let ini_section = SharedString::from(ini_section); + let section_to_fill = match glob_sections.entry(ini_section.clone()) { + hash_map::Entry::Occupied(mut o) => { + o.insert(Section::default()); + o.into_mut() + } + hash_map::Entry::Vacant(v) => { + glob_sections_order.push(ini_section); + v.insert(Section::default()) + } + }; + (true, section_to_fill) + } + None => (false, &mut main_section), + }; + + for (mut ini_property, mut ini_value) in ini_properties { + ini_property.make_ascii_lowercase(); + ini_value.make_ascii_lowercase(); + let ini_property = ini_property.as_str(); + let ini_value = ini_value.as_str(); + match ini_property { + "indent_style" => match ini_value { + "tab" => section_to_fill.indent_style = Some(IndentStyle::Tab), + "space" => section_to_fill.indent_style = Some(IndentStyle::Space), + _unknown => {} + }, + "indent_size" => match ini_value { + "tab" => section_to_fill.indent_size = Some(IndentSize::Tab), + value => { + section_to_fill.indent_size = Some(IndentSize::Value( + NonZeroU32::from_str(value).context("parsing indent_size")?, + )) + } + }, + "tab_width" => { + section_to_fill.tab_width = + Some(NonZeroU32::from_str(ini_value).context("parsing tab_width")?) + } + "trim_trailing_whitespace" => { + section_to_fill.trim_trailing_whitespace = Some( + bool::from_str(ini_value) + .context("parsing trim_trailing_whitespace")?, + ) + } + "insert_final_newline" => { + section_to_fill.insert_final_newline = Some( + bool::from_str(ini_value).context("parsing insert_final_newline")?, + ) + } + "max_line_length" => match ini_value { + "off" => section_to_fill.max_line_length = Some(MaxLineLength::Off), + value => { + section_to_fill.max_line_length = Some(MaxLineLength::Value( + NonZeroU32::from_str(value).context("parsing max_line_length")?, + )) + } + }, + "root" if !has_section => { + is_root = bool::from_str(ini_value).context("parsing root")?; + } + // unsupported + "end_of_line" => {} + "charset" => {} + "spelling_language" => {} + _unknown => {} + } + } + } + + Ok(Self { + main_section, + is_root, + glob_sections, + glob_sections_order, + }) + } +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 40c371d9951ff..c545301be0848 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,4 +1,5 @@ mod editable_setting_control; +mod editorconfig; mod json_schema; mod keymap_file; mod settings_file; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 14f4721b49371..2f1aab901cf26 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -18,7 +18,7 @@ use std::{ use tree_sitter::Query; use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; -use crate::{SettingsJsonSchemaParams, WorktreeId}; +use crate::{editorconfig::Editorconfig, SettingsJsonSchemaParams, WorktreeId}; /// A value that can be defined as a user setting. /// @@ -168,7 +168,7 @@ pub struct SettingsStore { raw_server_settings: Option, raw_extension_settings: serde_json::Value, raw_local_settings: BTreeMap<(WorktreeId, Arc), serde_json::Value>, - raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), String>, + raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, tab_size_callback: Option<( TypeId, Box Option + Send + Sync + 'static>, @@ -226,7 +226,7 @@ impl SettingsStore { raw_server_settings: None, raw_extension_settings: serde_json::json!({}), raw_local_settings: Default::default(), - raw_editorconfig_settings: Default::default(), + raw_editorconfig_settings: BTreeMap::default(), tab_size_callback: Default::default(), setting_file_updates_tx, _setting_file_updates: cx.spawn(|cx| async move { @@ -577,7 +577,9 @@ impl SettingsStore { .filter(|content| !content.is_empty()), ) { (LocalSettingsKind::Tasks, _) => { - anyhow::bail!("Attempted to submit tasks into the settings store"); + return Err(InvalidSettingsError::Other { + message: "Attempted to submit tasks into the settings store".to_string(), + }) } (LocalSettingsKind::Settings, None) => { zed_settings_changed = self @@ -593,11 +595,9 @@ impl SettingsStore { } (LocalSettingsKind::Settings, Some(settings_contents)) => { let new_settings = parse_json_with_comments::(settings_contents) - .map_err(|e| { - InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: e.to_string(), - } + .map_err(|e| InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), })?; match self .raw_local_settings @@ -621,12 +621,18 @@ impl SettingsStore { .entry((root_id, directory_path.clone())) { btree_map::Entry::Vacant(v) => { - v.insert(editorconfig_contents.to_owned()); + v.insert(( + editorconfig_contents.to_owned(), + editorconfig_contents.parse().log_err(), + )); editorconfig_settings_changed = true; } btree_map::Entry::Occupied(mut o) => { - if o.get() != editorconfig_contents { - o.insert(editorconfig_contents.to_owned()); + if o.get().0 != editorconfig_contents { + o.insert(( + editorconfig_contents.to_owned(), + editorconfig_contents.parse().log_err(), + )); editorconfig_settings_changed = true; } } @@ -638,7 +644,10 @@ impl SettingsStore { self.recompute_values(Some((root_id, &directory_path)), cx)?; } if editorconfig_settings_changed { - self.recompute_editorconfig_values((root_id, &directory_path), cx)?; + self.recompute_editorconfig_values(root_id, cx) + .map_err(|e| InvalidSettingsError::Editorconfig { + message: e.to_string(), + })?; } Ok(()) @@ -682,7 +691,7 @@ impl SettingsStore { pub fn local_editorconfig_settings( &self, root_id: WorktreeId, - ) -> impl '_ + Iterator, String)> { + ) -> impl '_ + Iterator, String, Option)> { self.raw_editorconfig_settings .range( (root_id, Path::new("").into()) @@ -691,7 +700,9 @@ impl SettingsStore { Path::new("").into(), ), ) - .map(|((_, path), content)| (path.clone(), content.clone())) + .map(|((_, path), (content, parsed_content))| { + (path.clone(), content.clone(), parsed_content.clone()) + }) } pub fn json_schema( @@ -805,7 +816,7 @@ impl SettingsStore { &mut self, changed_local_path: Option<(WorktreeId, &Path)>, cx: &mut AppContext, - ) -> Result<(), InvalidSettingsError> { + ) -> std::result::Result<(), InvalidSettingsError> { // Reload the global and local values for every setting. let mut project_settings_stack = Vec::::new(); let mut paths_stack = Vec::>::new(); @@ -907,7 +918,8 @@ impl SettingsStore { extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), - server: server_settings.as_ref(),project: &project_settings_stack.iter().collect::>(), + server: server_settings.as_ref(), + project: &project_settings_stack.iter().collect::>(), }, cx, ) @@ -930,10 +942,35 @@ impl SettingsStore { fn recompute_editorconfig_values( &self, - directory_of_item_changed: (WorktreeId, &Path), + for_worktree: WorktreeId, cx: &mut AppContext, ) -> Result<()> { - todo!("TODO kb sync editorconfig settings") + let parsed_editorconfigs = self + .local_editorconfig_settings(for_worktree) + .map(|(editorconfig_path, _, parsed_editorconfig)| { + Ok(( + editorconfig_path, + parsed_editorconfig.context("editorconfig is not parsed successfully")?, + )) + }) + .collect::>>() + .context("reloading worktree editorconfig settings")?; + if parsed_editorconfigs.is_empty() { + return Ok(()); + } + + let mut merged_editorconfigs = Vec::with_capacity(parsed_editorconfigs.len()); + for (path, mut editorconfig) in parsed_editorconfigs { + if !editorconfig.is_root() { + if let Some((_, parent_editorconfig)) = merged_editorconfigs.last() { + editorconfig.merge_with(parent_editorconfig); + } + } + merged_editorconfigs.push((path, editorconfig)); + } + + todo!("TODO kb call someone to accept the newly merged sequence"); + Ok(()) } } @@ -943,6 +980,8 @@ pub enum InvalidSettingsError { UserSettings { message: String }, ServerSettings { message: String }, DefaultSettings { message: String }, + Editorconfig { message: String }, + Other { message: String }, } impl std::fmt::Display for InvalidSettingsError { @@ -951,8 +990,10 @@ impl std::fmt::Display for InvalidSettingsError { InvalidSettingsError::LocalSettings { message, .. } | InvalidSettingsError::UserSettings { message } | InvalidSettingsError::ServerSettings { message } - | InvalidSettingsError::DefaultSettings { message } => { - write!(f, "{}", message) + | InvalidSettingsError::DefaultSettings { message } + | InvalidSettingsError::Other { message } + | InvalidSettingsError::Editorconfig { message } => { + write!(f, "{message}") } } } From 06fc4fb533d64bc36b62f6a2babb1ac109624b39 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Oct 2024 12:24:55 +0300 Subject: [PATCH 03/12] Store ec4rs data ready for properties retrieval --- Cargo.lock | 44 +----- Cargo.toml | 2 +- crates/settings/Cargo.toml | 2 +- crates/settings/src/editorconfig.rs | 189 -------------------------- crates/settings/src/settings.rs | 1 - crates/settings/src/settings_store.rs | 129 +++++++++++------- 6 files changed, 89 insertions(+), 278 deletions(-) delete mode 100644 crates/settings/src/editorconfig.rs diff --git a/Cargo.lock b/Cargo.lock index 5ad05f02196ec..30a36135e7cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3596,15 +3596,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "docs_preprocessor" version = "0.1.0" @@ -3658,6 +3649,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ec4rs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf65d056c7da9c971c2847ce250fd1f0f9659d5718845c3ec0ad95f5668352c" + [[package]] name = "ecdsa" version = "0.14.8" @@ -7682,16 +7679,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -9623,17 +9610,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust-ini" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" -dependencies = [ - "cfg-if", - "ordered-multimap", - "trim-in-place", -] - [[package]] name = "rust_decimal" version = "1.36.0" @@ -10331,6 +10307,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "ec4rs", "fs", "futures 0.3.30", "gpui", @@ -10340,7 +10317,6 @@ dependencies = [ "pretty_assertions", "release_channel", "rust-embed", - "rust-ini", "schemars", "serde", "serde_derive", @@ -12523,12 +12499,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 4aad09bccac35..9ffe48b4a1aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -347,6 +347,7 @@ ctor = "0.2.6" dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" +ec4rs = "1.1" emojis = "0.6.1" env_logger = "0.11" exec = "0.3.1" @@ -403,7 +404,6 @@ runtimelib = { version = "0.15", default-features = false, features = [ ] } rustc-demangle = "0.1.23" rust-embed = { version = "8.4", features = ["include-exclude"] } -rust-ini = "0.21" rustls = "0.20.3" rustls-native-certs = "0.8.0" schemars = { version = "0.8", features = ["impl_json_schema"] } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 0ea57d2a9f09b..cad4b2b0cf7d3 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "fs/test-support"] [dependencies] anyhow.workspace = true collections.workspace = true +ec4rs.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -25,7 +26,6 @@ log.workspace = true paths.workspace = true release_channel.workspace = true rust-embed.workspace = true -rust-ini.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/settings/src/editorconfig.rs b/crates/settings/src/editorconfig.rs deleted file mode 100644 index 01e3325b1006d..0000000000000 --- a/crates/settings/src/editorconfig.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::{num::NonZeroU32, str::FromStr}; - -use collections::{hash_map, HashMap}; -use gpui::SharedString; - -use anyhow::Context as _; -use ini::Ini; - -#[derive(Debug, Clone)] -pub struct Editorconfig { - main_section: Section, - glob_sections: HashMap, - glob_sections_order: Vec, - is_root: bool, -} - -impl Editorconfig { - pub fn merge_with(&mut self, parent_editorconfig: &Editorconfig) { - debug_assert!( - !self.is_root, - "Should not merge root editorconfig with more properties" - ); - - self.main_section - .merge_with(&parent_editorconfig.main_section); - for (parent_pattern, parent_section) in &parent_editorconfig.glob_sections { - match self.glob_sections.entry(parent_pattern.clone()) { - hash_map::Entry::Vacant(e) => { - self.glob_sections_order.push(parent_pattern.clone()); - e.insert(*parent_section); - } - hash_map::Entry::Occupied(mut e) => { - e.get_mut().merge_with(parent_section); - } - } - } - } - - pub fn is_root(&self) -> bool { - self.is_root - } -} - -#[derive(Debug, Default, Copy, Clone)] -struct Section { - indent_style: Option, - indent_size: Option, - tab_width: Option, - trim_trailing_whitespace: Option, - insert_final_newline: Option, - max_line_length: Option, - // Currently not supported by Zed - // end_of_line: Option, - // charset: Option, - // spelling_language: Option, -} -impl Section { - fn merge_with(&mut self, parent_section: &Section) { - if self.indent_style.is_none() { - self.indent_style = parent_section.indent_style; - } - if self.indent_size.is_none() { - self.indent_size = parent_section.indent_size; - } - if self.tab_width.is_none() { - self.tab_width = parent_section.tab_width; - } - if self.trim_trailing_whitespace.is_none() { - self.trim_trailing_whitespace = parent_section.trim_trailing_whitespace; - } - if self.insert_final_newline.is_none() { - self.insert_final_newline = parent_section.insert_final_newline; - } - if self.max_line_length.is_none() { - self.max_line_length = parent_section.max_line_length; - } - } -} - -#[derive(Debug, Copy, Clone)] -enum IndentStyle { - Tab, - Space, -} - -#[derive(Debug, Copy, Clone)] -enum IndentSize { - Tab, - Value(NonZeroU32), -} - -#[derive(Debug, Copy, Clone)] -enum MaxLineLength { - Off, - Value(NonZeroU32), -} - -impl FromStr for Editorconfig { - type Err = anyhow::Error; - - // TODO kb be more lenient and allow (omit) partially incorrect fields? - fn from_str(s: &str) -> Result { - let editorconfig_ini = - Ini::load_from_str(s).context("parsing editorconfig string as ini")?; - let mut is_root = false; - let mut main_section = Section::default(); - let mut glob_sections = HashMap::default(); - let mut glob_sections_order = Vec::new(); - for (ini_section, ini_properties) in editorconfig_ini { - let (has_section, section_to_fill) = match ini_section { - Some(ini_section) => { - let ini_section = SharedString::from(ini_section); - let section_to_fill = match glob_sections.entry(ini_section.clone()) { - hash_map::Entry::Occupied(mut o) => { - o.insert(Section::default()); - o.into_mut() - } - hash_map::Entry::Vacant(v) => { - glob_sections_order.push(ini_section); - v.insert(Section::default()) - } - }; - (true, section_to_fill) - } - None => (false, &mut main_section), - }; - - for (mut ini_property, mut ini_value) in ini_properties { - ini_property.make_ascii_lowercase(); - ini_value.make_ascii_lowercase(); - let ini_property = ini_property.as_str(); - let ini_value = ini_value.as_str(); - match ini_property { - "indent_style" => match ini_value { - "tab" => section_to_fill.indent_style = Some(IndentStyle::Tab), - "space" => section_to_fill.indent_style = Some(IndentStyle::Space), - _unknown => {} - }, - "indent_size" => match ini_value { - "tab" => section_to_fill.indent_size = Some(IndentSize::Tab), - value => { - section_to_fill.indent_size = Some(IndentSize::Value( - NonZeroU32::from_str(value).context("parsing indent_size")?, - )) - } - }, - "tab_width" => { - section_to_fill.tab_width = - Some(NonZeroU32::from_str(ini_value).context("parsing tab_width")?) - } - "trim_trailing_whitespace" => { - section_to_fill.trim_trailing_whitespace = Some( - bool::from_str(ini_value) - .context("parsing trim_trailing_whitespace")?, - ) - } - "insert_final_newline" => { - section_to_fill.insert_final_newline = Some( - bool::from_str(ini_value).context("parsing insert_final_newline")?, - ) - } - "max_line_length" => match ini_value { - "off" => section_to_fill.max_line_length = Some(MaxLineLength::Off), - value => { - section_to_fill.max_line_length = Some(MaxLineLength::Value( - NonZeroU32::from_str(value).context("parsing max_line_length")?, - )) - } - }, - "root" if !has_section => { - is_root = bool::from_str(ini_value).context("parsing root")?; - } - // unsupported - "end_of_line" => {} - "charset" => {} - "spelling_language" => {} - _unknown => {} - } - } - } - - Ok(Self { - main_section, - is_root, - glob_sections, - glob_sections_order, - }) - } -} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index c545301be0848..40c371d9951ff 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,4 @@ mod editable_setting_control; -mod editorconfig; mod json_schema; mod keymap_file; mod settings_file; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 2f1aab901cf26..30d680ccfd321 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, Context, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; +use ec4rs::{ConfigParser, Properties as EditorconfigProperties, PropertiesSource, Section}; use fs::Fs; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal}; -use paths::local_settings_file_relative_path; +use paths::{local_settings_file_relative_path, EDITORCONFIG_NAME}; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; use smallvec::SmallVec; @@ -12,13 +13,13 @@ use std::{ fmt::Debug, ops::Range, path::{Path, PathBuf}, - str, + str::{self, FromStr}, sync::{Arc, LazyLock}, }; use tree_sitter::Query; use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; -use crate::{editorconfig::Editorconfig, SettingsJsonSchemaParams, WorktreeId}; +use crate::{SettingsJsonSchemaParams, WorktreeId}; /// A value that can be defined as a user setting. /// @@ -179,6 +180,26 @@ pub struct SettingsStore { >, } +#[derive(Clone)] +pub struct Editorconfig { + pub is_root: bool, + pub sections: Vec
, +} + +impl FromStr for Editorconfig { + type Err = anyhow::Error; + + fn from_str(contents: &str) -> Result { + let parser = ConfigParser::new_buffered(contents.as_bytes()) + .context("creating editorconfig parser")?; + let is_root = parser.is_root; + let sections = parser + .collect::, _>>() + .context("parsing editorconfig sections")?; + Ok(Self { is_root, sections }) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum LocalSettingsKind { Settings, @@ -577,7 +598,7 @@ impl SettingsStore { .filter(|content| !content.is_empty()), ) { (LocalSettingsKind::Tasks, _) => { - return Err(InvalidSettingsError::Other { + return Err(InvalidSettingsError::Tasks { message: "Attempted to submit tasks into the settings store".to_string(), }) } @@ -620,20 +641,37 @@ impl SettingsStore { .raw_editorconfig_settings .entry((root_id, directory_path.clone())) { - btree_map::Entry::Vacant(v) => { - v.insert(( - editorconfig_contents.to_owned(), - editorconfig_contents.parse().log_err(), - )); - editorconfig_settings_changed = true; - } + btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() { + Ok(new_contents) => { + v.insert((editorconfig_contents.to_owned(), Some(new_contents))); + editorconfig_settings_changed = true; + } + Err(e) => { + v.insert((editorconfig_contents.to_owned(), None)); + return Err(InvalidSettingsError::Editorconfig { + message: e.to_string(), + path: directory_path.join(EDITORCONFIG_NAME), + }); + } + }, btree_map::Entry::Occupied(mut o) => { if o.get().0 != editorconfig_contents { - o.insert(( - editorconfig_contents.to_owned(), - editorconfig_contents.parse().log_err(), - )); - editorconfig_settings_changed = true; + match editorconfig_contents.parse() { + Ok(new_contents) => { + o.insert(( + editorconfig_contents.to_owned(), + Some(new_contents), + )); + editorconfig_settings_changed = true; + } + Err(e) => { + o.insert((editorconfig_contents.to_owned(), None)); + return Err(InvalidSettingsError::Editorconfig { + message: e.to_string(), + path: directory_path.join(EDITORCONFIG_NAME), + }); + } + } } } } @@ -644,10 +682,7 @@ impl SettingsStore { self.recompute_values(Some((root_id, &directory_path)), cx)?; } if editorconfig_settings_changed { - self.recompute_editorconfig_values(root_id, cx) - .map_err(|e| InvalidSettingsError::Editorconfig { - message: e.to_string(), - })?; + // TODO kb remove this all? } Ok(()) @@ -940,37 +975,33 @@ impl SettingsStore { Ok(()) } - fn recompute_editorconfig_values( + pub fn editorconfg_properties( &self, for_worktree: WorktreeId, - cx: &mut AppContext, - ) -> Result<()> { - let parsed_editorconfigs = self - .local_editorconfig_settings(for_worktree) - .map(|(editorconfig_path, _, parsed_editorconfig)| { - Ok(( - editorconfig_path, - parsed_editorconfig.context("editorconfig is not parsed successfully")?, - )) - }) - .collect::>>() - .context("reloading worktree editorconfig settings")?; - if parsed_editorconfigs.is_empty() { - return Ok(()); - } + for_path: &Path, + ) -> Option { + let mut properties = EditorconfigProperties::new(); + properties.use_fallbacks(); - let mut merged_editorconfigs = Vec::with_capacity(parsed_editorconfigs.len()); - for (path, mut editorconfig) in parsed_editorconfigs { - if !editorconfig.is_root() { - if let Some((_, parent_editorconfig)) = merged_editorconfigs.last() { - editorconfig.merge_with(parent_editorconfig); - } + for (directory_with_config, _, parsed_editorconfig) in + self.local_editorconfig_settings(for_worktree) + { + if !for_path.starts_with(&directory_with_config) { + return Some(properties); + } + let Some(parsed_editorconfig) = parsed_editorconfig else { + return None; + }; + if parsed_editorconfig.is_root { + properties = EditorconfigProperties::new(); + properties.use_fallbacks(); + } + for section in parsed_editorconfig.sections { + section.apply_to(&mut properties, for_path).log_err()?; } - merged_editorconfigs.push((path, editorconfig)); } - todo!("TODO kb call someone to accept the newly merged sequence"); - Ok(()) + Some(properties) } } @@ -980,8 +1011,8 @@ pub enum InvalidSettingsError { UserSettings { message: String }, ServerSettings { message: String }, DefaultSettings { message: String }, - Editorconfig { message: String }, - Other { message: String }, + Editorconfig { path: PathBuf, message: String }, + Tasks { message: String }, } impl std::fmt::Display for InvalidSettingsError { @@ -991,8 +1022,8 @@ impl std::fmt::Display for InvalidSettingsError { | InvalidSettingsError::UserSettings { message } | InvalidSettingsError::ServerSettings { message } | InvalidSettingsError::DefaultSettings { message } - | InvalidSettingsError::Other { message } - | InvalidSettingsError::Editorconfig { message } => { + | InvalidSettingsError::Tasks { message } + | InvalidSettingsError::Editorconfig { message, .. } => { write!(f, "{message}") } } From 82b9a8cd4cdf76a74519a9ae7c5f0345ec6ae6de Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Oct 2024 13:40:00 +0300 Subject: [PATCH 04/12] Tidy up files in language settings retrieval --- crates/editor/src/display_map.rs | 9 ++++----- crates/project/src/lsp_store.rs | 17 +++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 790a0a6a1eba7..8d4768384f8ed 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -423,11 +423,10 @@ impl DisplayMap { } fn tab_size(buffer: &Model, cx: &mut ModelContext) -> NonZeroU32 { - let language = buffer - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).language()); - language_settings(language, None, cx).tab_size + let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); + let language = buffer.and_then(|buffer| buffer.language()); + let file = buffer.and_then(|buffer| buffer.file()); + language_settings(language, file, cx).tab_size } #[cfg(test)] diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index fe0a6443bc811..6a150ad149e3d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1433,10 +1433,10 @@ impl LspStore { }); if let Some((language, adapter)) = language { let worktree = self.worktree_for_id(worktree_id, cx).ok(); - let file = worktree.as_ref().and_then(|tree| { - tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) - }); - if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { + if !self + .worktree_language_settings(worktree.as_ref(), &language.name(), cx) + .enable_language_server + { language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); } else if let Some(worktree) = worktree { let server_name = &adapter.name; @@ -5288,13 +5288,14 @@ impl LspStore { }) } - fn language_settings<'a>( + fn worktree_language_settings<'a>( &'a self, - worktree: &'a Model, + worktree: Option<&'a Model>, language: &LanguageName, cx: &'a mut ModelContext, ) -> &'a LanguageSettings { - let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx)); + let root_file = + worktree.and_then(|worktree| worktree.update(cx, |tree, cx| tree.root_file(cx))); all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language(Some(language)) } @@ -5304,7 +5305,7 @@ impl LspStore { language: LanguageName, cx: &mut ModelContext, ) { - let settings = self.language_settings(worktree, &language, cx); + let settings = self.worktree_language_settings(Some(worktree), &language, cx); if !settings.enable_language_server || self.mode.is_remote() { return; } From 29995d4b9bb234a81282b3a6a0528fc39d263588 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Oct 2024 17:41:35 +0300 Subject: [PATCH 05/12] Require location when querying for language settings --- .../remote_editing_collaboration_tests.rs | 6 +-- crates/copilot/src/copilot.rs | 6 ++- crates/editor/src/display_map.rs | 4 +- crates/editor/src/editor.rs | 17 ++++--- crates/editor/src/indent_guides.rs | 10 ++-- .../src/wasm_host/wit/since_v0_1_0.rs | 2 +- .../src/wasm_host/wit/since_v0_2_0.rs | 2 +- .../src/inline_completion_button.rs | 5 +- crates/language/src/buffer.rs | 17 +++++-- crates/language/src/language_settings.rs | 23 ++++++---- crates/languages/src/rust.rs | 6 +-- crates/languages/src/yaml.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 4 +- crates/prettier/src/prettier.rs | 2 +- crates/project/src/lsp_command.rs | 6 ++- crates/project/src/lsp_store.rs | 46 ++++++++----------- crates/project/src/project_tests.rs | 30 ++++-------- .../remote_server/src/remote_editing_tests.rs | 16 +++---- 18 files changed, 106 insertions(+), 98 deletions(-) diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index dae33457555ec..f4e96e1dd0571 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -3,7 +3,7 @@ use call::ActiveCall; use fs::{FakeFs, Fs as _}; use gpui::{Context as _, TestAppContext}; use http_client::BlockedHttpClient; -use language::{language_settings::all_language_settings, LanguageRegistry}; +use language::{language_settings::language_settings, LanguageRegistry}; use node_runtime::NodeRuntime; use project::ProjectPath; use remote::SshRemoteClient; @@ -134,9 +134,7 @@ async fn test_sharing_an_ssh_remote_project( cx_b.read(|cx| { let file = buffer_b.read(cx).file(); assert_eq!( - all_language_settings(file, cx) - .language(Some(&("Rust".into()))) - .language_servers, + language_settings(Some("Rust".into()), file, cx).language_servers, ["override-rust-analyzer".to_string()] ) }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index a1fd7a9bb9668..3773f5cdb2e3b 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -864,7 +864,11 @@ impl Copilot { let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); let position = position.to_point_utf16(buffer); - let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); + let settings = language_settings( + buffer.language_at(position).map(|l| l.name()), + buffer.file(), + cx, + ); let tab_size = settings.tab_size; let hard_tabs = settings.hard_tabs; let relative_path = buffer diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 8d4768384f8ed..699258f9a5849 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -424,7 +424,9 @@ impl DisplayMap { fn tab_size(buffer: &Model, cx: &mut ModelContext) -> NonZeroU32 { let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); - let language = buffer.and_then(|buffer| buffer.language()); + let language = buffer + .and_then(|buffer| buffer.language()) + .map(|l| l.name()); let file = buffer.and_then(|buffer| buffer.file()); language_settings(language, file, cx).tab_size } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba3841b4e2202..c014a8f679ec5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -90,7 +90,7 @@ pub use inline_completion_provider::*; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ - language_settings::{self, all_language_settings, InlayHintSettings}, + language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, @@ -428,8 +428,7 @@ impl Default for EditorStyle { } pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { - let show_background = all_language_settings(None, cx) - .language(None) + let show_background = language_settings::language_settings(None, None, cx) .inlay_hints .show_background; @@ -4248,7 +4247,10 @@ impl Editor { .text_anchor_for_position(position, cx)?; let settings = language_settings::language_settings( - buffer.read(cx).language_at(buffer_position).as_ref(), + buffer + .read(cx) + .language_at(buffer_position) + .map(|l| l.name()), buffer.read(cx).file(), cx, ); @@ -13376,11 +13378,8 @@ fn inlay_hint_settings( cx: &mut ViewContext<'_, Editor>, ) -> InlayHintSettings { let file = snapshot.file_at(location); - let language = snapshot.language_at(location); - let settings = all_language_settings(file, cx); - settings - .language(language.map(|l| l.name()).as_ref()) - .inlay_hints + let language = snapshot.language_at(location).map(|l| l.name()); + language_settings(language, file, cx).inlay_hints } fn consume_contiguous_rows( diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 520ca508b7648..815825b606bf1 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -39,9 +39,13 @@ impl Editor { ) -> Option> { let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx) - .indent_guides - .enabled + language_settings( + buffer.read(cx).language().map(|l| l.name()), + buffer.read(cx).file(), + cx, + ) + .indent_guides + .enabled } else { true } diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs index 862e2e7c7f789..4fa614ce78b41 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs @@ -357,7 +357,7 @@ impl ExtensionImports for WasmState { "language" => { let key = key.map(|k| LanguageName::new(&k)); let settings = - AllLanguageSettings::get(location, cx).language(key.as_ref()); + AllLanguageSettings::get(location, cx).language(location, key.as_ref()); Ok(serde_json::to_string(&settings::LanguageSettings { tab_size: settings.tab_size, })?) diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs index e7f5432e1d32c..46e9cc8b43973 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs @@ -403,7 +403,7 @@ impl ExtensionImports for WasmState { "language" => { let key = key.map(|k| LanguageName::new(&k)); let settings = - AllLanguageSettings::get(location, cx).language(key.as_ref()); + AllLanguageSettings::get(location, cx).language(location, key.as_ref()); Ok(serde_json::to_string(&settings::LanguageSettings { tab_size: settings.tab_size, })?) diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index abf8266320247..98bc258ffe138 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -248,8 +248,9 @@ impl InlineCompletionButton { if let Some(language) = self.language.clone() { let fs = fs.clone(); - let language_enabled = language_settings::language_settings(Some(&language), None, cx) - .show_inline_completions; + let language_enabled = + language_settings::language_settings(Some(language.name()), None, cx) + .show_inline_completions; menu = menu.entry( format!( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 92cd84202a250..4dd064feeaa16 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2490,7 +2490,11 @@ impl BufferSnapshot { /// Returns [`IndentSize`] for a given position that respects user settings /// and language preferences. pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { - let settings = language_settings(self.language_at(position), self.file(), cx); + let settings = language_settings( + self.language_at(position).map(|l| l.name()), + self.file(), + cx, + ); if settings.hard_tabs { IndentSize::tab() } else { @@ -2823,11 +2827,15 @@ impl BufferSnapshot { /// Returns the settings for the language at the given location. pub fn settings_at<'a, D: ToOffset>( - &self, + &'a self, position: D, cx: &'a AppContext, ) -> &'a LanguageSettings { - language_settings(self.language_at(position), self.file.as_ref(), cx) + language_settings( + self.language_at(position).map(|l| l.name()), + self.file.as_ref(), + cx, + ) } pub fn char_classifier_at(&self, point: T) -> CharClassifier { @@ -3529,7 +3537,8 @@ impl BufferSnapshot { ignore_disabled_for_language: bool, cx: &AppContext, ) -> Vec { - let language_settings = language_settings(self.language(), self.file.as_ref(), cx); + let language_settings = + language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx); let settings = language_settings.indent_guides; if !ignore_disabled_for_language && !settings.enabled { return Vec::new(); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index de37e52290bf4..b8e34dafef483 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -26,18 +26,21 @@ pub fn init(cx: &mut AppContext) { } /// Returns the settings for the specified language from the provided file. -pub fn language_settings<'a>( - language: Option<&Arc>, - file: Option<&Arc>, +pub fn language_settings<'a, 'b>( + language: Option, + file: Option<&'a Arc>, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language_name = language.map(|l| l.name()); - all_language_settings(file, cx).language(language_name.as_ref()) + let location = file.map(|f| SettingsLocation { + worktree_id: f.worktree_id(cx), + path: f.path().as_ref(), + }); + AllLanguageSettings::get(location, cx).language(location, language.as_ref()) } /// Returns the settings for all languages from the provided file. pub fn all_language_settings<'a>( - file: Option<&Arc>, + file: Option<&'a Arc>, cx: &'a AppContext, ) -> &'a AllLanguageSettings { let location = file.map(|f| SettingsLocation { @@ -810,7 +813,11 @@ impl InlayHintSettings { impl AllLanguageSettings { /// Returns the [`LanguageSettings`] for the language with the specified name. - pub fn language<'a>(&'a self, language_name: Option<&LanguageName>) -> &'a LanguageSettings { + pub fn language<'a>( + &'a self, + location: Option>, + language_name: Option<&LanguageName>, + ) -> &'a LanguageSettings { if let Some(name) = language_name { if let Some(overrides) = self.languages.get(name) { return overrides; @@ -840,7 +847,7 @@ impl AllLanguageSettings { } } - self.language(language.map(|l| l.name()).as_ref()) + self.language(None, language.map(|l| l.name()).as_ref()) .show_inline_completions } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 0d644e1bfef24..674ff1859b631 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -6,7 +6,6 @@ use futures::{io::BufReader, StreamExt}; use gpui::{AppContext, AsyncAppContext}; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; pub use language::*; -use language_settings::all_language_settings; use lsp::LanguageServerBinary; use regex::Regex; use smol::fs::{self, File}; @@ -21,6 +20,8 @@ use std::{ use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; use util::{fs::remove_matching, maybe, ResultExt}; +use crate::language_settings::language_settings; + pub struct RustLspAdapter; impl RustLspAdapter { @@ -424,8 +425,7 @@ impl ContextProvider for RustContextProvider { cx: &AppContext, ) -> Option { const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN"; - let package_to_run = all_language_settings(file.as_ref(), cx) - .language(Some(&"Rust".into())) + let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx) .tasks .variables .get(DEFAULT_RUN_NAME_STR); diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 642d6c030ac91..75453e0c56b96 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -101,7 +101,7 @@ impl LspAdapter for YamlLspAdapter { let tab_size = cx.update(|cx| { AllLanguageSettings::get(Some(location), cx) - .language(Some(&"YAML".into())) + .language(Some(location), Some(&"YAML".into())) .tab_size })?; let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}}); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f091c86ed92ab..0b0ea15a5d365 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1786,7 +1786,7 @@ impl MultiBuffer { language = buffer.language_at(offset); file = buffer.file(); } - language_settings(language.as_ref(), file, cx) + language_settings(language.map(|l| l.name()), file, cx) } pub fn for_each_buffer(&self, mut f: impl FnMut(&Model)) { @@ -3587,7 +3587,7 @@ impl MultiBufferSnapshot { language = buffer.language_at(offset); file = buffer.file(); } - language_settings(language, file, cx) + language_settings(language.map(|l| l.name()), file, cx) } pub fn language_scope_at(&self, point: T) -> Option { diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 012beb3fd7ab2..d2d56696a696e 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -205,7 +205,7 @@ impl Prettier { let params = buffer .update(cx, |buffer, cx| { let buffer_language = buffer.language(); - let language_settings = language_settings(buffer_language, buffer.file(), cx); + let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); let prettier_settings = &language_settings.prettier; anyhow::ensure!( prettier_settings.allowed, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4a80180d7cd88..bd9b452d182b7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2303,7 +2303,11 @@ impl LspCommand for OnTypeFormatting { .await?; let options = buffer.update(&mut cx, |buffer, cx| { - lsp_formatting_options(language_settings(buffer.language(), buffer.file(), cx)) + lsp_formatting_options(language_settings( + buffer.language().map(|l| l.name()), + buffer.file(), + cx, + )) })?; Ok(Self { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6a150ad149e3d..87915e3b51c79 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -30,8 +30,7 @@ use gpui::{ use http_client::HttpClient; use language::{ language_settings::{ - all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, - LanguageSettings, SelectedFormatter, + language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, }, markdown, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -223,7 +222,7 @@ impl LocalLspStore { })?; let settings = buffer.handle.update(&mut cx, |buffer, cx| { - language_settings(buffer.language(), buffer.file(), cx).clone() + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).clone() })?; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -280,7 +279,7 @@ impl LocalLspStore { .zip(buffer.abs_path.as_ref()); let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| { - language_settings(buffer.language(), buffer.file(), cx) + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) .prettier .clone() })?; @@ -1225,7 +1224,8 @@ impl LspStore { }); let buffer_file = buffer.read(cx).file().cloned(); - let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); + let settings = + language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).clone(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree_id = if let Some(file) = buffer_file { @@ -1400,7 +1400,7 @@ impl LspStore { let buffer = buffer.read(cx); let buffer_file = File::from_dyn(buffer.file()); let buffer_language = buffer.language(); - let settings = language_settings(buffer_language, buffer.file(), cx); + let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); if let Some(language) = buffer_language { if settings.enable_language_server { if let Some(file) = buffer_file { @@ -1433,10 +1433,13 @@ impl LspStore { }); if let Some((language, adapter)) = language { let worktree = self.worktree_for_id(worktree_id, cx).ok(); - if !self - .worktree_language_settings(worktree.as_ref(), &language.name(), cx) - .enable_language_server - { + let root_file = worktree.as_ref().and_then(|worktree| { + worktree + .update(cx, |tree, cx| tree.root_file(cx)) + .map(|f| f as _) + }); + let settings = language_settings(Some(language.name()), root_file.as_ref(), cx); + if !settings.enable_language_server { language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); } else if let Some(worktree) = worktree { let server_name = &adapter.name; @@ -1753,10 +1756,9 @@ impl LspStore { }) .filter(|_| { maybe!({ - let language_name = buffer.read(cx).language_at(position)?.name(); + let language = buffer.read(cx).language_at(position)?; Some( - AllLanguageSettings::get_global(cx) - .language(Some(&language_name)) + language_settings(Some(language.name()), buffer.read(cx).file(), cx) .linked_edits, ) }) == Some(true) @@ -1851,7 +1853,7 @@ impl LspStore { ) -> Task>> { let options = buffer.update(cx, |buffer, cx| { lsp_command::lsp_formatting_options(language_settings( - buffer.language_at(position).as_ref(), + buffer.language_at(position).map(|l| l.name()), buffer.file(), cx, )) @@ -5288,24 +5290,16 @@ impl LspStore { }) } - fn worktree_language_settings<'a>( - &'a self, - worktree: Option<&'a Model>, - language: &LanguageName, - cx: &'a mut ModelContext, - ) -> &'a LanguageSettings { - let root_file = - worktree.and_then(|worktree| worktree.update(cx, |tree, cx| tree.root_file(cx))); - all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language(Some(language)) - } - pub fn start_language_servers( &mut self, worktree: &Model, language: LanguageName, cx: &mut ModelContext, ) { - let settings = self.worktree_language_settings(Some(worktree), &language, cx); + let root_file = worktree + .update(cx, |tree, cx| tree.root_file(cx)) + .map(|f| f as _); + let settings = language_settings(Some(language.clone()), root_file.as_ref(), cx); if !settings.enable_language_server || self.mode.is_remote() { return; } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 8795af4cb44bd..db31455de7b46 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -146,26 +146,16 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) .update(|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(), - ) as _), - ), - cx, - ); - let settings_b = language_settings( - None, - Some( - &(File::for_entry( - tree.entry_for_path("b/b.rs").unwrap().clone(), - worktree.clone(), - ) as _), - ), - cx, - ); + let file_a = File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + ) as _; + let settings_a = language_settings(None, Some(&file_a), cx); + let file_b = File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + ) as _; + let settings_b = language_settings(None, Some(&file_b), cx); assert_eq!(settings_a.tab_size.get(), 8); assert_eq!(settings_b.tab_size.get(), 2); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 41065ad550831..e6a754e21caf2 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -5,7 +5,7 @@ use fs::{FakeFs, Fs}; use gpui::{Context, Model, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ - language_settings::{all_language_settings, AllLanguageSettings}, + language_settings::{language_settings, AllLanguageSettings}, Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName, LineEnding, }; @@ -208,7 +208,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo server_cx.read(|cx| { assert_eq!( AllLanguageSettings::get_global(cx) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into())) .language_servers, ["from-local-settings".to_string()] ) @@ -228,7 +228,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo server_cx.read(|cx| { assert_eq!( AllLanguageSettings::get_global(cx) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into())) .language_servers, ["from-server-settings".to_string()] ) @@ -287,7 +287,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo }), cx ) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into())) .language_servers, ["override-rust-analyzer".to_string()] ) @@ -296,9 +296,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo cx.read(|cx| { let file = buffer.read(cx).file(); assert_eq!( - all_language_settings(file, cx) - .language(Some(&"Rust".into())) - .language_servers, + language_settings(Some("Rust".into()), file, cx).language_servers, ["override-rust-analyzer".to_string()] ) }); @@ -379,9 +377,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext cx.read(|cx| { let file = buffer.read(cx).file(); assert_eq!( - all_language_settings(file, cx) - .language(Some(&"Rust".into())) - .language_servers, + language_settings(Some("Rust".into()), file, cx).language_servers, ["rust-analyzer".to_string()] ) }); From 88af7c2e061d544f751d52b6de85dab022ab58c3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Oct 2024 18:42:18 +0300 Subject: [PATCH 06/12] Prepare to merge editorconfig into the language settings --- .../src/copilot_completion_provider.rs | 4 +- .../src/wasm_host/wit/since_v0_1_0.rs | 7 ++- .../src/wasm_host/wit/since_v0_2_0.rs | 7 ++- .../src/inline_completion_button.rs | 9 +-- crates/language/src/buffer.rs | 3 +- crates/language/src/language_settings.rs | 60 +++++++++++++++---- crates/languages/src/rust.rs | 5 +- crates/languages/src/yaml.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 4 +- crates/project/src/lsp_command.rs | 8 +-- crates/project/src/lsp_store.rs | 24 +++++--- .../remote_server/src/remote_editing_tests.rs | 6 +- crates/settings/src/settings.rs | 4 +- crates/settings/src/settings_store.rs | 4 +- .../src/supermaven_completion_provider.rs | 2 +- 15 files changed, 101 insertions(+), 48 deletions(-) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 3a3361cda1996..059d3a4236adc 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -77,7 +77,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { let file = buffer.file(); let language = buffer.language_at(cursor_position); let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref())) + settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } fn refresh( @@ -209,7 +209,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { ) { let settings = AllLanguageSettings::get_global(cx); - let copilot_enabled = settings.inline_completions_enabled(None, None); + let copilot_enabled = settings.inline_completions_enabled(None, None, cx); if !copilot_enabled { return; diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs index 4fa614ce78b41..57b2edd301fe7 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs @@ -356,8 +356,11 @@ impl ExtensionImports for WasmState { cx.update(|cx| match category.as_str() { "language" => { let key = key.map(|k| LanguageName::new(&k)); - let settings = - AllLanguageSettings::get(location, cx).language(location, key.as_ref()); + let settings = AllLanguageSettings::get(location, cx).language( + location, + key.as_ref(), + cx, + ); Ok(serde_json::to_string(&settings::LanguageSettings { tab_size: settings.tab_size, })?) diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs index 46e9cc8b43973..da5632f3aecb8 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_2_0.rs @@ -402,8 +402,11 @@ impl ExtensionImports for WasmState { cx.update(|cx| match category.as_str() { "language" => { let key = key.map(|k| LanguageName::new(&k)); - let settings = - AllLanguageSettings::get(location, cx).language(location, key.as_ref()); + let settings = AllLanguageSettings::get(location, cx).language( + location, + key.as_ref(), + cx, + ); Ok(serde_json::to_string(&settings::LanguageSettings { tab_size: settings.tab_size, })?) diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 98bc258ffe138..8f727fd2fe5c8 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -62,7 +62,7 @@ impl Render for InlineCompletionButton { let status = copilot.read(cx).status(); let enabled = self.editor_enabled.unwrap_or_else(|| { - all_language_settings.inline_completions_enabled(None, None) + all_language_settings.inline_completions_enabled(None, None, cx) }); let icon = match status { @@ -293,7 +293,7 @@ impl InlineCompletionButton { ); } - let globally_enabled = settings.inline_completions_enabled(None, None); + let globally_enabled = settings.inline_completions_enabled(None, None, cx); menu.entry( if globally_enabled { "Hide Inline Completions for All Files" @@ -341,6 +341,7 @@ impl InlineCompletionButton { && all_language_settings(file, cx).inline_completions_enabled( language, file.map(|file| file.path().as_ref()), + cx, ), ) }; @@ -443,7 +444,7 @@ async fn configure_disabled_globs( fn toggle_inline_completions_globally(fs: Arc, cx: &mut AppContext) { let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(None, None); + all_language_settings(None, cx).inline_completions_enabled(None, None, cx); update_settings_file::(fs, cx, move |file, _| { file.defaults.show_inline_completions = Some(!show_inline_completions) }); @@ -467,7 +468,7 @@ fn toggle_inline_completions_for_language( cx: &mut AppContext, ) { let show_inline_completions = - all_language_settings(None, cx).inline_completions_enabled(Some(&language), None); + all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx); update_settings_file::(fs, cx, move |file, _| { file.languages .entry(language.name()) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4dd064feeaa16..132ace668304c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -37,6 +37,7 @@ use smallvec::SmallVec; use smol::future::yield_now; use std::{ any::Any, + borrow::Cow, cell::Cell, cmp::{self, Ordering, Reverse}, collections::BTreeMap, @@ -2830,7 +2831,7 @@ impl BufferSnapshot { &'a self, position: D, cx: &'a AppContext, - ) -> &'a LanguageSettings { + ) -> Cow<'a, LanguageSettings> { language_settings( self.language_at(position).map(|l| l.name()), self.file.as_ref(), diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index b8e34dafef483..de96569d95263 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -16,8 +16,11 @@ use serde::{ Deserialize, Deserializer, Serialize, }; use serde_json::Value; -use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; -use std::{num::NonZeroU32, path::Path, sync::Arc}; +use settings::{ + add_references_to_properties, EditorconfigProperties, Settings, SettingsLocation, + SettingsSources, SettingsStore, +}; +use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; use util::serde::default_true; /// Initializes the language settings. @@ -30,12 +33,12 @@ pub fn language_settings<'a, 'b>( language: Option, file: Option<&'a Arc>, cx: &'a AppContext, -) -> &'a LanguageSettings { +) -> Cow<'a, LanguageSettings> { let location = file.map(|f| SettingsLocation { worktree_id: f.worktree_id(cx), path: f.path().as_ref(), }); - AllLanguageSettings::get(location, cx).language(location, language.as_ref()) + AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx) } /// Returns the settings for all languages from the provided file. @@ -817,13 +820,23 @@ impl AllLanguageSettings { &'a self, location: Option>, language_name: Option<&LanguageName>, - ) -> &'a LanguageSettings { - if let Some(name) = language_name { - if let Some(overrides) = self.languages.get(name) { - return overrides; - } + cx: &'a AppContext, + ) -> Cow<'a, LanguageSettings> { + let settings = language_name + .and_then(|name| self.languages.get(name)) + .unwrap_or(&self.defaults); + + let editorconfig_properties = location.and_then(|location| { + cx.global::() + .editorconfg_properties(location.worktree_id, location.path) + }); + if let Some(editorconfig_properties) = editorconfig_properties { + let mut settings = settings.clone(); + merge_with_editorconfig(&mut settings, &editorconfig_properties); + Cow::Owned(settings) + } else { + Cow::Borrowed(settings) } - &self.defaults } /// Returns whether inline completions are enabled for the given path. @@ -840,6 +853,7 @@ impl AllLanguageSettings { &self, language: Option<&Arc>, path: Option<&Path>, + cx: &AppContext, ) -> bool { if let Some(path) = path { if !self.inline_completions_enabled_for_path(path) { @@ -847,11 +861,35 @@ impl AllLanguageSettings { } } - self.language(None, language.map(|l| l.name()).as_ref()) + self.language(None, language.map(|l| l.name()).as_ref(), cx) .show_inline_completions } } +fn merge_with_editorconfig(settings: &mut LanguageSettings, content: &EditorconfigProperties) { + // TODO kb + // fn merge(target: &mut T, value: Option) { + // if let Some(value) = value { + // *target = value; + // } + // } + // merge(&mut settings.tab_size, content.tab_size); + // merge(&mut settings.hard_tabs, content.hard_tabs); + // merge( + // &mut settings.remove_trailing_whitespace_on_save, + // content.remove_trailing_whitespace_on_save, + // ); + // merge( + // &mut settings.ensure_final_newline_on_save, + // content.ensure_final_newline_on_save, + // ); + // merge( + // &mut settings.preferred_line_length, + // content.preferred_line_length, + // ); + // merge(&mut settings.soft_wrap, content.soft_wrap); +} + /// The kind of an inlay hint. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InlayHintKind { diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 674ff1859b631..1f040eb6476e5 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -428,9 +428,10 @@ impl ContextProvider for RustContextProvider { let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx) .tasks .variables - .get(DEFAULT_RUN_NAME_STR); + .get(DEFAULT_RUN_NAME_STR) + .cloned(); let run_task_args = if let Some(package_to_run) = package_to_run { - vec!["run".into(), "-p".into(), package_to_run.clone()] + vec!["run".into(), "-p".into(), package_to_run] } else { vec!["run".into()] }; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 75453e0c56b96..9f1c468b876b4 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -101,7 +101,7 @@ impl LspAdapter for YamlLspAdapter { let tab_size = cx.update(|cx| { AllLanguageSettings::get(Some(location), cx) - .language(Some(location), Some(&"YAML".into())) + .language(Some(location), Some(&"YAML".into()), cx) .tab_size })?; let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}}); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0b0ea15a5d365..d19ec4501fefc 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1778,7 +1778,7 @@ impl MultiBuffer { &self, point: T, cx: &'a AppContext, - ) -> &'a LanguageSettings { + ) -> Cow<'a, LanguageSettings> { let mut language = None; let mut file = None; if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { @@ -3580,7 +3580,7 @@ impl MultiBufferSnapshot { &'a self, point: T, cx: &'a AppContext, - ) -> &'a LanguageSettings { + ) -> Cow<'a, LanguageSettings> { let mut language = None; let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index bd9b452d182b7..57f8cea348dac 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2303,11 +2303,9 @@ impl LspCommand for OnTypeFormatting { .await?; let options = buffer.update(&mut cx, |buffer, cx| { - lsp_formatting_options(language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - )) + lsp_formatting_options( + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(), + ) })?; Ok(Self { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 87915e3b51c79..534bd71d7f883 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -222,7 +222,8 @@ impl LocalLspStore { })?; let settings = buffer.handle.update(&mut cx, |buffer, cx| { - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).clone() + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .into_owned() })?; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -1225,7 +1226,7 @@ impl LspStore { let buffer_file = buffer.read(cx).file().cloned(); let settings = - language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).clone(); + language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree_id = if let Some(file) = buffer_file { @@ -1407,8 +1408,10 @@ impl LspStore { language_servers_to_start.push((file.worktree.clone(), language.name())); } } - language_formatters_to_check - .push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone())); + language_formatters_to_check.push(( + buffer_file.map(|f| f.worktree_id(cx)), + settings.into_owned(), + )); } } @@ -1852,11 +1855,14 @@ impl LspStore { cx: &mut ModelContext, ) -> Task>> { let options = buffer.update(cx, |buffer, cx| { - lsp_command::lsp_formatting_options(language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - )) + lsp_command::lsp_formatting_options( + language_settings( + buffer.language_at(position).map(|l| l.name()), + buffer.file(), + cx, + ) + .as_ref(), + ) }); self.request_lsp( buffer.clone(), diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index e6a754e21caf2..3cab99c58306a 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -208,7 +208,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo server_cx.read(|cx| { assert_eq!( AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["from-local-settings".to_string()] ) @@ -228,7 +228,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo server_cx.read(|cx| { assert_eq!( AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["from-server-settings".to_string()] ) @@ -287,7 +287,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo }), cx ) - .language(None, Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["override-rust-analyzer".to_string()] ) diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 40c371d9951ff..f7120d2b134dd 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -14,8 +14,8 @@ pub use json_schema::*; pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ - parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, - SettingsSources, SettingsStore, + parse_json_with_comments, EditorconfigProperties, InvalidSettingsError, LocalSettingsKind, + Settings, SettingsLocation, SettingsSources, SettingsStore, }; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 30d680ccfd321..3c182e637e3d1 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; -use ec4rs::{ConfigParser, Properties as EditorconfigProperties, PropertiesSource, Section}; +use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal}; @@ -19,6 +19,8 @@ use std::{ use tree_sitter::Query; use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; +pub type EditorconfigProperties = ec4rs::Properties; + use crate::{SettingsJsonSchemaParams, WorktreeId}; /// A value that can be defined as a user setting. diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 2a7fc31c0db8f..b9185c9762747 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -121,7 +121,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { let file = buffer.file(); let language = buffer.language_at(cursor_position); let settings = all_language_settings(file, cx); - settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref())) + settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) } fn refresh( From cdaa0a12be37fd2e3fbbf3eb46a9836cb1da8cd5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 00:23:27 +0300 Subject: [PATCH 07/12] Finish the editorconfig parsing part co-authored-by: Ulysse Buonomo --- Cargo.lock | 1 + crates/language/Cargo.toml | 1 + crates/language/src/language_settings.rs | 80 ++++++++++++----- crates/project/src/project_tests.rs | 107 ++++++++++++++++++++++- crates/settings/src/settings.rs | 4 +- crates/settings/src/settings_store.rs | 4 +- 6 files changed, 167 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a36135e7cf8..f4668261a4ab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6216,6 +6216,7 @@ dependencies = [ "clock", "collections", "ctor", + "ec4rs", "env_logger", "futures 0.3.30", "fuzzy", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 69c7dcce0dbef..b117b9682bb11 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -30,6 +30,7 @@ async-trait.workspace = true async-watch.workspace = true clock.workspace = true collections.workspace = true +ec4rs.workspace = true futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index de96569d95263..78d5f9a26d9d7 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -4,6 +4,10 @@ use crate::{File, Language, LanguageName, LanguageServerName}; use anyhow::Result; use collections::{HashMap, HashSet}; use core::slice; +use ec4rs::{ + property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, + Properties as EditorconfigProperties, +}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::AppContext; use itertools::{Either, Itertools}; @@ -17,8 +21,7 @@ use serde::{ }; use serde_json::Value; use settings::{ - add_references_to_properties, EditorconfigProperties, Settings, SettingsLocation, - SettingsSources, SettingsStore, + add_references_to_properties, Settings, SettingsLocation, SettingsSources, SettingsStore, }; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; use util::serde::default_true; @@ -866,28 +869,57 @@ impl AllLanguageSettings { } } -fn merge_with_editorconfig(settings: &mut LanguageSettings, content: &EditorconfigProperties) { - // TODO kb - // fn merge(target: &mut T, value: Option) { - // if let Some(value) = value { - // *target = value; - // } - // } - // merge(&mut settings.tab_size, content.tab_size); - // merge(&mut settings.hard_tabs, content.hard_tabs); - // merge( - // &mut settings.remove_trailing_whitespace_on_save, - // content.remove_trailing_whitespace_on_save, - // ); - // merge( - // &mut settings.ensure_final_newline_on_save, - // content.ensure_final_newline_on_save, - // ); - // merge( - // &mut settings.preferred_line_length, - // content.preferred_line_length, - // ); - // merge(&mut settings.soft_wrap, content.soft_wrap); +fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + let tab_size = cfg.get::().ok().and_then(|v| match v { + IndentSize::Value(u) => NonZeroU32::new(u as u32), + IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { + TabWidth::Value(u) => NonZeroU32::new(u as u32), + }), + }); + let hard_tabs = cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(); + let ensure_final_newline_on_save = cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(); + let remove_trailing_whitespace_on_save = cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(); + let preferred_line_length = max_line_length; + let soft_wrap = if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }; + + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } + merge(&mut settings.tab_size, tab_size); + merge(&mut settings.hard_tabs, hard_tabs); + merge( + &mut settings.remove_trailing_whitespace_on_save, + remove_trailing_whitespace_on_save, + ); + merge( + &mut settings.ensure_final_newline_on_save, + ensure_final_newline_on_save, + ); + merge(&mut settings.preferred_line_length, preferred_line_length); + merge(&mut settings.soft_wrap, soft_wrap); } /// The kind of an inlay hint. diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index db31455de7b46..1a0536d067378 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,7 +4,9 @@ use futures::{future, StreamExt}; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ - language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent}, + language_settings::{ + language_settings, AllLanguageSettings, LanguageSettingsContent, SoftWrap, + }, tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; @@ -15,7 +17,7 @@ use serde_json::json; #[cfg(not(windows))] use std::os; -use std::{mem, ops::Range, task::Poll}; +use std::{mem, num::NonZeroU32, ops::Range, task::Poll}; use task::{ResolvedTask, TaskContext}; use unindent::Unindent as _; use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _}; @@ -91,6 +93,107 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let dir = temp_tree(json!({ + ".editorconfig": r#" + root = true + [*.rs] + indent_style = tab + indent_size = 3 + end_of_line = lf + insert_final_newline = true + trim_trailing_whitespace = true + max_line_length = 80 + [*.js] + tab_width = 10 + "#, + ".zed": { + "settings.json": r#"{ + "tab_size": 8, + "hard_tabs": false, + "ensure_final_newline_on_save": false, + "remove_trailing_whitespace_on_save": false, + "preferred_line_length": 64, + "soft_wrap": "editor_width" + }"#, + }, + "a.rs": "fn a() {\n A\n}", + "b": { + ".editorconfig": r#" + [*.rs] + indent_size = 2 + max_line_length = off + "#, + "b.rs": "fn b() {\n B\n}", + }, + "c.js": "def c\n C\nend", + "README.json": "tabs are better\n", + })); + + let path = dir.path(); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree_from_real_fs(path, path).await; + let project = Project::test(fs, [path], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(js_lang()); + language_registry.add(json_lang()); + language_registry.add(rust_lang()); + + let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + cx.executor().run_until_parked(); + + cx.update(|cx| { + let tree = worktree.read(cx); + let settings_for = |path: &str| { + let file_entry = tree.entry_for_path(path).unwrap().clone(); + let file = File::for_entry(file_entry, worktree.clone()); + let file_language = project + .read(cx) + .languages() + .language_for_file_path(file.path.as_ref()); + let file_language = cx + .background_executor() + .block(file_language) + .expect("Failed to get file language"); + let file = file as _; + language_settings(Some(file_language.name()), Some(&file), cx).into_owned() + }; + + let settings_a = settings_for("a.rs"); + let settings_b = settings_for("b/b.rs"); + let settings_c = settings_for("c.js"); + let settings_readme = settings_for("README.json"); + + // .editorconfig overrides .zed/settings + assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); + assert_eq!(settings_a.hard_tabs, true); + assert_eq!(settings_a.ensure_final_newline_on_save, true); + assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); + assert_eq!(settings_a.preferred_line_length, 80); + + // "max_line_length" also sets "soft_wrap" + assert_eq!(settings_a.soft_wrap, SoftWrap::PreferredLineLength); + + // .editorconfig in b/ overrides .editorconfig in root + assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); + + // "indent_size" is not set, so "tab_width" is used + assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); + + // When max_line_length is "off", default to .zed/settings.json + assert_eq!(settings_b.preferred_line_length, 64); + assert_eq!(settings_b.soft_wrap, SoftWrap::EditorWidth); + + // README.md should not be affected by .editorconfig's globe "*.rs" + assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); + }); +} + #[gpui::test] async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f7120d2b134dd..40c371d9951ff 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -14,8 +14,8 @@ pub use json_schema::*; pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ - parse_json_with_comments, EditorconfigProperties, InvalidSettingsError, LocalSettingsKind, - Settings, SettingsLocation, SettingsSources, SettingsStore, + parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, + SettingsSources, SettingsStore, }; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 3c182e637e3d1..60fbcc7747aeb 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -983,12 +983,12 @@ impl SettingsStore { for_path: &Path, ) -> Option { let mut properties = EditorconfigProperties::new(); - properties.use_fallbacks(); for (directory_with_config, _, parsed_editorconfig) in self.local_editorconfig_settings(for_worktree) { if !for_path.starts_with(&directory_with_config) { + properties.use_fallbacks(); return Some(properties); } let Some(parsed_editorconfig) = parsed_editorconfig else { @@ -996,13 +996,13 @@ impl SettingsStore { }; if parsed_editorconfig.is_root { properties = EditorconfigProperties::new(); - properties.use_fallbacks(); } for section in parsed_editorconfig.sections { section.apply_to(&mut properties, for_path).log_err()?; } } + properties.use_fallbacks(); Some(properties) } } From 8b277083b5979e6f2973605aace364fbd8b89668 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 00:56:34 +0300 Subject: [PATCH 08/12] Clippy --- crates/language/src/language_settings.rs | 2 +- crates/settings/src/settings_store.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 78d5f9a26d9d7..f4083b2244513 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) { } /// Returns the settings for the specified language from the provided file. -pub fn language_settings<'a, 'b>( +pub fn language_settings<'a>( language: Option, file: Option<&'a Arc>, cx: &'a AppContext, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 60fbcc7747aeb..702d7db2fa8fe 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -991,9 +991,7 @@ impl SettingsStore { properties.use_fallbacks(); return Some(properties); } - let Some(parsed_editorconfig) = parsed_editorconfig else { - return None; - }; + let parsed_editorconfig = parsed_editorconfig?; if parsed_editorconfig.is_root { properties = EditorconfigProperties::new(); } From f84217dcb794feae1a0365cdf93b69340915efcc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 02:13:32 +0300 Subject: [PATCH 09/12] Draft a multiplayer test --- crates/collab/src/tests/editor_tests.rs | 192 +++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 16deef70d58aa..97a2a70369e51 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -60,7 +60,7 @@ async fn test_host_disconnect( .fs() .insert_tree( "/a", - serde_json::json!({ + json!({ "a.txt": "a-contents", "b.txt": "b-contents", }), @@ -2152,6 +2152,196 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA }); } +#[gpui::test(iterations = 30)] +async fn test_collaborating_with_editorconfig( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_b.update(editor::init); + + // Set up a fake language server. + client_a.language_registry().add(rust_lang()); + client_a + .fs() + .insert_tree( + "/a", + json!({ + "src": { + "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", + "other_mod": { + "other.rs": "pub fn foo() -> usize {\n 4\n}", + }, + ".editorconfig": "[*]\ntab_width = 2\n", + } + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let main_buffer_a = project_a + .update(cx_a, |p, cx| { + p.open_buffer((worktree_id, "src/main.rs"), cx) + }) + .await + .unwrap(); + let other_buffer_a = project_a + .update(cx_a, |p, cx| { + p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx) + }) + .await + .unwrap(); + let cx_a = cx_a.add_empty_window(); + let main_editor_a = + cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx)); + let other_editor_a = + cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx)); + let mut main_editor_cx_a = EditorTestContext { + cx: cx_a.clone(), + window: cx_a.handle(), + editor: main_editor_a, + assertion_cx: AssertionContextManager::new(), + }; + let mut other_editor_cx_a = EditorTestContext { + cx: cx_a.clone(), + window: cx_a.handle(), + editor: other_editor_a, + assertion_cx: AssertionContextManager::new(), + }; + + // Join the project as client B. + let project_b = client_b.join_remote_project(project_id, cx_b).await; + let main_buffer_b = project_b + .update(cx_b, |p, cx| { + p.open_buffer((worktree_id, "src/main.rs"), cx) + }) + .await + .unwrap(); + let other_buffer_b = project_b + .update(cx_b, |p, cx| { + p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx) + }) + .await + .unwrap(); + let cx_b = cx_b.add_empty_window(); + let main_editor_b = + cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx)); + let other_editor_b = + cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b), cx)); + let mut main_editor_cx_b = EditorTestContext { + cx: cx_b.clone(), + window: cx_b.handle(), + editor: main_editor_b, + assertion_cx: AssertionContextManager::new(), + }; + let mut other_editor_cx_b = EditorTestContext { + cx: cx_b.clone(), + window: cx_b.handle(), + editor: other_editor_b, + assertion_cx: AssertionContextManager::new(), + }; + + let initial_main = indoc! {" +ˇmod other; +fn main() { let foo = other::foo(); }"}; + let initial_other = indoc! {" +ˇpub fn foo() -> usize { + 4 +}"}; + + let first_tabbed_main = indoc! {" + ˇmod other; +fn main() { let foo = other::foo(); }"}; + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + first_tabbed_main, + true, + ); + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + first_tabbed_main, + false, + ); + + let first_tabbed_other = indoc! {" + ˇpub fn foo() -> usize { + 4 +}"}; + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + first_tabbed_other, + true, + ); + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + first_tabbed_other, + false, + ); + + // TODO kb why in tests editorconfig is not propagated to the client? + // TODO kb test cx_a editing the existing .editorconfig + cx_b tabbing + // TODO kb test cx_b creating a new .editorconfig + cx_a tabbing +} + +fn tab_undo_assert( + cx_a: &mut EditorTestContext, + cx_b: &mut EditorTestContext, + expected_initial: &str, + expected_tabbed: &str, + a_tabs: bool, +) { + cx_a.assert_editor_state(expected_initial); + cx_b.assert_editor_state(expected_initial); + + if a_tabs { + cx_a.update_editor(|editor, cx| { + editor.tab(&editor::actions::Tab, cx); + }); + } else { + cx_b.update_editor(|editor, cx| { + editor.tab(&editor::actions::Tab, cx); + }); + } + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + cx_a.assert_editor_state(expected_tabbed); + cx_b.assert_editor_state(expected_tabbed); + + if a_tabs { + cx_a.update_editor(|editor, cx| { + editor.undo(&editor::actions::Undo, cx); + }); + } else { + cx_b.update_editor(|editor, cx| { + editor.undo(&editor::actions::Undo, cx); + }); + } + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_a.assert_editor_state(expected_initial); + cx_b.assert_editor_state(expected_initial); +} + fn extract_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for hint in editor.inlay_hint_cache().hints() { From 138eb62014aaa56384c81a6e94369b92c40b0c2d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 02:24:21 +0300 Subject: [PATCH 10/12] Fix the test --- crates/collab/src/rpc.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 05d7726069959..865453555f1bf 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2237,7 +2237,7 @@ fn join_project_internal( worktree_id: worktree.id, path: settings_file.path, content: Some(settings_file.content), - kind: Some(proto::update_user_settings::Kind::Settings.into()), + kind: Some(settings_file.kind.to_proto() as i32), }, )?; } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 97a2a70369e51..9fdb875654523 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2298,7 +2298,6 @@ fn main() { let foo = other::foo(); }"}; // TODO kb why in tests editorconfig is not propagated to the client? // TODO kb test cx_a editing the existing .editorconfig + cx_b tabbing - // TODO kb test cx_b creating a new .editorconfig + cx_a tabbing } fn tab_undo_assert( From 57778562494e93a8dc0afa5341240f536b453ed4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 02:25:29 +0300 Subject: [PATCH 11/12] Clean up editorconfig update code --- crates/settings/src/settings_store.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 702d7db2fa8fe..4c85cf2e40553 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -592,7 +592,6 @@ impl SettingsStore { cx: &mut AppContext, ) -> std::result::Result<(), InvalidSettingsError> { let mut zed_settings_changed = false; - let mut editorconfig_settings_changed = false; match ( kind, settings_content @@ -611,10 +610,8 @@ impl SettingsStore { .is_some() } (LocalSettingsKind::Editorconfig, None) => { - editorconfig_settings_changed = self - .raw_editorconfig_settings - .remove(&(root_id, directory_path.clone())) - .is_some() + self.raw_editorconfig_settings + .remove(&(root_id, directory_path.clone())); } (LocalSettingsKind::Settings, Some(settings_contents)) => { let new_settings = parse_json_with_comments::(settings_contents) @@ -646,7 +643,6 @@ impl SettingsStore { btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() { Ok(new_contents) => { v.insert((editorconfig_contents.to_owned(), Some(new_contents))); - editorconfig_settings_changed = true; } Err(e) => { v.insert((editorconfig_contents.to_owned(), None)); @@ -664,7 +660,6 @@ impl SettingsStore { editorconfig_contents.to_owned(), Some(new_contents), )); - editorconfig_settings_changed = true; } Err(e) => { o.insert((editorconfig_contents.to_owned(), None)); @@ -683,9 +678,6 @@ impl SettingsStore { if zed_settings_changed { self.recompute_values(Some((root_id, &directory_path)), cx)?; } - if editorconfig_settings_changed { - // TODO kb remove this all? - } Ok(()) } From 2f771d0dad9072a91abe4c837f213128a5327812 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 19 Oct 2024 02:35:11 +0300 Subject: [PATCH 12/12] More tests --- crates/collab/src/tests/editor_tests.rs | 113 ++++++++++++++++++++++-- crates/settings/src/settings_store.rs | 5 +- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 9fdb875654523..2a3c643f6deeb 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -12,6 +12,7 @@ use editor::{ test::editor_test_context::{AssertionContextManager, EditorTestContext}, Editor, }; +use fs::Fs; use futures::StreamExt; use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; @@ -30,7 +31,7 @@ use serde_json::json; use settings::SettingsStore; use std::{ ops::Range, - path::Path, + path::{Path, PathBuf}, sync::{ atomic::{self, AtomicBool, AtomicUsize}, Arc, @@ -2178,9 +2179,10 @@ async fn test_collaborating_with_editorconfig( "main.rs": "mod other;\nfn main() { let foo = other::foo(); }", "other_mod": { "other.rs": "pub fn foo() -> usize {\n 4\n}", + ".editorconfig": "", }, - ".editorconfig": "[*]\ntab_width = 2\n", - } + }, + ".editorconfig": "[*]\ntab_width = 2\n", }), ) .await; @@ -2237,7 +2239,7 @@ async fn test_collaborating_with_editorconfig( let main_editor_b = cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx)); let other_editor_b = - cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b), cx)); + cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx)); let mut main_editor_cx_b = EditorTestContext { cx: cx_b.clone(), window: cx_b.handle(), @@ -2296,10 +2298,109 @@ fn main() { let foo = other::foo(); }"}; false, ); - // TODO kb why in tests editorconfig is not propagated to the client? - // TODO kb test cx_a editing the existing .editorconfig + cx_b tabbing + client_a + .fs() + .atomic_write( + PathBuf::from("/a/src/.editorconfig"), + "[*]\ntab_width = 3\n".to_owned(), + ) + .await + .unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let second_tabbed_main = indoc! {" + ˇmod other; +fn main() { let foo = other::foo(); }"}; + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + second_tabbed_main, + true, + ); + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + second_tabbed_main, + false, + ); + + let second_tabbed_other = indoc! {" + ˇpub fn foo() -> usize { + 4 +}"}; + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + second_tabbed_other, + true, + ); + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + second_tabbed_other, + false, + ); + + let editorconfig_buffer_b = project_b + .update(cx_b, |p, cx| { + p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx) + }) + .await + .unwrap(); + editorconfig_buffer_b.update(cx_b, |buffer, cx| { + buffer.set_text("[*.rs]\ntab_width = 6\n", cx); + }); + project_b + .update(cx_b, |project, cx| { + project.save_buffer(editorconfig_buffer_b.clone(), cx) + }) + .await + .unwrap(); + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + second_tabbed_main, + true, + ); + tab_undo_assert( + &mut main_editor_cx_a, + &mut main_editor_cx_b, + initial_main, + second_tabbed_main, + false, + ); + + let third_tabbed_other = indoc! {" + ˇpub fn foo() -> usize { + 4 +}"}; + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + third_tabbed_other, + true, + ); + + tab_undo_assert( + &mut other_editor_cx_a, + &mut other_editor_cx_b, + initial_other, + third_tabbed_other, + false, + ); } +#[track_caller] fn tab_undo_assert( cx_a: &mut EditorTestContext, cx_b: &mut EditorTestContext, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 4c85cf2e40553..a25eae7e2c6cb 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -185,7 +185,7 @@ pub struct SettingsStore { #[derive(Clone)] pub struct Editorconfig { pub is_root: bool, - pub sections: Vec
, + pub sections: SmallVec<[Section; 5]>, } impl FromStr for Editorconfig { @@ -196,7 +196,7 @@ impl FromStr for Editorconfig { .context("creating editorconfig parser")?; let is_root = parser.is_root; let sections = parser - .collect::, _>>() + .collect::, _>>() .context("parsing editorconfig sections")?; Ok(Self { is_root, sections }) } @@ -678,7 +678,6 @@ impl SettingsStore { if zed_settings_changed { self.recompute_values(Some((root_id, &directory_path)), cx)?; } - Ok(()) }