Detailed changes
@@ -146,6 +146,7 @@ CREATE TABLE "worktree_settings_files" (
"path" VARCHAR NOT NULL,
"content" TEXT,
"kind" VARCHAR,
+ "outside_worktree" BOOL NOT NULL DEFAULT FALSE,
PRIMARY KEY (project_id, worktree_id, path),
FOREIGN KEY (project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
@@ -503,7 +503,8 @@ CREATE TABLE public.worktree_settings_files (
worktree_id bigint NOT NULL,
path character varying NOT NULL,
content text NOT NULL,
- kind character varying
+ kind character varying,
+ outside_worktree boolean DEFAULT false NOT NULL
);
CREATE TABLE public.worktrees (
@@ -649,6 +649,7 @@ pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
pub kind: LocalSettingsKind,
+ pub outside_worktree: bool,
}
pub struct NewExtensionVersion {
@@ -760,6 +760,7 @@ impl Database {
path: ActiveValue::Set(update.path.clone()),
content: ActiveValue::Set(content.clone()),
kind: ActiveValue::Set(kind),
+ outside_worktree: ActiveValue::Set(update.outside_worktree.unwrap_or(false)),
})
.on_conflict(
OnConflict::columns([
@@ -767,7 +768,10 @@ impl Database {
worktree_settings_file::Column::WorktreeId,
worktree_settings_file::Column::Path,
])
- .update_column(worktree_settings_file::Column::Content)
+ .update_columns([
+ worktree_settings_file::Column::Content,
+ worktree_settings_file::Column::OutsideWorktree,
+ ])
.to_owned(),
)
.exec(&*tx)
@@ -1050,6 +1054,7 @@ impl Database {
path: db_settings_file.path,
content: db_settings_file.content,
kind: db_settings_file.kind,
+ outside_worktree: db_settings_file.outside_worktree,
});
}
}
@@ -834,6 +834,7 @@ impl Database {
path: db_settings_file.path,
content: db_settings_file.content,
kind: db_settings_file.kind,
+ outside_worktree: db_settings_file.outside_worktree,
});
}
}
@@ -12,6 +12,7 @@ pub struct Model {
pub path: String,
pub content: String,
pub kind: LocalSettingsKind,
+ pub outside_worktree: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -1555,6 +1555,7 @@ fn notify_rejoined_projects(
path: settings_file.path,
content: Some(settings_file.content),
kind: Some(settings_file.kind.to_proto().into()),
+ outside_worktree: Some(settings_file.outside_worktree),
},
)?;
}
@@ -1987,6 +1988,7 @@ async fn join_project(
path: settings_file.path,
content: Some(settings_file.content),
kind: Some(settings_file.kind.to_proto() as i32),
+ outside_worktree: Some(settings_file.outside_worktree),
},
)?;
}
@@ -21,7 +21,7 @@ use gpui::{
App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,
};
use indoc::indoc;
-use language::{FakeLspAdapter, rust_lang};
+use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
use lsp::LSP_REQUEST_TIMEOUT;
use pretty_assertions::assert_eq;
use project::{
@@ -35,6 +35,7 @@ use serde_json::json;
use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore};
use std::{
collections::BTreeSet,
+ num::NonZeroU32,
ops::{Deref as _, Range},
path::{Path, PathBuf},
sync::{
@@ -3978,6 +3979,110 @@ fn main() { let foo = other::foo(); }"};
);
}
+#[gpui::test(iterations = 10)]
+async fn test_collaborating_with_external_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);
+
+ client_a.language_registry().add(rust_lang());
+ client_b.language_registry().add(rust_lang());
+
+ // Set up external .editorconfig in parent directory
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 5\n",
+ "worktree": {
+ ".editorconfig": "[*]\n",
+ "src": {
+ "main.rs": "fn main() {}",
+ },
+ },
+ }),
+ )
+ .await;
+
+ let (project_a, worktree_id) = client_a
+ .build_local_project(path!("/parent/worktree"), cx_a)
+ .await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Open buffer on client A
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
+ })
+ .await
+ .unwrap();
+
+ cx_a.run_until_parked();
+
+ // Verify client A sees external editorconfig settings
+ cx_a.read(|cx| {
+ let file = buffer_a.read(cx).file();
+ let settings = language_settings(Some("Rust".into()), file, cx);
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
+ });
+
+ // Client B joins the project
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| {
+ p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
+ })
+ .await
+ .unwrap();
+
+ cx_b.run_until_parked();
+
+ // Verify client B also sees external editorconfig settings
+ cx_b.read(|cx| {
+ let file = buffer_b.read(cx).file();
+ let settings = language_settings(Some("Rust".into()), file, cx);
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
+ });
+
+ // Client A modifies the external .editorconfig
+ client_a
+ .fs()
+ .atomic_write(
+ PathBuf::from(path!("/parent/.editorconfig")),
+ "[*]\nindent_size = 9\n".to_owned(),
+ )
+ .await
+ .unwrap();
+
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ // Verify client A sees updated settings
+ cx_a.read(|cx| {
+ let file = buffer_a.read(cx).file();
+ let settings = language_settings(Some("Rust".into()), file, cx);
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
+ });
+
+ // Verify client B also sees updated settings
+ cx_b.read(|cx| {
+ let file = buffer_b.read(cx).file();
+ let settings = language_settings(Some("Rust".into()), file, cx);
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
+ });
+}
+
#[gpui::test]
async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let executor = cx_a.executor();
@@ -451,7 +451,9 @@ impl AllLanguageSettings {
let editorconfig_properties = location.and_then(|location| {
cx.global::<SettingsStore>()
- .editorconfig_properties(location.worktree_id, location.path)
+ .editorconfig_store
+ .read(cx)
+ .properties(location.worktree_id, location.path)
});
if let Some(editorconfig_properties) = editorconfig_properties {
let mut settings = settings.clone();
@@ -20,8 +20,9 @@ use serde::{Deserialize, Serialize};
pub use settings::DirenvSettings;
pub use settings::LspSettings;
use settings::{
- DapSettingsContent, InvalidSettingsError, LocalSettingsKind, RegisterSetting, Settings,
- SettingsLocation, SettingsStore, parse_json_with_comments, watch_config_file,
+ DapSettingsContent, EditorconfigEvent, InvalidSettingsError, LocalSettingsKind,
+ LocalSettingsPath, RegisterSetting, Settings, SettingsLocation, SettingsStore,
+ parse_json_with_comments, watch_config_file,
};
use std::{cell::OnceCell, collections::BTreeMap, path::PathBuf, sync::Arc, time::Duration};
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
@@ -670,6 +671,7 @@ pub struct SettingsObserver {
HashMap<PathTrust, BTreeMap<(WorktreeId, Arc<RelPath>), Option<String>>>,
_trusted_worktrees_watcher: Option<Subscription>,
_user_settings_watcher: Option<Subscription>,
+ _editorconfig_watcher: Option<Subscription>,
_global_task_config_watcher: Task<()>,
_global_debug_config_watcher: Task<()>,
}
@@ -708,9 +710,11 @@ impl SettingsObserver {
for ((worktree_id, directory_path), settings_contents) in
pending_local_settings
{
+ let path =
+ LocalSettingsPath::InWorktree(directory_path.clone());
apply_local_settings(
worktree_id,
- &directory_path,
+ path.clone(),
LocalSettingsKind::Settings,
&settings_contents,
cx,
@@ -722,7 +726,7 @@ impl SettingsObserver {
.send(proto::UpdateWorktreeSettings {
project_id: settings_observer.project_id,
worktree_id: worktree_id.to_proto(),
- path: directory_path.to_proto(),
+ path: path.to_proto(),
content: settings_contents,
kind: Some(
local_settings_kind_to_proto(
@@ -730,6 +734,7 @@ impl SettingsObserver {
)
.into(),
),
+ outside_worktree: Some(false),
})
.log_err();
}
@@ -742,6 +747,36 @@ impl SettingsObserver {
)
});
+ let editorconfig_store = cx.global::<SettingsStore>().editorconfig_store.clone();
+ let _editorconfig_watcher = cx.subscribe(
+ &editorconfig_store,
+ |this, _, event: &EditorconfigEvent, cx| {
+ let EditorconfigEvent::ExternalConfigChanged {
+ path,
+ content,
+ affected_worktree_ids,
+ } = event;
+ for worktree_id in affected_worktree_ids {
+ if let Some(worktree) = this
+ .worktree_store
+ .read(cx)
+ .worktree_for_id(*worktree_id, cx)
+ {
+ this.update_settings(
+ worktree,
+ [(
+ path.clone(),
+ LocalSettingsKind::Editorconfig,
+ content.clone(),
+ )],
+ false,
+ cx,
+ );
+ }
+ }
+ },
+ );
+
Self {
worktree_store,
task_store,
@@ -750,6 +785,7 @@ impl SettingsObserver {
_trusted_worktrees_watcher,
pending_local_settings: HashMap::default(),
_user_settings_watcher: None,
+ _editorconfig_watcher: Some(_editorconfig_watcher),
project_id: REMOTE_SERVER_PROJECT_ID,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
fs.clone(),
@@ -805,6 +841,7 @@ impl SettingsObserver {
_trusted_worktrees_watcher: None,
pending_local_settings: HashMap::default(),
_user_settings_watcher: user_settings_watcher,
+ _editorconfig_watcher: None,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
fs.clone(),
paths::tasks_file().clone(),
@@ -841,19 +878,25 @@ impl SettingsObserver {
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
),
+ outside_worktree: Some(false),
})
.log_err();
}
- for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
+ for (path, content, _) in store
+ .editorconfig_store
+ .read(cx)
+ .local_editorconfig_settings(worktree.read(cx).id())
+ {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_proto(),
- content: Some(content),
+ content: Some(content.to_owned()),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
),
+ outside_worktree: Some(path.is_outside_worktree()),
})
.log_err();
}
@@ -874,7 +917,12 @@ impl SettingsObserver {
.with_context(|| format!("unknown kind {kind}"))?,
None => proto::LocalSettingsKind::Settings,
};
- let path = RelPath::from_proto(&envelope.payload.path)?;
+
+ let path = LocalSettingsPath::from_proto(
+ &envelope.payload.path,
+ envelope.payload.outside_worktree.unwrap_or(false),
+ )?;
+
this.update(&mut cx, |this, cx| {
let is_via_collab = match &this.mode {
SettingsObserverMode::Local(..) => false,
@@ -1012,6 +1060,23 @@ impl SettingsObserver {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};
+ if matches!(change, PathChange::Loaded) || matches!(change, PathChange::Added) {
+ let worktree_id = worktree.read(cx).id();
+ let worktree_path = worktree.read(cx).abs_path();
+ let fs = fs.clone();
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store
+ .editorconfig_store
+ .update(cx, |editorconfig_store, cx| {
+ editorconfig_store.discover_local_external_configs_chain(
+ worktree_id,
+ worktree_path,
+ fs,
+ cx,
+ );
+ });
+ });
+ }
(settings_dir, LocalSettingsKind::Editorconfig)
} else {
continue;
@@ -1088,7 +1153,11 @@ impl SettingsObserver {
this.update_settings(
worktree,
settings_contents.into_iter().map(|(path, kind, content)| {
- (path, kind, content.and_then(|c| c.log_err()))
+ (
+ LocalSettingsPath::InWorktree(path),
+ kind,
+ content.and_then(|c| c.log_err()),
+ )
}),
false,
cx,
@@ -1102,7 +1171,9 @@ impl SettingsObserver {
fn update_settings(
&mut self,
worktree: Entity<Worktree>,
- settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
+ settings_contents: impl IntoIterator<
+ Item = (LocalSettingsPath, LocalSettingsKind, Option<String>),
+ >,
is_via_collab: bool,
cx: &mut Context<Self>,
) {
@@ -1114,10 +1185,10 @@ impl SettingsObserver {
} else {
OnceCell::new()
};
- for (directory, kind, file_content) in settings_contents {
+ for (directory_path, kind, file_content) in settings_contents {
let mut applied = true;
- match kind {
- LocalSettingsKind::Settings => {
+ match (&directory_path, kind) {
+ (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Settings) => {
if *can_trust_worktree.get_or_init(|| {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
@@ -1127,7 +1198,13 @@ impl SettingsObserver {
true
}
}) {
- apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
+ apply_local_settings(
+ worktree_id,
+ LocalSettingsPath::InWorktree(directory.clone()),
+ kind,
+ &file_content,
+ cx,
+ )
} else {
applied = false;
self.pending_local_settings
@@ -1136,10 +1213,7 @@ impl SettingsObserver {
.insert((worktree_id, directory.clone()), file_content.clone());
}
}
- LocalSettingsKind::Editorconfig => {
- apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
- }
- LocalSettingsKind::Tasks => {
+ (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Tasks) => {
let result = task_store.update(cx, |task_store, cx| {
task_store.update_user_tasks(
TaskSettingsLocation::Worktree(SettingsLocation {
@@ -1168,7 +1242,7 @@ impl SettingsObserver {
}
}
}
- LocalSettingsKind::Debug => {
+ (LocalSettingsPath::InWorktree(directory), LocalSettingsKind::Debug) => {
let result = task_store.update(cx, |task_store, cx| {
task_store.update_user_debug_scenarios(
TaskSettingsLocation::Worktree(SettingsLocation {
@@ -1199,6 +1273,17 @@ impl SettingsObserver {
}
}
}
+ (directory, LocalSettingsKind::Editorconfig) => {
+ apply_local_settings(worktree_id, directory.clone(), kind, &file_content, cx);
+ }
+ (LocalSettingsPath::OutsideWorktree(path), kind) => {
+ log::error!(
+ "OutsideWorktree path {:?} with kind {:?} is only supported by editorconfig",
+ path,
+ kind
+ );
+ continue;
+ }
};
if applied {
@@ -1207,9 +1292,10 @@ impl SettingsObserver {
.send(proto::UpdateWorktreeSettings {
project_id: self.project_id,
worktree_id: remote_worktree_id.to_proto(),
- path: directory.to_proto(),
+ path: directory_path.to_proto(),
content: file_content.clone(),
kind: Some(local_settings_kind_to_proto(kind).into()),
+ outside_worktree: Some(directory_path.is_outside_worktree()),
})
.log_err();
}
@@ -1323,19 +1409,14 @@ impl SettingsObserver {
fn apply_local_settings(
worktree_id: WorktreeId,
- directory: &Arc<RelPath>,
+ path: LocalSettingsPath,
kind: LocalSettingsKind,
file_content: &Option<String>,
cx: &mut Context<'_, SettingsObserver>,
) {
cx.update_global::<SettingsStore, _>(|store, cx| {
- let result = store.set_local_settings(
- worktree_id,
- directory.clone(),
- kind,
- file_content.as_deref(),
- cx,
- );
+ let result =
+ store.set_local_settings(worktree_id, path.clone(), kind, file_content.as_deref(), cx);
match result {
Err(InvalidSettingsError::LocalSettings { path, message }) => {
@@ -1345,9 +1426,17 @@ fn apply_local_settings(
)));
}
Err(e) => log::error!("Failed to set local settings: {e}"),
- Ok(()) => cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
- .as_std_path()
- .join(local_settings_file_relative_path().as_std_path())))),
+ Ok(()) => {
+ let settings_path = match &path {
+ LocalSettingsPath::InWorktree(rel_path) => rel_path
+ .as_std_path()
+ .join(local_settings_file_relative_path().as_std_path()),
+ LocalSettingsPath::OutsideWorktree(abs_path) => abs_path.to_path_buf(),
+ };
+ cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
+ settings_path,
+ )))
+ }
}
})
}
@@ -27,7 +27,7 @@ use language::{
ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList,
ToolchainLister,
language_settings::{LanguageSettingsContent, language_settings},
- rust_lang, tree_sitter_typescript,
+ markdown_lang, rust_lang, tree_sitter_typescript,
};
use lsp::{
DiagnosticSeverity, DocumentChanges, FileOperationFilter, NumberOrString, TextDocumentEdit,
@@ -244,6 +244,579 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
});
}
+#[gpui::test]
+async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/grandparent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 4\n",
+ "parent": {
+ ".editorconfig": "[*.rs]\nindent_size = 2\n",
+ "worktree": {
+ ".editorconfig": "[*.md]\nindent_size = 3\n",
+ "main.rs": "fn main() {}",
+ "README.md": "# README",
+ "other.txt": "other content",
+ }
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/grandparent/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ language_registry.add(markdown_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(rel_path(path)).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(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_rs = settings_for("main.rs");
+ let settings_md = settings_for("README.md");
+ let settings_txt = settings_for("other.txt");
+
+ // main.rs gets indent_size = 2 from parent's external .editorconfig
+ assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2));
+
+ // README.md gets indent_size = 3 from internal worktree .editorconfig
+ assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3));
+
+ // other.txt gets indent_size = 4 from grandparent's external .editorconfig
+ assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4));
+ });
+}
+
+#[gpui::test]
+async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 99\n",
+ "worktree": {
+ ".editorconfig": "root = true\n[*]\nindent_size = 2\n",
+ "file.rs": "fn main() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ 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 file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // file.rs gets indent_size = 2 from worktree's root config, NOT 99 from parent
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(2));
+ });
+}
+
+#[gpui::test]
+async fn test_external_editorconfig_root_in_parent_stops_traversal(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/grandparent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 99\n",
+ "parent": {
+ ".editorconfig": "root = true\n[*]\nindent_size = 4\n",
+ "worktree": {
+ "file.rs": "fn main() {}",
+ }
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/grandparent/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ 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 file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // file.rs gets indent_size = 4 from parent's root config, NOT 99 from grandparent
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(4));
+ });
+}
+
+#[gpui::test]
+async fn test_external_editorconfig_shared_across_worktrees(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "root = true\n[*]\nindent_size = 5\n",
+ "worktree_a": {
+ "file.rs": "fn a() {}",
+ ".editorconfig": "[*]\ninsert_final_newline = true\n",
+ },
+ "worktree_b": {
+ "file.rs": "fn b() {}",
+ ".editorconfig": "[*]\ninsert_final_newline = false\n",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(
+ fs,
+ [
+ path!("/parent/worktree_a").as_ref(),
+ path!("/parent/worktree_b").as_ref(),
+ ],
+ cx,
+ )
+ .await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let worktrees: Vec<_> = project.read(cx).worktrees(cx).collect();
+ assert_eq!(worktrees.len(), 2);
+
+ for worktree in worktrees {
+ let tree = worktree.read(cx);
+ let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings =
+ language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Both worktrees should get indent_size = 5 from shared parent .editorconfig
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
+ }
+ });
+}
+
+#[gpui::test]
+async fn test_external_editorconfig_not_loaded_without_internal_config(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 99\n",
+ "worktree": {
+ "file.rs": "fn main() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ 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 file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // file.rs should have default tab_size = 4, NOT 99 from parent's external .editorconfig
+ // because without an internal .editorconfig, external configs are not loaded
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(4));
+ });
+}
+
+#[gpui::test]
+async fn test_external_editorconfig_modification_triggers_refresh(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 4\n",
+ "worktree": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn main() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ 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 file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Test initial settings: tab_size = 4 from parent's external .editorconfig
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(4));
+ });
+
+ fs.atomic_write(
+ PathBuf::from(path!("/parent/.editorconfig")),
+ "[*]\nindent_size = 8\n".to_owned(),
+ )
+ .await
+ .unwrap();
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let tree = worktree.read(cx);
+ let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Test settings updated: tab_size = 8
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(8));
+ });
+}
+
+#[gpui::test]
+async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "root = true\n[*]\nindent_size = 7\n",
+ "existing_worktree": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn a() {}",
+ },
+ "new_worktree": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn b() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/parent/existing_worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let worktree = project.read(cx).worktrees(cx).next().unwrap();
+ let tree = worktree.read(cx);
+ let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Test existing worktree has tab_size = 7
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(7));
+ });
+
+ let (new_worktree, _) = project
+ .update(cx, |project, cx| {
+ project.find_or_create_worktree(path!("/parent/new_worktree"), true, cx)
+ })
+ .await
+ .unwrap();
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let tree = new_worktree.read(cx);
+ let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, new_worktree.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Verify new worktree also has tab_size = 7 from shared parent editorconfig
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(7));
+ });
+}
+
+#[gpui::test]
+async fn test_removing_worktree_cleans_up_external_editorconfig(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "[*]\nindent_size = 6\n",
+ "worktree": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn main() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/parent/worktree").as_ref()], cx).await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+
+ let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let store = cx.global::<SettingsStore>();
+ let (worktree_ids, external_paths, watcher_paths) =
+ store.editorconfig_store.read(cx).test_state();
+
+ // Test external config is loaded
+ assert!(worktree_ids.contains(&worktree_id));
+ assert!(!external_paths.is_empty());
+ assert!(!watcher_paths.is_empty());
+ });
+
+ project.update(cx, |project, cx| {
+ project.remove_worktree(worktree_id, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let store = cx.global::<SettingsStore>();
+ let (worktree_ids, external_paths, watcher_paths) =
+ store.editorconfig_store.read(cx).test_state();
+
+ // Test worktree state, external configs, and watchers all removed
+ assert!(!worktree_ids.contains(&worktree_id));
+ assert!(external_paths.is_empty());
+ assert!(watcher_paths.is_empty());
+ });
+}
+
+#[gpui::test]
+async fn test_shared_external_editorconfig_cleanup_with_multiple_worktrees(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/parent"),
+ json!({
+ ".editorconfig": "root = true\n[*]\nindent_size = 5\n",
+ "worktree_a": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn a() {}",
+ },
+ "worktree_b": {
+ ".editorconfig": "[*]\n",
+ "file.rs": "fn b() {}",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(
+ fs,
+ [
+ path!("/parent/worktree_a").as_ref(),
+ path!("/parent/worktree_b").as_ref(),
+ ],
+ cx,
+ )
+ .await;
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+
+ cx.executor().run_until_parked();
+
+ let (worktree_a_id, worktree_b, worktree_b_id) = cx.update(|cx| {
+ let worktrees: Vec<_> = project.read(cx).worktrees(cx).collect();
+ assert_eq!(worktrees.len(), 2);
+
+ let worktree_a = &worktrees[0];
+ let worktree_b = &worktrees[1];
+ let worktree_a_id = worktree_a.read(cx).id();
+ let worktree_b_id = worktree_b.read(cx).id();
+ (worktree_a_id, worktree_b.clone(), worktree_b_id)
+ });
+
+ cx.update(|cx| {
+ let store = cx.global::<SettingsStore>();
+ let (worktree_ids, external_paths, _) = store.editorconfig_store.read(cx).test_state();
+
+ // Test both worktrees have settings and share external config
+ assert!(worktree_ids.contains(&worktree_a_id));
+ assert!(worktree_ids.contains(&worktree_b_id));
+ assert_eq!(external_paths.len(), 1); // single shared external config
+ });
+
+ project.update(cx, |project, cx| {
+ project.remove_worktree(worktree_a_id, cx);
+ });
+
+ cx.executor().run_until_parked();
+
+ cx.update(|cx| {
+ let store = cx.global::<SettingsStore>();
+ let (worktree_ids, external_paths, watcher_paths) =
+ store.editorconfig_store.read(cx).test_state();
+
+ // Test worktree_a is gone but external config remains for worktree_b
+ assert!(!worktree_ids.contains(&worktree_a_id));
+ assert!(worktree_ids.contains(&worktree_b_id));
+ // External config should still exist because worktree_b uses it
+ assert_eq!(external_paths.len(), 1);
+ assert_eq!(watcher_paths.len(), 1);
+ });
+
+ cx.update(|cx| {
+ let tree = worktree_b.read(cx);
+ let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
+ let file = File::for_entry(file_entry, worktree_b.clone());
+ let file_language = project
+ .read(cx)
+ .languages()
+ .load_language_for_file_path(file.path.as_std_path());
+ let file_language = cx
+ .foreground_executor()
+ .block_on(file_language)
+ .expect("Failed to get file language");
+ let file = file as _;
+ let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
+
+ // Test worktree_b still has correct settings
+ assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
+ });
+}
+
#[gpui::test]
async fn test_git_provider_project_setting(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -146,6 +146,7 @@ message UpdateWorktreeSettings {
string path = 3;
optional string content = 4;
optional LocalSettingsKind kind = 5;
+ optional bool outside_worktree = 6;
}
enum LocalSettingsKind {
@@ -0,0 +1,385 @@
+use anyhow::{Context as _, Result};
+use collections::{BTreeMap, BTreeSet, HashSet};
+use ec4rs::{ConfigParser, PropertiesSource, Section};
+use fs::Fs;
+use futures::StreamExt;
+use gpui::{Context, EventEmitter, Task};
+use paths::EDITORCONFIG_NAME;
+use smallvec::SmallVec;
+use std::{path::Path, str::FromStr, sync::Arc};
+use util::{ResultExt as _, rel_path::RelPath};
+
+use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file};
+
+pub type EditorconfigProperties = ec4rs::Properties;
+
+#[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(Clone, Debug)]
+pub enum EditorconfigEvent {
+ ExternalConfigChanged {
+ path: LocalSettingsPath,
+ content: Option<String>,
+ affected_worktree_ids: Vec<WorktreeId>,
+ },
+}
+
+impl EventEmitter<EditorconfigEvent> for EditorconfigStore {}
+
+#[derive(Default)]
+pub struct EditorconfigStore {
+ external_configs: BTreeMap<Arc<Path>, (String, Option<Editorconfig>)>,
+ worktree_state: BTreeMap<WorktreeId, EditorconfigWorktreeState>,
+ local_external_config_watchers: BTreeMap<Arc<Path>, Task<()>>,
+ local_external_config_discovery_tasks: BTreeMap<WorktreeId, Task<()>>,
+}
+
+#[derive(Default)]
+struct EditorconfigWorktreeState {
+ internal_configs: BTreeMap<Arc<RelPath>, (String, Option<Editorconfig>)>,
+ external_config_paths: BTreeSet<Arc<Path>>,
+}
+
+impl EditorconfigStore {
+ pub(crate) fn set_configs(
+ &mut self,
+ worktree_id: WorktreeId,
+ path: LocalSettingsPath,
+ content: Option<&str>,
+ ) -> std::result::Result<(), InvalidSettingsError> {
+ match (&path, content) {
+ (LocalSettingsPath::InWorktree(rel_path), None) => {
+ if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
+ state.internal_configs.remove(rel_path);
+ }
+ }
+ (LocalSettingsPath::OutsideWorktree(abs_path), None) => {
+ if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
+ state.external_config_paths.remove(abs_path);
+ }
+ let still_in_use = self
+ .worktree_state
+ .values()
+ .any(|state| state.external_config_paths.contains(abs_path));
+ if !still_in_use {
+ self.external_configs.remove(abs_path);
+ self.local_external_config_watchers.remove(abs_path);
+ }
+ }
+ (LocalSettingsPath::InWorktree(rel_path), Some(content)) => {
+ let state = self.worktree_state.entry(worktree_id).or_default();
+ let should_update = state
+ .internal_configs
+ .get(rel_path)
+ .map_or(true, |entry| entry.0 != content);
+ if should_update {
+ let parsed = match content.parse::<Editorconfig>() {
+ Ok(parsed) => Some(parsed),
+ Err(e) => {
+ state
+ .internal_configs
+ .insert(rel_path.clone(), (content.to_owned(), None));
+ return Err(InvalidSettingsError::Editorconfig {
+ message: e.to_string(),
+ path: LocalSettingsPath::InWorktree(
+ rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
+ ),
+ });
+ }
+ };
+ state
+ .internal_configs
+ .insert(rel_path.clone(), (content.to_owned(), parsed));
+ }
+ }
+ (LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => {
+ let state = self.worktree_state.entry(worktree_id).or_default();
+ state.external_config_paths.insert(abs_path.clone());
+ let should_update = self
+ .external_configs
+ .get(abs_path)
+ .map_or(true, |entry| entry.0 != content);
+ if should_update {
+ let parsed = match content.parse::<Editorconfig>() {
+ Ok(parsed) => Some(parsed),
+ Err(e) => {
+ self.external_configs
+ .insert(abs_path.clone(), (content.to_owned(), None));
+ return Err(InvalidSettingsError::Editorconfig {
+ message: e.to_string(),
+ path: LocalSettingsPath::OutsideWorktree(
+ abs_path.join(EDITORCONFIG_NAME).into(),
+ ),
+ });
+ }
+ };
+ self.external_configs
+ .insert(abs_path.clone(), (content.to_owned(), parsed));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) {
+ self.local_external_config_discovery_tasks.remove(&root_id);
+ let Some(removed) = self.worktree_state.remove(&root_id) else {
+ return;
+ };
+ let paths_in_use: HashSet<_> = self
+ .worktree_state
+ .values()
+ .flat_map(|w| w.external_config_paths.iter())
+ .collect();
+ for path in removed.external_config_paths.iter() {
+ if !paths_in_use.contains(path) {
+ self.external_configs.remove(path);
+ self.local_external_config_watchers.remove(path);
+ }
+ }
+ }
+
+ fn internal_configs(
+ &self,
+ root_id: WorktreeId,
+ ) -> impl '_ + Iterator<Item = (&RelPath, &str, Option<&Editorconfig>)> {
+ self.worktree_state
+ .get(&root_id)
+ .into_iter()
+ .flat_map(|state| {
+ state
+ .internal_configs
+ .iter()
+ .map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref()))
+ })
+ }
+
+ fn external_configs(
+ &self,
+ worktree_id: WorktreeId,
+ ) -> impl '_ + Iterator<Item = (&Path, &str, Option<&Editorconfig>)> {
+ self.worktree_state
+ .get(&worktree_id)
+ .into_iter()
+ .flat_map(|state| {
+ state.external_config_paths.iter().filter_map(|path| {
+ self.external_configs
+ .get(path)
+ .map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref()))
+ })
+ })
+ }
+
+ pub fn local_editorconfig_settings(
+ &self,
+ worktree_id: WorktreeId,
+ ) -> impl '_ + Iterator<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {
+ let external = self
+ .external_configs(worktree_id)
+ .map(|(path, content, parsed)| {
+ (
+ LocalSettingsPath::OutsideWorktree(path.into()),
+ content,
+ parsed,
+ )
+ });
+ let internal = self
+ .internal_configs(worktree_id)
+ .map(|(path, content, parsed)| {
+ (LocalSettingsPath::InWorktree(path.into()), content, parsed)
+ });
+ external.chain(internal)
+ }
+
+ pub fn discover_local_external_configs_chain(
+ &mut self,
+ worktree_id: WorktreeId,
+ worktree_path: Arc<Path>,
+ fs: Arc<dyn Fs>,
+ cx: &mut Context<Self>,
+ ) {
+ // We should only have one discovery task per worktree.
+ if self
+ .local_external_config_discovery_tasks
+ .contains_key(&worktree_id)
+ {
+ return;
+ }
+
+ let task = cx.spawn({
+ let fs = fs.clone();
+ async move |this, cx| {
+ let discovered_paths = {
+ let mut paths = Vec::new();
+ let mut current = worktree_path.parent().map(|p| p.to_path_buf());
+ while let Some(dir) = current {
+ let dir_path: Arc<Path> = Arc::from(dir.as_path());
+ let path = dir.join(EDITORCONFIG_NAME);
+ if fs.load(&path).await.is_ok() {
+ paths.push(dir_path);
+ }
+ current = dir.parent().map(|p| p.to_path_buf());
+ }
+ paths
+ };
+
+ this.update(cx, |this, cx| {
+ for dir_path in discovered_paths {
+ // We insert it here so that watchers can send events to appropriate worktrees.
+ // external_config_paths gets populated again in set_configs.
+ this.worktree_state
+ .entry(worktree_id)
+ .or_default()
+ .external_config_paths
+ .insert(dir_path.clone());
+ match this.local_external_config_watchers.entry(dir_path.clone()) {
+ std::collections::btree_map::Entry::Occupied(_) => {
+ if let Some(existing_config) = this.external_configs.get(&dir_path)
+ {
+ cx.emit(EditorconfigEvent::ExternalConfigChanged {
+ path: LocalSettingsPath::OutsideWorktree(dir_path),
+ content: Some(existing_config.0.clone()),
+ affected_worktree_ids: vec![worktree_id],
+ });
+ } else {
+ log::error!("Watcher exists for {dir_path:?} but no config found in external_configs");
+ }
+ }
+ std::collections::btree_map::Entry::Vacant(entry) => {
+ let watcher =
+ Self::watch_local_external_config(fs.clone(), dir_path, cx);
+ entry.insert(watcher);
+ }
+ }
+ }
+ })
+ .ok();
+ }
+ });
+
+ self.local_external_config_discovery_tasks
+ .insert(worktree_id, task);
+ }
+
+ fn watch_local_external_config(
+ fs: Arc<dyn Fs>,
+ dir_path: Arc<Path>,
+ cx: &mut Context<Self>,
+ ) -> Task<()> {
+ let config_path = dir_path.join(EDITORCONFIG_NAME);
+ let mut config_rx = watch_config_file(cx.background_executor(), fs, config_path);
+
+ cx.spawn(async move |this, cx| {
+ while let Some(content) = config_rx.next().await {
+ let content = Some(content).filter(|c| !c.is_empty());
+ let dir_path = dir_path.clone();
+ this.update(cx, |this, cx| {
+ let affected_worktree_ids: Vec<WorktreeId> = this
+ .worktree_state
+ .iter()
+ .filter_map(|(id, state)| {
+ state
+ .external_config_paths
+ .contains(&dir_path)
+ .then_some(*id)
+ })
+ .collect();
+
+ cx.emit(EditorconfigEvent::ExternalConfigChanged {
+ path: LocalSettingsPath::OutsideWorktree(dir_path),
+ content,
+ affected_worktree_ids,
+ });
+ })
+ .ok();
+ }
+ })
+ }
+
+ pub fn properties(
+ &self,
+ for_worktree: WorktreeId,
+ for_path: &RelPath,
+ ) -> Option<EditorconfigProperties> {
+ let mut properties = EditorconfigProperties::new();
+ let state = self.worktree_state.get(&for_worktree);
+ let empty_path: Arc<RelPath> = RelPath::empty().into();
+ let internal_root_config_is_root = state
+ .and_then(|state| state.internal_configs.get(&empty_path))
+ .and_then(|data| data.1.as_ref())
+ .is_some_and(|ec| ec.is_root);
+
+ if !internal_root_config_is_root {
+ for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
+ if let Some(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.as_std_path())
+ .log_err()?;
+ }
+ }
+ }
+ }
+
+ for (directory_with_config, _, parsed_editorconfig) in self.internal_configs(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.as_std_path())
+ .log_err()?;
+ }
+ }
+
+ properties.use_fallbacks();
+ Some(properties)
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl EditorconfigStore {
+ pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
+ let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
+ let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
+ let watcher_paths: Vec<_> = self
+ .local_external_config_watchers
+ .keys()
+ .cloned()
+ .collect();
+ (worktree_ids, external_paths, watcher_paths)
+ }
+
+ pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
+ self.worktree_state
+ .get(&worktree_id)
+ .map(|state| state.external_config_paths.iter().cloned().collect())
+ .unwrap_or_default()
+ }
+}
@@ -1,6 +1,7 @@
mod base_keymap_setting;
mod content_into_gpui;
mod editable_setting_control;
+mod editorconfig_store;
mod keymap_file;
mod settings_file;
mod settings_store;
@@ -33,6 +34,9 @@ pub use ::settings_content::*;
pub use base_keymap_setting::*;
pub use content_into_gpui::IntoGpui;
pub use editable_setting_control::*;
+pub use editorconfig_store::{
+ Editorconfig, EditorconfigEvent, EditorconfigProperties, EditorconfigStore,
+};
pub use keymap_file::{
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
@@ -40,9 +44,9 @@ pub use keymap_file::{
pub use settings_file::*;
pub use settings_json::*;
pub use settings_store::{
- InvalidSettingsError, LSP_SETTINGS_SCHEMA_URL_PREFIX, LocalSettingsKind, MigrationStatus,
- Settings, SettingsFile, SettingsJsonSchemaParams, SettingsKey, SettingsLocation,
- SettingsParseResult, SettingsStore,
+ InvalidSettingsError, LSP_SETTINGS_SCHEMA_URL_PREFIX, LocalSettingsKind, LocalSettingsPath,
+ MigrationStatus, Settings, SettingsFile, SettingsJsonSchemaParams, SettingsKey,
+ SettingsLocation, SettingsParseResult, SettingsStore,
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
@@ -1,25 +1,26 @@
use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, btree_map, hash_map};
-use ec4rs::{ConfigParser, PropertiesSource, Section};
use fs::Fs;
use futures::{
FutureExt, StreamExt,
channel::{mpsc, oneshot},
future::LocalBoxFuture,
};
-use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal};
+use gpui::{
+ App, AppContext, AsyncApp, BorrowAppContext, Entity, Global, SharedString, Task, UpdateGlobal,
+};
-use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
+use paths::{local_settings_file_relative_path, task_file_name};
use schemars::{JsonSchema, json_schema};
use serde_json::Value;
-use smallvec::SmallVec;
+use settings_content::ParseStatus;
use std::{
any::{Any, TypeId, type_name},
fmt::Debug,
ops::Range,
- path::PathBuf,
+ path::{Path, PathBuf},
rc::Rc,
- str::{self, FromStr},
+ str,
sync::Arc,
};
use util::{
@@ -28,17 +29,17 @@ use util::{
schemars::{AllowTrailingCommas, DefaultDenyUnknownFields, replace_subschema},
};
-pub type EditorconfigProperties = ec4rs::Properties;
+use crate::editorconfig_store::EditorconfigStore;
-use crate::settings_content::{
- ExtensionsSettingsContent, FontFamilyName, IconThemeName, LanguageSettingsContent,
- LanguageToSettingsMap, LspSettings, LspSettingsMap, ProjectSettingsContent, SettingsContent,
- ThemeName, UserSettingsContent,
-};
use crate::{
- ActiveSettingsProfileName, ParseStatus, UserSettingsContentExt, VsCodeSettings, WorktreeId,
+ ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
+ LanguageToSettingsMap, LspSettings, LspSettingsMap, ThemeName, UserSettingsContentExt,
+ VsCodeSettings, WorktreeId,
+ settings_content::{
+ ExtensionsSettingsContent, ProjectSettingsContent, RootUserSettings, SettingsContent,
+ UserSettingsContent, merge_from::MergeFrom,
+ },
};
-use settings_content::{RootUserSettings, merge_from::MergeFrom};
use settings_json::{infer_json_indent_size, update_value_in_json_text};
@@ -153,7 +154,7 @@ pub struct SettingsStore {
merged_settings: Rc<SettingsContent>,
local_settings: BTreeMap<(WorktreeId, Arc<RelPath>), SettingsContent>,
- raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<RelPath>), (String, Option<Editorconfig>)>,
+ pub editorconfig_store: Entity<EditorconfigStore>,
_setting_file_updates: Task<()>,
setting_file_updates_tx:
@@ -201,26 +202,6 @@ impl Ord for SettingsFile {
}
}
-#[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)]
pub enum LocalSettingsKind {
Settings,
@@ -229,6 +210,33 @@ pub enum LocalSettingsKind {
Debug,
}
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum LocalSettingsPath {
+ InWorktree(Arc<RelPath>),
+ OutsideWorktree(Arc<Path>),
+}
+
+impl LocalSettingsPath {
+ pub fn is_outside_worktree(&self) -> bool {
+ matches!(self, Self::OutsideWorktree(_))
+ }
+
+ pub fn to_proto(&self) -> String {
+ match self {
+ Self::InWorktree(path) => path.to_proto(),
+ Self::OutsideWorktree(path) => path.to_string_lossy().to_string(),
+ }
+ }
+
+ pub fn from_proto(path: &str, is_outside_worktree: bool) -> anyhow::Result<Self> {
+ if is_outside_worktree {
+ Ok(Self::OutsideWorktree(PathBuf::from(path).into()))
+ } else {
+ Ok(Self::InWorktree(RelPath::from_proto(path)?))
+ }
+ }
+}
+
impl Global for SettingsStore {}
#[doc(hidden)]
@@ -263,7 +271,7 @@ pub struct SettingsJsonSchemaParams<'a> {
}
impl SettingsStore {
- pub fn new(cx: &App, default_settings: &str) -> Self {
+ pub fn new(cx: &mut App, default_settings: &str) -> Self {
let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
let default_settings: Rc<SettingsContent> =
SettingsContent::parse_json_with_comments(default_settings)
@@ -279,7 +287,7 @@ impl SettingsStore {
merged_settings: default_settings,
local_settings: BTreeMap::default(),
- raw_editorconfig_settings: BTreeMap::default(),
+ editorconfig_store: cx.new(|_| EditorconfigStore::default()),
setting_file_updates_tx,
_setting_file_updates: cx.spawn(async move |cx| {
while let Some(setting_file_update) = setting_file_updates_rx.next().await {
@@ -835,19 +843,17 @@ impl SettingsStore {
pub fn set_local_settings(
&mut self,
root_id: WorktreeId,
- directory_path: Arc<RelPath>,
+ path: LocalSettingsPath,
kind: LocalSettingsKind,
settings_content: Option<&str>,
cx: &mut App,
) -> std::result::Result<(), InvalidSettingsError> {
+ let content = settings_content
+ .map(|content| content.trim())
+ .filter(|content| !content.is_empty());
let mut zed_settings_changed = false;
- match (
- kind,
- settings_content
- .map(|content| content.trim())
- .filter(|content| !content.is_empty()),
- ) {
- (LocalSettingsKind::Tasks, _) => {
+ match (path.clone(), kind, content) {
+ (LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Tasks, _) => {
return Err(InvalidSettingsError::Tasks {
message: "Attempted to submit tasks into the settings store".to_string(),
path: directory_path
@@ -856,7 +862,7 @@ impl SettingsStore {
.to_path_buf(),
});
}
- (LocalSettingsKind::Debug, _) => {
+ (LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Debug, _) => {
return Err(InvalidSettingsError::Debug {
message: "Attempted to submit debugger config into the settings store"
.to_string(),
@@ -866,19 +872,19 @@ impl SettingsStore {
.to_path_buf(),
});
}
- (LocalSettingsKind::Settings, None) => {
+ (LocalSettingsPath::InWorktree(directory_path), LocalSettingsKind::Settings, None) => {
zed_settings_changed = self
.local_settings
.remove(&(root_id, directory_path.clone()))
.is_some();
self.file_errors
- .remove(&SettingsFile::Project((root_id, directory_path.clone())));
- }
- (LocalSettingsKind::Editorconfig, None) => {
- self.raw_editorconfig_settings
- .remove(&(root_id, directory_path.clone()));
+ .remove(&SettingsFile::Project((root_id, directory_path)));
}
- (LocalSettingsKind::Settings, Some(settings_contents)) => {
+ (
+ LocalSettingsPath::InWorktree(directory_path),
+ LocalSettingsKind::Settings,
+ Some(settings_contents),
+ ) => {
let (new_settings, parse_result) = self
.parse_and_migrate_zed_settings::<ProjectSettingsContent>(
settings_contents,
@@ -892,7 +898,7 @@ impl SettingsStore {
}),
}?;
if let Some(new_settings) = new_settings {
- match self.local_settings.entry((root_id, directory_path.clone())) {
+ match self.local_settings.entry((root_id, directory_path)) {
btree_map::Entry::Vacant(v) => {
v.insert(SettingsContent {
project: new_settings,
@@ -912,50 +918,24 @@ impl SettingsStore {
}
}
}
- (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(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
- });
- }
- },
- 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(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
- });
- }
- }
- }
- }
- }
+ (directory_path, LocalSettingsKind::Editorconfig, editorconfig_contents) => {
+ self.editorconfig_store.update(cx, |store, _| {
+ store.set_configs(root_id, directory_path, editorconfig_contents)
+ })?;
+ }
+ (LocalSettingsPath::OutsideWorktree(path), kind, _) => {
+ log::error!(
+ "OutsideWorktree path {:?} with kind {:?} is only supported by editorconfig",
+ path,
+ kind
+ );
+ return Ok(());
+ }
+ }
+ if let LocalSettingsPath::InWorktree(directory_path) = &path {
+ if zed_settings_changed {
+ self.recompute_values(Some((root_id, &directory_path)), cx);
}
- };
-
- if zed_settings_changed {
- self.recompute_values(Some((root_id, &directory_path)), cx);
}
Ok(())
}
@@ -980,8 +960,10 @@ impl SettingsStore {
pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
self.local_settings
.retain(|(worktree_id, _), _| worktree_id != &root_id);
- self.raw_editorconfig_settings
- .retain(|(worktree_id, _), _| worktree_id != &root_id);
+
+ self.editorconfig_store
+ .update(cx, |store, _cx| store.remove_for_worktree(root_id));
+
for setting_value in self.setting_values.values_mut() {
setting_value.clear_local_values(root_id);
}
@@ -1004,23 +986,6 @@ impl SettingsStore {
.map(|((_, path), content)| (path.clone(), &content.project))
}
- pub fn local_editorconfig_settings(
- &self,
- root_id: WorktreeId,
- ) -> impl '_ + Iterator<Item = (Arc<RelPath>, String, Option<Editorconfig>)> {
- self.raw_editorconfig_settings
- .range(
- (root_id, RelPath::empty().into())
- ..(
- WorktreeId::from_usize(root_id.to_usize() + 1),
- RelPath::empty().into(),
- ),
- )
- .map(|((_, path), (content, parsed_content))| {
- (path.clone(), content.clone(), parsed_content.clone())
- })
- }
-
pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value {
let mut generator = schemars::generate::SchemaSettings::draft2019_09()
.with_transform(DefaultDenyUnknownFields)
@@ -1181,35 +1146,6 @@ impl SettingsStore {
}
}
}
-
- pub fn editorconfig_properties(
- &self,
- for_worktree: WorktreeId,
- for_path: &RelPath,
- ) -> 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.as_std_path())
- .log_err()?;
- }
- }
-
- properties.use_fallbacks();
- Some(properties)
- }
}
/// The result of parsing settings, including any migration attempts
@@ -1296,13 +1232,31 @@ impl SettingsParseResult {
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidSettingsError {
- LocalSettings { path: Arc<RelPath>, message: String },
- UserSettings { message: String },
- ServerSettings { message: String },
- DefaultSettings { message: String },
- Editorconfig { path: Arc<RelPath>, message: String },
- Tasks { path: PathBuf, message: String },
- Debug { path: PathBuf, message: String },
+ LocalSettings {
+ path: Arc<RelPath>,
+ message: String,
+ },
+ UserSettings {
+ message: String,
+ },
+ ServerSettings {
+ message: String,
+ },
+ DefaultSettings {
+ message: String,
+ },
+ Editorconfig {
+ path: LocalSettingsPath,
+ message: String,
+ },
+ Tasks {
+ path: PathBuf,
+ message: String,
+ },
+ Debug {
+ path: PathBuf,
+ message: String,
+ },
}
impl std::fmt::Display for InvalidSettingsError {
@@ -1505,7 +1459,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- rel_path("root1").into(),
+ LocalSettingsPath::InWorktree(rel_path("root1").into()),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 5 }"#),
cx,
@@ -1514,7 +1468,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- rel_path("root1/subdir").into(),
+ LocalSettingsPath::InWorktree(rel_path("root1/subdir").into()),
LocalSettingsKind::Settings,
Some(r#"{ "preferred_line_length": 50 }"#),
cx,
@@ -1524,7 +1478,7 @@ mod tests {
store
.set_local_settings(
WorktreeId::from_usize(1),
- rel_path("root2").into(),
+ LocalSettingsPath::InWorktree(rel_path("root2").into()),
LocalSettingsKind::Settings,
Some(r#"{ "tab_size": 9, "auto_update": true}"#),
cx,
@@ -1995,7 +1949,7 @@ mod tests {
store
.set_local_settings(
local.0,
- local.1.clone(),
+ LocalSettingsPath::InWorktree(local.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2029,7 +1983,7 @@ mod tests {
store
.set_local_settings(
local.0,
- local.1.clone(),
+ LocalSettingsPath::InWorktree(local.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 80}"#),
cx,
@@ -2086,7 +2040,7 @@ mod tests {
store
.set_local_settings(
local_1.0,
- local_1.1.clone(),
+ LocalSettingsPath::InWorktree(local_1.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 1}"#),
cx,
@@ -2095,7 +2049,7 @@ mod tests {
store
.set_local_settings(
local_1_child.0,
- local_1_child.1.clone(),
+ LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2104,7 +2058,7 @@ mod tests {
store
.set_local_settings(
local_2.0,
- local_2.1.clone(),
+ LocalSettingsPath::InWorktree(local_2.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 2}"#),
cx,
@@ -2113,7 +2067,7 @@ mod tests {
store
.set_local_settings(
local_2_child.0,
- local_2_child.1.clone(),
+ LocalSettingsPath::InWorktree(local_2_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2135,7 +2089,7 @@ mod tests {
store
.set_local_settings(
local_1_adjacent_child.0,
- local_1_adjacent_child.1.clone(),
+ LocalSettingsPath::InWorktree(local_1_adjacent_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2144,7 +2098,7 @@ mod tests {
store
.set_local_settings(
local_1_child.0,
- local_1_child.1.clone(),
+ LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 3}"#),
cx,
@@ -2158,7 +2112,7 @@ mod tests {
store
.set_local_settings(
local_1_adjacent_child.0,
- local_1_adjacent_child.1,
+ LocalSettingsPath::InWorktree(local_1_adjacent_child.1),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 3}"#),
cx,
@@ -2167,7 +2121,7 @@ mod tests {
store
.set_local_settings(
local_1_child.0,
- local_1_child.1.clone(),
+ LocalSettingsPath::InWorktree(local_1_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2202,7 +2156,7 @@ mod tests {
store
.set_local_settings(
wt0_root.0,
- wt0_root.1.clone(),
+ LocalSettingsPath::InWorktree(wt0_root.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 80}"#),
cx,
@@ -2211,7 +2165,7 @@ mod tests {
store
.set_local_settings(
wt0_child1.0,
- wt0_child1.1.clone(),
+ LocalSettingsPath::InWorktree(wt0_child1.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 120}"#),
cx,
@@ -2220,7 +2174,7 @@ mod tests {
store
.set_local_settings(
wt0_child2.0,
- wt0_child2.1.clone(),
+ LocalSettingsPath::InWorktree(wt0_child2.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2230,7 +2184,7 @@ mod tests {
store
.set_local_settings(
wt1_root.0,
- wt1_root.1.clone(),
+ LocalSettingsPath::InWorktree(wt1_root.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 90}"#),
cx,
@@ -2239,7 +2193,7 @@ mod tests {
store
.set_local_settings(
wt1_subdir.0,
- wt1_subdir.1.clone(),
+ LocalSettingsPath::InWorktree(wt1_subdir.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{}"#),
cx,
@@ -2290,7 +2244,7 @@ mod tests {
store
.set_local_settings(
wt0_deep_child.0,
- wt0_deep_child.1.clone(),
+ LocalSettingsPath::InWorktree(wt0_deep_child.1.clone()),
LocalSettingsKind::Settings,
Some(r#"{"preferred_line_length": 140}"#),
cx,