Local project settings

Conrad Irwin created

Change summary

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(-)

Detailed changes

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<T: Settings> AnySettingValue for SettingValue<T> {
 
 #[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<Self> {
+            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::<AutoUpdateSetting>(cx);
         store.register_setting::<TitleBarSettings>(cx);
-        // store.register_setting::<MultiKeySettings>(cx);
+        store.register_setting::<DefaultLanguageSettings>(cx);
 
         assert_eq!(
             store.get::<AutoUpdateSetting>(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::<UserSettings>(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::<UserSettings>(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::<UserSettings>(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::<MultiKeySettings>(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::<DefaultLanguageSettings>(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::<DefaultLanguageSettings>(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::<DefaultLanguageSettings>(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::<TitleBarSettings>(Some(SettingsLocation {
+                worktree_id: WorktreeId::from_usize(1),
+                path: Path::new("/root2/something")
+            })),
+            &TitleBarSettings {
+                show: TitleBarVisibilityContent::Never,
+                show_branch_name: true,
+            }
+        );
     }
 
     #[gpui::test]

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<UiDensity> for String {
     }
 }
 
+impl From<settings::UiDensity> 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<IconTheme>,
     /// 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();

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);

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};

crates/ui_macros/src/dynamic_spacing.rs 🔗

@@ -66,9 +66,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream {
                     let n = n.base10_parse::<f32>().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::<f32>().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,
                         }
                     }
                 }