settings_ui: Write local settings files (#39408)

Ben Kunkle created

Closes #ISSUE

Release Notes:

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

Change summary

crates/project/src/project.rs         | 45 +++++++++++++
crates/settings_ui/Cargo.toml         |  1 
crates/settings_ui/src/settings_ui.rs | 94 ++++++++++++++++++++--------
3 files changed, 114 insertions(+), 26 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -5294,6 +5294,51 @@ impl Project {
     pub fn path_style(&self, cx: &App) -> PathStyle {
         self.worktree_store.read(cx).path_style()
     }
+
+    pub fn contains_local_settings_file(
+        &self,
+        worktree_id: WorktreeId,
+        rel_path: &RelPath,
+        cx: &App,
+    ) -> bool {
+        self.worktree_for_id(worktree_id, cx)
+            .map_or(false, |worktree| {
+                worktree.read(cx).entry_for_path(rel_path).is_some()
+            })
+    }
+
+    pub fn update_local_settings_file(
+        &self,
+        worktree_id: WorktreeId,
+        rel_path: Arc<RelPath>,
+        cx: &mut App,
+        update: impl 'static + Send + FnOnce(&mut settings::SettingsContent, &App),
+    ) {
+        let Some(worktree) = self.worktree_for_id(worktree_id, cx) else {
+            // todo(settings_ui) error?
+            return;
+        };
+        cx.spawn(async move |cx| {
+            let file = worktree
+                .update(cx, |worktree, cx| worktree.load_file(&rel_path, cx))?
+                .await
+                .context("Failed to load settings file")?;
+
+            let new_text = cx.read_global::<SettingsStore, _>(|store, cx| {
+                store.new_text_for_update(file.text, move |settings| update(settings, cx))
+            })?;
+            worktree
+                .update(cx, |worktree, cx| {
+                    let line_ending = text::LineEnding::detect(&new_text);
+                    worktree.write_file(rel_path.clone(), new_text.into(), line_ending, cx)
+                })?
+                .await
+                .context("Failed to write settings file")?;
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
 }
 
 pub struct PathMatchCandidateSet {

crates/settings_ui/Cargo.toml 🔗

@@ -24,6 +24,7 @@ fs.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
+paths.workspace = true
 project.workspace = true
 serde.workspace = true
 settings.workspace = true

crates/settings_ui/src/settings_ui.rs 🔗

@@ -1,5 +1,6 @@
 //! # settings_ui
 mod components;
+use anyhow::Result;
 use editor::{Editor, EditorEvent};
 use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
 use fuzzy::StringMatchCandidate;
@@ -25,7 +26,7 @@ use ui::{
     ContextMenu, Divider, DropdownMenu, DropdownStyle, Switch, SwitchColor, TreeViewItem,
     prelude::*,
 };
-use util::{paths::PathStyle, rel_path::RelPath};
+use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
 
 use crate::components::SettingsEditor;
 
@@ -3308,6 +3309,55 @@ impl Render for SettingsWindow {
     }
 }
 
+fn update_settings_file(
+    file: SettingsUiFile,
+    cx: &mut App,
+    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
+) -> Result<()> {
+    match file {
+        SettingsUiFile::Local((worktree_id, rel_path)) => {
+            fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
+                workspace::AppState::global(cx)
+                    .upgrade()
+                    .map(|app_state| {
+                        app_state
+                            .workspace_store
+                            .read(cx)
+                            .workspaces()
+                            .iter()
+                            .filter_map(|workspace| {
+                                Some(workspace.read(cx).ok()?.project().clone())
+                            })
+                    })
+                    .into_iter()
+                    .flatten()
+            }
+            let rel_path = rel_path.join(paths::local_settings_file_relative_path());
+            let project = all_projects(cx).find(|project| {
+                project.read_with(cx, |project, cx| {
+                    project.contains_local_settings_file(worktree_id, &rel_path, cx)
+                })
+            });
+            let Some(project) = project else {
+                anyhow::bail!(
+                    "Could not find worktree containing settings file: {}",
+                    &rel_path.display(PathStyle::local())
+                );
+            };
+            project.update(cx, |project, cx| {
+                project.update_local_settings_file(worktree_id, rel_path, cx, update);
+            });
+            return Ok(());
+        }
+        SettingsUiFile::User => {
+            // todo(settings_ui) error?
+            SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
+            Ok(())
+        }
+        SettingsUiFile::Server(_) => unimplemented!(),
+    }
+}
+
 fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
     field: SettingField<T>,
     file: SettingsUiFile,
@@ -3326,12 +3376,13 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
             metadata.and_then(|metadata| metadata.placeholder),
             |editor, placeholder| editor.with_placeholder(placeholder),
         )
-        .on_confirm(move |new_text, cx: &mut App| {
-            cx.update_global(move |store: &mut SettingsStore, cx| {
-                store.update_settings_file(<dyn fs::Fs>::global(cx), move |settings, _cx| {
+        .on_confirm({
+            move |new_text, cx| {
+                update_settings_file(file.clone(), cx, move |settings, _cx| {
                     *(field.pick_mut)(settings) = new_text.map(Into::into);
-                });
-            });
+                })
+                .log_err(); // todo(settings_ui) don't log err
+            }
         })
         .into_any_element()
 }
@@ -3354,12 +3405,10 @@ 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;
-                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());
-                    });
-                });
+                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                    *(field.pick_mut)(settings) = Some(state.into());
+                })
+                .log_err(); // todo(settings_ui) don't log err
             }
         })
         .color(SwitchColor::Accent)
@@ -3373,7 +3422,7 @@ fn render_dropdown<T>(
     cx: &mut App,
 ) -> AnyElement
 where
-    T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + 'static,
+    T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
 {
     let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
     let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
@@ -3388,11 +3437,8 @@ where
         "dropdown",
         current_value_label,
         ContextMenu::build(window, cx, move |mut menu, _, _| {
-            for (value, label) in variants()
-                .into_iter()
-                .copied()
-                .zip(labels().into_iter().copied())
-            {
+            for (&value, &label) in std::iter::zip(variants(), labels()) {
+                let file = file.clone();
                 menu = menu.toggleable_entry(
                     label,
                     value == current_value,
@@ -3402,14 +3448,10 @@ where
                         if value == current_value {
                             return;
                         }
-                        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(value);
-                                },
-                            );
-                        });
+                        update_settings_file(file.clone(), cx, move |settings, _cx| {
+                            *(field.pick_mut)(settings) = Some(value);
+                        })
+                        .log_err(); // todo(settings_ui) don't log err
                     },
                 );
             }