diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index c0e506fcd1..fe67d931bd 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -191,6 +191,12 @@ pub fn settings_file() -> &'static PathBuf { SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json")) } +/// Returns the path to the global settings file. +pub fn global_settings_file() -> &'static PathBuf { + static GLOBAL_SETTINGS_FILE: OnceLock = OnceLock::new(); + GLOBAL_SETTINGS_FILE.get_or_init(|| config_dir().join("global_settings.json")) +} + /// Returns the path to the `settings_backup.json` file. pub fn settings_backup_file() -> &'static PathBuf { static SETTINGS_FILE: OnceLock = OnceLock::new(); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 73963431ed..59c9357915 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -120,6 +120,8 @@ pub trait Settings: 'static + Send + Sync { pub struct SettingsSources<'a, T> { /// The default Zed settings. pub default: &'a T, + /// Global settings (loaded before user settings). + pub global: Option<&'a T>, /// Settings provided by extensions. pub extensions: Option<&'a T>, /// The user settings. @@ -140,8 +142,9 @@ impl<'a, T: Serialize> SettingsSources<'a, T> { /// Returns an iterator over all of the settings customizations. pub fn customizations(&self) -> impl Iterator { - self.extensions + self.global .into_iter() + .chain(self.extensions) .chain(self.user) .chain(self.release_channel) .chain(self.server) @@ -180,6 +183,7 @@ pub struct SettingsLocation<'a> { pub struct SettingsStore { setting_values: HashMap>, raw_default_settings: Value, + raw_global_settings: Option, raw_user_settings: Value, raw_server_settings: Option, raw_extension_settings: Value, @@ -272,6 +276,7 @@ impl SettingsStore { Self { setting_values: Default::default(), raw_default_settings: serde_json::json!({}), + raw_global_settings: None, raw_user_settings: serde_json::json!({}), raw_server_settings: None, raw_extension_settings: serde_json::json!({}), @@ -341,6 +346,7 @@ impl SettingsStore { .load_setting( SettingsSources { default: &default_settings, + global: None, extensions: extension_value.as_ref(), user: user_value.as_ref(), release_channel: release_channel_value.as_ref(), @@ -388,6 +394,11 @@ impl SettingsStore { &self.raw_user_settings } + /// Access the raw JSON value of the global settings. + pub fn raw_global_settings(&self) -> Option<&Value> { + self.raw_global_settings.as_ref() + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> Self { let mut this = Self::new(cx); @@ -426,6 +437,20 @@ impl SettingsStore { } } + pub async fn load_global_settings(fs: &Arc) -> Result { + match fs.load(paths::global_settings_file()).await { + result @ Ok(_) => result, + Err(err) => { + if let Some(e) = err.downcast_ref::() { + if e.kind() == std::io::ErrorKind::NotFound { + return Ok("{}".to_string()); + } + } + Err(err) + } + } + } + pub fn update_settings_file( &self, fs: Arc, @@ -637,6 +662,24 @@ impl SettingsStore { Ok(settings) } + /// Sets the global settings via a JSON string. + pub fn set_global_settings( + &mut self, + global_settings_content: &str, + cx: &mut App, + ) -> Result { + let settings: Value = if global_settings_content.is_empty() { + parse_json_with_comments("{}")? + } else { + parse_json_with_comments(global_settings_content)? + }; + + anyhow::ensure!(settings.is_object(), "settings must be an object"); + self.raw_global_settings = Some(settings.clone()); + self.recompute_values(None, cx)?; + Ok(settings) + } + pub fn set_server_settings( &mut self, server_settings_content: &str, @@ -935,6 +978,11 @@ impl SettingsStore { message: e.to_string(), })?; + let global_settings = self + .raw_global_settings + .as_ref() + .and_then(|setting| setting_value.deserialize_setting(setting).log_err()); + let extension_settings = setting_value .deserialize_setting(&self.raw_extension_settings) .log_err(); @@ -972,6 +1020,7 @@ impl SettingsStore { .load_setting( SettingsSources { default: &default_settings, + global: global_settings.as_ref(), extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), @@ -1023,6 +1072,7 @@ impl SettingsStore { .load_setting( SettingsSources { default: &default_settings, + global: global_settings.as_ref(), extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), @@ -1139,6 +1189,9 @@ impl AnySettingValue for SettingValue { Ok(Box::new(T::load( SettingsSources { default: values.default.0.downcast_ref::().unwrap(), + global: values + .global + .map(|value| value.0.downcast_ref::().unwrap()), extensions: values .extensions .map(|value| value.0.downcast_ref::().unwrap()), @@ -2072,6 +2125,70 @@ mod tests { } } + #[gpui::test] + fn test_global_settings(cx: &mut App) { + let mut store = SettingsStore::new(cx); + store.register_setting::(cx); + store + .set_default_settings( + r#"{ + "user": { + "name": "John Doe", + "age": 30, + "staff": false + } + }"#, + cx, + ) + .unwrap(); + + // Set global settings - these should override defaults but not user settings + store + .set_global_settings( + r#"{ + "user": { + "name": "Global User", + "age": 35, + "staff": true + } + }"#, + cx, + ) + .unwrap(); + + // Before user settings, global settings should apply + assert_eq!( + store.get::(None), + &UserSettings { + name: "Global User".to_string(), + age: 35, + staff: true, + } + ); + + // Set user settings - these should override both defaults and global + store + .set_user_settings( + r#"{ + "user": { + "age": 40 + } + }"#, + cx, + ) + .unwrap(); + + // User settings should override global settings + assert_eq!( + store.get::(None), + &UserSettings { + name: "Global User".to_string(), // Name from global settings + age: 40, // Age from user settings + staff: true, // Staff from global settings + } + ); + } + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] struct LanguageSettings { #[serde(default)] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index dac531133b..ee74752971 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -294,6 +294,11 @@ fn main() { fs.clone(), paths::settings_file().clone(), ); + let global_settings_file_rx = watch_config_file( + &app.background_executor(), + fs.clone(), + paths::global_settings_file().clone(), + ); let user_keymap_file_rx = watch_config_file( &app.background_executor(), fs.clone(), @@ -340,7 +345,12 @@ fn main() { } settings::init(cx); zlog_settings::init(cx); - handle_settings_file_changes(user_settings_file_rx, cx, handle_settings_changed); + handle_settings_file_changes( + user_settings_file_rx, + global_settings_file_rx, + cx, + handle_settings_changed, + ); handle_keymap_file_changes(user_keymap_file_rx, cx); client::init_settings(cx); let user_agent = format!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 314c38b740..2d092f7eb4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -21,6 +21,7 @@ use debugger_ui::debugger_panel::DebugPanel; use editor::ProposedChangesEditorToolbar; use editor::{Editor, MultiBuffer, scroll::Autoscroll}; use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt}; +use futures::future::Either; use futures::{StreamExt, channel::mpsc, select_biased}; use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; @@ -1089,58 +1090,84 @@ fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Contex pub fn handle_settings_file_changes( mut user_settings_file_rx: mpsc::UnboundedReceiver, + mut global_settings_file_rx: mpsc::UnboundedReceiver, cx: &mut App, settings_changed: impl Fn(Option, &mut App) + 'static, ) { MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx); - let content = cx + + // Helper function to process settings content + let process_settings = + move |content: String, is_user: bool, store: &mut SettingsStore, cx: &mut App| -> bool { + // Apply migrations to both user and global settings + let (processed_content, content_migrated) = + if let Ok(Some(migrated_content)) = migrate_settings(&content) { + (migrated_content, true) + } else { + (content, false) + }; + + let result = if is_user { + store.set_user_settings(&processed_content, cx) + } else { + store.set_global_settings(&processed_content, cx) + }; + + if let Err(err) = &result { + let settings_type = if is_user { "user" } else { "global" }; + log::error!("Failed to load {} settings: {err}", settings_type); + } + + settings_changed(result.err(), cx); + + content_migrated + }; + + // Initial load of both settings files + let global_content = cx + .background_executor() + .block(global_settings_file_rx.next()) + .unwrap(); + let user_content = cx .background_executor() .block(user_settings_file_rx.next()) .unwrap(); - let user_settings_content = if let Ok(Some(migrated_content)) = migrate_settings(&content) { - migrated_content - } else { - content - }; + SettingsStore::update_global(cx, |store, cx| { - let result = store.set_user_settings(&user_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load user settings: {err}"); - } - settings_changed(result.err(), cx); + process_settings(global_content, false, store, cx); + process_settings(user_content, true, store, cx); }); + + // Watch for changes in both files cx.spawn(async move |cx| { - while let Some(content) = user_settings_file_rx.next().await { - let user_settings_content; - let content_migrated; + let mut settings_streams = futures::stream::select( + global_settings_file_rx.map(Either::Left), + user_settings_file_rx.map(Either::Right), + ); - if let Ok(Some(migrated_content)) = migrate_settings(&content) { - user_settings_content = migrated_content; - content_migrated = true; - } else { - user_settings_content = content; - content_migrated = false; - } + while let Some(content) = settings_streams.next().await { + let (content, is_user) = match content { + Either::Left(content) => (content, false), + Either::Right(content) => (content, true), + }; - cx.update(|cx| { - if let Some(notifier) = MigrationNotification::try_global(cx) { - notifier.update(cx, |_, cx| { - cx.emit(MigrationEvent::ContentChanged { - migration_type: MigrationType::Settings, - migrated: content_migrated, - }); - }); - } - }) - .ok(); let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = store.set_user_settings(&user_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load user settings: {err}"); + let content_migrated = process_settings(content, is_user, store, cx); + + if content_migrated { + if let Some(notifier) = MigrationNotification::try_global(cx) { + notifier.update(cx, |_, cx| { + cx.emit(MigrationEvent::ContentChanged { + migration_type: MigrationType::Settings, + migrated: true, + }); + }); + } } - settings_changed(result.err(), cx); + cx.refresh_windows(); }); + if result.is_err() { break; // App dropped } @@ -3888,7 +3915,12 @@ mod tests { app_state.fs.clone(), PathBuf::from("/keymap.json"), ); - handle_settings_file_changes(settings_rx, cx, |_, _| {}); + let global_settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/global_settings.json"), + ); + handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {}); handle_keymap_file_changes(keymap_rx, cx); }); workspace @@ -4002,7 +4034,12 @@ mod tests { PathBuf::from("/keymap.json"), ); - handle_settings_file_changes(settings_rx, cx, |_, _| {}); + let global_settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/global_settings.json"), + ); + handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {}); handle_keymap_file_changes(keymap_rx, cx); });