settings_ui: Collect all settings files (#38816)

Ben Kunkle created

Closes #ISSUE

Updates the settings editor to collect all known settings files from the
settings store, in order to show them in the UI. Additionally adds a
fake worktree instantiation in the settings UI example binary in order
to have more than one file available when testing.

Release Notes:

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

Change summary

Cargo.lock                                     |  3 
crates/session/src/session.rs                  |  2 
crates/settings/src/settings.rs                |  3 
crates/settings/src/settings_store.rs          | 40 +++++++++++
crates/settings_ui/Cargo.toml                  |  5 +
crates/settings_ui/examples/.zed/settings.json |  1 
crates/settings_ui/examples/ui.rs              | 71 +++++++++++++++++++
crates/settings_ui/src/settings_ui.rs          | 51 ++++++++++---
8 files changed, 157 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14508,6 +14508,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "assets",
+ "client",
  "command_palette_hooks",
  "editor",
  "feature_flags",
@@ -14516,9 +14517,11 @@ dependencies = [
  "gpui",
  "language",
  "menu",
+ "node_runtime",
  "paths",
  "project",
  "serde",
+ "session",
  "settings",
  "theme",
  "ui",

crates/session/src/session.rs 🔗

@@ -43,7 +43,7 @@ impl Session {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
+    // #[cfg(any(test, feature = "test-support"))]
     pub fn test() -> Self {
         Self {
             session_id: Uuid::new_v4().to_string(),

crates/settings/src/settings.rs 🔗

@@ -24,7 +24,8 @@ pub use keymap_file::{
 pub use settings_file::*;
 pub use settings_json::*;
 pub use settings_store::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, SettingsStore,
+    InvalidSettingsError, LocalSettingsKind, Settings, SettingsFile, SettingsKey, SettingsLocation,
+    SettingsStore,
 };
 
 pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};

crates/settings/src/settings_store.rs 🔗

@@ -156,6 +156,16 @@ pub struct SettingsStore {
         mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
 }
 
+#[derive(Clone, PartialEq)]
+pub enum SettingsFile {
+    User,
+    Global,
+    Extension,
+    Server,
+    Default,
+    Local((WorktreeId, Arc<Path>)),
+}
+
 #[derive(Clone)]
 pub struct Editorconfig {
     pub is_root: bool,
@@ -479,6 +489,36 @@ impl SettingsStore {
             })
         })
     }
+
+    pub fn get_all_files(&self) -> Vec<SettingsFile> {
+        let mut files = Vec::from_iter(
+            self.local_settings
+                .keys()
+                // rev because these are sorted by path, so highest precedence is last
+                .rev()
+                .cloned()
+                .map(SettingsFile::Local),
+        );
+
+        if self.server_settings.is_some() {
+            files.push(SettingsFile::Server);
+        }
+        // ignoring profiles
+        // ignoring os profiles
+        // ignoring release channel profiles
+
+        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
+    }
 }
 
 impl SettingsStore {

crates/settings_ui/Cargo.toml 🔗

@@ -32,12 +32,15 @@ workspace-hack.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]
-settings = { workspace = true, features = ["test-support"] }
+settings.workspace = true
 futures.workspace = true
 language.workspace = true
 assets.workspace = true
 paths.workspace = true
 zlog.workspace = true
+client.workspace = true
+session.workspace = true
+node_runtime.workspace = true
 
 [[example]]
 name = "ui"

crates/settings_ui/examples/ui.rs 🔗

@@ -1,11 +1,42 @@
 use std::sync::Arc;
 
 use futures::StreamExt;
+use gpui::AppContext as _;
 use settings::{DEFAULT_KEYMAP_PATH, KeymapFile, SettingsStore, watch_config_file};
 use settings_ui::open_settings_editor;
 use ui::BorrowAppContext;
 
+fn merge_paths(a: &std::path::Path, b: &std::path::Path) -> std::path::PathBuf {
+    let a_parts: Vec<_> = a.components().collect();
+    let b_parts: Vec<_> = b.components().collect();
+
+    let mut overlap = 0;
+    for i in 0..=a_parts.len().min(b_parts.len()) {
+        if a_parts[a_parts.len() - i..] == b_parts[..i] {
+            overlap = i;
+        }
+    }
+
+    let mut result = std::path::PathBuf::new();
+    for part in &a_parts {
+        result.push(part.as_os_str());
+    }
+    for part in &b_parts[overlap..] {
+        result.push(part.as_os_str());
+    }
+    result
+}
+
 fn main() {
+    zlog::init();
+    zlog::init_output_stderr();
+
+    let [crate_path, file_path] = [env!("CARGO_MANIFEST_DIR"), file!()].map(std::path::Path::new);
+    let example_dir_abs_path = merge_paths(crate_path, file_path)
+        .parent()
+        .unwrap()
+        .to_path_buf();
+
     let app = gpui::Application::new().with_assets(assets::Assets);
 
     let fs = Arc::new(fs::RealFs::new(None, app.background_executor()));
@@ -14,15 +45,49 @@ fn main() {
         fs.clone(),
         paths::settings_file().clone(),
     );
-    zlog::init();
-    zlog::init_output_stderr();
 
     app.run(move |cx| {
         <dyn fs::Fs>::set_global(fs.clone(), cx);
         settings::init(cx);
         theme::init(theme::LoadThemes::JustBase, cx);
+
+        client::init_settings(cx);
         workspace::init_settings(cx);
-        project::Project::init_settings(cx);
+        // production client because fake client requires gpui/test-support
+        // and that causes issues with the real stuff we want to do
+        let client = client::Client::production(cx);
+        let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
+        let languages = Arc::new(language::LanguageRegistry::new(
+            cx.background_executor().clone(),
+        ));
+
+        client::init(&client, cx);
+
+        project::Project::init(&client, cx);
+
+        zlog::info!(
+            "Creating fake worktree in {}",
+            example_dir_abs_path.display(),
+        );
+        let project = project::Project::local(
+            client.clone(),
+            node_runtime::NodeRuntime::unavailable(),
+            user_store,
+            languages,
+            fs.clone(),
+            Some(Default::default()), // WARN: if None is passed here, prepare to be process bombed
+            cx,
+        );
+        let worktree_task = project.update(cx, |project, cx| {
+            project.create_worktree(example_dir_abs_path, true, cx)
+        });
+        cx.spawn(async move |_| {
+            let worktree = worktree_task.await.unwrap();
+            std::mem::forget(worktree);
+        })
+        .detach();
+        std::mem::forget(project);
+
         language::init(cx);
         editor::init(cx);
         menu::init();

crates/settings_ui/src/settings_ui.rs 🔗

@@ -200,7 +200,7 @@ struct SettingItem {
 }
 
 #[allow(unused)]
-#[derive(Clone)]
+#[derive(Clone, PartialEq)]
 enum SettingsFile {
     User,                           // Uses all settings.
     Local((WorktreeId, Arc<Path>)), // Has a special name, and special set of settings
@@ -216,11 +216,11 @@ impl SettingsFile {
         }
     }
 
-    fn name(&self) -> String {
+    fn name(&self) -> SharedString {
         match self {
-            SettingsFile::User => "User".to_string(),
-            SettingsFile::Local((_, path)) => format!("Local ({})", path.display()),
-            SettingsFile::Server(file) => format!("Server ({})", file),
+            SettingsFile::User => SharedString::new_static("User"),
+            SettingsFile::Local((_, path)) => format!("Local ({})", path.display()).into(),
+            SettingsFile::Server(file) => format!("Server ({})", file).into(),
         }
     }
 }
@@ -234,22 +234,18 @@ impl SettingsWindow {
             editor
         });
         let mut this = Self {
-            files: vec![
-                SettingsFile::User,
-                SettingsFile::Local((
-                    WorktreeId::from_usize(0),
-                    Arc::from(Path::new("/my-project/")),
-                )),
-            ],
+            files: vec![],
             current_file: current_file,
             pages: vec![],
             current_page: 0,
             search,
         };
-        cx.observe_global_in::<SettingsStore>(window, move |_, _, cx| {
+        cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
+            this.fetch_files(cx);
             cx.notify();
         })
         .detach();
+        this.fetch_files(cx);
 
         this.build_ui();
         this
@@ -259,7 +255,36 @@ impl SettingsWindow {
         self.pages = self.current_file.pages();
     }
 
+    fn fetch_files(&mut self, cx: &mut App) {
+        let settings_store = cx.global::<SettingsStore>();
+        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),
+            };
+            ui_files.push(settings_ui_file);
+        }
+        ui_files.reverse();
+        if !ui_files.contains(&self.current_file) {
+            self.change_file(0);
+        }
+        self.files = ui_files;
+    }
+
     fn change_file(&mut self, ix: usize) {
+        if ix >= self.files.len() {
+            self.current_file = SettingsFile::User;
+            return;
+        }
+        if self.files[ix] == self.current_file {
+            return;
+        }
         self.current_file = self.files[ix].clone();
         self.build_ui();
     }