Support .editorconfig (#19455)

Closes https://github.com/zed-industries/zed/issues/8534
Supersedes https://github.com/zed-industries/zed/pull/16349

Potential concerns:
* we do not follow up to the `/` when looking for `.editorconfig`, only
up to the worktree root.
Seems fine for most of the cases, and the rest should be solved
generically later, as the same issue exists for settings.json
* `fn language` in `AllLanguageSettings` is very hot, called very
frequently during rendering. We accumulate and parse all `.editorconfig`
file contents beforehand, but have to go over globs and match these
against the path given + merge the properties still.
This does not seem to be very bad, but needs more testing and
potentially some extra caching.


Release Notes:

- Added .editorconfig support

---------

Co-authored-by: Ulysse Buonomo <buonomo.ulysse@gmail.com>
This commit is contained in:
Kirill Bulatov
2024-10-21 13:05:30 +03:00
committed by GitHub
co-authored by Ulysse Buonomo
parent d95a4f8671
commit d3cb08bf35
30 changed files with 869 additions and 263 deletions
+3 -1
View File
@@ -2303,7 +2303,9 @@ impl LspCommand for OnTypeFormatting {
.await?;
let options = buffer.update(&mut cx, |buffer, cx| {
lsp_formatting_options(language_settings(buffer.language(), buffer.file(), cx))
lsp_formatting_options(
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(),
)
})?;
Ok(Self {
+31 -30
View File
@@ -30,8 +30,7 @@ use gpui::{
use http_client::HttpClient;
use language::{
language_settings::{
all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter,
LanguageSettings, SelectedFormatter,
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
},
markdown, point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
@@ -223,7 +222,8 @@ impl LocalLspStore {
})?;
let settings = buffer.handle.update(&mut cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx).clone()
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.into_owned()
})?;
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@@ -280,7 +280,7 @@ impl LocalLspStore {
.zip(buffer.abs_path.as_ref());
let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx)
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.prettier
.clone()
})?;
@@ -1225,7 +1225,8 @@ impl LspStore {
});
let buffer_file = buffer.read(cx).file().cloned();
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let settings =
language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree_id = if let Some(file) = buffer_file {
@@ -1400,15 +1401,17 @@ impl LspStore {
let buffer = buffer.read(cx);
let buffer_file = File::from_dyn(buffer.file());
let buffer_language = buffer.language();
let settings = language_settings(buffer_language, buffer.file(), cx);
let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
if let Some(language) = buffer_language {
if settings.enable_language_server {
if let Some(file) = buffer_file {
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(),
));
}
}
@@ -1433,10 +1436,13 @@ impl LspStore {
});
if let Some((language, adapter)) = language {
let worktree = self.worktree_for_id(worktree_id, cx).ok();
let file = worktree.as_ref().and_then(|tree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
let root_file = worktree.as_ref().and_then(|worktree| {
worktree
.update(cx, |tree, cx| tree.root_file(cx))
.map(|f| f as _)
});
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
let settings = language_settings(Some(language.name()), root_file.as_ref(), cx);
if !settings.enable_language_server {
language_servers_to_stop.push((worktree_id, started_lsp_name.clone()));
} else if let Some(worktree) = worktree {
let server_name = &adapter.name;
@@ -1753,10 +1759,9 @@ impl LspStore {
})
.filter(|_| {
maybe!({
let language_name = buffer.read(cx).language_at(position)?.name();
let language = buffer.read(cx).language_at(position)?;
Some(
AllLanguageSettings::get_global(cx)
.language(Some(&language_name))
language_settings(Some(language.name()), buffer.read(cx).file(), cx)
.linked_edits,
)
}) == Some(true)
@@ -1850,11 +1855,14 @@ impl LspStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
let options = buffer.update(cx, |buffer, cx| {
lsp_command::lsp_formatting_options(language_settings(
buffer.language_at(position).as_ref(),
buffer.file(),
cx,
))
lsp_command::lsp_formatting_options(
language_settings(
buffer.language_at(position).map(|l| l.name()),
buffer.file(),
cx,
)
.as_ref(),
)
});
self.request_lsp(
buffer.clone(),
@@ -5288,23 +5296,16 @@ impl LspStore {
})
}
fn language_settings<'a>(
&'a self,
worktree: &'a Model<Worktree>,
language: &LanguageName,
cx: &'a mut ModelContext<Self>,
) -> &'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))
}
pub fn start_language_servers(
&mut self,
worktree: &Model<Worktree>,
language: LanguageName,
cx: &mut ModelContext<Self>,
) {
let settings = self.language_settings(worktree, &language, cx);
let root_file = worktree
.update(cx, |tree, cx| tree.root_file(cx))
.map(|f| f as _);
let settings = language_settings(Some(language.clone()), root_file.as_ref(), cx);
if !settings.enable_language_server || self.mode.is_remote() {
return;
}
+23 -3
View File
@@ -5,7 +5,7 @@ use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, M
use language::LanguageServerName;
use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path,
local_vscode_tasks_file_relative_path,
local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use schemars::JsonSchema;
@@ -287,14 +287,29 @@ impl SettingsObserver {
let store = cx.global::<SettingsStore>();
for worktree in self.worktree_store.read(cx).worktrees() {
let worktree_id = worktree.read(cx).id().to_proto();
for (path, kind, content) in store.local_settings(worktree.read(cx).id()) {
for (path, content) in store.local_settings(worktree.read(cx).id()) {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
kind: Some(local_settings_kind_to_proto(kind).into()),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
),
})
.log_err();
}
for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
),
})
.log_err();
}
@@ -453,6 +468,11 @@ impl SettingsObserver {
.unwrap(),
);
(settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(EDITORCONFIG_NAME) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};
(settings_dir, LocalSettingsKind::Editorconfig)
} else {
continue;
};
+115 -22
View File
@@ -4,7 +4,9 @@ use futures::{future, StreamExt};
use gpui::{AppContext, SemanticVersion, UpdateGlobal};
use http_client::Url;
use language::{
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
language_settings::{
language_settings, AllLanguageSettings, LanguageSettingsContent, SoftWrap,
},
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
};
@@ -15,7 +17,7 @@ use serde_json::json;
#[cfg(not(windows))]
use std::os;
use std::{mem, ops::Range, task::Poll};
use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
use task::{ResolvedTask, TaskContext};
use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _};
@@ -91,6 +93,107 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
init_test(cx);
let dir = temp_tree(json!({
".editorconfig": r#"
root = true
[*.rs]
indent_style = tab
indent_size = 3
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 80
[*.js]
tab_width = 10
"#,
".zed": {
"settings.json": r#"{
"tab_size": 8,
"hard_tabs": false,
"ensure_final_newline_on_save": false,
"remove_trailing_whitespace_on_save": false,
"preferred_line_length": 64,
"soft_wrap": "editor_width"
}"#,
},
"a.rs": "fn a() {\n A\n}",
"b": {
".editorconfig": r#"
[*.rs]
indent_size = 2
max_line_length = off
"#,
"b.rs": "fn b() {\n B\n}",
},
"c.js": "def c\n C\nend",
"README.json": "tabs are better\n",
}));
let path = dir.path();
let fs = FakeFs::new(cx.executor());
fs.insert_tree_from_real_fs(path, path).await;
let project = Project::test(fs, [path], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(js_lang());
language_registry.add(json_lang());
language_registry.add(rust_lang());
let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
cx.executor().run_until_parked();
cx.update(|cx| {
let tree = worktree.read(cx);
let settings_for = |path: &str| {
let file_entry = tree.entry_for_path(path).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.language_for_file_path(file.path.as_ref());
let file_language = cx
.background_executor()
.block(file_language)
.expect("Failed to get file language");
let file = file as _;
language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
};
let settings_a = settings_for("a.rs");
let settings_b = settings_for("b/b.rs");
let settings_c = settings_for("c.js");
let settings_readme = settings_for("README.json");
// .editorconfig overrides .zed/settings
assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
assert_eq!(settings_a.hard_tabs, true);
assert_eq!(settings_a.ensure_final_newline_on_save, true);
assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
assert_eq!(settings_a.preferred_line_length, 80);
// "max_line_length" also sets "soft_wrap"
assert_eq!(settings_a.soft_wrap, SoftWrap::PreferredLineLength);
// .editorconfig in b/ overrides .editorconfig in root
assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
// "indent_size" is not set, so "tab_width" is used
assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
// When max_line_length is "off", default to .zed/settings.json
assert_eq!(settings_b.preferred_line_length, 64);
assert_eq!(settings_b.soft_wrap, SoftWrap::EditorWidth);
// README.md should not be affected by .editorconfig's globe "*.rs"
assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
});
}
#[gpui::test]
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -146,26 +249,16 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
.update(|cx| {
let tree = worktree.read(cx);
let settings_a = language_settings(
None,
Some(
&(File::for_entry(
tree.entry_for_path("a/a.rs").unwrap().clone(),
worktree.clone(),
) as _),
),
cx,
);
let settings_b = language_settings(
None,
Some(
&(File::for_entry(
tree.entry_for_path("b/b.rs").unwrap().clone(),
worktree.clone(),
) as _),
),
cx,
);
let file_a = File::for_entry(
tree.entry_for_path("a/a.rs").unwrap().clone(),
worktree.clone(),
) as _;
let settings_a = language_settings(None, Some(&file_a), cx);
let file_b = File::for_entry(
tree.entry_for_path("b/b.rs").unwrap().clone(),
worktree.clone(),
) as _;
let settings_b = language_settings(None, Some(&file_b), cx);
assert_eq!(settings_a.tab_size.get(), 8);
assert_eq!(settings_b.tab_size.get(), 2);