diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5c2c396160..5764aceea5 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -112,6 +112,7 @@ CREATE TABLE "worktree_settings_files" ( "worktree_id" INTEGER NOT NULL, "path" VARCHAR NOT NULL, "content" TEXT, + "kind" VARCHAR, PRIMARY KEY(project_id, worktree_id, path), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE ); diff --git a/crates/collab/migrations/20241002120231_add_local_settings_kind.sql b/crates/collab/migrations/20241002120231_add_local_settings_kind.sql new file mode 100644 index 0000000000..aec4ffb8f8 --- /dev/null +++ b/crates/collab/migrations/20241002120231_add_local_settings_kind.sql @@ -0,0 +1 @@ +ALTER TABLE "worktree_settings_files" ADD COLUMN "kind" VARCHAR; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5c30a85738..f717566824 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -35,6 +35,7 @@ use std::{ }; use time::PrimitiveDateTime; use tokio::sync::{Mutex, OwnedMutexGuard}; +use worktree_settings_file::LocalSettingsKind; #[cfg(test)] pub use tests::TestDb; @@ -766,6 +767,7 @@ pub struct Worktree { pub struct WorktreeSettingsFile { pub path: String, pub content: String, + pub kind: LocalSettingsKind, } pub struct NewExtensionVersion { @@ -783,3 +785,21 @@ pub struct ExtensionVersionConstraints { pub schema_versions: RangeInclusive, pub wasm_api_versions: RangeInclusive, } + +impl LocalSettingsKind { + pub fn from_proto(proto_kind: proto::LocalSettingsKind) -> Self { + match proto_kind { + proto::LocalSettingsKind::Settings => Self::Settings, + proto::LocalSettingsKind::Tasks => Self::Tasks, + proto::LocalSettingsKind::Editorconfig => Self::Editorconfig, + } + } + + pub fn to_proto(&self) -> proto::LocalSettingsKind { + match self { + Self::Settings => proto::LocalSettingsKind::Settings, + Self::Tasks => proto::LocalSettingsKind::Tasks, + Self::Editorconfig => proto::LocalSettingsKind::Editorconfig, + } + } +} diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 8091c66205..ceac78203d 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1,3 +1,4 @@ +use anyhow::Context as _; use util::ResultExt; use super::*; @@ -527,6 +528,12 @@ impl Database { connection: ConnectionId, ) -> Result>> { let project_id = ProjectId::from_proto(update.project_id); + let kind = match update.kind { + Some(kind) => proto::LocalSettingsKind::from_i32(kind) + .with_context(|| format!("unknown worktree settings kind: {kind}"))?, + None => proto::LocalSettingsKind::Settings, + }; + let kind = LocalSettingsKind::from_proto(kind); self.project_transaction(project_id, |tx| async move { // Ensure the update comes from the host. let project = project::Entity::find_by_id(project_id) @@ -543,6 +550,7 @@ impl Database { worktree_id: ActiveValue::Set(update.worktree_id as i64), path: ActiveValue::Set(update.path.clone()), content: ActiveValue::Set(content.clone()), + kind: ActiveValue::Set(kind), }) .on_conflict( OnConflict::columns([ @@ -800,6 +808,7 @@ impl Database { worktree.settings_files.push(WorktreeSettingsFile { path: db_settings_file.path, content: db_settings_file.content, + kind: db_settings_file.kind, }); } } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 635e2d232f..baba0f2cf9 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -735,6 +735,7 @@ impl Database { worktree.settings_files.push(WorktreeSettingsFile { path: db_settings_file.path, content: db_settings_file.content, + kind: db_settings_file.kind, }); } } diff --git a/crates/collab/src/db/tables/worktree_settings_file.rs b/crates/collab/src/db/tables/worktree_settings_file.rs index 92348c1ec9..71f7b73fc1 100644 --- a/crates/collab/src/db/tables/worktree_settings_file.rs +++ b/crates/collab/src/db/tables/worktree_settings_file.rs @@ -11,9 +11,25 @@ pub struct Model { #[sea_orm(primary_key)] pub path: String, pub content: String, + pub kind: LocalSettingsKind, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize, +)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] +#[serde(rename_all = "snake_case")] +pub enum LocalSettingsKind { + #[default] + #[sea_orm(string_value = "settings")] + Settings, + #[sea_orm(string_value = "tasks")] + Tasks, + #[sea_orm(string_value = "editorconfig")] + Editorconfig, +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index d9683fb8b3..5f21df4ab9 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1739,6 +1739,7 @@ fn notify_rejoined_projects( worktree_id: worktree.id, path: settings_file.path, content: Some(settings_file.content), + kind: Some(settings_file.kind.to_proto().into()), }, )?; } @@ -2220,6 +2221,7 @@ fn join_project_internal( worktree_id: worktree.id, path: settings_file.path, content: Some(settings_file.content), + kind: Some(proto::update_user_settings::Kind::Settings.into()), }, )?; } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 615ad52e2e..2859113634 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -33,7 +33,7 @@ use project::{ }; use rand::prelude::*; use serde_json::json; -use settings::SettingsStore; +use settings::{LocalSettingsKind, SettingsStore}; use std::{ cell::{Cell, RefCell}, env, future, mem, @@ -3327,8 +3327,16 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ( + Path::new("").into(), + LocalSettingsKind::Settings, + r#"{"tab_size":2}"#.to_string() + ), + ( + Path::new("a").into(), + LocalSettingsKind::Settings, + r#"{"tab_size":8}"#.to_string() + ), ] ) }); @@ -3346,8 +3354,16 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - (Path::new("").into(), r#"{}"#.to_string()), - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ( + Path::new("").into(), + LocalSettingsKind::Settings, + r#"{}"#.to_string() + ), + ( + Path::new("a").into(), + LocalSettingsKind::Settings, + r#"{"tab_size":8}"#.to_string() + ), ] ) }); @@ -3375,8 +3391,16 @@ async fn test_local_settings( .local_settings(worktree_b.read(cx).id()) .collect::>(), &[ - (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), - (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), + ( + Path::new("a").into(), + LocalSettingsKind::Settings, + r#"{"tab_size":8}"#.to_string() + ), + ( + Path::new("b").into(), + LocalSettingsKind::Settings, + r#"{"tab_size":4}"#.to_string() + ), ] ) }); @@ -3406,7 +3430,11 @@ async fn test_local_settings( store .local_settings(worktree_b.read(cx).id()) .collect::>(), - &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] + &[( + Path::new("a").into(), + LocalSettingsKind::Settings, + r#"{"hard_tabs":true}"#.to_string() + ),] ) }); } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index d794563672..87150587b3 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use collections::HashMap; use fs::Fs; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext}; @@ -6,7 +7,7 @@ use paths::local_settings_file_relative_path; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{InvalidSettingsError, Settings, SettingsSources, SettingsStore}; +use settings::{InvalidSettingsError, LocalSettingsKind, Settings, SettingsSources, SettingsStore}; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -266,13 +267,14 @@ impl SettingsObserver { let store = cx.global::(); for worktree in self.worktree_store.read(cx).worktrees() { let worktree_id = worktree.read(cx).id().to_proto(); - for (path, content) in store.local_settings(worktree.read(cx).id()) { + for (path, kind, content) in store.local_settings(worktree.read(cx).id()) { downstream_client .send(proto::UpdateWorktreeSettings { project_id, worktree_id, path: path.to_string_lossy().into(), content: Some(content), + kind: Some(local_settings_kind_to_proto(kind).into()), }) .log_err(); } @@ -288,6 +290,11 @@ impl SettingsObserver { envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> anyhow::Result<()> { + let kind = match envelope.payload.kind { + Some(kind) => proto::LocalSettingsKind::from_i32(kind) + .with_context(|| format!("unknown kind {kind}"))?, + None => proto::LocalSettingsKind::Settings, + }; this.update(&mut cx, |this, cx| { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let Some(worktree) = this @@ -297,10 +304,12 @@ impl SettingsObserver { else { return; }; + this.update_settings( worktree, [( PathBuf::from(&envelope.payload.path).into(), + local_settings_kind_from_proto(kind), envelope.payload.content, )], cx, @@ -327,6 +336,7 @@ impl SettingsObserver { ssh.send(proto::UpdateUserSettings { project_id: 0, content, + kind: Some(proto::LocalSettingsKind::Settings.into()), }) .log_err(); } @@ -342,6 +352,7 @@ impl SettingsObserver { ssh.send(proto::UpdateUserSettings { project_id: 0, content, + kind: Some(proto::LocalSettingsKind::Settings.into()), }) .log_err(); } @@ -397,6 +408,7 @@ impl SettingsObserver { settings_contents.push(async move { ( settings_dir, + LocalSettingsKind::Settings, if removed { None } else { @@ -413,15 +425,15 @@ impl SettingsObserver { let worktree = worktree.clone(); cx.spawn(move |this, cx| async move { - let settings_contents: Vec<(Arc, _)> = + let settings_contents: Vec<(Arc, _, _)> = futures::future::join_all(settings_contents).await; cx.update(|cx| { this.update(cx, |this, cx| { this.update_settings( worktree, - settings_contents - .into_iter() - .map(|(path, content)| (path, content.and_then(|c| c.log_err()))), + settings_contents.into_iter().map(|(path, kind, content)| { + (path, kind, content.and_then(|c| c.log_err())) + }), cx, ) }) @@ -433,17 +445,18 @@ impl SettingsObserver { fn update_settings( &mut self, worktree: Model, - settings_contents: impl IntoIterator, Option)>, + settings_contents: impl IntoIterator, LocalSettingsKind, Option)>, cx: &mut ModelContext, ) { let worktree_id = worktree.read(cx).id(); let remote_worktree_id = worktree.read(cx).id(); let result = cx.update_global::>(|store, cx| { - for (directory, file_content) in settings_contents { + for (directory, kind, file_content) in settings_contents { store.set_local_settings( worktree_id, directory.clone(), + kind, file_content.as_deref(), cx, )?; @@ -455,6 +468,7 @@ impl SettingsObserver { worktree_id: remote_worktree_id.to_proto(), path: directory.to_string_lossy().into_owned(), content: file_content, + kind: Some(local_settings_kind_to_proto(kind).into()), }) .log_err(); } @@ -481,3 +495,19 @@ impl SettingsObserver { } } } + +pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind { + match kind { + proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings, + proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks, + proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig, + } +} + +pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind { + match kind { + LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings, + LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks, + LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig, + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 07f64557f4..f6e9645e9c 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -642,6 +642,13 @@ message UpdateWorktreeSettings { uint64 worktree_id = 2; string path = 3; optional string content = 4; + optional LocalSettingsKind kind = 5; +} + +enum LocalSettingsKind { + Settings = 0; + Tasks = 1; + Editorconfig = 2; } message CreateProjectEntry { @@ -2487,6 +2494,12 @@ message AddWorktreeResponse { message UpdateUserSettings { uint64 project_id = 1; string content = 2; + optional Kind kind = 3; + + enum Kind { + Settings = 0; + Tasks = 1; + } } message CheckFileExists { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f1f8591bba..2ed01dc7c7 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -14,7 +14,8 @@ pub use json_schema::*; pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{ - InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore, + InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, + SettingsStore, }; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 20bf52f2c5..445420c1db 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -157,13 +157,14 @@ pub struct SettingsLocation<'a> { pub path: &'a Path, } -/// A set of strongly-typed setting values defined via multiple JSON files. +/// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, raw_default_settings: serde_json::Value, raw_user_settings: serde_json::Value, raw_extension_settings: serde_json::Value, - raw_local_settings: BTreeMap<(WorktreeId, Arc), serde_json::Value>, + raw_local_settings: + BTreeMap<(WorktreeId, Arc), HashMap>, tab_size_callback: Option<( TypeId, Box Option + Send + Sync + 'static>, @@ -174,6 +175,13 @@ pub struct SettingsStore { >, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum LocalSettingsKind { + Settings, + Tasks, + Editorconfig, +} + impl Global for SettingsStore {} #[derive(Debug)] @@ -520,19 +528,21 @@ impl SettingsStore { pub fn set_local_settings( &mut self, root_id: WorktreeId, - path: Arc, + directory_path: Arc, + kind: LocalSettingsKind, settings_content: Option<&str>, cx: &mut AppContext, ) -> Result<()> { + let raw_local_settings = self + .raw_local_settings + .entry((root_id, directory_path.clone())) + .or_default(); if settings_content.is_some_and(|content| !content.is_empty()) { - self.raw_local_settings.insert( - (root_id, path.clone()), - parse_json_with_comments(settings_content.unwrap())?, - ); + raw_local_settings.insert(kind, parse_json_with_comments(settings_content.unwrap())?); } else { - self.raw_local_settings.remove(&(root_id, path.clone())); + raw_local_settings.remove(&kind); } - self.recompute_values(Some((root_id, &path)), cx)?; + self.recompute_values(Some((root_id, &directory_path)), cx)?; Ok(()) } @@ -553,7 +563,8 @@ impl SettingsStore { /// Add or remove a set of local settings via a JSON string. pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut AppContext) -> Result<()> { - self.raw_local_settings.retain(|k, _| k.0 != root_id); + self.raw_local_settings + .retain(|(worktree_id, _), _| worktree_id != &root_id); self.recompute_values(Some((root_id, "".as_ref())), cx)?; Ok(()) } @@ -561,7 +572,7 @@ impl SettingsStore { pub fn local_settings( &self, root_id: WorktreeId, - ) -> impl '_ + Iterator, String)> { + ) -> impl '_ + Iterator, LocalSettingsKind, String)> { self.raw_local_settings .range( (root_id, Path::new("").into()) @@ -570,7 +581,12 @@ impl SettingsStore { Path::new("").into(), ), ) - .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + .flat_map(|((_, path), content)| { + content.iter().filter_map(|(&kind, raw_content)| { + let parsed_content = serde_json::to_string(raw_content).log_err()?; + Some((path.clone(), kind, parsed_content)) + }) + }) } pub fn json_schema( @@ -739,56 +755,63 @@ impl SettingsStore { // Reload the local values for the setting. paths_stack.clear(); project_settings_stack.clear(); - for ((root_id, path), local_settings) in &self.raw_local_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry { - if root_id != prev_root_id || !path.starts_with(prev_path) { - paths_stack.pop(); - project_settings_stack.pop(); - continue; + for ((root_id, directory_path), local_settings) in &self.raw_local_settings { + if let Some(local_settings) = local_settings.get(&LocalSettingsKind::Settings) { + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !directory_path.starts_with(prev_path) { + paths_stack.pop(); + project_settings_stack.pop(); + continue; + } } + break; } - break; - } - match setting_value.deserialize_setting(local_settings) { - Ok(local_settings) => { - paths_stack.push(Some((*root_id, path.as_ref()))); - project_settings_stack.push(local_settings); + match setting_value.deserialize_setting(local_settings) { + Ok(local_settings) => { + paths_stack.push(Some((*root_id, directory_path.as_ref()))); + project_settings_stack.push(local_settings); - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.map_or( - false, - |(changed_root_id, changed_local_path)| { - *root_id != changed_root_id || !path.starts_with(changed_local_path) - }, - ) { - continue; - } - - if let Some(value) = setting_value - .load_setting( - SettingsSources { - default: &default_settings, - extensions: extension_settings.as_ref(), - user: user_settings.as_ref(), - release_channel: release_channel_settings.as_ref(), - project: &project_settings_stack.iter().collect::>(), + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or( + false, + |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id + || !directory_path.starts_with(changed_local_path) }, - cx, - ) - .log_err() - { - setting_value.set_local_value(*root_id, path.clone(), value); + ) { + continue; + } + + if let Some(value) = setting_value + .load_setting( + SettingsSources { + default: &default_settings, + extensions: extension_settings.as_ref(), + user: user_settings.as_ref(), + release_channel: release_channel_settings.as_ref(), + project: &project_settings_stack.iter().collect::>(), + }, + cx, + ) + .log_err() + { + setting_value.set_local_value( + *root_id, + directory_path.clone(), + value, + ); + } + } + Err(error) => { + return Err(anyhow!(InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: error.to_string() + })); } - } - Err(error) => { - return Err(anyhow!(InvalidSettingsError::LocalSettings { - path: path.join(local_settings_file_relative_path()), - message: error.to_string() - })); } } } @@ -1201,6 +1224,7 @@ mod tests { .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1").into(), + LocalSettingsKind::Settings, Some(r#"{ "user": { "staff": true } }"#), cx, ) @@ -1209,6 +1233,7 @@ mod tests { .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1/subdir").into(), + LocalSettingsKind::Settings, Some(r#"{ "user": { "name": "Jane Doe" } }"#), cx, ) @@ -1218,6 +1243,7 @@ mod tests { .set_local_settings( WorktreeId::from_usize(1), Path::new("/root2").into(), + LocalSettingsKind::Settings, Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), cx, )