Prepare to sync other kinds of settings (#18616)

This PR does not change how things work for settings, but lays the
ground work for the future functionality.
After this change, Zed is prepared to sync more than just
`settings.json` files from local worktree and user config.

* ssh tasks

Part of this work is to streamline the task sync mechanism.
Instead of having an extra set of requests to fetch the task contents
from the server (as remote-via-collab does now and does not cover all
sync cases), we want to reuse the existing mechanism for synchronizing
user and local settings.

* editorconfig

Part of the task is to sync .editorconfig file changes to everyone which
involves sending and storing those configs.


Both ssh (and remove-over-collab) .zed/tasks.json and .editorconfig
files behave similar to .zed/settings.json local files: they belong to a
certain path in a certain worktree; may update over time, changing Zed's
functionality; can be merged hierarchically.
Settings sync follows the same "config file changed -> send to watchers
-> parse and merge locally and on watchers" path that's needed for both
new kinds of files, ergo the messaging layer is extended to send more
types of settings for future watch & parse and merge impls to follow.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2024-10-02 22:00:40 +03:00 committed by GitHub
parent 7c4615519b
commit 778dedec6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 221 additions and 73 deletions

View file

@ -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)]

View file

@ -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);
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::<Vec<_>>(),
// 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,
)