From 6254673c91c40927baeda0136c8d609548f0fcc0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:26:04 -0600 Subject: [PATCH] Local project settings --- crates/settings/src/settings_store.rs | 203 ++++++++++-------- crates/theme/src/settings.rs | 21 +- crates/theme/src/theme.rs | 4 + crates/theme_importer/src/vscode/converter.rs | 4 +- crates/ui_macros/src/dynamic_spacing.rs | 12 +- 5 files changed, 139 insertions(+), 105 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index b687df1b062f99080c7fc95faddcf1be5189a280..92213dc10fe35805d035e76d7952603cd5b1aa1a 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -944,26 +944,21 @@ impl SettingsStore { break; } - // NOTE: this kind of condition existing in the old code too, - // but is there a problem when a setting is removed from a file? - if setting_value.from_file(local_settings, cx).is_some() { - 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.is_some_and(|(changed_root_id, changed_local_path)| { - *root_id != changed_root_id - || !directory_path.starts_with(changed_local_path) - }) { - continue; - } - - let mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); - setting_value.refine(value.as_mut(), &refinements, cx); - setting_value.refine(value.as_mut(), &project_settings_stack, cx); - setting_value.set_local_value(*root_id, directory_path.clone(), value); + 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.is_some_and(|(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) + }) { + continue; } + + let mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); + setting_value.refine(value.as_mut(), &refinements, cx); + setting_value.refine(value.as_mut(), &project_settings_stack, cx); + setting_value.set_local_value(*root_id, directory_path.clone(), value); } } Ok(()) @@ -1111,6 +1106,8 @@ impl AnySettingValue for SettingValue { #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use crate::{ TitleBarSettingsContent, TitleBarVisibilityContent, default_settings, settings_content::LanguageSettingsContent, test_settings, @@ -1166,12 +1163,37 @@ mod tests { fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} } + #[derive(Debug, PartialEq)] + struct DefaultLanguageSettings { + tab_size: NonZeroU32, + preferred_line_length: u32, + } + + impl Settings for DefaultLanguageSettings { + fn from_file(content: &SettingsContent, _: &mut App) -> Option { + let content = &content.project.all_languages.defaults; + Some(DefaultLanguageSettings { + tab_size: content.tab_size?, + preferred_line_length: content.preferred_line_length?, + }) + } + + fn refine(&mut self, content: &SettingsContent, _: &mut App) { + let content = &content.project.all_languages.defaults; + self.tab_size.merge_from(&content.tab_size); + self.preferred_line_length + .merge_from(&content.preferred_line_length); + } + + fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + } + #[gpui::test] fn test_settings_store_basic(cx: &mut App) { let mut store = SettingsStore::new(cx, &default_settings()); store.register_setting::(cx); store.register_setting::(cx); - // store.register_setting::(cx); + store.register_setting::(cx); assert_eq!( store.get::(None), @@ -1204,78 +1226,75 @@ mod tests { ); // todo!() - // store - // .set_local_settings( - // WorktreeId::from_usize(1), - // Path::new("/root1").into(), - // LocalSettingsKind::Settings, - // Some(r#"{ "user": { "staff": true } }"#), - // cx, - // ) - // .unwrap(); - // store - // .set_local_settings( - // WorktreeId::from_usize(1), - // Path::new("/root1/subdir").into(), - // LocalSettingsKind::Settings, - // Some(r#"{ "user": { "name": "Jane Doe" } }"#), - // cx, - // ) - // .unwrap(); - - // store - // .set_local_settings( - // WorktreeId::from_usize(1), - // Path::new("/root2").into(), - // LocalSettingsKind::Settings, - // Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), - // cx, - // ) - // .unwrap(); - - // assert_eq!( - // store.get::(Some(SettingsLocation { - // worktree_id: WorktreeId::from_usize(1), - // path: Path::new("/root1/something"), - // })), - // &UserSettings { - // name: "John Doe".to_string(), - // age: 31, - // staff: true - // } - // ); - // assert_eq!( - // store.get::(Some(SettingsLocation { - // worktree_id: WorktreeId::from_usize(1), - // path: Path::new("/root1/subdir/something") - // })), - // &UserSettings { - // name: "Jane Doe".to_string(), - // age: 31, - // staff: true - // } - // ); - // assert_eq!( - // store.get::(Some(SettingsLocation { - // worktree_id: WorktreeId::from_usize(1), - // path: Path::new("/root2/something") - // })), - // &UserSettings { - // name: "John Doe".to_string(), - // age: 42, - // staff: false - // } - // ); - // assert_eq!( - // store.get::(Some(SettingsLocation { - // worktree_id: WorktreeId::from_usize(1), - // path: Path::new("/root2/something") - // })), - // &MultiKeySettings { - // key1: "a".to_string(), - // key2: "b".to_string(), - // } - // ); + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root1").into(), + LocalSettingsKind::Settings, + Some(r#"{ "tab_size": 5 }"#), + cx, + ) + .unwrap(); + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root1/subdir").into(), + LocalSettingsKind::Settings, + Some(r#"{ "preferred_line_length": 50 }"#), + cx, + ) + .unwrap(); + + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root2").into(), + LocalSettingsKind::Settings, + Some(r#"{ "tab_size": 9, "title_bar": { "show_branch_name": false } }"#), + cx, + ) + .unwrap(); + + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root1/something"), + })), + &DefaultLanguageSettings { + preferred_line_length: 80, + tab_size: 5.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root1/subdir/something") + })), + &DefaultLanguageSettings { + preferred_line_length: 50, + tab_size: 5.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root2/something") + })), + &DefaultLanguageSettings { + preferred_line_length: 80, + tab_size: 9.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root2/something") + })), + &TitleBarSettings { + show: TitleBarVisibilityContent::Never, + show_branch_name: true, + } + ); } #[gpui::test] diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index cb11546a4b055bbf0b7449ce5e1d11973a365d72..568e9e1044ea281f1305864a18329466735f6102 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,8 +1,8 @@ use crate::fallback_themes::zed_default_dark; use crate::{ Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme, - ThemeNotFoundError, ThemeRegistry, - status_colors_refinement, syntax_overrides, theme_colors_refinement, + ThemeNotFoundError, ThemeRegistry, status_colors_refinement, syntax_overrides, + theme_colors_refinement, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -89,6 +89,16 @@ impl From for String { } } +impl From for UiDensity { + fn from(val: settings::UiDensity) -> Self { + match val { + settings::UiDensity::Compact => Self::Compact, + settings::UiDensity::Default => Self::Default, + settings::UiDensity::Comfortable => Self::Comfortable, + } + } +} + /// Customizable settings for the UI and theme system. #[derive(Clone, PartialEq)] pub struct ThemeSettings { @@ -131,7 +141,7 @@ pub struct ThemeSettings { pub active_icon_theme: Arc, /// The density of the UI. /// Note: This setting is still experimental. See [this tracking issue]( - pub ui_density: settings::UiDensity, + pub ui_density: UiDensity, /// The amount of fading applied to unnecessary code. pub unnecessary_code_fade: f32, } @@ -822,7 +832,7 @@ impl settings::Settings for ThemeSettings { .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance)) .ok()?, icon_theme_selection: Some(icon_theme_selection), - ui_density: content.ui_density?, + ui_density: content.ui_density?.into(), unnecessary_code_fade: content.unnecessary_code_fade?, }; @@ -835,7 +845,8 @@ impl settings::Settings for ThemeSettings { let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); - self.ui_density.merge_from(&value.ui_density); + self.ui_density + .merge_from(&value.ui_density.map(Into::into)); if let Some(value) = value.buffer_font_family.clone() { self.buffer_font.family = value.0.into(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c17761b7dfd828337132fb2c21b716fa0a24d8b6..29269c15074a158acd742c7f6e258b0c94b7bebe 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -44,6 +44,10 @@ pub use crate::scale::*; pub use crate::schema::*; pub use crate::settings::*; pub use crate::styles::*; +pub use ::settings::{ + FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, + ThemeStyleContent, +}; /// Defines window border radius for platforms that use client side decorations. pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 33bbb8b18abcfb078f81ea19beac76c702ede37e..b3b846d91d5ac3a7e3d88983787d23fe9f0adece 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -1,10 +1,10 @@ use anyhow::Result; use indexmap::IndexMap; -use settings::{ +use strum::IntoEnumIterator; +use theme::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, ThemeContent, ThemeStyleContent, }; -use strum::IntoEnumIterator; use crate::ThemeMetadata; use crate::vscode::{VsCodeTheme, VsCodeTokenScope}; diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs index fa1a90a15f6ec26e94b02d3d267ebbd3d04085ec..15ba3e241ec43d02b83e4143eb620505a0a2f02e 100644 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ b/crates/ui_macros/src/dynamic_spacing.rs @@ -66,9 +66,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let n = n.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - settings::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, } } } @@ -78,9 +78,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let c = c.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - settings::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, } } }