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:
parent
d95a4f8671
commit
d3cb08bf35
30 changed files with 869 additions and 263 deletions
|
@ -2237,7 +2237,7 @@ fn join_project_internal(
|
|||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
kind: Some(proto::update_user_settings::Kind::Settings.into()),
|
||||
kind: Some(settings_file.kind.to_proto() as i32),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use editor::{
|
|||
test::editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||
Editor,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
|
@ -30,7 +31,7 @@ use serde_json::json;
|
|||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool, AtomicUsize},
|
||||
Arc,
|
||||
|
@ -60,7 +61,7 @@ async fn test_host_disconnect(
|
|||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
serde_json::json!({
|
||||
json!({
|
||||
"a.txt": "a-contents",
|
||||
"b.txt": "b-contents",
|
||||
}),
|
||||
|
@ -2152,6 +2153,295 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_collaborating_with_editorconfig(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// Set up a fake language server.
|
||||
client_a.language_registry().add(rust_lang());
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
|
||||
"other_mod": {
|
||||
"other.rs": "pub fn foo() -> usize {\n 4\n}",
|
||||
".editorconfig": "",
|
||||
},
|
||||
},
|
||||
".editorconfig": "[*]\ntab_width = 2\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let main_buffer_a = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_buffer((worktree_id, "src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let other_buffer_a = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_a = cx_a.add_empty_window();
|
||||
let main_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
|
||||
let other_editor_a =
|
||||
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
|
||||
let mut main_editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
editor: main_editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
let mut other_editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
editor: other_editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let main_buffer_b = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer((worktree_id, "src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let other_buffer_b = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let cx_b = cx_b.add_empty_window();
|
||||
let main_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
|
||||
let other_editor_b =
|
||||
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
|
||||
let mut main_editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
editor: main_editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
let mut other_editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
editor: other_editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
|
||||
let initial_main = indoc! {"
|
||||
ˇmod other;
|
||||
fn main() { let foo = other::foo(); }"};
|
||||
let initial_other = indoc! {"
|
||||
ˇpub fn foo() -> usize {
|
||||
4
|
||||
}"};
|
||||
|
||||
let first_tabbed_main = indoc! {"
|
||||
ˇmod other;
|
||||
fn main() { let foo = other::foo(); }"};
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
first_tabbed_main,
|
||||
true,
|
||||
);
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
first_tabbed_main,
|
||||
false,
|
||||
);
|
||||
|
||||
let first_tabbed_other = indoc! {"
|
||||
ˇpub fn foo() -> usize {
|
||||
4
|
||||
}"};
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
first_tabbed_other,
|
||||
true,
|
||||
);
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
first_tabbed_other,
|
||||
false,
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.atomic_write(
|
||||
PathBuf::from("/a/src/.editorconfig"),
|
||||
"[*]\ntab_width = 3\n".to_owned(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let second_tabbed_main = indoc! {"
|
||||
ˇmod other;
|
||||
fn main() { let foo = other::foo(); }"};
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
second_tabbed_main,
|
||||
true,
|
||||
);
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
second_tabbed_main,
|
||||
false,
|
||||
);
|
||||
|
||||
let second_tabbed_other = indoc! {"
|
||||
ˇpub fn foo() -> usize {
|
||||
4
|
||||
}"};
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
second_tabbed_other,
|
||||
true,
|
||||
);
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
second_tabbed_other,
|
||||
false,
|
||||
);
|
||||
|
||||
let editorconfig_buffer_b = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
editorconfig_buffer_b.update(cx_b, |buffer, cx| {
|
||||
buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
|
||||
});
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.save_buffer(editorconfig_buffer_b.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
second_tabbed_main,
|
||||
true,
|
||||
);
|
||||
tab_undo_assert(
|
||||
&mut main_editor_cx_a,
|
||||
&mut main_editor_cx_b,
|
||||
initial_main,
|
||||
second_tabbed_main,
|
||||
false,
|
||||
);
|
||||
|
||||
let third_tabbed_other = indoc! {"
|
||||
ˇpub fn foo() -> usize {
|
||||
4
|
||||
}"};
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
third_tabbed_other,
|
||||
true,
|
||||
);
|
||||
|
||||
tab_undo_assert(
|
||||
&mut other_editor_cx_a,
|
||||
&mut other_editor_cx_b,
|
||||
initial_other,
|
||||
third_tabbed_other,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn tab_undo_assert(
|
||||
cx_a: &mut EditorTestContext,
|
||||
cx_b: &mut EditorTestContext,
|
||||
expected_initial: &str,
|
||||
expected_tabbed: &str,
|
||||
a_tabs: bool,
|
||||
) {
|
||||
cx_a.assert_editor_state(expected_initial);
|
||||
cx_b.assert_editor_state(expected_initial);
|
||||
|
||||
if a_tabs {
|
||||
cx_a.update_editor(|editor, cx| {
|
||||
editor.tab(&editor::actions::Tab, cx);
|
||||
});
|
||||
} else {
|
||||
cx_b.update_editor(|editor, cx| {
|
||||
editor.tab(&editor::actions::Tab, cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
cx_a.assert_editor_state(expected_tabbed);
|
||||
cx_b.assert_editor_state(expected_tabbed);
|
||||
|
||||
if a_tabs {
|
||||
cx_a.update_editor(|editor, cx| {
|
||||
editor.undo(&editor::actions::Undo, cx);
|
||||
});
|
||||
} else {
|
||||
cx_b.update_editor(|editor, cx| {
|
||||
editor.undo(&editor::actions::Undo, cx);
|
||||
});
|
||||
}
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
cx_a.assert_editor_state(expected_initial);
|
||||
cx_b.assert_editor_state(expected_initial);
|
||||
}
|
||||
|
||||
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for hint in editor.inlay_hint_cache().hints() {
|
||||
|
|
|
@ -34,7 +34,7 @@ use project::{
|
|||
};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{LocalSettingsKind, SettingsStore};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
env, future, mem,
|
||||
|
@ -3328,16 +3328,8 @@ async fn test_local_settings(
|
|||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Path::new("").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":2}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
@ -3355,16 +3347,8 @@ async fn test_local_settings(
|
|||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Path::new("").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
@ -3392,16 +3376,8 @@ async fn test_local_settings(
|
|||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":8}"#.to_string()
|
||||
),
|
||||
(
|
||||
Path::new("b").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"tab_size":4}"#.to_string()
|
||||
),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
@ -3431,11 +3407,7 @@ async fn test_local_settings(
|
|||
store
|
||||
.local_settings(worktree_b.read(cx).id())
|
||||
.collect::<Vec<_>>(),
|
||||
&[(
|
||||
Path::new("a").into(),
|
||||
LocalSettingsKind::Settings,
|
||||
r#"{"hard_tabs":true}"#.to_string()
|
||||
),]
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use call::ActiveCall;
|
|||
use fs::{FakeFs, Fs as _};
|
||||
use gpui::{Context as _, TestAppContext};
|
||||
use http_client::BlockedHttpClient;
|
||||
use language::{language_settings::all_language_settings, LanguageRegistry};
|
||||
use language::{language_settings::language_settings, LanguageRegistry};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ProjectPath;
|
||||
use remote::SshRemoteClient;
|
||||
|
@ -135,9 +135,7 @@ async fn test_sharing_an_ssh_remote_project(
|
|||
cx_b.read(|cx| {
|
||||
let file = buffer_b.read(cx).file();
|
||||
assert_eq!(
|
||||
all_language_settings(file, cx)
|
||||
.language(Some(&("Rust".into())))
|
||||
.language_servers,
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["override-rust-analyzer".to_string()]
|
||||
)
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue