Add SettingsFile APIs to SettingsStore (#39129)

Mikayla Maki , Ben Kunkle , Anthony Eid , Danilo Leal , and Anthony created

Closes #ISSUE

Adds a couple functions to the `SettingsStore`:
- `get_value_from_file`: Gets a value from a given settings file
(`Local`, `User`, etc) and if the value isn't found in the requested
file, walks the known settings files in the order in which they are
merged to find the settings value in lower precedence settings files
(i.e. if value not set anywhere will always return default value)
- `get_overrides_for_field`: Returns a list of settings files where a
given setting is set that have higher precedence than the passed in
file. e.g. passing in user will result in project settings files where
the value is set being returned.

Additionally changes the default for the `project_name` setting to
uphold the rules we are attempting to enforce on the settings, namely:
- All settings fields should be of the form `Option<T>`
- `None` (or `null` in JSON) should never be a meaningful value

Follow up PRs will handle implementing a function to write to an
arbitrary settings file, and passing through metadata to the above
functions to control how overrides are determined for more complicated
cases like `SaturatingBool` (`disable_ai`) and `ExtendingVec`

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>

Change summary

assets/settings/default.json                    |   4 
crates/settings/src/settings_content/project.rs |   4 
crates/settings/src/settings_store.rs           | 406 ++++++++++++++++++
crates/settings_ui/src/settings_ui.rs           | 183 ++++---
crates/worktree/src/worktree_settings.rs        |   2 
5 files changed, 506 insertions(+), 93 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

@@ -1,5 +1,7 @@
 {
-  "project_name": null,
+  /// The displayed name of this project. If not set or empty, the root directory name
+  /// will be displayed.
+  "project_name": "",
   // The name of the Zed theme to use for the UI.
   //
   // `mode` is one of:

crates/settings/src/settings_content/project.rs πŸ”—

@@ -50,10 +50,10 @@ pub struct ProjectSettingsContent {
 #[skip_serializing_none]
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorktreeSettingsContent {
-    /// The displayed name of this project. If not set, the root directory name
+    /// The displayed name of this project. If not set or empty, the root directory name
     /// will be displayed.
     ///
-    /// Default: none
+    /// Default: ""
     pub project_name: Option<String>,
 
     /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides

crates/settings/src/settings_store.rs πŸ”—

@@ -157,13 +157,12 @@ pub struct SettingsStore {
         mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
 }
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Debug)]
 pub enum SettingsFile {
     User,
-    Global,
-    Extension,
     Server,
     Default,
+    /// Local also represents project settings in ssh projects as well as local projects
     Local((WorktreeId, Arc<RelPath>)),
 }
 
@@ -479,19 +478,98 @@ impl SettingsStore {
         // ignoring profiles
         // ignoring os profiles
         // ignoring release channel profiles
+        // ignoring global
+        // ignoring extension
 
         if self.user_settings.is_some() {
             files.push(SettingsFile::User);
         }
-        if self.extension_settings.is_some() {
-            files.push(SettingsFile::Extension);
-        }
-        if self.global_settings.is_some() {
-            files.push(SettingsFile::Global);
-        }
         files.push(SettingsFile::Default);
         files
     }
+
+    fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> {
+        match file {
+            SettingsFile::User => self
+                .user_settings
+                .as_ref()
+                .map(|settings| settings.content.as_ref()),
+            SettingsFile::Default => Some(self.default_settings.as_ref()),
+            SettingsFile::Server => self.server_settings.as_deref(),
+            SettingsFile::Local(ref key) => self.local_settings.get(key),
+        }
+    }
+
+    pub fn get_overrides_for_field<T>(
+        &self,
+        target_file: SettingsFile,
+        get: fn(&SettingsContent) -> &Option<T>,
+    ) -> Vec<SettingsFile> {
+        let all_files = self.get_all_files();
+        let mut found_file = false;
+        let mut overrides = Vec::new();
+
+        for file in all_files.into_iter().rev() {
+            if !found_file {
+                found_file = file == target_file;
+                continue;
+            }
+
+            if let SettingsFile::Local((wt_id, ref path)) = file
+                && let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
+                && (wt_id != target_wt_id || !target_path.starts_with(path))
+            {
+                // if requesting value from a local file, don't return values from local files in different worktrees
+                continue;
+            }
+
+            let Some(content) = self.get_content_for_file(file.clone()) else {
+                continue;
+            };
+            if get(content).is_some() {
+                overrides.push(file);
+            }
+        }
+
+        overrides
+    }
+
+    pub fn get_value_from_file<T>(
+        &self,
+        target_file: SettingsFile,
+        pick: fn(&SettingsContent) -> &Option<T>,
+    ) -> (SettingsFile, &T) {
+        // TODO: Add a metadata field for overriding the "overrides" tag, for contextually different settings
+        //  e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
+
+        // todo(settings_ui) cache all files
+        let all_files = self.get_all_files();
+        let mut found_file = false;
+
+        for file in all_files.into_iter() {
+            if !found_file && file != target_file && file != SettingsFile::Default {
+                continue;
+            }
+            found_file = true;
+
+            if let SettingsFile::Local((wt_id, ref path)) = file
+                && let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
+                && (wt_id != target_wt_id || !target_path.starts_with(&path))
+            {
+                // if requesting value from a local file, don't return values from local files in different worktrees
+                continue;
+            }
+
+            let Some(content) = self.get_content_for_file(file.clone()) else {
+                continue;
+            };
+            if let Some(value) = pick(content).as_ref() {
+                return (file, value);
+            }
+        }
+
+        unreachable!("All values should have defaults");
+    }
 }
 
 impl SettingsStore {
@@ -1609,4 +1687,314 @@ mod tests {
             }
         );
     }
+
+    #[gpui::test]
+    fn test_get_value_for_field_basic(cx: &mut App) {
+        let mut store = SettingsStore::new(cx, &test_settings());
+        store.register_setting::<DefaultLanguageSettings>(cx);
+
+        store
+            .set_user_settings(r#"{"preferred_line_length": 0}"#, cx)
+            .unwrap();
+        let local = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
+        store
+            .set_local_settings(
+                local.0,
+                local.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+
+        fn get(content: &SettingsContent) -> &Option<u32> {
+            &content.project.all_languages.defaults.preferred_line_length
+        }
+
+        let default_value = get(&store.default_settings).unwrap();
+
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            (SettingsFile::User, &0)
+        );
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::User, get),
+            (SettingsFile::User, &0)
+        );
+        store.set_user_settings(r#"{}"#, cx).unwrap();
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            (SettingsFile::Default, &default_value)
+        );
+        store
+            .set_local_settings(
+                local.0,
+                local.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 80}"#),
+                cx,
+            )
+            .unwrap();
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            (SettingsFile::Local(local), &80)
+        );
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::User, get),
+            (SettingsFile::Default, &default_value)
+        );
+    }
+
+    #[gpui::test]
+    fn test_get_value_for_field_local_worktrees_dont_interfere(cx: &mut App) {
+        let mut store = SettingsStore::new(cx, &test_settings());
+        store.register_setting::<DefaultLanguageSettings>(cx);
+        store.register_setting::<AutoUpdateSetting>(cx);
+
+        let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
+
+        let local_1_child = (
+            WorktreeId::from_usize(0),
+            RelPath::new(
+                std::path::Path::new("child1"),
+                util::paths::PathStyle::Posix,
+            )
+            .unwrap()
+            .into_arc(),
+        );
+
+        let local_2 = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
+        let local_2_child = (
+            WorktreeId::from_usize(1),
+            RelPath::new(
+                std::path::Path::new("child2"),
+                util::paths::PathStyle::Posix,
+            )
+            .unwrap()
+            .into_arc(),
+        );
+
+        fn get(content: &SettingsContent) -> &Option<u32> {
+            &content.project.all_languages.defaults.preferred_line_length
+        }
+
+        store
+            .set_local_settings(
+                local_1.0,
+                local_1.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 1}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                local_1_child.0,
+                local_1_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                local_2.0,
+                local_2.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 2}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                local_2_child.0,
+                local_2_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+
+        // each local child should only inherit from it's parent
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local_2_child), get),
+            (SettingsFile::Local(local_2), &2)
+        );
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local_1_child.clone()), get),
+            (SettingsFile::Local(local_1.clone()), &1)
+        );
+
+        // adjacent children should be treated as siblings not inherit from each other
+        let local_1_adjacent_child = (local_1.0, rel_path("adjacent_child").into_arc());
+        store
+            .set_local_settings(
+                local_1_adjacent_child.0,
+                local_1_adjacent_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                local_1_child.0,
+                local_1_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 3}"#),
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local_1_adjacent_child.clone()), get),
+            (SettingsFile::Local(local_1.clone()), &1)
+        );
+        store
+            .set_local_settings(
+                local_1_adjacent_child.0,
+                local_1_adjacent_child.1,
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 3}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                local_1_child.0,
+                local_1_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+        assert_eq!(
+            store.get_value_from_file(SettingsFile::Local(local_1_child), get),
+            (SettingsFile::Local(local_1), &1)
+        );
+    }
+
+    #[gpui::test]
+    fn test_get_overrides_for_field(cx: &mut App) {
+        let mut store = SettingsStore::new(cx, &test_settings());
+        store.register_setting::<DefaultLanguageSettings>(cx);
+
+        let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
+        let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc());
+        let wt0_child2 = (WorktreeId::from_usize(0), rel_path("child2").into_arc());
+
+        let wt1_root = (WorktreeId::from_usize(1), RelPath::empty().into_arc());
+        let wt1_subdir = (WorktreeId::from_usize(1), rel_path("subdir").into_arc());
+
+        fn get(content: &SettingsContent) -> &Option<u32> {
+            &content.project.all_languages.defaults.preferred_line_length
+        }
+
+        store
+            .set_user_settings(r#"{"preferred_line_length": 100}"#, cx)
+            .unwrap();
+
+        store
+            .set_local_settings(
+                wt0_root.0,
+                wt0_root.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 80}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                wt0_child1.0,
+                wt0_child1.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 120}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                wt0_child2.0,
+                wt0_child2.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+
+        store
+            .set_local_settings(
+                wt1_root.0,
+                wt1_root.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 90}"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                wt1_subdir.0,
+                wt1_subdir.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{}"#),
+                cx,
+            )
+            .unwrap();
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Default, get);
+        assert_eq!(
+            overrides,
+            vec![
+                SettingsFile::User,
+                SettingsFile::Local(wt0_root.clone()),
+                SettingsFile::Local(wt0_child1.clone()),
+                SettingsFile::Local(wt1_root.clone()),
+            ]
+        );
+
+        let overrides = store.get_overrides_for_field(SettingsFile::User, get);
+        assert_eq!(
+            overrides,
+            vec![
+                SettingsFile::Local(wt0_root.clone()),
+                SettingsFile::Local(wt0_child1.clone()),
+                SettingsFile::Local(wt1_root.clone()),
+            ]
+        );
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_root), get);
+        assert_eq!(overrides, vec![]);
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1.clone()), get);
+        assert_eq!(overrides, vec![]);
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child2), get);
+        assert_eq!(overrides, vec![]);
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_root), get);
+        assert_eq!(overrides, vec![]);
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_subdir), get);
+        assert_eq!(overrides, vec![]);
+
+        let wt0_deep_child = (
+            WorktreeId::from_usize(0),
+            rel_path("child1/subdir").into_arc(),
+        );
+        store
+            .set_local_settings(
+                wt0_deep_child.0,
+                wt0_deep_child.1.clone(),
+                LocalSettingsKind::Settings,
+                Some(r#"{"preferred_line_length": 140}"#),
+                cx,
+            )
+            .unwrap();
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_deep_child), get);
+        assert_eq!(overrides, vec![]);
+
+        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1), get);
+        assert_eq!(overrides, vec![]);
+    }
 }

crates/settings_ui/src/settings_ui.rs πŸ”—

@@ -22,16 +22,17 @@ use util::{paths::PathStyle, rel_path::RelPath};
 
 use crate::components::SettingsEditor;
 
-#[derive(Clone)]
+#[derive(Clone, Copy)]
 struct SettingField<T: 'static> {
-    pick: fn(&SettingsContent) -> &T,
-    pick_mut: fn(&mut SettingsContent) -> &mut T,
+    pick: fn(&SettingsContent) -> &Option<T>,
+    pick_mut: fn(&mut SettingsContent) -> &mut Option<T>,
 }
 
 trait AnySettingField {
     fn as_any(&self) -> &dyn Any;
     fn type_name(&self) -> &'static str;
     fn type_id(&self) -> TypeId;
+    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile;
 }
 
 impl<T> AnySettingField for SettingField<T> {
@@ -46,6 +47,13 @@ impl<T> AnySettingField for SettingField<T> {
     fn type_id(&self) -> TypeId {
         TypeId::of::<T>()
     }
+
+    fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile {
+        let (file, _) = cx
+            .global::<SettingsStore>()
+            .get_value_from_file(file.to_settings(), self.pick);
+        return file;
+    }
 }
 
 #[derive(Default, Clone)]
@@ -57,6 +65,7 @@ struct SettingFieldRenderer {
                 Box<
                     dyn Fn(
                         &dyn AnySettingField,
+                        SettingsUiFile,
                         Option<&SettingsFieldMetadata>,
                         &mut Window,
                         &mut App,
@@ -74,6 +83,7 @@ impl SettingFieldRenderer {
         &mut self,
         renderer: impl Fn(
             &SettingField<T>,
+            SettingsUiFile,
             Option<&SettingsFieldMetadata>,
             &mut Window,
             &mut App,
@@ -83,6 +93,7 @@ impl SettingFieldRenderer {
         let key = TypeId::of::<T>();
         let renderer = Box::new(
             move |any_setting_field: &dyn AnySettingField,
+                  settings_file: SettingsUiFile,
                   metadata: Option<&SettingsFieldMetadata>,
                   window: &mut Window,
                   cx: &mut App| {
@@ -90,7 +101,7 @@ impl SettingFieldRenderer {
                     .as_any()
                     .downcast_ref::<SettingField<T>>()
                     .unwrap();
-                renderer(field, metadata, window, cx)
+                renderer(field, settings_file, metadata, window, cx)
             },
         );
         self.renderers.borrow_mut().insert(key, renderer);
@@ -100,13 +111,14 @@ impl SettingFieldRenderer {
     fn render(
         &self,
         any_setting_field: &dyn AnySettingField,
+        settings_file: SettingsUiFile,
         metadata: Option<&SettingsFieldMetadata>,
         window: &mut Window,
         cx: &mut App,
     ) -> AnyElement {
         let key = any_setting_field.type_id();
         if let Some(renderer) = self.renderers.borrow().get(&key) {
-            renderer(any_setting_field, metadata, window, cx)
+            renderer(any_setting_field, settings_file, metadata, window, cx)
         } else {
             panic!(
                 "No renderer found for type: {}",
@@ -269,18 +281,19 @@ pub fn init(cx: &mut App) {
 }
 
 fn init_renderers(cx: &mut App) {
+    // fn (field: SettingsField, current_file: SettingsFile, cx) -> (currently_set_in: SettingsFile, overridden_in: Vec<SettingsFile>)
     cx.default_global::<SettingFieldRenderer>()
-        .add_renderer::<Option<bool>>(|settings_field, _, _, cx| {
-            render_toggle_button(settings_field.clone(), cx).into_any_element()
+        .add_renderer::<bool>(|settings_field, file, _, _, cx| {
+            render_toggle_button(*settings_field, file, cx).into_any_element()
         })
-        .add_renderer::<Option<String>>(|settings_field, metadata, _, cx| {
-            render_text_field(settings_field.clone(), metadata, cx)
+        .add_renderer::<String>(|settings_field, file, metadata, _, cx| {
+            render_text_field(settings_field.clone(), file, metadata, cx)
         })
-        .add_renderer::<Option<SaturatingBool>>(|settings_field, _, _, cx| {
-            render_toggle_button(settings_field.clone(), cx)
+        .add_renderer::<SaturatingBool>(|settings_field, file, _, _, cx| {
+            render_toggle_button(*settings_field, file, cx)
         })
-        .add_renderer::<Option<CursorShape>>(|settings_field, _, window, cx| {
-            render_dropdown(settings_field.clone(), window, cx)
+        .add_renderer::<CursorShape>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
         });
 }
 
@@ -304,8 +317,8 @@ pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<Setting
 }
 
 pub struct SettingsWindow {
-    files: Vec<SettingsFile>,
-    current_file: SettingsFile,
+    files: Vec<SettingsUiFile>,
+    current_file: SettingsUiFile,
     pages: Vec<SettingsPage>,
     search: Entity<Editor>,
     navbar_entry: usize, // Index into pages - should probably be (usize, Option<usize>) for section + page
@@ -340,7 +353,7 @@ enum SettingsPageItem {
 }
 
 impl SettingsPageItem {
-    fn render(&self, _file: SettingsFile, window: &mut Window, cx: &mut App) -> AnyElement {
+    fn render(&self, file: SettingsUiFile, window: &mut Window, cx: &mut App) -> AnyElement {
         match self {
             SettingsPageItem::SectionHeader(header) => v_flex()
                 .w_full()
@@ -350,6 +363,9 @@ impl SettingsPageItem {
                 .into_any_element(),
             SettingsPageItem::SettingItem(setting_item) => {
                 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
+                let file_set_in =
+                    SettingsUiFile::from_settings(setting_item.field.file_set_in(file.clone(), cx));
+
                 h_flex()
                     .id(setting_item.title)
                     .w_full()
@@ -361,8 +377,25 @@ impl SettingsPageItem {
                             .max_w_1_2()
                             .flex_shrink()
                             .child(
-                                Label::new(SharedString::new_static(setting_item.title))
-                                    .size(LabelSize::Default),
+                                h_flex()
+                                    .w_full()
+                                    .gap_4()
+                                    .child(
+                                        Label::new(SharedString::new_static(setting_item.title))
+                                            .size(LabelSize::Default),
+                                    )
+                                    .when_some(
+                                        file_set_in.filter(|file_set_in| file_set_in != &file),
+                                        |elem, file_set_in| {
+                                            elem.child(
+                                                Label::new(format!(
+                                                    "set in {}",
+                                                    file_set_in.name()
+                                                ))
+                                                .color(Color::Muted),
+                                            )
+                                        },
+                                    ),
                             )
                             .child(
                                 Label::new(SharedString::new_static(setting_item.description))
@@ -372,6 +405,7 @@ impl SettingsPageItem {
                     )
                     .child(renderer.render(
                         setting_item.field.as_ref(),
+                        file,
                         setting_item.metadata.as_deref(),
                         window,
                         cx,
@@ -391,36 +425,53 @@ struct SettingItem {
 
 #[allow(unused)]
 #[derive(Clone, PartialEq)]
-enum SettingsFile {
+enum SettingsUiFile {
     User,                              // Uses all settings.
     Local((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
     Server(&'static str),              // Uses a special name, and the user settings
 }
 
-impl SettingsFile {
+impl SettingsUiFile {
     fn pages(&self) -> Vec<SettingsPage> {
         match self {
-            SettingsFile::User => user_settings_data(),
-            SettingsFile::Local(_) => project_settings_data(),
-            SettingsFile::Server(_) => user_settings_data(),
+            SettingsUiFile::User => user_settings_data(),
+            SettingsUiFile::Local(_) => project_settings_data(),
+            SettingsUiFile::Server(_) => user_settings_data(),
         }
     }
 
     fn name(&self) -> SharedString {
         match self {
-            SettingsFile::User => SharedString::new_static("User"),
+            SettingsUiFile::User => SharedString::new_static("User"),
             // TODO is PathStyle::local() ever not appropriate?
-            SettingsFile::Local((_, path)) => {
+            SettingsUiFile::Local((_, path)) => {
                 format!("Local ({})", path.display(PathStyle::local())).into()
             }
-            SettingsFile::Server(file) => format!("Server ({})", file).into(),
+            SettingsUiFile::Server(file) => format!("Server ({})", file).into(),
+        }
+    }
+
+    fn from_settings(file: settings::SettingsFile) -> Option<Self> {
+        Some(match file {
+            settings::SettingsFile::User => SettingsUiFile::User,
+            settings::SettingsFile::Local(location) => SettingsUiFile::Local(location),
+            settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
+            settings::SettingsFile::Default => return None,
+        })
+    }
+
+    fn to_settings(&self) -> settings::SettingsFile {
+        match self {
+            SettingsUiFile::User => settings::SettingsFile::User,
+            SettingsUiFile::Local(location) => settings::SettingsFile::Local(location.clone()),
+            SettingsUiFile::Server(_) => settings::SettingsFile::Server,
         }
     }
 }
 
 impl SettingsWindow {
     pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let current_file = SettingsFile::User;
+        let current_file = SettingsUiFile::User;
         let search = cx.new(|cx| {
             let mut editor = Editor::single_line(window, cx);
             editor.set_placeholder_text("Search settings…", window, cx);
@@ -496,26 +547,21 @@ impl SettingsWindow {
         let mut ui_files = vec![];
         let all_files = settings_store.get_all_files();
         for file in all_files {
-            let settings_ui_file = match file {
-                settings::SettingsFile::User => SettingsFile::User,
-                settings::SettingsFile::Global => continue,
-                settings::SettingsFile::Extension => continue,
-                settings::SettingsFile::Server => SettingsFile::Server("todo: server name"),
-                settings::SettingsFile::Default => continue,
-                settings::SettingsFile::Local(location) => SettingsFile::Local(location),
+            let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
+                continue;
             };
             ui_files.push(settings_ui_file);
         }
         ui_files.reverse();
-        if !ui_files.contains(&self.current_file) {
+        self.files = ui_files;
+        if !self.files.contains(&self.current_file) {
             self.change_file(0, cx);
         }
-        self.files = ui_files;
     }
 
     fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
         if ix >= self.files.len() {
-            self.current_file = SettingsFile::User;
+            self.current_file = SettingsUiFile::User;
             return;
         }
         if self.files[ix] == self.current_file {
@@ -682,25 +728,19 @@ impl Render for SettingsWindow {
     }
 }
 
+// fn read_field<T>(pick: fn(&SettingsContent) -> &Option<T>, file: SettingsFile, cx: &App) -> Option<T> {
+//     let (_, value) = cx.global::<SettingsStore>().get_value_from_file(file.to_settings(), (), pick);
+// }
+
 fn render_text_field(
-    field: SettingField<Option<String>>,
+    field: SettingField<String>,
+    file: SettingsUiFile,
     metadata: Option<&SettingsFieldMetadata>,
     cx: &mut App,
 ) -> AnyElement {
-    // TODO: in settings window state
-    let store = SettingsStore::global(cx);
-
-    // TODO: This clone needs to go!!
-    let defaults = store.raw_default_settings().clone();
-    let user_settings = store
-        .raw_user_settings()
-        .cloned()
-        .unwrap_or_default()
-        .content;
-
-    let initial_text = (field.pick)(&user_settings)
-        .clone()
-        .or_else(|| (field.pick)(&defaults).clone());
+    let (_, initial_text) =
+        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+    let initial_text = Some(initial_text.clone()).filter(|s| !s.is_empty());
 
     SettingsEditor::new()
         .when_some(initial_text, |editor, text| editor.with_initial_text(text))
@@ -719,24 +759,13 @@ fn render_text_field(
 }
 
 fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
-    field: SettingField<Option<B>>,
+    field: SettingField<B>,
+    file: SettingsUiFile,
     cx: &mut App,
 ) -> AnyElement {
-    // TODO: in settings window state
-    let store = SettingsStore::global(cx);
-
-    // TODO: This clone needs to go!!
-    let defaults = store.raw_default_settings().clone();
-    let user_settings = store
-        .raw_user_settings()
-        .cloned()
-        .unwrap_or_default()
-        .content;
-
-    let toggle_state = if (field.pick)(&user_settings)
-        .unwrap_or_else(|| (field.pick)(&defaults).unwrap())
-        .into()
-    {
+    let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
+
+    let toggle_state = if value.into() {
         ui::ToggleState::Selected
     } else {
         ui::ToggleState::Unselected
@@ -746,7 +775,7 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
         .on_click({
             move |state, _window, cx| {
                 let state = *state == ui::ToggleState::Selected;
-                let field = field.clone();
+                let field = field;
                 cx.update_global(move |store: &mut SettingsStore, cx| {
                     store.update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _cx| {
                         *(field.pick_mut)(settings) = Some(state.into());
@@ -758,7 +787,8 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
 }
 
 fn render_dropdown<T>(
-    field: SettingField<Option<T>>,
+    field: SettingField<T>,
+    file: SettingsUiFile,
     window: &mut Window,
     cx: &mut App,
 ) -> AnyElement
@@ -768,16 +798,9 @@ where
     let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
     let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
 
-    let store = SettingsStore::global(cx);
-    let defaults = store.raw_default_settings().clone();
-    let user_settings = store
-        .raw_user_settings()
-        .cloned()
-        .unwrap_or_default()
-        .content;
+    let (_, &current_value) =
+        SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
 
-    let current_value =
-        (field.pick)(&user_settings).unwrap_or_else(|| (field.pick)(&defaults).unwrap());
     let current_value_label =
         labels()[variants().iter().position(|v| *v == current_value).unwrap()];
 
@@ -894,7 +917,7 @@ mod test {
 
         let mut settings_window = SettingsWindow {
             files: Vec::default(),
-            current_file: crate::SettingsFile::User,
+            current_file: crate::SettingsUiFile::User,
             pages,
             search: cx.new(|cx| Editor::single_line(window, cx)),
             navbar_entry: selected_idx.unwrap(),

crates/worktree/src/worktree_settings.rs πŸ”—

@@ -51,7 +51,7 @@ impl Settings for WorktreeSettings {
             .collect();
 
         Self {
-            project_name: None,
+            project_name: worktree.project_name.filter(|p| !p.is_empty()),
             file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
                 .log_err()
                 .unwrap_or_default(),