Detailed changes
@@ -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<PathBuf> = 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<PathBuf> = OnceLock::new();
@@ -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<Item = &T> {
- 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<TypeId, Box<dyn AnySettingValue>>,
raw_default_settings: Value,
+ raw_global_settings: Option<Value>,
raw_user_settings: Value,
raw_server_settings: Option<Value>,
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<dyn Fs>) -> Result<String> {
+ match fs.load(paths::global_settings_file()).await {
+ result @ Ok(_) => result,
+ Err(err) => {
+ if let Some(e) = err.downcast_ref::<std::io::Error>() {
+ if e.kind() == std::io::ErrorKind::NotFound {
+ return Ok("{}".to_string());
+ }
+ }
+ Err(err)
+ }
+ }
+ }
+
pub fn update_settings_file<T: Settings>(
&self,
fs: Arc<dyn Fs>,
@@ -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<Value> {
+ 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<T: Settings> AnySettingValue for SettingValue<T> {
Ok(Box::new(T::load(
SettingsSources {
default: values.default.0.downcast_ref::<T::FileContent>().unwrap(),
+ global: values
+ .global
+ .map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
extensions: values
.extensions
.map(|value| value.0.downcast_ref::<T::FileContent>().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::<UserSettings>(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::<UserSettings>(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::<UserSettings>(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)]
@@ -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!(
@@ -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<String>,
+ mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
cx: &mut App,
settings_changed: impl Fn(Option<anyhow::Error>, &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);
});