Detailed changes
@@ -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
);
@@ -0,0 +1 @@
+ALTER TABLE "worktree_settings_files" ADD COLUMN "kind" VARCHAR;
@@ -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<i32>,
pub wasm_api_versions: RangeInclusive<SemanticVersion>,
}
+
+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,
+ }
+ }
+}
@@ -1,3 +1,4 @@
+use anyhow::Context as _;
use util::ResultExt;
use super::*;
@@ -527,6 +528,12 @@ impl Database {
connection: ConnectionId,
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
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,
});
}
}
@@ -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,
});
}
}
@@ -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,
+}
@@ -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()),
},
)?;
}
@@ -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::<Vec<_>>(),
&[
- (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::<Vec<_>>(),
&[
- (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::<Vec<_>>(),
&[
- (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::<Vec<_>>(),
- &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
+ &[(
+ Path::new("a").into(),
+ LocalSettingsKind::Settings,
+ r#"{"hard_tabs":true}"#.to_string()
+ ),]
)
});
}
@@ -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::<SettingsStore>();
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<proto::UpdateWorktreeSettings>,
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<Path>, _)> =
+ let settings_contents: Vec<(Arc<Path>, _, _)> =
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<Worktree>,
- settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
+ settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
cx: &mut ModelContext<Self>,
) {
let worktree_id = worktree.read(cx).id();
let remote_worktree_id = worktree.read(cx).id();
let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|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,
+ }
+}
@@ -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 {
@@ -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)]
@@ -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<TypeId, Box<dyn AnySettingValue>>,
raw_default_settings: serde_json::Value,
raw_user_settings: serde_json::Value,
raw_extension_settings: serde_json::Value,
- raw_local_settings: BTreeMap<(WorktreeId, Arc<Path>), serde_json::Value>,
+ raw_local_settings:
+ BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>,
tab_size_callback: Option<(
TypeId,
Box<dyn Fn(&dyn Any) -> Option<usize> + 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<Path>,
+ directory_path: Arc<Path>,
+ 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<Item = (Arc<Path>, String)> {
+ ) -> impl '_ + Iterator<Item = (Arc<Path>, 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);
-
- // 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::<Vec<_>>(),
+ 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
+ || !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::<Vec<_>>(),
+ },
+ 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,
)