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
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -3649,6 +3649,12 @@ version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ec4rs"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acf65d056c7da9c971c2847ce250fd1f0f9659d5718845c3ec0ad95f5668352c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecdsa"
|
name = "ecdsa"
|
||||||
version = "0.14.8"
|
version = "0.14.8"
|
||||||
|
@ -6210,6 +6216,7 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
"ec4rs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.30",
|
"futures 0.3.30",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
@ -10302,6 +10309,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
|
"ec4rs",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.30",
|
"futures 0.3.30",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
|
|
@ -347,6 +347,7 @@ ctor = "0.2.6"
|
||||||
dashmap = "6.0"
|
dashmap = "6.0"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
|
ec4rs = "1.1"
|
||||||
emojis = "0.6.1"
|
emojis = "0.6.1"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
exec = "0.3.1"
|
exec = "0.3.1"
|
||||||
|
|
|
@ -2237,7 +2237,7 @@ fn join_project_internal(
|
||||||
worktree_id: worktree.id,
|
worktree_id: worktree.id,
|
||||||
path: settings_file.path,
|
path: settings_file.path,
|
||||||
content: Some(settings_file.content),
|
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},
|
test::editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||||
Editor,
|
Editor,
|
||||||
};
|
};
|
||||||
|
use fs::Fs;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
@ -30,7 +31,7 @@ use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{self, AtomicBool, AtomicUsize},
|
atomic::{self, AtomicBool, AtomicUsize},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -60,7 +61,7 @@ async fn test_host_disconnect(
|
||||||
.fs()
|
.fs()
|
||||||
.insert_tree(
|
.insert_tree(
|
||||||
"/a",
|
"/a",
|
||||||
serde_json::json!({
|
json!({
|
||||||
"a.txt": "a-contents",
|
"a.txt": "a-contents",
|
||||||
"b.txt": "b-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> {
|
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||||
let mut labels = Vec::new();
|
let mut labels = Vec::new();
|
||||||
for hint in editor.inlay_hint_cache().hints() {
|
for hint in editor.inlay_hint_cache().hints() {
|
||||||
|
|
|
@ -34,7 +34,7 @@ use project::{
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{LocalSettingsKind, SettingsStore};
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
env, future, mem,
|
env, future, mem,
|
||||||
|
@ -3328,16 +3328,8 @@ async fn test_local_settings(
|
||||||
.local_settings(worktree_b.read(cx).id())
|
.local_settings(worktree_b.read(cx).id())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||||
Path::new("").into(),
|
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"tab_size":2}"#.to_string()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Path::new("a").into(),
|
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"tab_size":8}"#.to_string()
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -3355,16 +3347,8 @@ async fn test_local_settings(
|
||||||
.local_settings(worktree_b.read(cx).id())
|
.local_settings(worktree_b.read(cx).id())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("").into(), r#"{}"#.to_string()),
|
||||||
Path::new("").into(),
|
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{}"#.to_string()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Path::new("a").into(),
|
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"tab_size":8}"#.to_string()
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -3392,16 +3376,8 @@ async fn test_local_settings(
|
||||||
.local_settings(worktree_b.read(cx).id())
|
.local_settings(worktree_b.read(cx).id())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
(
|
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||||
Path::new("a").into(),
|
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"tab_size":8}"#.to_string()
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Path::new("b").into(),
|
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"tab_size":4}"#.to_string()
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -3431,11 +3407,7 @@ async fn test_local_settings(
|
||||||
store
|
store
|
||||||
.local_settings(worktree_b.read(cx).id())
|
.local_settings(worktree_b.read(cx).id())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[(
|
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||||
Path::new("a").into(),
|
|
||||||
LocalSettingsKind::Settings,
|
|
||||||
r#"{"hard_tabs":true}"#.to_string()
|
|
||||||
),]
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use call::ActiveCall;
|
||||||
use fs::{FakeFs, Fs as _};
|
use fs::{FakeFs, Fs as _};
|
||||||
use gpui::{Context as _, TestAppContext};
|
use gpui::{Context as _, TestAppContext};
|
||||||
use http_client::BlockedHttpClient;
|
use http_client::BlockedHttpClient;
|
||||||
use language::{language_settings::all_language_settings, LanguageRegistry};
|
use language::{language_settings::language_settings, LanguageRegistry};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use remote::SshRemoteClient;
|
use remote::SshRemoteClient;
|
||||||
|
@ -135,9 +135,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||||
cx_b.read(|cx| {
|
cx_b.read(|cx| {
|
||||||
let file = buffer_b.read(cx).file();
|
let file = buffer_b.read(cx).file();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all_language_settings(file, cx)
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||||
.language(Some(&("Rust".into())))
|
|
||||||
.language_servers,
|
|
||||||
["override-rust-analyzer".to_string()]
|
["override-rust-analyzer".to_string()]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -864,7 +864,11 @@ impl Copilot {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
let uri = registered_buffer.uri.clone();
|
let uri = registered_buffer.uri.clone();
|
||||||
let position = position.to_point_utf16(buffer);
|
let position = position.to_point_utf16(buffer);
|
||||||
let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
|
let settings = language_settings(
|
||||||
|
buffer.language_at(position).map(|l| l.name()),
|
||||||
|
buffer.file(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
let tab_size = settings.tab_size;
|
let tab_size = settings.tab_size;
|
||||||
let hard_tabs = settings.hard_tabs;
|
let hard_tabs = settings.hard_tabs;
|
||||||
let relative_path = buffer
|
let relative_path = buffer
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||||
let file = buffer.file();
|
let file = buffer.file();
|
||||||
let language = buffer.language_at(cursor_position);
|
let language = buffer.language_at(cursor_position);
|
||||||
let settings = all_language_settings(file, cx);
|
let settings = all_language_settings(file, cx);
|
||||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(
|
fn refresh(
|
||||||
|
@ -209,7 +209,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||||
) {
|
) {
|
||||||
let settings = AllLanguageSettings::get_global(cx);
|
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 {
|
if !copilot_enabled {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -423,11 +423,12 @@ impl DisplayMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
|
fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
|
||||||
|
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
|
||||||
let language = buffer
|
let language = buffer
|
||||||
.read(cx)
|
.and_then(|buffer| buffer.language())
|
||||||
.as_singleton()
|
.map(|l| l.name());
|
||||||
.and_then(|buffer| buffer.read(cx).language());
|
let file = buffer.and_then(|buffer| buffer.file());
|
||||||
language_settings(language, None, cx).tab_size
|
language_settings(language, file, cx).tab_size
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -90,7 +90,7 @@ pub use inline_completion_provider::*;
|
||||||
pub use items::MAX_TAB_TITLE_LEN;
|
pub use items::MAX_TAB_TITLE_LEN;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
|
||||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||||
Point, Selection, SelectionGoal, TransactionId,
|
Point, Selection, SelectionGoal, TransactionId,
|
||||||
|
@ -428,8 +428,7 @@ impl Default for EditorStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
|
pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
|
||||||
let show_background = all_language_settings(None, cx)
|
let show_background = language_settings::language_settings(None, None, cx)
|
||||||
.language(None)
|
|
||||||
.inlay_hints
|
.inlay_hints
|
||||||
.show_background;
|
.show_background;
|
||||||
|
|
||||||
|
@ -4248,7 +4247,10 @@ impl Editor {
|
||||||
.text_anchor_for_position(position, cx)?;
|
.text_anchor_for_position(position, cx)?;
|
||||||
|
|
||||||
let settings = language_settings::language_settings(
|
let settings = language_settings::language_settings(
|
||||||
buffer.read(cx).language_at(buffer_position).as_ref(),
|
buffer
|
||||||
|
.read(cx)
|
||||||
|
.language_at(buffer_position)
|
||||||
|
.map(|l| l.name()),
|
||||||
buffer.read(cx).file(),
|
buffer.read(cx).file(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -13374,11 +13376,8 @@ fn inlay_hint_settings(
|
||||||
cx: &mut ViewContext<'_, Editor>,
|
cx: &mut ViewContext<'_, Editor>,
|
||||||
) -> InlayHintSettings {
|
) -> InlayHintSettings {
|
||||||
let file = snapshot.file_at(location);
|
let file = snapshot.file_at(location);
|
||||||
let language = snapshot.language_at(location);
|
let language = snapshot.language_at(location).map(|l| l.name());
|
||||||
let settings = all_language_settings(file, cx);
|
language_settings(language, file, cx).inlay_hints
|
||||||
settings
|
|
||||||
.language(language.map(|l| l.name()).as_ref())
|
|
||||||
.inlay_hints
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn consume_contiguous_rows(
|
fn consume_contiguous_rows(
|
||||||
|
|
|
@ -39,9 +39,13 @@ impl Editor {
|
||||||
) -> Option<Vec<MultiBufferIndentGuide>> {
|
) -> Option<Vec<MultiBufferIndentGuide>> {
|
||||||
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
|
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||||
language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx)
|
language_settings(
|
||||||
.indent_guides
|
buffer.read(cx).language().map(|l| l.name()),
|
||||||
.enabled
|
buffer.read(cx).file(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.indent_guides
|
||||||
|
.enabled
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,8 +356,11 @@ impl ExtensionImports for WasmState {
|
||||||
cx.update(|cx| match category.as_str() {
|
cx.update(|cx| match category.as_str() {
|
||||||
"language" => {
|
"language" => {
|
||||||
let key = key.map(|k| LanguageName::new(&k));
|
let key = key.map(|k| LanguageName::new(&k));
|
||||||
let settings =
|
let settings = AllLanguageSettings::get(location, cx).language(
|
||||||
AllLanguageSettings::get(location, cx).language(key.as_ref());
|
location,
|
||||||
|
key.as_ref(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
Ok(serde_json::to_string(&settings::LanguageSettings {
|
Ok(serde_json::to_string(&settings::LanguageSettings {
|
||||||
tab_size: settings.tab_size,
|
tab_size: settings.tab_size,
|
||||||
})?)
|
})?)
|
||||||
|
|
|
@ -402,8 +402,11 @@ impl ExtensionImports for WasmState {
|
||||||
cx.update(|cx| match category.as_str() {
|
cx.update(|cx| match category.as_str() {
|
||||||
"language" => {
|
"language" => {
|
||||||
let key = key.map(|k| LanguageName::new(&k));
|
let key = key.map(|k| LanguageName::new(&k));
|
||||||
let settings =
|
let settings = AllLanguageSettings::get(location, cx).language(
|
||||||
AllLanguageSettings::get(location, cx).language(key.as_ref());
|
location,
|
||||||
|
key.as_ref(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
Ok(serde_json::to_string(&settings::LanguageSettings {
|
Ok(serde_json::to_string(&settings::LanguageSettings {
|
||||||
tab_size: settings.tab_size,
|
tab_size: settings.tab_size,
|
||||||
})?)
|
})?)
|
||||||
|
|
|
@ -62,7 +62,7 @@ impl Render for InlineCompletionButton {
|
||||||
let status = copilot.read(cx).status();
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
let enabled = self.editor_enabled.unwrap_or_else(|| {
|
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 {
|
let icon = match status {
|
||||||
|
@ -248,8 +248,9 @@ impl InlineCompletionButton {
|
||||||
|
|
||||||
if let Some(language) = self.language.clone() {
|
if let Some(language) = self.language.clone() {
|
||||||
let fs = fs.clone();
|
let fs = fs.clone();
|
||||||
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
|
let language_enabled =
|
||||||
.show_inline_completions;
|
language_settings::language_settings(Some(language.name()), None, cx)
|
||||||
|
.show_inline_completions;
|
||||||
|
|
||||||
menu = menu.entry(
|
menu = menu.entry(
|
||||||
format!(
|
format!(
|
||||||
|
@ -292,7 +293,7 @@ impl InlineCompletionButton {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let globally_enabled = settings.inline_completions_enabled(None, None);
|
let globally_enabled = settings.inline_completions_enabled(None, None, cx);
|
||||||
menu.entry(
|
menu.entry(
|
||||||
if globally_enabled {
|
if globally_enabled {
|
||||||
"Hide Inline Completions for All Files"
|
"Hide Inline Completions for All Files"
|
||||||
|
@ -340,6 +341,7 @@ impl InlineCompletionButton {
|
||||||
&& all_language_settings(file, cx).inline_completions_enabled(
|
&& all_language_settings(file, cx).inline_completions_enabled(
|
||||||
language,
|
language,
|
||||||
file.map(|file| file.path().as_ref()),
|
file.map(|file| file.path().as_ref()),
|
||||||
|
cx,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -442,7 +444,7 @@ async fn configure_disabled_globs(
|
||||||
|
|
||||||
fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
let show_inline_completions =
|
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::<AllLanguageSettings>(fs, cx, move |file, _| {
|
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
|
||||||
file.defaults.show_inline_completions = Some(!show_inline_completions)
|
file.defaults.show_inline_completions = Some(!show_inline_completions)
|
||||||
});
|
});
|
||||||
|
@ -466,7 +468,7 @@ fn toggle_inline_completions_for_language(
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) {
|
) {
|
||||||
let show_inline_completions =
|
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::<AllLanguageSettings>(fs, cx, move |file, _| {
|
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
|
||||||
file.languages
|
file.languages
|
||||||
.entry(language.name())
|
.entry(language.name())
|
||||||
|
|
|
@ -30,6 +30,7 @@ async-trait.workspace = true
|
||||||
async-watch.workspace = true
|
async-watch.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
ec4rs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
|
|
|
@ -37,6 +37,7 @@ use smallvec::SmallVec;
|
||||||
use smol::future::yield_now;
|
use smol::future::yield_now;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
borrow::Cow,
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
cmp::{self, Ordering, Reverse},
|
cmp::{self, Ordering, Reverse},
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
|
@ -2490,7 +2491,11 @@ impl BufferSnapshot {
|
||||||
/// Returns [`IndentSize`] for a given position that respects user settings
|
/// Returns [`IndentSize`] for a given position that respects user settings
|
||||||
/// and language preferences.
|
/// and language preferences.
|
||||||
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
|
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
|
||||||
let settings = language_settings(self.language_at(position), self.file(), cx);
|
let settings = language_settings(
|
||||||
|
self.language_at(position).map(|l| l.name()),
|
||||||
|
self.file(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
if settings.hard_tabs {
|
if settings.hard_tabs {
|
||||||
IndentSize::tab()
|
IndentSize::tab()
|
||||||
} else {
|
} else {
|
||||||
|
@ -2823,11 +2828,15 @@ impl BufferSnapshot {
|
||||||
|
|
||||||
/// Returns the settings for the language at the given location.
|
/// Returns the settings for the language at the given location.
|
||||||
pub fn settings_at<'a, D: ToOffset>(
|
pub fn settings_at<'a, D: ToOffset>(
|
||||||
&self,
|
&'a self,
|
||||||
position: D,
|
position: D,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a LanguageSettings {
|
) -> Cow<'a, LanguageSettings> {
|
||||||
language_settings(self.language_at(position), self.file.as_ref(), cx)
|
language_settings(
|
||||||
|
self.language_at(position).map(|l| l.name()),
|
||||||
|
self.file.as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
|
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
|
||||||
|
@ -3529,7 +3538,8 @@ impl BufferSnapshot {
|
||||||
ignore_disabled_for_language: bool,
|
ignore_disabled_for_language: bool,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Vec<IndentGuide> {
|
) -> Vec<IndentGuide> {
|
||||||
let language_settings = language_settings(self.language(), self.file.as_ref(), cx);
|
let language_settings =
|
||||||
|
language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx);
|
||||||
let settings = language_settings.indent_guides;
|
let settings = language_settings.indent_guides;
|
||||||
if !ignore_disabled_for_language && !settings.enabled {
|
if !ignore_disabled_for_language && !settings.enabled {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|
|
@ -4,6 +4,10 @@ use crate::{File, Language, LanguageName, LanguageServerName};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use core::slice;
|
use core::slice;
|
||||||
|
use ec4rs::{
|
||||||
|
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
|
||||||
|
Properties as EditorconfigProperties,
|
||||||
|
};
|
||||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use itertools::{Either, Itertools};
|
use itertools::{Either, Itertools};
|
||||||
|
@ -16,8 +20,10 @@ use serde::{
|
||||||
Deserialize, Deserializer, Serialize,
|
Deserialize, Deserializer, Serialize,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources};
|
use settings::{
|
||||||
use std::{num::NonZeroU32, path::Path, sync::Arc};
|
add_references_to_properties, Settings, SettingsLocation, SettingsSources, SettingsStore,
|
||||||
|
};
|
||||||
|
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
|
||||||
use util::serde::default_true;
|
use util::serde::default_true;
|
||||||
|
|
||||||
/// Initializes the language settings.
|
/// Initializes the language settings.
|
||||||
|
@ -27,17 +33,20 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
/// Returns the settings for the specified language from the provided file.
|
/// Returns the settings for the specified language from the provided file.
|
||||||
pub fn language_settings<'a>(
|
pub fn language_settings<'a>(
|
||||||
language: Option<&Arc<Language>>,
|
language: Option<LanguageName>,
|
||||||
file: Option<&Arc<dyn File>>,
|
file: Option<&'a Arc<dyn File>>,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a LanguageSettings {
|
) -> Cow<'a, LanguageSettings> {
|
||||||
let language_name = language.map(|l| l.name());
|
let location = file.map(|f| SettingsLocation {
|
||||||
all_language_settings(file, cx).language(language_name.as_ref())
|
worktree_id: f.worktree_id(cx),
|
||||||
|
path: f.path().as_ref(),
|
||||||
|
});
|
||||||
|
AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the settings for all languages from the provided file.
|
/// Returns the settings for all languages from the provided file.
|
||||||
pub fn all_language_settings<'a>(
|
pub fn all_language_settings<'a>(
|
||||||
file: Option<&Arc<dyn File>>,
|
file: Option<&'a Arc<dyn File>>,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a AllLanguageSettings {
|
) -> &'a AllLanguageSettings {
|
||||||
let location = file.map(|f| SettingsLocation {
|
let location = file.map(|f| SettingsLocation {
|
||||||
|
@ -810,13 +819,27 @@ impl InlayHintSettings {
|
||||||
|
|
||||||
impl AllLanguageSettings {
|
impl AllLanguageSettings {
|
||||||
/// Returns the [`LanguageSettings`] for the language with the specified name.
|
/// Returns the [`LanguageSettings`] for the language with the specified name.
|
||||||
pub fn language<'a>(&'a self, language_name: Option<&LanguageName>) -> &'a LanguageSettings {
|
pub fn language<'a>(
|
||||||
if let Some(name) = language_name {
|
&'a self,
|
||||||
if let Some(overrides) = self.languages.get(name) {
|
location: Option<SettingsLocation<'a>>,
|
||||||
return overrides;
|
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);
|
||||||
|
|
||||||
|
let editorconfig_properties = location.and_then(|location| {
|
||||||
|
cx.global::<SettingsStore>()
|
||||||
|
.editorconfg_properties(location.worktree_id, location.path)
|
||||||
|
});
|
||||||
|
if let Some(editorconfig_properties) = editorconfig_properties {
|
||||||
|
let mut settings = settings.clone();
|
||||||
|
merge_with_editorconfig(&mut settings, &editorconfig_properties);
|
||||||
|
Cow::Owned(settings)
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(settings)
|
||||||
}
|
}
|
||||||
&self.defaults
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether inline completions are enabled for the given path.
|
/// Returns whether inline completions are enabled for the given path.
|
||||||
|
@ -833,6 +856,7 @@ impl AllLanguageSettings {
|
||||||
&self,
|
&self,
|
||||||
language: Option<&Arc<Language>>,
|
language: Option<&Arc<Language>>,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
|
cx: &AppContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
if !self.inline_completions_enabled_for_path(path) {
|
if !self.inline_completions_enabled_for_path(path) {
|
||||||
|
@ -840,11 +864,64 @@ impl AllLanguageSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.language(language.map(|l| l.name()).as_ref())
|
self.language(None, language.map(|l| l.name()).as_ref(), cx)
|
||||||
.show_inline_completions
|
.show_inline_completions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
|
||||||
|
let max_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
|
||||||
|
MaxLineLen::Value(u) => Some(u as u32),
|
||||||
|
MaxLineLen::Off => None,
|
||||||
|
});
|
||||||
|
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
|
||||||
|
IndentSize::Value(u) => NonZeroU32::new(u as u32),
|
||||||
|
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
|
||||||
|
TabWidth::Value(u) => NonZeroU32::new(u as u32),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let hard_tabs = cfg
|
||||||
|
.get::<IndentStyle>()
|
||||||
|
.map(|v| v.eq(&IndentStyle::Tabs))
|
||||||
|
.ok();
|
||||||
|
let ensure_final_newline_on_save = cfg
|
||||||
|
.get::<FinalNewline>()
|
||||||
|
.map(|v| match v {
|
||||||
|
FinalNewline::Value(b) => b,
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
let remove_trailing_whitespace_on_save = cfg
|
||||||
|
.get::<TrimTrailingWs>()
|
||||||
|
.map(|v| match v {
|
||||||
|
TrimTrailingWs::Value(b) => b,
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
let preferred_line_length = max_line_length;
|
||||||
|
let soft_wrap = if max_line_length.is_some() {
|
||||||
|
Some(SoftWrap::PreferredLineLength)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||||
|
if let Some(value) = value {
|
||||||
|
*target = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merge(&mut settings.tab_size, tab_size);
|
||||||
|
merge(&mut settings.hard_tabs, hard_tabs);
|
||||||
|
merge(
|
||||||
|
&mut settings.remove_trailing_whitespace_on_save,
|
||||||
|
remove_trailing_whitespace_on_save,
|
||||||
|
);
|
||||||
|
merge(
|
||||||
|
&mut settings.ensure_final_newline_on_save,
|
||||||
|
ensure_final_newline_on_save,
|
||||||
|
);
|
||||||
|
merge(&mut settings.preferred_line_length, preferred_line_length);
|
||||||
|
merge(&mut settings.soft_wrap, soft_wrap);
|
||||||
|
}
|
||||||
|
|
||||||
/// The kind of an inlay hint.
|
/// The kind of an inlay hint.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum InlayHintKind {
|
pub enum InlayHintKind {
|
||||||
|
|
|
@ -6,7 +6,6 @@ use futures::{io::BufReader, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext};
|
use gpui::{AppContext, AsyncAppContext};
|
||||||
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
|
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
|
||||||
pub use language::*;
|
pub use language::*;
|
||||||
use language_settings::all_language_settings;
|
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use smol::fs::{self, File};
|
use smol::fs::{self, File};
|
||||||
|
@ -21,6 +20,8 @@ use std::{
|
||||||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||||||
use util::{fs::remove_matching, maybe, ResultExt};
|
use util::{fs::remove_matching, maybe, ResultExt};
|
||||||
|
|
||||||
|
use crate::language_settings::language_settings;
|
||||||
|
|
||||||
pub struct RustLspAdapter;
|
pub struct RustLspAdapter;
|
||||||
|
|
||||||
impl RustLspAdapter {
|
impl RustLspAdapter {
|
||||||
|
@ -424,13 +425,13 @@ impl ContextProvider for RustContextProvider {
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<TaskTemplates> {
|
) -> Option<TaskTemplates> {
|
||||||
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
|
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
|
||||||
let package_to_run = all_language_settings(file.as_ref(), cx)
|
let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
|
||||||
.language(Some(&"Rust".into()))
|
|
||||||
.tasks
|
.tasks
|
||||||
.variables
|
.variables
|
||||||
.get(DEFAULT_RUN_NAME_STR);
|
.get(DEFAULT_RUN_NAME_STR)
|
||||||
|
.cloned();
|
||||||
let run_task_args = if let Some(package_to_run) = package_to_run {
|
let run_task_args = if let Some(package_to_run) = package_to_run {
|
||||||
vec!["run".into(), "-p".into(), package_to_run.clone()]
|
vec!["run".into(), "-p".into(), package_to_run]
|
||||||
} else {
|
} else {
|
||||||
vec!["run".into()]
|
vec!["run".into()]
|
||||||
};
|
};
|
||||||
|
|
|
@ -101,7 +101,7 @@ impl LspAdapter for YamlLspAdapter {
|
||||||
|
|
||||||
let tab_size = cx.update(|cx| {
|
let tab_size = cx.update(|cx| {
|
||||||
AllLanguageSettings::get(Some(location), cx)
|
AllLanguageSettings::get(Some(location), cx)
|
||||||
.language(Some(&"YAML".into()))
|
.language(Some(location), Some(&"YAML".into()), cx)
|
||||||
.tab_size
|
.tab_size
|
||||||
})?;
|
})?;
|
||||||
let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}});
|
let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}});
|
||||||
|
|
|
@ -1778,7 +1778,7 @@ impl MultiBuffer {
|
||||||
&self,
|
&self,
|
||||||
point: T,
|
point: T,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a LanguageSettings {
|
) -> Cow<'a, LanguageSettings> {
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
let mut file = None;
|
let mut file = None;
|
||||||
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
|
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
|
||||||
|
@ -1786,7 +1786,7 @@ impl MultiBuffer {
|
||||||
language = buffer.language_at(offset);
|
language = buffer.language_at(offset);
|
||||||
file = buffer.file();
|
file = buffer.file();
|
||||||
}
|
}
|
||||||
language_settings(language.as_ref(), file, cx)
|
language_settings(language.map(|l| l.name()), file, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_each_buffer(&self, mut f: impl FnMut(&Model<Buffer>)) {
|
pub fn for_each_buffer(&self, mut f: impl FnMut(&Model<Buffer>)) {
|
||||||
|
@ -3580,14 +3580,14 @@ impl MultiBufferSnapshot {
|
||||||
&'a self,
|
&'a self,
|
||||||
point: T,
|
point: T,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a LanguageSettings {
|
) -> Cow<'a, LanguageSettings> {
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
let mut file = None;
|
let mut file = None;
|
||||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
|
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
|
||||||
language = buffer.language_at(offset);
|
language = buffer.language_at(offset);
|
||||||
file = buffer.file();
|
file = buffer.file();
|
||||||
}
|
}
|
||||||
language_settings(language, file, cx)
|
language_settings(language.map(|l| l.name()), file, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {
|
pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {
|
||||||
|
|
|
@ -293,3 +293,6 @@ pub fn local_tasks_file_relative_path() -> &'static Path {
|
||||||
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
||||||
Path::new(".vscode/tasks.json")
|
Path::new(".vscode/tasks.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A default editorconfig file name to use when resolving project settings.
|
||||||
|
pub const EDITORCONFIG_NAME: &str = ".editorconfig";
|
||||||
|
|
|
@ -205,7 +205,7 @@ impl Prettier {
|
||||||
let params = buffer
|
let params = buffer
|
||||||
.update(cx, |buffer, cx| {
|
.update(cx, |buffer, cx| {
|
||||||
let buffer_language = buffer.language();
|
let buffer_language = buffer.language();
|
||||||
let language_settings = language_settings(buffer_language, buffer.file(), cx);
|
let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
|
||||||
let prettier_settings = &language_settings.prettier;
|
let prettier_settings = &language_settings.prettier;
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
prettier_settings.allowed,
|
prettier_settings.allowed,
|
||||||
|
|
|
@ -2303,7 +2303,9 @@ impl LspCommand for OnTypeFormatting {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let options = buffer.update(&mut cx, |buffer, cx| {
|
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 {
|
Ok(Self {
|
||||||
|
|
|
@ -30,8 +30,7 @@ use gpui::{
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{
|
||||||
all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter,
|
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
|
||||||
LanguageSettings, SelectedFormatter,
|
|
||||||
},
|
},
|
||||||
markdown, point_to_lsp, prepare_completion_documentation,
|
markdown, point_to_lsp, prepare_completion_documentation,
|
||||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||||
|
@ -223,7 +222,8 @@ impl LocalLspStore {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let settings = buffer.handle.update(&mut cx, |buffer, cx| {
|
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;
|
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||||
|
@ -280,7 +280,7 @@ impl LocalLspStore {
|
||||||
.zip(buffer.abs_path.as_ref());
|
.zip(buffer.abs_path.as_ref());
|
||||||
|
|
||||||
let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
|
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
|
.prettier
|
||||||
.clone()
|
.clone()
|
||||||
})?;
|
})?;
|
||||||
|
@ -1225,7 +1225,8 @@ impl LspStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
let buffer_file = buffer.read(cx).file().cloned();
|
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 buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||||
|
|
||||||
let worktree_id = if let Some(file) = buffer_file {
|
let worktree_id = if let Some(file) = buffer_file {
|
||||||
|
@ -1400,15 +1401,17 @@ impl LspStore {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
let buffer_file = File::from_dyn(buffer.file());
|
let buffer_file = File::from_dyn(buffer.file());
|
||||||
let buffer_language = buffer.language();
|
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 let Some(language) = buffer_language {
|
||||||
if settings.enable_language_server {
|
if settings.enable_language_server {
|
||||||
if let Some(file) = buffer_file {
|
if let Some(file) = buffer_file {
|
||||||
language_servers_to_start.push((file.worktree.clone(), language.name()));
|
language_servers_to_start.push((file.worktree.clone(), language.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
language_formatters_to_check
|
language_formatters_to_check.push((
|
||||||
.push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone()));
|
buffer_file.map(|f| f.worktree_id(cx)),
|
||||||
|
settings.into_owned(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1433,10 +1436,13 @@ impl LspStore {
|
||||||
});
|
});
|
||||||
if let Some((language, adapter)) = language {
|
if let Some((language, adapter)) = language {
|
||||||
let worktree = self.worktree_for_id(worktree_id, cx).ok();
|
let worktree = self.worktree_for_id(worktree_id, cx).ok();
|
||||||
let file = worktree.as_ref().and_then(|tree| {
|
let root_file = worktree.as_ref().and_then(|worktree| {
|
||||||
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
|
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()));
|
language_servers_to_stop.push((worktree_id, started_lsp_name.clone()));
|
||||||
} else if let Some(worktree) = worktree {
|
} else if let Some(worktree) = worktree {
|
||||||
let server_name = &adapter.name;
|
let server_name = &adapter.name;
|
||||||
|
@ -1753,10 +1759,9 @@ impl LspStore {
|
||||||
})
|
})
|
||||||
.filter(|_| {
|
.filter(|_| {
|
||||||
maybe!({
|
maybe!({
|
||||||
let language_name = buffer.read(cx).language_at(position)?.name();
|
let language = buffer.read(cx).language_at(position)?;
|
||||||
Some(
|
Some(
|
||||||
AllLanguageSettings::get_global(cx)
|
language_settings(Some(language.name()), buffer.read(cx).file(), cx)
|
||||||
.language(Some(&language_name))
|
|
||||||
.linked_edits,
|
.linked_edits,
|
||||||
)
|
)
|
||||||
}) == Some(true)
|
}) == Some(true)
|
||||||
|
@ -1850,11 +1855,14 @@ impl LspStore {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Transaction>>> {
|
) -> Task<Result<Option<Transaction>>> {
|
||||||
let options = buffer.update(cx, |buffer, cx| {
|
let options = buffer.update(cx, |buffer, cx| {
|
||||||
lsp_command::lsp_formatting_options(language_settings(
|
lsp_command::lsp_formatting_options(
|
||||||
buffer.language_at(position).as_ref(),
|
language_settings(
|
||||||
buffer.file(),
|
buffer.language_at(position).map(|l| l.name()),
|
||||||
cx,
|
buffer.file(),
|
||||||
))
|
cx,
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
self.request_lsp(
|
self.request_lsp(
|
||||||
buffer.clone(),
|
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(
|
pub fn start_language_servers(
|
||||||
&mut self,
|
&mut self,
|
||||||
worktree: &Model<Worktree>,
|
worktree: &Model<Worktree>,
|
||||||
language: LanguageName,
|
language: LanguageName,
|
||||||
cx: &mut ModelContext<Self>,
|
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() {
|
if !settings.enable_language_server || self.mode.is_remote() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, M
|
||||||
use language::LanguageServerName;
|
use language::LanguageServerName;
|
||||||
use paths::{
|
use paths::{
|
||||||
local_settings_file_relative_path, local_tasks_file_relative_path,
|
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 rpc::{proto, AnyProtoClient, TypedEnvelope};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -287,14 +287,29 @@ impl SettingsObserver {
|
||||||
let store = cx.global::<SettingsStore>();
|
let store = cx.global::<SettingsStore>();
|
||||||
for worktree in self.worktree_store.read(cx).worktrees() {
|
for worktree in self.worktree_store.read(cx).worktrees() {
|
||||||
let worktree_id = worktree.read(cx).id().to_proto();
|
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
|
downstream_client
|
||||||
.send(proto::UpdateWorktreeSettings {
|
.send(proto::UpdateWorktreeSettings {
|
||||||
project_id,
|
project_id,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: path.to_string_lossy().into(),
|
path: path.to_string_lossy().into(),
|
||||||
content: Some(content),
|
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();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
@ -453,6 +468,11 @@ impl SettingsObserver {
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
(settings_dir, LocalSettingsKind::Tasks)
|
(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 {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,9 @@ use futures::{future, StreamExt};
|
||||||
use gpui::{AppContext, SemanticVersion, UpdateGlobal};
|
use gpui::{AppContext, SemanticVersion, UpdateGlobal};
|
||||||
use http_client::Url;
|
use http_client::Url;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
|
language_settings::{
|
||||||
|
language_settings, AllLanguageSettings, LanguageSettingsContent, SoftWrap,
|
||||||
|
},
|
||||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
|
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
|
||||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
|
||||||
};
|
};
|
||||||
|
@ -15,7 +17,7 @@ use serde_json::json;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::os;
|
use std::os;
|
||||||
|
|
||||||
use std::{mem, ops::Range, task::Poll};
|
use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
|
||||||
use task::{ResolvedTask, TaskContext};
|
use task::{ResolvedTask, TaskContext};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt 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]
|
#[gpui::test]
|
||||||
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
|
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -146,26 +249,16 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
let tree = worktree.read(cx);
|
let tree = worktree.read(cx);
|
||||||
|
|
||||||
let settings_a = language_settings(
|
let file_a = File::for_entry(
|
||||||
None,
|
tree.entry_for_path("a/a.rs").unwrap().clone(),
|
||||||
Some(
|
worktree.clone(),
|
||||||
&(File::for_entry(
|
) as _;
|
||||||
tree.entry_for_path("a/a.rs").unwrap().clone(),
|
let settings_a = language_settings(None, Some(&file_a), cx);
|
||||||
worktree.clone(),
|
let file_b = File::for_entry(
|
||||||
) as _),
|
tree.entry_for_path("b/b.rs").unwrap().clone(),
|
||||||
),
|
worktree.clone(),
|
||||||
cx,
|
) as _;
|
||||||
);
|
let settings_b = language_settings(None, Some(&file_b), cx);
|
||||||
let settings_b = language_settings(
|
|
||||||
None,
|
|
||||||
Some(
|
|
||||||
&(File::for_entry(
|
|
||||||
tree.entry_for_path("b/b.rs").unwrap().clone(),
|
|
||||||
worktree.clone(),
|
|
||||||
) as _),
|
|
||||||
),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(settings_a.tab_size.get(), 8);
|
assert_eq!(settings_a.tab_size.get(), 8);
|
||||||
assert_eq!(settings_b.tab_size.get(), 2);
|
assert_eq!(settings_b.tab_size.get(), 2);
|
||||||
|
|
|
@ -5,7 +5,7 @@ use fs::{FakeFs, Fs};
|
||||||
use gpui::{Context, Model, TestAppContext};
|
use gpui::{Context, Model, TestAppContext};
|
||||||
use http_client::{BlockedHttpClient, FakeHttpClient};
|
use http_client::{BlockedHttpClient, FakeHttpClient};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{all_language_settings, AllLanguageSettings},
|
language_settings::{language_settings, AllLanguageSettings},
|
||||||
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
|
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
|
||||||
LineEnding,
|
LineEnding,
|
||||||
};
|
};
|
||||||
|
@ -208,7 +208,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
|
||||||
server_cx.read(|cx| {
|
server_cx.read(|cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
AllLanguageSettings::get_global(cx)
|
AllLanguageSettings::get_global(cx)
|
||||||
.language(Some(&"Rust".into()))
|
.language(None, Some(&"Rust".into()), cx)
|
||||||
.language_servers,
|
.language_servers,
|
||||||
["from-local-settings".to_string()]
|
["from-local-settings".to_string()]
|
||||||
)
|
)
|
||||||
|
@ -228,7 +228,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
|
||||||
server_cx.read(|cx| {
|
server_cx.read(|cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
AllLanguageSettings::get_global(cx)
|
AllLanguageSettings::get_global(cx)
|
||||||
.language(Some(&"Rust".into()))
|
.language(None, Some(&"Rust".into()), cx)
|
||||||
.language_servers,
|
.language_servers,
|
||||||
["from-server-settings".to_string()]
|
["from-server-settings".to_string()]
|
||||||
)
|
)
|
||||||
|
@ -287,7 +287,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
|
||||||
}),
|
}),
|
||||||
cx
|
cx
|
||||||
)
|
)
|
||||||
.language(Some(&"Rust".into()))
|
.language(None, Some(&"Rust".into()), cx)
|
||||||
.language_servers,
|
.language_servers,
|
||||||
["override-rust-analyzer".to_string()]
|
["override-rust-analyzer".to_string()]
|
||||||
)
|
)
|
||||||
|
@ -296,9 +296,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let file = buffer.read(cx).file();
|
let file = buffer.read(cx).file();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all_language_settings(file, cx)
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||||
.language(Some(&"Rust".into()))
|
|
||||||
.language_servers,
|
|
||||||
["override-rust-analyzer".to_string()]
|
["override-rust-analyzer".to_string()]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -379,9 +377,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let file = buffer.read(cx).file();
|
let file = buffer.read(cx).file();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
all_language_settings(file, cx)
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||||
.language(Some(&"Rust".into()))
|
|
||||||
.language_servers,
|
|
||||||
["rust-analyzer".to_string()]
|
["rust-analyzer".to_string()]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "fs/test-support"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
ec4rs.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
||||||
|
use ec4rs::{ConfigParser, PropertiesSource, Section};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal};
|
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal};
|
||||||
use paths::local_settings_file_relative_path;
|
use paths::{local_settings_file_relative_path, EDITORCONFIG_NAME};
|
||||||
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
||||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -12,12 +13,14 @@ use std::{
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str,
|
str::{self, FromStr},
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
};
|
};
|
||||||
use tree_sitter::Query;
|
use tree_sitter::Query;
|
||||||
use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
|
use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
|
||||||
|
|
||||||
|
pub type EditorconfigProperties = ec4rs::Properties;
|
||||||
|
|
||||||
use crate::{SettingsJsonSchemaParams, WorktreeId};
|
use crate::{SettingsJsonSchemaParams, WorktreeId};
|
||||||
|
|
||||||
/// A value that can be defined as a user setting.
|
/// A value that can be defined as a user setting.
|
||||||
|
@ -167,8 +170,8 @@ pub struct SettingsStore {
|
||||||
raw_user_settings: serde_json::Value,
|
raw_user_settings: serde_json::Value,
|
||||||
raw_server_settings: Option<serde_json::Value>,
|
raw_server_settings: Option<serde_json::Value>,
|
||||||
raw_extension_settings: serde_json::Value,
|
raw_extension_settings: serde_json::Value,
|
||||||
raw_local_settings:
|
raw_local_settings: BTreeMap<(WorktreeId, Arc<Path>), serde_json::Value>,
|
||||||
BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>,
|
raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
|
||||||
tab_size_callback: Option<(
|
tab_size_callback: Option<(
|
||||||
TypeId,
|
TypeId,
|
||||||
Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
|
Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
|
||||||
|
@ -179,6 +182,26 @@ pub struct SettingsStore {
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Editorconfig {
|
||||||
|
pub is_root: bool,
|
||||||
|
pub sections: SmallVec<[Section; 5]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Editorconfig {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(contents: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parser = ConfigParser::new_buffered(contents.as_bytes())
|
||||||
|
.context("creating editorconfig parser")?;
|
||||||
|
let is_root = parser.is_root;
|
||||||
|
let sections = parser
|
||||||
|
.collect::<Result<SmallVec<_>, _>>()
|
||||||
|
.context("parsing editorconfig sections")?;
|
||||||
|
Ok(Self { is_root, sections })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum LocalSettingsKind {
|
pub enum LocalSettingsKind {
|
||||||
Settings,
|
Settings,
|
||||||
|
@ -226,6 +249,7 @@ impl SettingsStore {
|
||||||
raw_server_settings: None,
|
raw_server_settings: None,
|
||||||
raw_extension_settings: serde_json::json!({}),
|
raw_extension_settings: serde_json::json!({}),
|
||||||
raw_local_settings: Default::default(),
|
raw_local_settings: Default::default(),
|
||||||
|
raw_editorconfig_settings: BTreeMap::default(),
|
||||||
tab_size_callback: Default::default(),
|
tab_size_callback: Default::default(),
|
||||||
setting_file_updates_tx,
|
setting_file_updates_tx,
|
||||||
_setting_file_updates: cx.spawn(|cx| async move {
|
_setting_file_updates: cx.spawn(|cx| async move {
|
||||||
|
@ -567,33 +591,91 @@ impl SettingsStore {
|
||||||
settings_content: Option<&str>,
|
settings_content: Option<&str>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> std::result::Result<(), InvalidSettingsError> {
|
) -> std::result::Result<(), InvalidSettingsError> {
|
||||||
debug_assert!(
|
let mut zed_settings_changed = false;
|
||||||
kind != LocalSettingsKind::Tasks,
|
match (
|
||||||
"Attempted to submit tasks into the settings store"
|
kind,
|
||||||
);
|
settings_content
|
||||||
|
.map(|content| content.trim())
|
||||||
let raw_local_settings = self
|
.filter(|content| !content.is_empty()),
|
||||||
.raw_local_settings
|
) {
|
||||||
.entry((root_id, directory_path.clone()))
|
(LocalSettingsKind::Tasks, _) => {
|
||||||
.or_default();
|
return Err(InvalidSettingsError::Tasks {
|
||||||
let changed = if settings_content.is_some_and(|content| !content.is_empty()) {
|
message: "Attempted to submit tasks into the settings store".to_string(),
|
||||||
let new_contents =
|
})
|
||||||
parse_json_with_comments(settings_content.unwrap()).map_err(|e| {
|
}
|
||||||
InvalidSettingsError::LocalSettings {
|
(LocalSettingsKind::Settings, None) => {
|
||||||
|
zed_settings_changed = self
|
||||||
|
.raw_local_settings
|
||||||
|
.remove(&(root_id, directory_path.clone()))
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
(LocalSettingsKind::Editorconfig, None) => {
|
||||||
|
self.raw_editorconfig_settings
|
||||||
|
.remove(&(root_id, directory_path.clone()));
|
||||||
|
}
|
||||||
|
(LocalSettingsKind::Settings, Some(settings_contents)) => {
|
||||||
|
let new_settings = parse_json_with_comments::<serde_json::Value>(settings_contents)
|
||||||
|
.map_err(|e| InvalidSettingsError::LocalSettings {
|
||||||
path: directory_path.join(local_settings_file_relative_path()),
|
path: directory_path.join(local_settings_file_relative_path()),
|
||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
|
})?;
|
||||||
|
match self
|
||||||
|
.raw_local_settings
|
||||||
|
.entry((root_id, directory_path.clone()))
|
||||||
|
{
|
||||||
|
btree_map::Entry::Vacant(v) => {
|
||||||
|
v.insert(new_settings);
|
||||||
|
zed_settings_changed = true;
|
||||||
}
|
}
|
||||||
})?;
|
btree_map::Entry::Occupied(mut o) => {
|
||||||
if Some(&new_contents) == raw_local_settings.get(&kind) {
|
if o.get() != &new_settings {
|
||||||
false
|
o.insert(new_settings);
|
||||||
} else {
|
zed_settings_changed = true;
|
||||||
raw_local_settings.insert(kind, new_contents);
|
}
|
||||||
true
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => {
|
||||||
|
match self
|
||||||
|
.raw_editorconfig_settings
|
||||||
|
.entry((root_id, directory_path.clone()))
|
||||||
|
{
|
||||||
|
btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() {
|
||||||
|
Ok(new_contents) => {
|
||||||
|
v.insert((editorconfig_contents.to_owned(), Some(new_contents)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
v.insert((editorconfig_contents.to_owned(), None));
|
||||||
|
return Err(InvalidSettingsError::Editorconfig {
|
||||||
|
message: e.to_string(),
|
||||||
|
path: directory_path.join(EDITORCONFIG_NAME),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
btree_map::Entry::Occupied(mut o) => {
|
||||||
|
if o.get().0 != editorconfig_contents {
|
||||||
|
match editorconfig_contents.parse() {
|
||||||
|
Ok(new_contents) => {
|
||||||
|
o.insert((
|
||||||
|
editorconfig_contents.to_owned(),
|
||||||
|
Some(new_contents),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
o.insert((editorconfig_contents.to_owned(), None));
|
||||||
|
return Err(InvalidSettingsError::Editorconfig {
|
||||||
|
message: e.to_string(),
|
||||||
|
path: directory_path.join(EDITORCONFIG_NAME),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
raw_local_settings.remove(&kind).is_some()
|
|
||||||
};
|
};
|
||||||
if changed {
|
|
||||||
|
if zed_settings_changed {
|
||||||
self.recompute_values(Some((root_id, &directory_path)), cx)?;
|
self.recompute_values(Some((root_id, &directory_path)), cx)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -605,13 +687,10 @@ impl SettingsStore {
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let settings: serde_json::Value = serde_json::to_value(content)?;
|
let settings: serde_json::Value = serde_json::to_value(content)?;
|
||||||
if settings.is_object() {
|
anyhow::ensure!(settings.is_object(), "settings must be an object");
|
||||||
self.raw_extension_settings = settings;
|
self.raw_extension_settings = settings;
|
||||||
self.recompute_values(None, cx)?;
|
self.recompute_values(None, cx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
|
||||||
Err(anyhow!("settings must be an object"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add or remove a set of local settings via a JSON string.
|
/// Add or remove a set of local settings via a JSON string.
|
||||||
|
@ -625,7 +704,7 @@ impl SettingsStore {
|
||||||
pub fn local_settings(
|
pub fn local_settings(
|
||||||
&self,
|
&self,
|
||||||
root_id: WorktreeId,
|
root_id: WorktreeId,
|
||||||
) -> impl '_ + Iterator<Item = (Arc<Path>, LocalSettingsKind, String)> {
|
) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
|
||||||
self.raw_local_settings
|
self.raw_local_settings
|
||||||
.range(
|
.range(
|
||||||
(root_id, Path::new("").into())
|
(root_id, Path::new("").into())
|
||||||
|
@ -634,11 +713,23 @@ impl SettingsStore {
|
||||||
Path::new("").into(),
|
Path::new("").into(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.flat_map(|((_, path), content)| {
|
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
|
||||||
content.iter().filter_map(|(&kind, raw_content)| {
|
}
|
||||||
let parsed_content = serde_json::to_string(raw_content).log_err()?;
|
|
||||||
Some((path.clone(), kind, parsed_content))
|
pub fn local_editorconfig_settings(
|
||||||
})
|
&self,
|
||||||
|
root_id: WorktreeId,
|
||||||
|
) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
|
||||||
|
self.raw_editorconfig_settings
|
||||||
|
.range(
|
||||||
|
(root_id, Path::new("").into())
|
||||||
|
..(
|
||||||
|
WorktreeId::from_usize(root_id.to_usize() + 1),
|
||||||
|
Path::new("").into(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map(|((_, path), (content, parsed_content))| {
|
||||||
|
(path.clone(), content.clone(), parsed_content.clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -753,7 +844,7 @@ impl SettingsStore {
|
||||||
&mut self,
|
&mut self,
|
||||||
changed_local_path: Option<(WorktreeId, &Path)>,
|
changed_local_path: Option<(WorktreeId, &Path)>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Result<(), InvalidSettingsError> {
|
) -> std::result::Result<(), InvalidSettingsError> {
|
||||||
// Reload the global and local values for every setting.
|
// Reload the global and local values for every setting.
|
||||||
let mut project_settings_stack = Vec::<DeserializedSetting>::new();
|
let mut project_settings_stack = Vec::<DeserializedSetting>::new();
|
||||||
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
|
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
|
||||||
|
@ -819,69 +910,90 @@ impl SettingsStore {
|
||||||
paths_stack.clear();
|
paths_stack.clear();
|
||||||
project_settings_stack.clear();
|
project_settings_stack.clear();
|
||||||
for ((root_id, directory_path), local_settings) in &self.raw_local_settings {
|
for ((root_id, directory_path), local_settings) in &self.raw_local_settings {
|
||||||
if let Some(local_settings) = local_settings.get(&LocalSettingsKind::Settings) {
|
// Build a stack of all of the local values for that setting.
|
||||||
// Build a stack of all of the local values for that setting.
|
while let Some(prev_entry) = paths_stack.last() {
|
||||||
while let Some(prev_entry) = paths_stack.last() {
|
if let Some((prev_root_id, prev_path)) = prev_entry {
|
||||||
if let Some((prev_root_id, prev_path)) = prev_entry {
|
if root_id != prev_root_id || !directory_path.starts_with(prev_path) {
|
||||||
if root_id != prev_root_id || !directory_path.starts_with(prev_path) {
|
paths_stack.pop();
|
||||||
paths_stack.pop();
|
project_settings_stack.pop();
|
||||||
project_settings_stack.pop();
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
match setting_value.deserialize_setting(local_settings) {
|
match setting_value.deserialize_setting(local_settings) {
|
||||||
Ok(local_settings) => {
|
Ok(local_settings) => {
|
||||||
paths_stack.push(Some((*root_id, directory_path.as_ref())));
|
paths_stack.push(Some((*root_id, directory_path.as_ref())));
|
||||||
project_settings_stack.push(local_settings);
|
project_settings_stack.push(local_settings);
|
||||||
|
|
||||||
// If a local settings file changed, then avoid recomputing local
|
// If a local settings file changed, then avoid recomputing local
|
||||||
// settings for any path outside of that directory.
|
// settings for any path outside of that directory.
|
||||||
if changed_local_path.map_or(
|
if changed_local_path.map_or(
|
||||||
false,
|
false,
|
||||||
|(changed_root_id, changed_local_path)| {
|
|(changed_root_id, changed_local_path)| {
|
||||||
*root_id != changed_root_id
|
*root_id != changed_root_id
|
||||||
|| !directory_path.starts_with(changed_local_path)
|
|| !directory_path.starts_with(changed_local_path)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = setting_value
|
||||||
|
.load_setting(
|
||||||
|
SettingsSources {
|
||||||
|
default: &default_settings,
|
||||||
|
extensions: extension_settings.as_ref(),
|
||||||
|
user: user_settings.as_ref(),
|
||||||
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
server: server_settings.as_ref(),
|
||||||
|
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||||
},
|
},
|
||||||
) {
|
cx,
|
||||||
continue;
|
)
|
||||||
}
|
.log_err()
|
||||||
|
{
|
||||||
if let Some(value) = setting_value
|
setting_value.set_local_value(*root_id, directory_path.clone(), value);
|
||||||
.load_setting(
|
|
||||||
SettingsSources {
|
|
||||||
default: &default_settings,
|
|
||||||
extensions: extension_settings.as_ref(),
|
|
||||||
user: user_settings.as_ref(),
|
|
||||||
release_channel: release_channel_settings.as_ref(),
|
|
||||||
server: server_settings.as_ref(),
|
|
||||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.log_err()
|
|
||||||
{
|
|
||||||
setting_value.set_local_value(
|
|
||||||
*root_id,
|
|
||||||
directory_path.clone(),
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
return Err(InvalidSettingsError::LocalSettings {
|
|
||||||
path: directory_path.join(local_settings_file_relative_path()),
|
|
||||||
message: error.to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(InvalidSettingsError::LocalSettings {
|
||||||
|
path: directory_path.join(local_settings_file_relative_path()),
|
||||||
|
message: error.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn editorconfg_properties(
|
||||||
|
&self,
|
||||||
|
for_worktree: WorktreeId,
|
||||||
|
for_path: &Path,
|
||||||
|
) -> Option<EditorconfigProperties> {
|
||||||
|
let mut properties = EditorconfigProperties::new();
|
||||||
|
|
||||||
|
for (directory_with_config, _, parsed_editorconfig) in
|
||||||
|
self.local_editorconfig_settings(for_worktree)
|
||||||
|
{
|
||||||
|
if !for_path.starts_with(&directory_with_config) {
|
||||||
|
properties.use_fallbacks();
|
||||||
|
return Some(properties);
|
||||||
|
}
|
||||||
|
let parsed_editorconfig = parsed_editorconfig?;
|
||||||
|
if parsed_editorconfig.is_root {
|
||||||
|
properties = EditorconfigProperties::new();
|
||||||
|
}
|
||||||
|
for section in parsed_editorconfig.sections {
|
||||||
|
section.apply_to(&mut properties, for_path).log_err()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.use_fallbacks();
|
||||||
|
Some(properties)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -890,6 +1002,8 @@ pub enum InvalidSettingsError {
|
||||||
UserSettings { message: String },
|
UserSettings { message: String },
|
||||||
ServerSettings { message: String },
|
ServerSettings { message: String },
|
||||||
DefaultSettings { message: String },
|
DefaultSettings { message: String },
|
||||||
|
Editorconfig { path: PathBuf, message: String },
|
||||||
|
Tasks { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InvalidSettingsError {
|
impl std::fmt::Display for InvalidSettingsError {
|
||||||
|
@ -898,8 +1012,10 @@ impl std::fmt::Display for InvalidSettingsError {
|
||||||
InvalidSettingsError::LocalSettings { message, .. }
|
InvalidSettingsError::LocalSettings { message, .. }
|
||||||
| InvalidSettingsError::UserSettings { message }
|
| InvalidSettingsError::UserSettings { message }
|
||||||
| InvalidSettingsError::ServerSettings { message }
|
| InvalidSettingsError::ServerSettings { message }
|
||||||
| InvalidSettingsError::DefaultSettings { message } => {
|
| InvalidSettingsError::DefaultSettings { message }
|
||||||
write!(f, "{}", message)
|
| InvalidSettingsError::Tasks { message }
|
||||||
|
| InvalidSettingsError::Editorconfig { message, .. } => {
|
||||||
|
write!(f, "{message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||||
let file = buffer.file();
|
let file = buffer.file();
|
||||||
let language = buffer.language_at(cursor_position);
|
let language = buffer.language_at(cursor_position);
|
||||||
let settings = all_language_settings(file, cx);
|
let settings = all_language_settings(file, cx);
|
||||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(
|
fn refresh(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue