Support external .editorconfig (#46332)
Closes #41832 Extends https://github.com/zed-industries/zed/pull/19455 When an internal `.editorconfig` is detected in the worktree, we traverse parent directories up to the filesystem root looking for additional `.editorconfig` files. All discovered external configs are loaded and cached (shared when multiple worktrees reference the same parent directories). When computing settings for a file, external configs are applied first (from furthest to closest), then internal configs. For local projects, file watchers are set up for each external config so changes are applied immediately. When a project is shared via collab, external configs are sent to guests through the existing `UpdateWorktreeSettings` proto message (with a new `outside_worktree` field). SSH remoting works similarly. Limitations: We don't currently take creation of new external editor config files into account since they are loaded once on worktree add. Release Notes: - Added support for `.editorconfig` files outside the project directory. Zed now traverses parent directories to find and apply EditorConfig settings. Use `root = true` in any `.editorconfig` to stop inheriting settings from parent directories.
This commit is contained in:
1 parent
25264b2cc3
commit
2f7dbbdac7
15 files changed
+1334
-209
No files matched your search
@@ -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())));
|
||||
.remove(&SettingsFile::Project((root_id, directory_path)));
|
||||
}
|
||||
(LocalSettingsKind::Editorconfig, None) => {
|
||||
self.raw_editorconfig_settings
|
||||
.remove(&(root_id, directory_path.clone()));
|
||||
}
|
||||
(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,
|
||||
|
||||
Reference in new issue
Block a user