Implement global settings file (#30444)
Adds a `global_settings.json` file which can be set up by enterprises with automation, enabling setting settings like edit provider by default without interfering with user's settings files. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
c6e69fae17
commit
90c2d17042
4 changed files with 210 additions and 40 deletions
|
@ -191,6 +191,12 @@ pub fn settings_file() -> &'static PathBuf {
|
||||||
SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json"))
|
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.
|
/// Returns the path to the `settings_backup.json` file.
|
||||||
pub fn settings_backup_file() -> &'static PathBuf {
|
pub fn settings_backup_file() -> &'static PathBuf {
|
||||||
static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
|
static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
|
@ -120,6 +120,8 @@ pub trait Settings: 'static + Send + Sync {
|
||||||
pub struct SettingsSources<'a, T> {
|
pub struct SettingsSources<'a, T> {
|
||||||
/// The default Zed settings.
|
/// The default Zed settings.
|
||||||
pub default: &'a T,
|
pub default: &'a T,
|
||||||
|
/// Global settings (loaded before user settings).
|
||||||
|
pub global: Option<&'a T>,
|
||||||
/// Settings provided by extensions.
|
/// Settings provided by extensions.
|
||||||
pub extensions: Option<&'a T>,
|
pub extensions: Option<&'a T>,
|
||||||
/// The user settings.
|
/// The user settings.
|
||||||
|
@ -140,8 +142,9 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
|
||||||
|
|
||||||
/// Returns an iterator over all of the settings customizations.
|
/// Returns an iterator over all of the settings customizations.
|
||||||
pub fn customizations(&self) -> impl Iterator<Item = &T> {
|
pub fn customizations(&self) -> impl Iterator<Item = &T> {
|
||||||
self.extensions
|
self.global
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(self.extensions)
|
||||||
.chain(self.user)
|
.chain(self.user)
|
||||||
.chain(self.release_channel)
|
.chain(self.release_channel)
|
||||||
.chain(self.server)
|
.chain(self.server)
|
||||||
|
@ -180,6 +183,7 @@ pub struct SettingsLocation<'a> {
|
||||||
pub struct SettingsStore {
|
pub struct SettingsStore {
|
||||||
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
||||||
raw_default_settings: Value,
|
raw_default_settings: Value,
|
||||||
|
raw_global_settings: Option<Value>,
|
||||||
raw_user_settings: Value,
|
raw_user_settings: Value,
|
||||||
raw_server_settings: Option<Value>,
|
raw_server_settings: Option<Value>,
|
||||||
raw_extension_settings: Value,
|
raw_extension_settings: Value,
|
||||||
|
@ -272,6 +276,7 @@ impl SettingsStore {
|
||||||
Self {
|
Self {
|
||||||
setting_values: Default::default(),
|
setting_values: Default::default(),
|
||||||
raw_default_settings: serde_json::json!({}),
|
raw_default_settings: serde_json::json!({}),
|
||||||
|
raw_global_settings: None,
|
||||||
raw_user_settings: serde_json::json!({}),
|
raw_user_settings: serde_json::json!({}),
|
||||||
raw_server_settings: None,
|
raw_server_settings: None,
|
||||||
raw_extension_settings: serde_json::json!({}),
|
raw_extension_settings: serde_json::json!({}),
|
||||||
|
@ -341,6 +346,7 @@ impl SettingsStore {
|
||||||
.load_setting(
|
.load_setting(
|
||||||
SettingsSources {
|
SettingsSources {
|
||||||
default: &default_settings,
|
default: &default_settings,
|
||||||
|
global: None,
|
||||||
extensions: extension_value.as_ref(),
|
extensions: extension_value.as_ref(),
|
||||||
user: user_value.as_ref(),
|
user: user_value.as_ref(),
|
||||||
release_channel: release_channel_value.as_ref(),
|
release_channel: release_channel_value.as_ref(),
|
||||||
|
@ -388,6 +394,11 @@ impl SettingsStore {
|
||||||
&self.raw_user_settings
|
&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"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &mut App) -> Self {
|
pub fn test(cx: &mut App) -> Self {
|
||||||
let mut this = Self::new(cx);
|
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>(
|
pub fn update_settings_file<T: Settings>(
|
||||||
&self,
|
&self,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -637,6 +662,24 @@ impl SettingsStore {
|
||||||
Ok(settings)
|
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(
|
pub fn set_server_settings(
|
||||||
&mut self,
|
&mut self,
|
||||||
server_settings_content: &str,
|
server_settings_content: &str,
|
||||||
|
@ -935,6 +978,11 @@ impl SettingsStore {
|
||||||
message: e.to_string(),
|
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
|
let extension_settings = setting_value
|
||||||
.deserialize_setting(&self.raw_extension_settings)
|
.deserialize_setting(&self.raw_extension_settings)
|
||||||
.log_err();
|
.log_err();
|
||||||
|
@ -972,6 +1020,7 @@ impl SettingsStore {
|
||||||
.load_setting(
|
.load_setting(
|
||||||
SettingsSources {
|
SettingsSources {
|
||||||
default: &default_settings,
|
default: &default_settings,
|
||||||
|
global: global_settings.as_ref(),
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_settings.as_ref(),
|
release_channel: release_channel_settings.as_ref(),
|
||||||
|
@ -1023,6 +1072,7 @@ impl SettingsStore {
|
||||||
.load_setting(
|
.load_setting(
|
||||||
SettingsSources {
|
SettingsSources {
|
||||||
default: &default_settings,
|
default: &default_settings,
|
||||||
|
global: global_settings.as_ref(),
|
||||||
extensions: extension_settings.as_ref(),
|
extensions: extension_settings.as_ref(),
|
||||||
user: user_settings.as_ref(),
|
user: user_settings.as_ref(),
|
||||||
release_channel: release_channel_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(
|
Ok(Box::new(T::load(
|
||||||
SettingsSources {
|
SettingsSources {
|
||||||
default: values.default.0.downcast_ref::<T::FileContent>().unwrap(),
|
default: values.default.0.downcast_ref::<T::FileContent>().unwrap(),
|
||||||
|
global: values
|
||||||
|
.global
|
||||||
|
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||||
extensions: values
|
extensions: values
|
||||||
.extensions
|
.extensions
|
||||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
.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)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
struct LanguageSettings {
|
struct LanguageSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
@ -294,6 +294,11 @@ fn main() {
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
paths::settings_file().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(
|
let user_keymap_file_rx = watch_config_file(
|
||||||
&app.background_executor(),
|
&app.background_executor(),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
@ -340,7 +345,12 @@ fn main() {
|
||||||
}
|
}
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
zlog_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);
|
handle_keymap_file_changes(user_keymap_file_rx, cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
let user_agent = format!(
|
let user_agent = format!(
|
||||||
|
|
|
@ -21,6 +21,7 @@ use debugger_ui::debugger_panel::DebugPanel;
|
||||||
use editor::ProposedChangesEditorToolbar;
|
use editor::ProposedChangesEditorToolbar;
|
||||||
use editor::{Editor, MultiBuffer, scroll::Autoscroll};
|
use editor::{Editor, MultiBuffer, scroll::Autoscroll};
|
||||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||||
|
use futures::future::Either;
|
||||||
use futures::{StreamExt, channel::mpsc, select_biased};
|
use futures::{StreamExt, channel::mpsc, select_biased};
|
||||||
use git_ui::git_panel::GitPanel;
|
use git_ui::git_panel::GitPanel;
|
||||||
use git_ui::project_diff::ProjectDiffToolbar;
|
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(
|
pub fn handle_settings_file_changes(
|
||||||
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
|
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
|
||||||
|
mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
|
settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
|
||||||
) {
|
) {
|
||||||
MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
|
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()
|
.background_executor()
|
||||||
.block(user_settings_file_rx.next())
|
.block(user_settings_file_rx.next())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let user_settings_content = if let Ok(Some(migrated_content)) = migrate_settings(&content) {
|
|
||||||
migrated_content
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
};
|
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
let result = store.set_user_settings(&user_settings_content, cx);
|
process_settings(global_content, false, store, cx);
|
||||||
if let Err(err) = &result {
|
process_settings(user_content, true, store, cx);
|
||||||
log::error!("Failed to load user settings: {err}");
|
|
||||||
}
|
|
||||||
settings_changed(result.err(), cx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for changes in both files
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
while let Some(content) = user_settings_file_rx.next().await {
|
let mut settings_streams = futures::stream::select(
|
||||||
let user_settings_content;
|
global_settings_file_rx.map(Either::Left),
|
||||||
let content_migrated;
|
user_settings_file_rx.map(Either::Right),
|
||||||
|
);
|
||||||
|
|
||||||
if let Ok(Some(migrated_content)) = migrate_settings(&content) {
|
while let Some(content) = settings_streams.next().await {
|
||||||
user_settings_content = migrated_content;
|
let (content, is_user) = match content {
|
||||||
content_migrated = true;
|
Either::Left(content) => (content, false),
|
||||||
} else {
|
Either::Right(content) => (content, true),
|
||||||
user_settings_content = content;
|
};
|
||||||
content_migrated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
let result = store.set_user_settings(&user_settings_content, cx);
|
let content_migrated = process_settings(content, is_user, store, cx);
|
||||||
if let Err(err) = &result {
|
|
||||||
log::error!("Failed to load user settings: {err}");
|
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();
|
cx.refresh_windows();
|
||||||
});
|
});
|
||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
break; // App dropped
|
break; // App dropped
|
||||||
}
|
}
|
||||||
|
@ -3888,7 +3915,12 @@ mod tests {
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
PathBuf::from("/keymap.json"),
|
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);
|
handle_keymap_file_changes(keymap_rx, cx);
|
||||||
});
|
});
|
||||||
workspace
|
workspace
|
||||||
|
@ -4002,7 +4034,12 @@ mod tests {
|
||||||
PathBuf::from("/keymap.json"),
|
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);
|
handle_keymap_file_changes(keymap_rx, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue