From dbfc56030ad72b4fcbf5d9aa085f3f5922524635 Mon Sep 17 00:00:00 2001 From: Ulysse Buonomo Date: Fri, 16 Aug 2024 13:49:37 +0200 Subject: [PATCH] feat: Add editorconfig support Fixes #8534 --- Cargo.lock | 7 +++ crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 6 +- crates/language/src/language_settings.rs | 71 ++++++++++++++++++++++-- crates/multi_buffer/src/multi_buffer.rs | 8 +-- crates/project/src/lsp_command.rs | 2 +- crates/project/src/project.rs | 2 +- crates/project/src/project_tests.rs | 67 ++++++++++++---------- 8 files changed, 119 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c338d6346e96ee..c76342cdaa43bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3502,6 +3502,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" @@ -5934,6 +5940,7 @@ dependencies = [ "clock", "collections", "ctor", + "ec4rs", "env_logger", "futures 0.3.30", "fuzzy", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 378a5ca537d379..3b0c36ee6b19d7 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 = "1.1" futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3ee2d6ca55532c..9a6629ca5d3c82 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2624,11 +2624,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 4a21ba8ee0a484..9d4c4e09dae2e7 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -4,6 +4,7 @@ use crate::{File, Language, 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}; @@ -35,13 +36,75 @@ 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_deref()) + let mut settings = all_language_settings(file, cx) + .language(language_name.as_deref()) + .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(); + let editor_config_content = LanguageSettingsContent { + 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(), + hard_tabs: cfg + .get::() + .map(|v| v.eq(&IndentStyle::Tabs)) + .ok(), + soft_wrap: None, + preferred_line_length: None, + show_wrap_guides: None, + wrap_guides: None, + format_on_save: None, + remove_trailing_whitespace_on_save: cfg + .get::() + .map(|v| match v { + TrimTrailingWs::Value(b) => b, + }) + .ok(), + ensure_final_newline_on_save: cfg + .get::() + .map(|v| match v { + FinalNewline::Value(b) => b, + }) + .ok(), + formatter: None, + prettier: None, + enable_language_server: None, + show_whitespaces: None, + extend_comment_on_newline: None, + inlay_hints: None, + use_autoclose: None, + code_actions_on_format: None, + indent_guides: None, + language_servers: None, + show_inline_completions: None, + use_auto_surround: None, + always_treat_brackets_as_autoclosed: None, + linked_edits: None, + tasks: None, + }; + merge_settings(&mut settings, &editor_config_content); + 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 a734f5c8625f20..f8895ef3077c73 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1721,11 +1721,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) { @@ -3461,7 +3457,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 f0773abe331586..fc09c77b74a968 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2278,7 +2278,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.rs b/crates/project/src/project.rs index af5cc97c4f9da0..db1b392056a351 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7029,7 +7029,7 @@ impl Project { 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, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index d9df60c099deee..53c46f4c575d8e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -93,38 +93,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(); @@ -136,7 +144,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(), }; @@ -194,7 +202,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(), @@ -235,7 +243,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)), @@ -267,7 +278,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(), @@ -284,7 +295,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(),