From 268fe944167c30235ca59d7a70d91b3e9de4c660 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 16 Aug 2024 13:49:37 +0200 Subject: [PATCH 01/12] feat: Add editorconfig support Fixes #8534 --- Cargo.lock | 7 +++ Cargo.toml | 1 + crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 6 +-- crates/language/src/language_settings.rs | 61 +++++++++++++++++++-- crates/multi_buffer/src/multi_buffer.rs | 8 +-- crates/project/src/lsp_command.rs | 2 +- crates/project/src/project_tests.rs | 67 ++++++++++++++---------- 8 files changed, 109 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 652c584fd53795..2fa69cd7f6e951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3685,6 +3685,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ec4rs" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc37e66bbd9a1c221e31206689b5ff85961d830623ab0be9c6967d941094962c" + [[package]] name = "ecdsa" version = "0.14.8" @@ -6213,6 +6219,7 @@ dependencies = [ "clock", "collections", "ctor", + "ec4rs", "env_logger", "futures 0.3.30", "fuzzy", diff --git a/Cargo.toml b/Cargo.toml index c72fec020fe678..88dbe439a49b01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -345,6 +345,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/language/Cargo.toml b/crates/language/Cargo.toml index 69c7dcce0dbef4..b117b9682bb116 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/buffer.rs b/crates/language/src/buffer.rs index 43fe1565acb796..102c6a8de83df7 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2654,11 +2654,7 @@ impl BufferSnapshot { } /// Returns the settings for the language at the given location. - pub fn settings_at<'a, D: ToOffset>( - &self, - position: D, - cx: &'a AppContext, - ) -> &'a LanguageSettings { + pub fn settings_at(&self, position: D, cx: &AppContext) -> LanguageSettings { language_settings(self.language_at(position), self.file.as_ref(), cx) } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 77c9a1d18cee14..867cfff1187b7c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -4,6 +4,7 @@ use crate::{File, Language, LanguageName, LanguageServerName}; use anyhow::Result; use collections::{HashMap, HashSet}; use core::slice; +use ec4rs::property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::AppContext; use itertools::{Either, Itertools}; @@ -26,13 +27,65 @@ pub fn init(cx: &mut AppContext) { } /// Returns the settings for the specified language from the provided file. -pub fn language_settings<'a>( +pub fn language_settings( language: Option<&Arc>, file: Option<&Arc>, - cx: &'a AppContext, -) -> &'a LanguageSettings { + cx: &AppContext, +) -> LanguageSettings { let language_name = language.map(|l| l.name()); - all_language_settings(file, cx).language(language_name.as_ref()) + let mut settings = all_language_settings(file, cx) + .language(language_name.as_ref()) + .clone(); + let path = file + .and_then(|f| f.as_local()) + .map(|f| f.abs_path(cx)) + .unwrap_or_else(|| std::path::PathBuf::new()); + let mut cfg = ec4rs::properties_of(path).unwrap(); + cfg.use_fallbacks(); + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } + merge( + &mut settings.tab_size, + cfg.get::() + .map(|v| match v { + IndentSize::Value(u) => NonZeroU32::new(u as u32), + IndentSize::UseTabWidth => cfg + .get::() + .map(|w| match w { + TabWidth::Value(u) => NonZeroU32::new(u as u32), + }) + .ok() + .flatten(), + }) + .ok() + .flatten(), + ); + merge( + &mut settings.hard_tabs, + cfg.get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + ); + merge( + &mut settings.remove_trailing_whitespace_on_save, + cfg.get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + ); + merge( + &mut settings.ensure_final_newline_on_save, + cfg.get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(), + ); + settings } /// Returns the settings for all languages from the provided file. diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 9dee092dea9f29..2ae28fbd4057b8 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1723,11 +1723,7 @@ impl MultiBuffer { .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) } - pub fn settings_at<'a, T: ToOffset>( - &self, - point: T, - cx: &'a AppContext, - ) -> &'a LanguageSettings { + pub fn settings_at(&self, point: T, cx: &AppContext) -> LanguageSettings { let mut language = None; let mut file = None; if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { @@ -3462,7 +3458,7 @@ impl MultiBufferSnapshot { &'a self, point: T, cx: &'a AppContext, - ) -> &'a LanguageSettings { + ) -> 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 2b7b10d9b369a9..3867cc989bb535 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2304,7 +2304,7 @@ 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(), buffer.file(), cx)) })?; Ok(Self { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 72a38ccba7d781..cfa51a845fba42 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -94,38 +94,46 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/the-root", - json!({ - ".zed": { - "settings.json": r#"{ "tab_size": 8 }"#, - "tasks.json": r#"[{ + let dir = temp_tree(json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 8, "ensure_final_newline_on_save": false }"#, + "tasks.json": r#"[{ "label": "cargo check", "command": "cargo", "args": ["check", "--all"] },]"#, - }, - "a": { - "a.rs": "fn a() {\n A\n}" - }, - "b": { - ".zed": { - "settings.json": r#"{ "tab_size": 2 }"#, - "tasks.json": r#"[{ + }, + ".editorconfig": r#"root = true + [*.rs] + insert_final_newline = true + "#, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"#, + "tasks.json": r#"[{ "label": "cargo check", "command": "cargo", "args": ["check"] },]"#, - }, - "b.rs": "fn b() {\n B\n}" - } - }), - ) - .await; + }, + "b.rs": "fn b() {\n B\n}" + }, + "c": { + "c.rs": "fn c() {\n C\n}", + ".editorconfig": r#"[*.rs] + indent_size = 4 + "#, + } + })); - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let path = dir.path(); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree_from_real_fs(path, path).await; + + let project = Project::test(fs.clone(), [path], cx).await; let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); let task_context = TaskContext::default(); @@ -137,7 +145,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) }); let global_task_source_kind = TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/.zed/tasks.json"), + abs_path: PathBuf::from(format!("{}/.zed/tasks.json", path.display())), id_base: "local_tasks_for_worktree".into(), }; @@ -195,7 +203,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), + abs_path: PathBuf::from(format!("{}/b/.zed/tasks.json", path.display())), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), @@ -236,7 +244,10 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) cx.update(|cx| { project.update(cx, |project, cx| { project.task_inventory().update(cx, |inventory, cx| { - inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json")); + inventory.remove_local_static_source(&PathBuf::from(format!( + "{}/.zed/tasks.json", + path.display() + ))); inventory.add_source( global_task_source_kind.clone(), |tx, cx| StaticSource::new(TrackedFile::new(rx, tx, cx)), @@ -268,7 +279,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/.zed/tasks.json"), + abs_path: PathBuf::from(format!("{}/.zed/tasks.json", path.display())), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), @@ -285,7 +296,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), + abs_path: PathBuf::from(format!("{}/b/.zed/tasks.json", path.display())), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), From 82d0b17586eb929a5038de380d71519cb116f39e Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 16 Aug 2024 16:02:25 +0200 Subject: [PATCH 02/12] review --- crates/language/src/buffer.rs | 7 +- crates/language/src/language_settings.rs | 120 ++++++++++++++++------- crates/multi_buffer/src/multi_buffer.rs | 8 +- 3 files changed, 95 insertions(+), 40 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 102c6a8de83df7..30e7946640014b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -35,6 +35,7 @@ use smallvec::SmallVec; use smol::future::yield_now; use std::{ any::Any, + borrow::Cow, cell::Cell, cmp::{self, Ordering, Reverse}, collections::BTreeMap, @@ -2654,7 +2655,11 @@ impl BufferSnapshot { } /// Returns the settings for the language at the given location. - pub fn settings_at(&self, position: D, cx: &AppContext) -> LanguageSettings { + pub fn settings_at<'a, D: ToOffset>( + &self, + position: D, + cx: &'a AppContext, + ) -> Cow<'a, LanguageSettings> { language_settings(self.language_at(position), self.file.as_ref(), cx) } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 867cfff1187b7c..784228f401bb7a 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -4,7 +4,9 @@ use crate::{File, Language, LanguageName, LanguageServerName}; use anyhow::Result; use collections::{HashMap, HashSet}; use core::slice; -use ec4rs::property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}; +use ec4rs::property::{ + FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs, +}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::AppContext; use itertools::{Either, Itertools}; @@ -18,7 +20,7 @@ use serde::{ }; use serde_json::Value; use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; -use std::{num::NonZeroU32, path::Path, sync::Arc}; +use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; use util::serde::default_true; /// Initializes the language settings. @@ -27,29 +29,41 @@ pub fn init(cx: &mut AppContext) { } /// Returns the settings for the specified language from the provided file. -pub fn language_settings( +pub fn language_settings<'a>( language: Option<&Arc>, file: Option<&Arc>, - cx: &AppContext, -) -> LanguageSettings { + cx: &'a AppContext, +) -> Cow<'a, LanguageSettings> { let language_name = language.map(|l| l.name()); - let mut settings = all_language_settings(file, cx) - .language(language_name.as_ref()) - .clone(); - let path = file - .and_then(|f| f.as_local()) - .map(|f| f.abs_path(cx)) - .unwrap_or_else(|| std::path::PathBuf::new()); - let mut cfg = ec4rs::properties_of(path).unwrap(); - cfg.use_fallbacks(); - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } + let settings = all_language_settings(file, cx).language(language_name.as_ref()); + if let Some(content) = editorconfig_settings(file, cx) { + let mut settings = settings.clone(); + merge_with_editorconfig(&mut settings, &content); + Cow::Owned(settings) + } else { + Cow::Borrowed(settings) } - merge( - &mut settings.tab_size, - cfg.get::() +} + +fn editorconfig_settings( + file: Option<&Arc>, + cx: &AppContext, +) -> Option { + let path = file.and_then(|f| f.as_local()).map(|f| f.abs_path(cx)); + + if path.is_none() { + return None; + } + + let mut cfg = ec4rs::properties_of(path.unwrap()).unwrap_or_default(); + cfg.use_fallbacks(); + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + Some(EditorConfigContent { + tab_size: cfg + .get::() .map(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg @@ -62,30 +76,29 @@ pub fn language_settings( }) .ok() .flatten(), - ); - merge( - &mut settings.hard_tabs, - cfg.get::() + hard_tabs: cfg + .get::() .map(|v| v.eq(&IndentStyle::Tabs)) .ok(), - ); - merge( - &mut settings.remove_trailing_whitespace_on_save, - cfg.get::() + ensure_final_newline_on_save: cfg + .get::() .map(|v| match v { - TrimTrailingWs::Value(b) => b, + FinalNewline::Value(b) => b, }) .ok(), - ); - merge( - &mut settings.ensure_final_newline_on_save, - cfg.get::() + remove_trailing_whitespace_on_save: cfg + .get::() .map(|v| match v { - FinalNewline::Value(b) => b, + TrimTrailingWs::Value(b) => b, }) .ok(), - ); - settings + preferred_line_length: max_line_length, + soft_wrap: if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }, + }) } /// Returns the settings for all languages from the provided file. @@ -264,6 +277,17 @@ pub struct AllLanguageSettingsContent { pub file_types: HashMap, Vec>, } +/// The settings available in `.editorconfig` files and compatible with Zed. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct EditorConfigContent { + hard_tabs: Option, + tab_size: Option, + ensure_final_newline_on_save: Option, + remove_trailing_whitespace_on_save: Option, + preferred_line_length: Option, + soft_wrap: Option, +} + /// The settings for a particular language. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { @@ -1142,6 +1166,28 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.inlay_hints, src.inlay_hints); } +fn merge_with_editorconfig(settings: &mut LanguageSettings, content: &EditorConfigContent) { + 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); +} /// Allows to enable/disable formatting with Prettier /// and configure default Prettier, used when no project-level Prettier installation is found. /// Prettier formatting is disabled by default. diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 2ae28fbd4057b8..99010f258f10ab 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1723,7 +1723,11 @@ impl MultiBuffer { .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) } - pub fn settings_at(&self, point: T, cx: &AppContext) -> LanguageSettings { + pub fn settings_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Cow<'a, LanguageSettings> { let mut language = None; let mut file = None; if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { @@ -3458,7 +3462,7 @@ impl MultiBufferSnapshot { &'a self, point: T, cx: &'a AppContext, - ) -> LanguageSettings { + ) -> Cow<'a, LanguageSettings> { let mut language = None; let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { From d76d02fa268bcd02192371c07fa52c820d404e52 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 16 Aug 2024 22:33:01 +0300 Subject: [PATCH 03/12] Fix the compilation --- crates/project/src/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f4816cf0cde66f..89ef54136a5a6c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2575,7 +2575,7 @@ impl Project { })?; let settings = buffer.update(&mut cx, |buffer, cx| { - language_settings(buffer.language(), buffer.file(), cx).clone() + language_settings(buffer.language(), buffer.file(), cx).into_owned() })?; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; From cf2ca6e5d7dd483974195e2b694e2f1627d6faa7 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 16 Aug 2024 22:39:20 +0200 Subject: [PATCH 04/12] better cache hit --- crates/language/src/language_settings.rs | 88 +++++++++++------------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 784228f401bb7a..c8e0bfac9e286f 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -49,56 +49,48 @@ fn editorconfig_settings( file: Option<&Arc>, cx: &AppContext, ) -> Option { - let path = file.and_then(|f| f.as_local()).map(|f| f.abs_path(cx)); - - if path.is_none() { - return None; - } - - let mut cfg = ec4rs::properties_of(path.unwrap()).unwrap_or_default(); - cfg.use_fallbacks(); - let max_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); - Some(EditorConfigContent { - tab_size: cfg - .get::() - .map(|v| match v { - IndentSize::Value(u) => NonZeroU32::new(u as u32), - IndentSize::UseTabWidth => cfg - .get::() - .map(|w| match w { + file.and_then(|f| f.as_local()) + // If we don't hit an `editorconfig` file, we don't update configs. This + // avoids a performance hit of cloning settings. We could consider showing + // the error to the user though (it may be a parsing error). + .and_then(|local_file| ec4rs::properties_of(local_file.abs_path(cx)).ok()) + .map(|mut cfg| { + cfg.use_fallbacks(); + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + EditorConfigContent { + 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), + }), + }), + hard_tabs: cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + ensure_final_newline_on_save: cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, }) - .ok() - .flatten(), - }) - .ok() - .flatten(), - hard_tabs: cfg - .get::() - .map(|v| v.eq(&IndentStyle::Tabs)) - .ok(), - ensure_final_newline_on_save: cfg - .get::() - .map(|v| match v { - FinalNewline::Value(b) => b, - }) - .ok(), - remove_trailing_whitespace_on_save: cfg - .get::() - .map(|v| match v { - TrimTrailingWs::Value(b) => b, - }) - .ok(), - preferred_line_length: max_line_length, - soft_wrap: if max_line_length.is_some() { - Some(SoftWrap::PreferredLineLength) - } else { - None - }, - }) + .ok(), + remove_trailing_whitespace_on_save: cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + preferred_line_length: max_line_length, + soft_wrap: if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }, + } + }) } /// Returns the settings for all languages from the provided file. From 5bf365c3136bcd3b94c821c1a59f9cf5a9a8950b Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Sat, 17 Aug 2024 10:13:54 +0200 Subject: [PATCH 05/12] dedicated tests for editorconfig --- crates/language/src/language_settings.rs | 1 + crates/project/src/project_tests.rs | 159 +++++++++++++++++------ 2 files changed, 121 insertions(+), 39 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c8e0bfac9e286f..0ed572857c5ce4 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -54,6 +54,7 @@ fn editorconfig_settings( // avoids a performance hit of cloning settings. We could consider showing // the error to the user though (it may be a parsing error). .and_then(|local_file| ec4rs::properties_of(local_file.abs_path(cx)).ok()) + .and_then(|cfg| if cfg.is_empty() { None } else { Some(cfg) }) .map(|mut cfg| { cfg.use_fallbacks(); let max_line_length = cfg.get::().ok().and_then(|v| match v { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index cfa51a845fba42..9db3f9c93bdffa 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,7 +4,7 @@ use futures::{future, StreamExt}; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ - language_settings::{AllLanguageSettings, LanguageSettingsContent}, + language_settings::{AllLanguageSettings, LanguageSettingsContent, SoftWrap}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; @@ -15,7 +15,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, TaskTemplate, TaskTemplates}; use unindent::Unindent as _; use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _}; @@ -92,48 +92,132 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { +async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); + cx.executor().allow_parking(); + 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 + [*.rb] + tab_width = 10 + "#, ".zed": { - "settings.json": r#"{ "tab_size": 8, "ensure_final_newline_on_save": false }"#, - "tasks.json": r#"[{ - "label": "cargo check", - "command": "cargo", - "args": ["check", "--all"] - },]"#, + "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" + }"#, }, - ".editorconfig": r#"root = true - [*.rs] - insert_final_newline = true + "a.rs": "fn a() {\n A\n}", + "b": { + ".editorconfig": r#" + [*.rs] + indent_size = 2 + max_line_length = off "#, - "a": { - "a.rs": "fn a() {\n A\n}" + "b.rs": "fn b() {\n B\n}", }, - "b": { + "c.rb": "def c\n C\nend", + "README.md": "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 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| { + language_settings( + None, + Some( + &(File::for_entry(tree.entry_for_path(path).unwrap().clone(), worktree.clone()) + as _), + ), + cx, + ) + }; + + let settings_a = settings_for("a.rs"); + let settings_b = settings_for("b/b.rs"); + let settings_c = settings_for("c.rb"); + let settings_readme = settings_for("README.md"); + + // .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); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/the-root", + json!({ ".zed": { - "settings.json": r#"{ "tab_size": 2 }"#, + "settings.json": r#"{ "tab_size": 8 }"#, "tasks.json": r#"[{ + "label": "cargo check", + "command": "cargo", + "args": ["check", "--all"] + },]"#, + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"#, + "tasks.json": r#"[{ "label": "cargo check", "command": "cargo", "args": ["check"] },]"#, - }, - "b.rs": "fn b() {\n B\n}" - }, - "c": { - "c.rs": "fn c() {\n C\n}", - ".editorconfig": r#"[*.rs] - indent_size = 4 - "#, - } - })); - - let path = dir.path(); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree_from_real_fs(path, path).await; + }, + "b.rs": "fn b() {\n B\n}" + } + }), + ) + .await; - let project = Project::test(fs.clone(), [path], cx).await; + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); let task_context = TaskContext::default(); @@ -145,7 +229,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) }); let global_task_source_kind = TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from(format!("{}/.zed/tasks.json", path.display())), + abs_path: PathBuf::from("/the-root/.zed/tasks.json"), id_base: "local_tasks_for_worktree".into(), }; @@ -203,7 +287,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from(format!("{}/b/.zed/tasks.json", path.display())), + abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), @@ -244,10 +328,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) cx.update(|cx| { project.update(cx, |project, cx| { project.task_inventory().update(cx, |inventory, cx| { - inventory.remove_local_static_source(&PathBuf::from(format!( - "{}/.zed/tasks.json", - path.display() - ))); + inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json")); inventory.add_source( global_task_source_kind.clone(), |tx, cx| StaticSource::new(TrackedFile::new(rx, tx, cx)), @@ -279,7 +360,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from(format!("{}/.zed/tasks.json", path.display())), + abs_path: PathBuf::from("/the-root/.zed/tasks.json"), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), @@ -296,7 +377,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) ( TaskSourceKind::Worktree { id: worktree_id, - abs_path: PathBuf::from(format!("{}/b/.zed/tasks.json", path.display())), + abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"), id_base: "local_tasks_for_worktree".into(), }, "cargo check".to_string(), From 4ac8ee57ebd0ee4531e38214b323df64dcc20541 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 26 Aug 2024 17:00:25 +0300 Subject: [PATCH 06/12] Style fixes --- crates/language/src/language_settings.rs | 88 ++++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0ed572857c5ce4..9d1a4ca44912c5 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -21,7 +21,7 @@ use serde::{ use serde_json::Value; use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; -use util::serde::default_true; +use util::{serde::default_true, ResultExt}; /// Initializes the language settings. pub fn init(cx: &mut AppContext) { @@ -49,49 +49,49 @@ fn editorconfig_settings( file: Option<&Arc>, cx: &AppContext, ) -> Option { - file.and_then(|f| f.as_local()) - // If we don't hit an `editorconfig` file, we don't update configs. This - // avoids a performance hit of cloning settings. We could consider showing - // the error to the user though (it may be a parsing error). - .and_then(|local_file| ec4rs::properties_of(local_file.abs_path(cx)).ok()) - .and_then(|cfg| if cfg.is_empty() { None } else { Some(cfg) }) - .map(|mut cfg| { - cfg.use_fallbacks(); - let max_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); - EditorConfigContent { - 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), - }), - }), - hard_tabs: cfg - .get::() - .map(|v| v.eq(&IndentStyle::Tabs)) - .ok(), - ensure_final_newline_on_save: cfg - .get::() - .map(|v| match v { - FinalNewline::Value(b) => b, - }) - .ok(), - remove_trailing_whitespace_on_save: cfg - .get::() - .map(|v| match v { - TrimTrailingWs::Value(b) => b, - }) - .ok(), - preferred_line_length: max_line_length, - soft_wrap: if max_line_length.is_some() { - Some(SoftWrap::PreferredLineLength) - } else { - None - }, - } - }) + let file_path = file?.as_local()?.abs_path(cx); + // TODO kb + let mut cfg = ec4rs::properties_of(file_path).log_err()?; + if cfg.is_empty() { + return None; + } + + cfg.use_fallbacks(); + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + let editorconfig = EditorConfigContent { + 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), + }), + }), + hard_tabs: cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + ensure_final_newline_on_save: cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(), + remove_trailing_whitespace_on_save: cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + preferred_line_length: max_line_length, + soft_wrap: if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }, + }; + Some(editorconfig) } /// Returns the settings for all languages from the provided file. From 83385f770df76c6f674b751c4d5e7eceab6b9762 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 26 Aug 2024 17:38:45 +0300 Subject: [PATCH 07/12] Push editorconfig merging down the stack --- .../src/copilot_completion_provider.rs | 4 +- crates/editor/src/editor.rs | 2 +- .../src/wasm_host/wit/since_v0_1_0.rs | 7 +- .../src/inline_completion_button.rs | 14 +- crates/language/src/language_settings.rs | 137 +++++++++--------- crates/languages/src/rust.rs | 8 +- crates/languages/src/yaml.rs | 2 +- .../src/supermaven_completion_provider.rs | 2 +- 8 files changed, 90 insertions(+), 86 deletions(-) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index c54fefad6fe599..1deccddb60ce13 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, 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/editor/src/editor.rs b/crates/editor/src/editor.rs index f797f82832f0ad..d2d9be3bc1b62f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12679,7 +12679,7 @@ fn inlay_hint_settings( let language = snapshot.language_at(location); let settings = all_language_settings(file, cx); settings - .language(language.map(|l| l.name()).as_ref()) + .language(file, language.map(|l| l.name()).as_ref(), cx) .inlay_hints } 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 50547b6371c697..9f1a8bcf763f03 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 @@ -355,8 +355,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(key.as_ref()); + let settings = AllLanguageSettings::get(location, cx).language( + None, + 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 abf8266320247e..24cdf096dc144d 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 { @@ -292,7 +292,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" @@ -337,10 +337,8 @@ impl InlineCompletionButton { let file = file.as_ref(); Some( file.map(|file| !file.is_private()).unwrap_or(true) - && all_language_settings(file, cx).inline_completions_enabled( - language, - file.map(|file| file.path().as_ref()), - ), + && all_language_settings(file, cx) + .inline_completions_enabled(language, file, cx), ) }; self.language = language.cloned(); @@ -442,7 +440,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) }); @@ -466,7 +464,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/language_settings.rs b/crates/language/src/language_settings.rs index 9d1a4ca44912c5..fab3dad5035417 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -35,63 +35,7 @@ pub fn language_settings<'a>( cx: &'a AppContext, ) -> Cow<'a, LanguageSettings> { let language_name = language.map(|l| l.name()); - let settings = all_language_settings(file, cx).language(language_name.as_ref()); - if let Some(content) = editorconfig_settings(file, cx) { - let mut settings = settings.clone(); - merge_with_editorconfig(&mut settings, &content); - Cow::Owned(settings) - } else { - Cow::Borrowed(settings) - } -} - -fn editorconfig_settings( - file: Option<&Arc>, - cx: &AppContext, -) -> Option { - let file_path = file?.as_local()?.abs_path(cx); - // TODO kb - let mut cfg = ec4rs::properties_of(file_path).log_err()?; - if cfg.is_empty() { - return None; - } - - cfg.use_fallbacks(); - let max_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); - let editorconfig = EditorConfigContent { - 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), - }), - }), - hard_tabs: cfg - .get::() - .map(|v| v.eq(&IndentStyle::Tabs)) - .ok(), - ensure_final_newline_on_save: cfg - .get::() - .map(|v| match v { - FinalNewline::Value(b) => b, - }) - .ok(), - remove_trailing_whitespace_on_save: cfg - .get::() - .map(|v| match v { - TrimTrailingWs::Value(b) => b, - }) - .ok(), - preferred_line_length: max_line_length, - soft_wrap: if max_line_length.is_some() { - Some(SoftWrap::PreferredLineLength) - } else { - None - }, - }; - Some(editorconfig) + all_language_settings(file, cx).language(file, language_name.as_ref(), cx) } /// Returns the settings for all languages from the provided file. @@ -869,13 +813,23 @@ 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 { - if let Some(name) = language_name { - if let Some(overrides) = self.languages.get(name) { - return overrides; - } + pub fn language<'a>( + &'a self, + file: Option<&Arc>, + language_name: Option<&LanguageName>, + cx: &'a AppContext, + ) -> Cow<'a, LanguageSettings> { + let settings = language_name + .and_then(|name| self.languages.get(name)) + .unwrap_or(&self.defaults); + + if let Some(content) = self.editorconfig_settings(file, cx) { + let mut settings = settings.clone(); + merge_with_editorconfig(&mut settings, &content); + Cow::Owned(settings) + } else { + Cow::Borrowed(settings) } - &self.defaults } /// Returns whether inline completions are enabled for the given path. @@ -891,17 +845,68 @@ impl AllLanguageSettings { pub fn inline_completions_enabled( &self, language: Option<&Arc>, - path: Option<&Path>, + file: Option<&Arc>, + cx: &AppContext, ) -> bool { - if let Some(path) = path { + if let Some(path) = file.map(|f| f.path()) { if !self.inline_completions_enabled_for_path(path) { return false; } } - self.language(language.map(|l| l.name()).as_ref()) + self.language(file, language.map(|l| l.name()).as_ref(), cx) .show_inline_completions } + + fn editorconfig_settings( + &self, + file: Option<&Arc>, + cx: &AppContext, + ) -> Option { + let file_path = file?.as_local()?.abs_path(cx); + // TODO kb this goes recursively up the directory tree + let mut cfg = ec4rs::properties_of(file_path).log_err()?; + if cfg.is_empty() { + return None; + } + + cfg.use_fallbacks(); + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + let editorconfig = EditorConfigContent { + 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), + }), + }), + hard_tabs: cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + ensure_final_newline_on_save: cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(), + remove_trailing_whitespace_on_save: cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + preferred_line_length: max_line_length, + soft_wrap: if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }, + }; + Some(editorconfig) + } } /// The kind of an inlay hint. diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index a32ffe50f519f1..f1effcab9c5691 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -480,11 +480,9 @@ 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())) - .tasks - .variables - .get(DEFAULT_RUN_NAME_STR); + let language_settings = + all_language_settings(file.as_ref(), cx).language(file.as_ref(), Some(&"Rust".into()), cx); + let package_to_run = language_settings.tasks.variables.get(DEFAULT_RUN_NAME_STR); let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run.clone()] } else { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 06360847acc803..b8db0b2c8e4c4b 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -139,7 +139,7 @@ impl LspAdapter for YamlLspAdapter { let tab_size = cx.update(|cx| { AllLanguageSettings::get(Some(location), cx) - .language(Some(&"YAML".into())) + .language(None, Some(&"YAML".into()), cx) .tab_size })?; let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}}); diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 261ce372d9f717..47a1a0082c6b03 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -119,7 +119,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, cx) } fn refresh( From 965302d7a3f4013f4d8a39b07f6e667e3002fd11 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 26 Aug 2024 18:40:02 +0300 Subject: [PATCH 08/12] Cache editorconfig resolutions --- crates/language/src/language_settings.rs | 51 +++++++++++++++++++++++- crates/project/src/project.rs | 16 ++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index fab3dad5035417..9b085c1fe439fc 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -10,6 +10,7 @@ use ec4rs::property::{ use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::AppContext; use itertools::{Either, Itertools}; +use parking_lot::RwLock; use schemars::{ schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec}, JsonSchema, @@ -20,7 +21,12 @@ use serde::{ }; use serde_json::Value; use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; -use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; +use std::{ + borrow::Cow, + num::NonZeroU32, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{serde::default_true, ResultExt}; /// Initializes the language settings. @@ -58,6 +64,8 @@ pub struct AllLanguageSettings { defaults: LanguageSettings, languages: HashMap, pub(crate) file_types: HashMap, GlobSet>, + pub editorconfig_files: HashSet, + pub resolved_editorconfig_chains: Arc, EditorConfigContent>>>, } /// The settings for a particular language. @@ -858,13 +866,45 @@ impl AllLanguageSettings { .show_inline_completions } + pub fn unregister_editorconfig_path(&mut self, abs_path: &PathBuf) { + self.resolved_editorconfig_chains + .write() + .retain(|chain, _| !chain.contains(abs_path)); + self.editorconfig_files.remove(abs_path); + } + + pub fn register_editorconfig_path(&mut self, abs_path: PathBuf) { + self.resolved_editorconfig_chains + .write() + .retain(|chain, _| !chain.contains(&abs_path)); + self.editorconfig_files.insert(abs_path); + } + fn editorconfig_settings( &self, file: Option<&Arc>, cx: &AppContext, ) -> Option { let file_path = file?.as_local()?.abs_path(cx); - // TODO kb this goes recursively up the directory tree + let mut editorconfig_chain = Vec::new(); + for file_path in file_path.ancestors() { + if self.editorconfig_files.contains(file_path) { + editorconfig_chain.push(file_path.to_owned()); + } + } + if editorconfig_chain.is_empty() { + return None; + } + + { + let resolved_editorconfig_chains = self.resolved_editorconfig_chains.read(); + if let Some(content) = resolved_editorconfig_chains.get(&editorconfig_chain) { + return Some(content.clone()); + } + drop(resolved_editorconfig_chains); + } + + // FS operation that goes recursively up the directory tree, may be slow let mut cfg = ec4rs::properties_of(file_path).log_err()?; if cfg.is_empty() { return None; @@ -905,6 +945,11 @@ impl AllLanguageSettings { None }, }; + + self.resolved_editorconfig_chains + .write() + .insert(editorconfig_chain, editorconfig.clone()); + Some(editorconfig) } } @@ -1052,6 +1097,8 @@ impl settings::Settings for AllLanguageSettings { .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher())) .collect(), }, + editorconfig_files: HashSet::default(), + resolved_editorconfig_chains: Arc::default(), defaults, languages, file_types, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 89ef54136a5a6c..e1d594a3fa93be 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -110,6 +110,8 @@ const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; const MAX_SEARCH_RESULT_FILES: usize = 5_000; const MAX_SEARCH_RESULT_RANGES: usize = 10_000; +const EDITORCONFIG_FILE_NAME: &str = ".editorconfig"; + pub trait Item { fn try_open( project: &Model, @@ -3809,6 +3811,20 @@ impl Project { ); } }) + } else if path.ends_with(EDITORCONFIG_FILE_NAME) { + let current_settings = AllLanguageSettings::get_global(cx); + let has_file = current_settings.editorconfig_files.contains(&abs_path); + let needs_removal = removed && has_file; + let needs_addition = !has_file; + if needs_removal || needs_addition { + let mut new_settings = current_settings.clone(); + if needs_removal { + new_settings.unregister_editorconfig_path(&abs_path); + } else if needs_addition { + new_settings.register_editorconfig_path(abs_path); + } + AllLanguageSettings::override_global(new_settings, cx); + } } } } From ff105b9eddd2035d7bc263dfd5166a3bec859932 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 27 Aug 2024 20:29:08 +0300 Subject: [PATCH 09/12] Move editorconfig state into a proper struct --- Cargo.lock | 3 +- crates/assistant/src/assistant_panel.rs | 6 +- crates/assistant/src/inline_assistant.rs | 6 +- crates/assistant/src/prompt_library.rs | 4 +- .../src/terminal_inline_assistant.rs | 2 +- crates/assistant/src/workflow/step_view.rs | 0 .../src/chat_panel/message_editor.rs | 5 +- crates/editor/src/editor.rs | 28 ++-- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/element.rs | 5 +- crates/editor/src/hunk_diff.rs | 2 +- crates/language/Cargo.toml | 1 - crates/language/src/language.rs | 3 +- crates/language/src/language_settings.rs | 146 ++---------------- crates/project/src/project.rs | 17 +- crates/project/src/project_tests.rs | 3 +- crates/settings/Cargo.toml | 2 + crates/settings/src/settings.rs | 4 +- crates/settings/src/settings_store.rs | 139 ++++++++++++++++- .../src/stories/auto_height_editor.rs | 2 +- .../src/test/neovim_backed_test_context.rs | 4 +- 21 files changed, 196 insertions(+), 190 deletions(-) create mode 100644 crates/assistant/src/workflow/step_view.rs diff --git a/Cargo.lock b/Cargo.lock index 2fa69cd7f6e951..def4b1e859eb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6219,7 +6219,6 @@ dependencies = [ "clock", "collections", "ctor", - "ec4rs", "env_logger", "futures 0.3.30", "fuzzy", @@ -10164,11 +10163,13 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "ec4rs", "fs", "futures 0.3.30", "gpui", "indoc", "log", + "parking_lot", "paths", "pretty_assertions", "release_channel", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 5d06720fe0095b..0f721a52a9d14b 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -44,9 +44,7 @@ use gpui::{ Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext, }; use indexed_docs::IndexedDocsStore; -use language::{ - language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset, -}; +use language::{Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset}; use language_model::{ provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, @@ -58,7 +56,7 @@ use project::lsp_store::LocalLspAdapterDelegate; use project::{Project, Worktree}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; -use settings::{update_settings_file, Settings}; +use settings::{update_settings_file, Settings, SoftWrap}; use smol::stream::StreamExt; use std::{ borrow::Cow, diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index c9360213ae5138..32cb2214f2a964 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1162,7 +1162,7 @@ impl InlineAssistant { enum DeletedLines {} let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); @@ -1616,7 +1616,7 @@ impl PromptEditor { false, cx, ); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); // Since the prompt editors for all inline assistants are linked, // always show the cursor (even when it isn't focused) because // typing in one will make what you typed appear in all of them. @@ -1677,7 +1677,7 @@ impl PromptEditor { let focus = self.editor.focus_handle(cx).contains_focused(cx); self.editor = cx.new_view(|cx| { let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text("Add a prompt…", cx); editor.set_text(prompt, cx); if focus { diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 76ee95d5070b82..2f3c37ca0c1d50 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -17,7 +17,7 @@ use heed::{ types::{SerdeBincode, SerdeJson, Str}, Database, RoTxn, }; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; +use language::{Buffer, LanguageRegistry}; use language_model::{ LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; @@ -26,7 +26,7 @@ use picker::{Picker, PickerDelegate}; use release_channel::ReleaseChannel; use rope::Rope; use serde::{Deserialize, Serialize}; -use settings::Settings; +use settings::{Settings, SoftWrap}; use std::{ cmp::Reverse, future::Future, diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index caf819bae535ee..15d73cb93f1989 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -663,7 +663,7 @@ impl PromptEditor { false, cx, ); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); editor.set_placeholder_text("Add a prompt…", cx); editor }); diff --git a/crates/assistant/src/workflow/step_view.rs b/crates/assistant/src/workflow/step_view.rs new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 38aaf2faa3b329..d275f275607ce7 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -9,12 +9,11 @@ use gpui::{ Render, Task, TextStyle, View, ViewContext, WeakView, }; use language::{ - language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, - LanguageServerId, ToOffset, + Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, LanguageServerId, ToOffset, }; use parking_lot::RwLock; use project::{search::SearchQuery, Completion}; -use settings::Settings; +use settings::{Settings, SoftWrap}; use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration}; use theme::ThemeSettings; use ui::{prelude::*, TextSize}; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d2d9be3bc1b62f..00cab0e5c16373 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -91,7 +91,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::{all_language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, @@ -531,7 +531,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, - soft_wrap_mode_override: Option, + soft_wrap_mode_override: Option, project: Option>, completion_provider: Option>, collaboration_hub: Option>, @@ -1804,8 +1804,8 @@ impl Editor { let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::PreferLine); + let soft_wrap_mode_override = + matches!(mode, EditorMode::SingleLine { .. }).then(|| settings::SoftWrap::PreferLine); let mut project_subscriptions = Vec::new(); if mode == EditorMode::Full { @@ -10784,10 +10784,10 @@ impl Editor { let settings = self.buffer.read(cx).settings_at(0, cx); let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); match mode { - language_settings::SoftWrap::None => SoftWrap::None, - language_settings::SoftWrap::PreferLine => SoftWrap::PreferLine, - language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - language_settings::SoftWrap::PreferredLineLength => { + settings::SoftWrap::None => SoftWrap::None, + settings::SoftWrap::PreferLine => SoftWrap::PreferLine, + settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + settings::SoftWrap::PreferredLineLength => { SoftWrap::Column(settings.preferred_line_length) } language_settings::SoftWrap::Bounded => { @@ -10796,11 +10796,7 @@ impl Editor { } } - pub fn set_soft_wrap_mode( - &mut self, - mode: language_settings::SoftWrap, - cx: &mut ViewContext, - ) { + pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { self.soft_wrap_mode_override = Some(mode); cx.notify(); } @@ -10833,10 +10829,8 @@ impl Editor { self.soft_wrap_mode_override.take(); } else { let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::None | SoftWrap::PreferLine => language_settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { - language_settings::SoftWrap::PreferLine - } + SoftWrap::None | SoftWrap::PreferLine => settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => settings::SoftWrap::PreferLine, }; self.soft_wrap_mode_override = Some(soft_wrap); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e11b38ba59680d..ad065f57ae6983 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -16,14 +16,14 @@ use gpui::{ use indoc::indoc; use language::{ language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings, + self, AllLanguageSettings, AllLanguageSettingsContent, Formatter, FormatterList, + IndentGuideSettings, LanguageSettingsContent, PrettierSettings, }, BracketPairConfig, Capability::ReadWrite, FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, Override, ParsedMarkdown, Point, }; -use language_settings::{Formatter, FormatterList, IndentGuideSettings}; use multi_buffer::MultiBufferIndentGuide; use parking_lot::Mutex; use project::FakeFs; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 47107b97546871..e37af55cf5c26a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6329,7 +6329,6 @@ mod tests { Editor, MultiBuffer, }; use gpui::{TestAppContext, VisualTestContext}; - use language::language_settings; use log::info; use std::num::NonZeroU32; use ui::Context; @@ -6730,7 +6729,7 @@ mod tests { s.defaults.tab_size = NonZeroU32::new(tab_size); s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); s.defaults.preferred_line_length = Some(editor_width as u32); - s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); + s.defaults.soft_wrap = Some(settings::SoftWrap::PreferredLineLength); }); let actual_invisibles = collect_invisibles_from_new_editor( @@ -6785,7 +6784,7 @@ mod tests { let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); window .update(cx, |editor, cx| { - editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); editor.set_wrap_width(Some(editor_width), cx); }) .unwrap(); diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 5dc73634bda774..31ef78f45c9370 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -777,7 +777,7 @@ fn editor_with_deleted_text( }); let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b117b9682bb116..69c7dcce0dbef4 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -30,7 +30,6 @@ 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.rs b/crates/language/src/language.rs index 309a67a1a96a41..e0c7a5b8e67d5b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -20,7 +20,6 @@ mod task_context; pub mod buffer_tests; pub mod markdown; -use crate::language_settings::SoftWrap; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet}; @@ -39,7 +38,7 @@ use schemars::{ }; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; -use settings::WorktreeId; +use settings::{SoftWrap, WorktreeId}; use smol::future::FutureExt as _; use std::num::NonZeroU32; use std::{ diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9b085c1fe439fc..201101c4fe11dd 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -4,13 +4,9 @@ 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, -}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::AppContext; use itertools::{Either, Itertools}; -use parking_lot::RwLock; use schemars::{ schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec}, JsonSchema, @@ -20,14 +16,12 @@ use serde::{ Deserialize, Deserializer, Serialize, }; use serde_json::Value; -use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; -use std::{ - borrow::Cow, - num::NonZeroU32, - path::{Path, PathBuf}, - sync::Arc, +use settings::{ + add_references_to_properties, EditorConfigContent, Settings, SettingsLocation, SettingsSources, + SettingsStore, SoftWrap, }; -use util::{serde::default_true, ResultExt}; +use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; +use util::serde::default_true; /// Initializes the language settings. pub fn init(cx: &mut AppContext) { @@ -64,8 +58,6 @@ pub struct AllLanguageSettings { defaults: LanguageSettings, languages: HashMap, pub(crate) file_types: HashMap, GlobSet>, - pub editorconfig_files: HashSet, - pub resolved_editorconfig_chains: Arc, EditorConfigContent>>>, } /// The settings for a particular language. @@ -222,17 +214,6 @@ pub struct AllLanguageSettingsContent { pub file_types: HashMap, Vec>, } -/// The settings available in `.editorconfig` files and compatible with Zed. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct EditorConfigContent { - hard_tabs: Option, - tab_size: Option, - ensure_final_newline_on_save: Option, - remove_trailing_whitespace_on_save: Option, - preferred_line_length: Option, - soft_wrap: Option, -} - /// The settings for a particular language. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { @@ -387,22 +368,6 @@ pub struct FeaturesContent { pub inline_completion_provider: Option, } -/// Controls the soft-wrapping behavior in the editor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - /// Do not soft wrap. - None, - /// Prefer a single line generally, unless an overly long line is encountered. - PreferLine, - /// Soft wrap lines that exceed the editor width - EditorWidth, - /// Soft wrap lines at the preferred line length - PreferredLineLength, - /// Soft wrap line at the preferred line length or the editor width (whichever is smaller) - Bounded, -} - /// Controls the behavior of formatting files when they are saved. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FormatOnSave { @@ -831,7 +796,17 @@ impl AllLanguageSettings { .and_then(|name| self.languages.get(name)) .unwrap_or(&self.defaults); - if let Some(content) = self.editorconfig_settings(file, cx) { + let editorconfig_settings = file + .and_then(|file| file.as_local()) + .map(|file| (file.worktree_id(), file.abs_path(cx))) + .and_then(|(worktree_id, file_abs_path)| { + cx.global::().editorconfig_settings( + worktree_id, + language_name.map(ToOwned::to_owned), + &file_abs_path, + ) + }); + if let Some(content) = editorconfig_settings { let mut settings = settings.clone(); merge_with_editorconfig(&mut settings, &content); Cow::Owned(settings) @@ -865,93 +840,6 @@ impl AllLanguageSettings { self.language(file, language.map(|l| l.name()).as_ref(), cx) .show_inline_completions } - - pub fn unregister_editorconfig_path(&mut self, abs_path: &PathBuf) { - self.resolved_editorconfig_chains - .write() - .retain(|chain, _| !chain.contains(abs_path)); - self.editorconfig_files.remove(abs_path); - } - - pub fn register_editorconfig_path(&mut self, abs_path: PathBuf) { - self.resolved_editorconfig_chains - .write() - .retain(|chain, _| !chain.contains(&abs_path)); - self.editorconfig_files.insert(abs_path); - } - - fn editorconfig_settings( - &self, - file: Option<&Arc>, - cx: &AppContext, - ) -> Option { - let file_path = file?.as_local()?.abs_path(cx); - let mut editorconfig_chain = Vec::new(); - for file_path in file_path.ancestors() { - if self.editorconfig_files.contains(file_path) { - editorconfig_chain.push(file_path.to_owned()); - } - } - if editorconfig_chain.is_empty() { - return None; - } - - { - let resolved_editorconfig_chains = self.resolved_editorconfig_chains.read(); - if let Some(content) = resolved_editorconfig_chains.get(&editorconfig_chain) { - return Some(content.clone()); - } - drop(resolved_editorconfig_chains); - } - - // FS operation that goes recursively up the directory tree, may be slow - let mut cfg = ec4rs::properties_of(file_path).log_err()?; - if cfg.is_empty() { - return None; - } - - cfg.use_fallbacks(); - let max_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); - let editorconfig = EditorConfigContent { - 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), - }), - }), - hard_tabs: cfg - .get::() - .map(|v| v.eq(&IndentStyle::Tabs)) - .ok(), - ensure_final_newline_on_save: cfg - .get::() - .map(|v| match v { - FinalNewline::Value(b) => b, - }) - .ok(), - remove_trailing_whitespace_on_save: cfg - .get::() - .map(|v| match v { - TrimTrailingWs::Value(b) => b, - }) - .ok(), - preferred_line_length: max_line_length, - soft_wrap: if max_line_length.is_some() { - Some(SoftWrap::PreferredLineLength) - } else { - None - }, - }; - - self.resolved_editorconfig_chains - .write() - .insert(editorconfig_chain, editorconfig.clone()); - - Some(editorconfig) - } } /// The kind of an inlay hint. @@ -1097,8 +985,6 @@ impl settings::Settings for AllLanguageSettings { .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher())) .collect(), }, - editorconfig_files: HashSet::default(), - resolved_editorconfig_chains: Arc::default(), defaults, languages, file_types, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e1d594a3fa93be..20728de17e6f1b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3812,18 +3812,11 @@ impl Project { } }) } else if path.ends_with(EDITORCONFIG_FILE_NAME) { - let current_settings = AllLanguageSettings::get_global(cx); - let has_file = current_settings.editorconfig_files.contains(&abs_path); - let needs_removal = removed && has_file; - let needs_addition = !has_file; - if needs_removal || needs_addition { - let mut new_settings = current_settings.clone(); - if needs_removal { - new_settings.unregister_editorconfig_path(&abs_path); - } else if needs_addition { - new_settings.register_editorconfig_path(abs_path); - } - AllLanguageSettings::override_global(new_settings, cx); + let settings_store = cx.global_mut::(); + if removed { + settings_store.unregister_editorconfig_path(&abs_path); + } else { + settings_store.register_editorconfig_path(abs_path); } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 9db3f9c93bdffa..b82b37c2514a8a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,7 +4,7 @@ use futures::{future, StreamExt}; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ - language_settings::{AllLanguageSettings, LanguageSettingsContent, SoftWrap}, + language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; @@ -12,6 +12,7 @@ use lsp::{DiagnosticSeverity, NumberOrString}; use parking_lot::Mutex; use pretty_assertions::assert_eq; use serde_json::json; +use settings::SoftWrap; #[cfg(not(windows))] use std::os; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index e9f6f6e4899cc7..b95c3d41e466f3 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -18,11 +18,13 @@ 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 log.workspace = true paths.workspace = true +parking_lot.workspace = true release_channel.workspace = true rust-embed.workspace = true schemars.workspace = true diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5ece3f867e4ff4..05881670ac0968 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -13,7 +13,9 @@ pub use editable_setting_control::*; pub use json_schema::*; pub use keymap_file::KeymapFile; pub use settings_file::*; -pub use settings_store::{Settings, SettingsLocation, SettingsSources, SettingsStore}; +pub use settings_store::{ + EditorConfigContent, Settings, SettingsLocation, SettingsSources, SettingsStore, SoftWrap, +}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 3ef8bffe2d3ded..395243a58f07e8 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,16 +1,21 @@ use anyhow::{anyhow, Context, Result}; -use collections::{btree_map, hash_map, BTreeMap, HashMap}; +use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet}; +use ec4rs::property::{ + FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs, +}; use fs::Fs; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal}; +use parking_lot::RwLock; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; -use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use smallvec::SmallVec; use std::{ any::{type_name, Any, TypeId}, fmt::Debug, + num::NonZeroU32, ops::Range, - path::Path, + path::{Path, PathBuf}, str, sync::{Arc, LazyLock}, }; @@ -156,6 +161,31 @@ pub struct SettingsLocation<'a> { pub path: &'a Path, } +/// The settings available in `.editorconfig` files and compatible with Zed. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct EditorConfigContent { + pub hard_tabs: Option, + pub tab_size: Option, + pub ensure_final_newline_on_save: Option, + pub remove_trailing_whitespace_on_save: Option, + pub preferred_line_length: Option, + pub soft_wrap: Option, +} + +/// Controls the soft-wrapping behavior in the editor. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SoftWrap { + /// Do not soft wrap. + None, + /// Prefer a single line generally, unless an overly long line is encountered. + PreferLine, + /// Soft wrap lines that overflow the editor + EditorWidth, + /// Soft wrap lines at the preferred line length + PreferredLineLength, +} + /// A set of strongly-typed setting values defined via multiple JSON files. pub struct SettingsStore { setting_values: HashMap>, @@ -171,6 +201,15 @@ pub struct SettingsStore { setting_file_updates_tx: mpsc::UnboundedSender< Box LocalBoxFuture<'static, Result<()>>>, >, + editorconfig_files: HashSet, + resolved_editorconfig_chains: Arc>>, +} + +#[derive(Debug, Eq, PartialEq, Hash)] +struct EditorConfigKey { + language_name: Option, + worktree_id: usize, + editorconfig_chain: SmallVec<[PathBuf; 3]>, } impl Global for SettingsStore {} @@ -219,6 +258,8 @@ impl SettingsStore { (setting_file_update)(cx.clone()).await.log_err(); } }), + editorconfig_files: HashSet::default(), + resolved_editorconfig_chains: Arc::default(), } } @@ -780,6 +821,98 @@ impl SettingsStore { } Ok(()) } + + pub fn unregister_editorconfig_path(&mut self, abs_path: &PathBuf) { + self.resolved_editorconfig_chains + .write() + .retain(|key, _| !key.editorconfig_chain.contains(abs_path)); + self.editorconfig_files.remove(abs_path); + } + + pub fn register_editorconfig_path(&mut self, abs_path: PathBuf) { + self.resolved_editorconfig_chains + .write() + .retain(|key, _| !key.editorconfig_chain.contains(&abs_path)); + self.editorconfig_files.insert(abs_path); + } + + pub fn editorconfig_settings( + &self, + worktree_id: usize, + language_name: Option, + file_abs_path: &Path, + ) -> Option { + let mut editorconfig_chain = SmallVec::new(); + for ancestor_path in file_abs_path.ancestors() { + if self.editorconfig_files.contains(ancestor_path) { + editorconfig_chain.push(ancestor_path.to_owned()); + } + } + if editorconfig_chain.is_empty() { + return None; + } + + let chain_key = EditorConfigKey { + language_name, + worktree_id, + editorconfig_chain, + }; + + { + let resolved_editorconfig_chains = self.resolved_editorconfig_chains.read(); + if let Some(content) = resolved_editorconfig_chains.get(&chain_key) { + return Some(content.clone()); + } + } + + // FS operation that goes recursively up the directory tree, may be slow + let mut cfg = ec4rs::properties_of(file_abs_path).log_err()?; + if cfg.is_empty() { + return None; + } + + cfg.use_fallbacks(); + let max_line_length = cfg.get::().ok().and_then(|v| match v { + MaxLineLen::Value(u) => Some(u as u32), + MaxLineLen::Off => None, + }); + let editorconfig = EditorConfigContent { + 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), + }), + }), + hard_tabs: cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + ensure_final_newline_on_save: cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(), + remove_trailing_whitespace_on_save: cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + preferred_line_length: max_line_length, + soft_wrap: if max_line_length.is_some() { + Some(SoftWrap::PreferredLineLength) + } else { + None + }, + }; + + self.resolved_editorconfig_chains + .write() + .insert(chain_key, editorconfig.clone()); + + Some(editorconfig) + } } impl Debug for SettingsStore { diff --git a/crates/storybook/src/stories/auto_height_editor.rs b/crates/storybook/src/stories/auto_height_editor.rs index 7b3cee92c8bad7..c4b0aadabf3ffb 100644 --- a/crates/storybook/src/stories/auto_height_editor.rs +++ b/crates/storybook/src/stories/auto_height_editor.rs @@ -18,7 +18,7 @@ impl AutoHeightEditorStory { cx.new_view(|cx| Self { editor: cx.new_view(|cx| { let mut editor = Editor::auto_height(3, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); editor }), }) diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 16ab7771bdd76e..9e18ac1325b405 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,12 +1,12 @@ use gpui::{px, size, Context, UpdateGlobal}; use indoc::indoc; -use settings::SettingsStore; +use settings::{SettingsStore, SoftWrap}; use std::{ ops::{Deref, DerefMut}, panic, thread, }; -use language::language_settings::{AllLanguageSettings, SoftWrap}; +use language::language_settings::AllLanguageSettings; use util::test::marked_text_offsets; use super::{neovim_connection::NeovimConnection, VimTestContext}; From d34482ce53d15bb9d9548b64f48b06b73e7e9874 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 27 Aug 2024 21:26:37 +0300 Subject: [PATCH 10/12] Fix the test --- crates/project/src/project.rs | 11 +++++--- crates/project/src/project_tests.rs | 36 ++++++++++++++++----------- crates/settings/src/settings_store.rs | 14 +++++------ 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 20728de17e6f1b..edfa1484a7a750 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3813,10 +3813,13 @@ impl Project { }) } else if path.ends_with(EDITORCONFIG_FILE_NAME) { let settings_store = cx.global_mut::(); - if removed { - settings_store.unregister_editorconfig_path(&abs_path); - } else { - settings_store.register_editorconfig_path(abs_path); + if let Some(directory_abs_path) = abs_path.parent() { + let directory_abs_path = directory_abs_path.to_owned(); + if removed { + settings_store.unregister_editorconfig_directory(&directory_abs_path); + } else { + settings_store.register_editorconfig_directory(directory_abs_path); + } } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index b82b37c2514a8a..55672ad2ea3f90 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -95,7 +95,6 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); - cx.executor().allow_parking(); let dir = temp_tree(json!({ ".editorconfig": r#" @@ -107,7 +106,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { insert_final_newline = true trim_trailing_whitespace = true max_line_length = 80 - [*.rb] + [*.js] tab_width = 10 "#, ".zed": { @@ -129,14 +128,20 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "#, "b.rs": "fn b() {\n B\n}", }, - "c.rb": "def c\n C\nend", - "README.md": "tabs are better\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(); @@ -144,20 +149,23 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { cx.update(|cx| { let tree = worktree.read(cx); let settings_for = |path: &str| { - language_settings( - None, - Some( - &(File::for_entry(tree.entry_for_path(path).unwrap().clone(), worktree.clone()) - as _), - ), - cx, - ) + 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"); + language_settings(Some(&file_language), Some(&(file as _)), cx) }; let settings_a = settings_for("a.rs"); let settings_b = settings_for("b/b.rs"); - let settings_c = settings_for("c.rb"); - let settings_readme = settings_for("README.md"); + 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)); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 395243a58f07e8..1e57b4fc422dd0 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -201,7 +201,7 @@ pub struct SettingsStore { setting_file_updates_tx: mpsc::UnboundedSender< Box LocalBoxFuture<'static, Result<()>>>, >, - editorconfig_files: HashSet, + dirs_with_editorconfig: HashSet, resolved_editorconfig_chains: Arc>>, } @@ -258,7 +258,7 @@ impl SettingsStore { (setting_file_update)(cx.clone()).await.log_err(); } }), - editorconfig_files: HashSet::default(), + dirs_with_editorconfig: HashSet::default(), resolved_editorconfig_chains: Arc::default(), } } @@ -822,18 +822,18 @@ impl SettingsStore { Ok(()) } - pub fn unregister_editorconfig_path(&mut self, abs_path: &PathBuf) { + pub fn unregister_editorconfig_directory(&mut self, abs_path: &PathBuf) { self.resolved_editorconfig_chains .write() .retain(|key, _| !key.editorconfig_chain.contains(abs_path)); - self.editorconfig_files.remove(abs_path); + self.dirs_with_editorconfig.remove(abs_path); } - pub fn register_editorconfig_path(&mut self, abs_path: PathBuf) { + pub fn register_editorconfig_directory(&mut self, abs_path: PathBuf) { self.resolved_editorconfig_chains .write() .retain(|key, _| !key.editorconfig_chain.contains(&abs_path)); - self.editorconfig_files.insert(abs_path); + self.dirs_with_editorconfig.insert(abs_path); } pub fn editorconfig_settings( @@ -844,7 +844,7 @@ impl SettingsStore { ) -> Option { let mut editorconfig_chain = SmallVec::new(); for ancestor_path in file_abs_path.ancestors() { - if self.editorconfig_files.contains(ancestor_path) { + if self.dirs_with_editorconfig.contains(ancestor_path) { editorconfig_chain.push(ancestor_path.to_owned()); } } From cdcba726ef5f21d0ef31e7ee7819aea65b9a24a4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 28 Aug 2024 01:31:04 +0300 Subject: [PATCH 11/12] Properly pass file locations when resolving editorconfig settings --- crates/copilot/src/copilot.rs | 6 +++- crates/editor/src/editor.rs | 6 +++- .../src/wasm_host/wit/since_v0_1_0.rs | 3 +- crates/language/src/buffer.rs | 8 +++++ crates/language/src/language_settings.rs | 29 +++++++++++++------ crates/languages/src/rust.rs | 7 +++-- crates/worktree/src/worktree.rs | 4 +++ 7 files changed, 49 insertions(+), 14 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cdbe65ba1dcca1..c3521cfc6aaccb 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1235,7 +1235,11 @@ mod tests { unimplemented!() } - fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { + fn file_name(&self, _: &AppContext) -> &std::ffi::OsStr { + unimplemented!() + } + + fn abs_path_in_worktree(&self, _: &AppContext) -> Result { unimplemented!() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00cab0e5c16373..ecc2966cdf608c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12673,7 +12673,11 @@ fn inlay_hint_settings( let language = snapshot.language_at(location); let settings = all_language_settings(file, cx); settings - .language(file, language.map(|l| l.name()).as_ref(), cx) + .language( + file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + language.map(|l| l.name()).as_ref(), + cx, + ) .inlay_hints } 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 9f1a8bcf763f03..a22708df1cccd9 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,7 +356,8 @@ impl ExtensionImports for WasmState { "language" => { let key = key.map(|k| LanguageName::new(&k)); let settings = AllLanguageSettings::get(location, cx).language( - None, + location + .map(|location| (location.worktree_id, location.path.to_owned())), key.as_ref(), cx, ); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 30e7946640014b..8aa39c28a19bcb 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -359,6 +359,10 @@ pub trait File: Send + Sync { /// includes the name of the worktree's root folder). fn full_path(&self, cx: &AppContext) -> PathBuf; + /// Returns the full path to this file, resolved in its worktree. + /// This may be a non-existing path if the file is not local. + fn abs_path_in_worktree(&self, cx: &AppContext) -> Result; + /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; @@ -4179,6 +4183,10 @@ impl File for TestFile { self.path().file_name().unwrap_or(self.root_name.as_ref()) } + fn abs_path_in_worktree(&self, _: &AppContext) -> Result { + unimplemented!() + } + fn worktree_id(&self, _: &AppContext) -> WorktreeId { WorktreeId::from_usize(0) } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 201101c4fe11dd..8635b7b72806ab 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -20,7 +20,12 @@ use settings::{ add_references_to_properties, EditorConfigContent, Settings, SettingsLocation, SettingsSources, SettingsStore, SoftWrap, }; -use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; +use std::{ + borrow::Cow, + num::NonZeroU32, + path::{Path, PathBuf}, + sync::Arc, +}; use util::serde::default_true; /// Initializes the language settings. @@ -35,7 +40,11 @@ pub fn language_settings<'a>( cx: &'a AppContext, ) -> Cow<'a, LanguageSettings> { let language_name = language.map(|l| l.name()); - all_language_settings(file, cx).language(file, language_name.as_ref(), cx) + all_language_settings(file, cx).language( + file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + language_name.as_ref(), + cx, + ) } /// Returns the settings for all languages from the provided file. @@ -788,7 +797,7 @@ impl AllLanguageSettings { /// Returns the [`LanguageSettings`] for the language with the specified name. pub fn language<'a>( &'a self, - file: Option<&Arc>, + abs_path_in_worktree: Option<(usize, PathBuf)>, language_name: Option<&LanguageName>, cx: &'a AppContext, ) -> Cow<'a, LanguageSettings> { @@ -796,10 +805,8 @@ impl AllLanguageSettings { .and_then(|name| self.languages.get(name)) .unwrap_or(&self.defaults); - let editorconfig_settings = file - .and_then(|file| file.as_local()) - .map(|file| (file.worktree_id(), file.abs_path(cx))) - .and_then(|(worktree_id, file_abs_path)| { + let editorconfig_settings = + abs_path_in_worktree.and_then(|(worktree_id, file_abs_path)| { cx.global::().editorconfig_settings( worktree_id, language_name.map(ToOwned::to_owned), @@ -837,8 +844,12 @@ impl AllLanguageSettings { } } - self.language(file, language.map(|l| l.name()).as_ref(), cx) - .show_inline_completions + self.language( + file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + language.map(|l| l.name()).as_ref(), + cx, + ) + .show_inline_completions } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f1effcab9c5691..fc45f9fbdecd42 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -480,8 +480,11 @@ impl ContextProvider for RustContextProvider { cx: &AppContext, ) -> Option { const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN"; - let language_settings = - all_language_settings(file.as_ref(), cx).language(file.as_ref(), Some(&"Rust".into()), cx); + let language_settings = all_language_settings(file.as_ref(), cx).language( + file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + Some(&"Rust".into()), + cx, + ); let package_to_run = language_settings.tasks.variables.get(DEFAULT_RUN_NAME_STR); let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run.clone()] diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index d8555b71a4f67c..899cb3d6f5418f 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3093,6 +3093,10 @@ impl language::File for File { full_path } + fn abs_path_in_worktree(&self, cx: &AppContext) -> Result { + self.worktree.read(cx).absolutize(&self.path) + } + /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr { From c048993199cc64c91fce2a0df20e6141d2c58851 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 2 Sep 2024 15:24:56 +0300 Subject: [PATCH 12/12] Post-rebase fixes --- .../remote_editing_collaboration_tests.rs | 2 +- crates/editor/src/editor.rs | 12 +++---- .../src/wasm_host/wit/since_v0_2_0.rs | 3 +- crates/language/src/language_settings.rs | 11 ++++--- crates/languages/src/rust.rs | 2 +- crates/project/src/lsp_store.rs | 31 ++++++++++++++----- .../remote_server/src/remote_editing_tests.rs | 8 ++--- crates/settings/src/settings_store.rs | 6 ++-- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index c4410fd776be7d..83aa341226af5d 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -100,7 +100,7 @@ async fn test_sharing_an_ssh_remote_project( let file = buffer_b.read(cx).file(); assert_eq!( all_language_settings(file, cx) - .language(Some(&("Rust".into()))) + .language(None, Some(&("Rust".into())), cx) .language_servers, ["override-rust-analyzer".into()] ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ecc2966cdf608c..ca90c8c0bcfa55 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -414,7 +414,7 @@ impl Default for EditorStyle { pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { let show_background = all_language_settings(None, cx) - .language(None) + .language(None, None, cx) .inlay_hints .show_background; @@ -10790,9 +10790,7 @@ impl Editor { settings::SoftWrap::PreferredLineLength => { SoftWrap::Column(settings.preferred_line_length) } - language_settings::SoftWrap::Bounded => { - SoftWrap::Bounded(settings.preferred_line_length) - } + settings::SoftWrap::Bounded => SoftWrap::Bounded(settings.preferred_line_length), } } @@ -10830,7 +10828,9 @@ impl Editor { } else { let soft_wrap = match self.soft_wrap_mode(cx) { SoftWrap::None | SoftWrap::PreferLine => settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => settings::SoftWrap::PreferLine, + SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { + settings::SoftWrap::PreferLine + } }; self.soft_wrap_mode_override = Some(soft_wrap); } @@ -12674,7 +12674,7 @@ fn inlay_hint_settings( let settings = all_language_settings(file, cx); settings .language( - file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + file.and_then(|file| Some((file.worktree_id(cx), file.abs_path_in_worktree(cx).ok()?))), language.map(|l| l.name()).as_ref(), cx, ) 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 7fa79c2544475b..9115a3ca5022d1 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,7 +402,8 @@ impl ExtensionImports for WasmState { "language" => { let key = key.map(|k| LanguageName::new(&k)); let settings = - AllLanguageSettings::get(location, cx).language(key.as_ref()); + // TODO kb use `location` instead of `None` + AllLanguageSettings::get(location, cx).language(None, key.as_ref(), cx); Ok(serde_json::to_string(&settings::LanguageSettings { tab_size: settings.tab_size, })?) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 8635b7b72806ab..d1cf2ef4a849cc 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -18,7 +18,7 @@ use serde::{ use serde_json::Value; use settings::{ add_references_to_properties, EditorConfigContent, Settings, SettingsLocation, SettingsSources, - SettingsStore, SoftWrap, + SettingsStore, SoftWrap, WorktreeId, }; use std::{ borrow::Cow, @@ -41,7 +41,7 @@ pub fn language_settings<'a>( ) -> Cow<'a, LanguageSettings> { let language_name = language.map(|l| l.name()); all_language_settings(file, cx).language( - file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + file.and_then(|file| Some((file.worktree_id(cx), file.abs_path_in_worktree(cx).ok()?))), language_name.as_ref(), cx, ) @@ -797,7 +797,8 @@ impl AllLanguageSettings { /// Returns the [`LanguageSettings`] for the language with the specified name. pub fn language<'a>( &'a self, - abs_path_in_worktree: Option<(usize, PathBuf)>, + // TODO kb wrong API, store the previous file in the `AllLanguageSettings` instead of requiring it here + abs_path_in_worktree: Option<(WorktreeId, PathBuf)>, language_name: Option<&LanguageName>, cx: &'a AppContext, ) -> Cow<'a, LanguageSettings> { @@ -809,7 +810,7 @@ impl AllLanguageSettings { abs_path_in_worktree.and_then(|(worktree_id, file_abs_path)| { cx.global::().editorconfig_settings( worktree_id, - language_name.map(ToOwned::to_owned), + language_name.map(|name| name.0.to_string()), &file_abs_path, ) }); @@ -845,7 +846,7 @@ impl AllLanguageSettings { } self.language( - file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + file.and_then(|file| Some((file.worktree_id(cx), file.abs_path_in_worktree(cx).ok()?))), language.map(|l| l.name()).as_ref(), cx, ) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index fc45f9fbdecd42..9869caac661595 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -481,7 +481,7 @@ impl ContextProvider for RustContextProvider { ) -> Option { const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN"; let language_settings = all_language_settings(file.as_ref(), cx).language( - file.and_then(|file| Some((file.worktree_id(), file.abs_path_in_worktree(cx).ok()?))), + file.and_then(|file| Some((file.worktree_id(cx), file.abs_path_in_worktree(cx).ok()?))), Some(&"Rust".into()), cx, ); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 58d9ba8926737d..f7fd00abf0f765 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -59,6 +59,7 @@ use smol::channel::Sender; use snippet::Snippet; use std::{ any::Any, + borrow::Cow, cmp::Ordering, convert::TryInto, ffi::OsStr, @@ -725,7 +726,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), 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 { @@ -900,8 +902,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(), + )); } } @@ -1247,9 +1251,14 @@ impl LspStore { .filter(|_| { maybe!({ let language_name = buffer.read(cx).language_at(position)?.name(); + let abs_path_in_a_worktree = buffer.read(cx).file().and_then(|file| { + let worktree_id = file.worktree_id(cx); + let abs_path = file.abs_path_in_worktree(cx).ok()?; + Some((worktree_id, abs_path)) + }); Some( AllLanguageSettings::get_global(cx) - .language(Some(&language_name)) + .language(abs_path_in_a_worktree, Some(&language_name), cx) .linked_edits, ) }) == Some(true) @@ -1348,7 +1357,7 @@ impl LspStore { cx: &mut ModelContext, ) -> Task>> { let options = buffer.update(cx, |buffer, cx| { - lsp_command::lsp_formatting_options(language_settings( + lsp_command::lsp_formatting_options(&language_settings( buffer.language_at(position).as_ref(), buffer.file(), cx, @@ -4677,9 +4686,17 @@ impl LspStore { worktree: &'a Model, language: &LanguageName, cx: &'a mut ModelContext, - ) -> &'a LanguageSettings { + ) -> Cow<'a, LanguageSettings> { let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx)); - all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language(Some(language)) + let abs_path_in_worktree = root_file.as_ref().and_then(|f| { + let worktree = worktree.read(cx); + Some((worktree.id(), f.abs_path_in_worktree(cx).ok()?)) + }); + all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language( + abs_path_in_worktree, + Some(language), + cx, + ) } pub fn start_language_servers( diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index b7fc56d3c60262..42e5e93475f454 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -203,7 +203,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()), cx) .language_servers, ["custom-rust-analyzer".into()] ) @@ -262,7 +262,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo }), cx ) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["override-rust-analyzer".into()] ) @@ -272,7 +272,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo let file = buffer.read(cx).file(); assert_eq!( all_language_settings(file, cx) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["override-rust-analyzer".into()] ) @@ -355,7 +355,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext let file = buffer.read(cx).file(); assert_eq!( all_language_settings(file, cx) - .language(Some(&"Rust".into())) + .language(None, Some(&"Rust".into()), cx) .language_servers, ["rust-analyzer".into()] ) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 1e57b4fc422dd0..5337a94c8e5a17 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -184,6 +184,8 @@ pub enum SoftWrap { EditorWidth, /// Soft wrap lines at the preferred line length PreferredLineLength, + /// Soft wrap line at the preferred line length or the editor width (whichever is smaller) + Bounded, } /// A set of strongly-typed setting values defined via multiple JSON files. @@ -208,7 +210,7 @@ pub struct SettingsStore { #[derive(Debug, Eq, PartialEq, Hash)] struct EditorConfigKey { language_name: Option, - worktree_id: usize, + worktree_id: WorktreeId, editorconfig_chain: SmallVec<[PathBuf; 3]>, } @@ -838,7 +840,7 @@ impl SettingsStore { pub fn editorconfig_settings( &self, - worktree_id: usize, + worktree_id: WorktreeId, language_name: Option, file_abs_path: &Path, ) -> Option {