settings_ui: Make open project settings action open settings UI (#42669)

Anthony Eid created

This PR makes the `OpenProjectSettings` action open the settings UI in
project settings mode for the first visible worktree, instead of opening
the file. It also adds a `OpenProjectSettingsFile` action that maintains
the old behavior.

Finally, this PR partially fixes a bug where the settings UI won't load
project settings when the settings window is loaded before opening a
project/workspace. This happens because the global `app_state` isn't
correct in the `Subscription` that refreshes the available setting files
to open. The bug is still present in some cases, but it's out of scope
for this PR.

Release Notes:

- settings ui: Project Settings action now opens settings UI instead of
a file

Change summary

crates/settings_ui/src/settings_ui.rs | 119 +++++++++++++++++++++++-----
crates/zed/src/zed.rs                 |   8 
crates/zed/src/zed/app_menus.rs       |   6 +
crates/zed_actions/src/lib.rs         |   3 
4 files changed, 107 insertions(+), 29 deletions(-)

Detailed changes

crates/settings_ui/src/settings_ui.rs 🔗

@@ -15,7 +15,7 @@ use project::{Project, WorktreeId};
 use release_channel::ReleaseChannel;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use settings::{Settings, SettingsContent, SettingsStore};
+use settings::{Settings, SettingsContent, SettingsStore, initial_project_settings_content};
 use std::{
     any::{Any, TypeId, type_name},
     cell::RefCell,
@@ -35,7 +35,7 @@ use ui::{
 use ui_input::{NumberField, NumberFieldType};
 use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
 use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
-use zed_actions::{OpenSettings, OpenSettingsAt};
+use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
 
 use crate::components::{
     EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker,
@@ -379,26 +379,30 @@ pub fn init(cx: &mut App) {
     init_renderers(cx);
 
     cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
-        workspace.register_action(
-            |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
+        workspace
+            .register_action(
+                |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| {
+                    let window_handle = window
+                        .window_handle()
+                        .downcast::<Workspace>()
+                        .expect("Workspaces are root Windows");
+                    open_settings_editor(workspace, Some(&path), false, window_handle, cx);
+                },
+            )
+            .register_action(|workspace, _: &OpenSettings, window, cx| {
                 let window_handle = window
                     .window_handle()
                     .downcast::<Workspace>()
                     .expect("Workspaces are root Windows");
-                open_settings_editor(workspace, Some(&path), window_handle, cx);
-            },
-        );
-    })
-    .detach();
-
-    cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
-        workspace.register_action(|workspace, _: &OpenSettings, window, cx| {
-            let window_handle = window
-                .window_handle()
-                .downcast::<Workspace>()
-                .expect("Workspaces are root Windows");
-            open_settings_editor(workspace, None, window_handle, cx);
-        });
+                open_settings_editor(workspace, None, false, window_handle, cx);
+            })
+            .register_action(|workspace, _: &OpenProjectSettings, window, cx| {
+                let window_handle = window
+                    .window_handle()
+                    .downcast::<Workspace>()
+                    .expect("Workspaces are root Windows");
+                open_settings_editor(workspace, None, true, window_handle, cx);
+            });
     })
     .detach();
 }
@@ -506,6 +510,7 @@ fn init_renderers(cx: &mut App) {
 pub fn open_settings_editor(
     _workspace: &mut Workspace,
     path: Option<&str>,
+    open_project_settings: bool,
     workspace_handle: WindowHandle<Workspace>,
     cx: &mut App,
 ) {
@@ -514,6 +519,8 @@ pub fn open_settings_editor(
     /// Assumes a settings GUI window is already open
     fn open_path(
         path: &str,
+        // Note: This option is unsupported right now
+        _open_project_settings: bool,
         settings_window: &mut SettingsWindow,
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
@@ -540,7 +547,17 @@ pub fn open_settings_editor(
                 settings_window.original_window = Some(workspace_handle);
                 window.activate_window();
                 if let Some(path) = path {
-                    open_path(path, settings_window, window, cx);
+                    open_path(path, open_project_settings, settings_window, window, cx);
+                } else if open_project_settings {
+                    if let Some(file_index) = settings_window
+                        .files
+                        .iter()
+                        .position(|(file, _)| file.worktree_id().is_some())
+                    {
+                        settings_window.change_file(file_index, window, cx);
+                    }
+
+                    cx.notify();
                 }
             })
             .ok();
@@ -588,7 +605,17 @@ pub fn open_settings_editor(
                     cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx));
                 settings_window.update(cx, |settings_window, cx| {
                     if let Some(path) = path {
-                        open_path(&path, settings_window, window, cx);
+                        open_path(&path, open_project_settings, settings_window, window, cx);
+                    } else if open_project_settings {
+                        if let Some(file_index) = settings_window
+                            .files
+                            .iter()
+                            .position(|(file, _)| file.worktree_id().is_some())
+                        {
+                            settings_window.change_file(file_index, window, cx);
+                        }
+
+                        settings_window.fetch_files(window, cx);
                     }
                 });
 
@@ -1159,7 +1186,7 @@ fn all_language_names(cx: &App) -> Vec<SharedString> {
 }
 
 #[allow(unused)]
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Debug)]
 enum SettingsUiFile {
     User,                                // Uses all settings.
     Project((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
@@ -1283,15 +1310,34 @@ impl SettingsWindow {
                 })
                 .collect::<Vec<_>>()
             {
+                cx.observe_release_in(&project, window, |this, _, window, cx| {
+                    this.fetch_files(window, cx)
+                })
+                .detach();
                 cx.subscribe_in(&project, window, Self::handle_project_event)
                     .detach();
             }
+
+            for workspace in app_state
+                .workspace_store
+                .read(cx)
+                .workspaces()
+                .iter()
+                .filter_map(|space| space.entity(cx).ok())
+            {
+                cx.observe_release_in(&workspace, window, |this, _, window, cx| {
+                    this.fetch_files(window, cx)
+                })
+                .detach();
+            }
         } else {
             log::error!("App state doesn't exist when creating a new settings window");
         }
 
         let this_weak = cx.weak_entity();
         cx.observe_new::<Project>({
+            let this_weak = this_weak.clone();
+
             move |_, window, cx| {
                 let project = cx.entity();
                 let Some(window) = window else {
@@ -1299,7 +1345,13 @@ impl SettingsWindow {
                 };
 
                 this_weak
-                    .update(cx, |_, cx| {
+                    .update(cx, |this, cx| {
+                        this.fetch_files(window, cx);
+                        cx.observe_release_in(&project, window, |_, _, window, cx| {
+                            cx.defer_in(window, |this, window, cx| this.fetch_files(window, cx));
+                        })
+                        .detach();
+
                         cx.subscribe_in(&project, window, Self::handle_project_event)
                             .detach();
                     })
@@ -1308,6 +1360,24 @@ impl SettingsWindow {
         })
         .detach();
 
+        cx.observe_new::<Workspace>(move |_, window, cx| {
+            let workspace = cx.entity();
+            let Some(window) = window else {
+                return;
+            };
+
+            this_weak
+                .update(cx, |this, cx| {
+                    this.fetch_files(window, cx);
+                    cx.observe_release_in(&workspace, window, |this, _, window, cx| {
+                        this.fetch_files(window, cx)
+                    })
+                    .detach();
+                })
+                .ok();
+        })
+        .detach();
+
         let title_bar = if !cfg!(target_os = "macos") {
             Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx)))
         } else {
@@ -1818,6 +1888,7 @@ impl SettingsWindow {
         cx.notify();
     }
 
+    #[track_caller]
     fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         self.worktree_root_dirs.clear();
         let prev_files = self.files.clone();
@@ -1870,7 +1941,7 @@ impl SettingsWindow {
         let mut missing_worktrees = Vec::new();
 
         for worktree in all_projects(cx)
-            .flat_map(|project| project.read(cx).worktrees(cx))
+            .flat_map(|project| project.read(cx).visible_worktrees(cx))
             .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id()))
         {
             let worktree = worktree.read(cx);
@@ -3028,7 +3099,7 @@ impl SettingsWindow {
                         tree.create_entry(
                             settings_path.clone(),
                             false,
-                            Some("{\n\n}".as_bytes().to_vec()),
+                            Some(initial_project_settings_content().as_bytes().to_vec()),
                             cx,
                         )
                     }))

crates/zed/src/zed.rs 🔗

@@ -107,8 +107,8 @@ actions!(
         Minimize,
         /// Opens the default settings file.
         OpenDefaultSettings,
-        /// Opens project-specific settings.
-        OpenProjectSettings,
+        /// Opens project-specific settings file.
+        OpenProjectSettingsFile,
         /// Opens the project tasks configuration.
         OpenProjectTasks,
         /// Opens the tasks panel.
@@ -1783,7 +1783,7 @@ pub fn open_new_ssh_project_from_project(
 
 fn open_project_settings_file(
     workspace: &mut Workspace,
-    _: &OpenProjectSettings,
+    _: &OpenProjectSettingsFile,
     window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
@@ -5067,7 +5067,7 @@ mod tests {
             .update(cx, |workspace, window, cx| {
                 // Call the exact function that contains the bug
                 eprintln!("About to call open_project_settings_file");
-                open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
+                open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
             })
             .unwrap();
 

crates/zed/src/zed/app_menus.rs 🔗

@@ -69,7 +69,11 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
                     items: vec![
                         MenuItem::action("Open Settings", zed_actions::OpenSettings),
                         MenuItem::action("Open Settings File", super::OpenSettingsFile),
-                        MenuItem::action("Open Project Settings", super::OpenProjectSettings),
+                        MenuItem::action("Open Project Settings", zed_actions::OpenProjectSettings),
+                        MenuItem::action(
+                            "Open Project Settings File",
+                            super::OpenProjectSettingsFile,
+                        ),
                         MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
                         MenuItem::separator(),
                         MenuItem::action("Open Keymap", zed_actions::OpenKeymap),

crates/zed_actions/src/lib.rs 🔗

@@ -43,6 +43,9 @@ actions!(
         /// Opens the settings JSON file.
         #[action(deprecated_aliases = ["zed_actions::OpenSettings"])]
         OpenSettingsFile,
+        /// Opens project-specific settings.
+        #[action(deprecated_aliases = ["zed_actions::OpenProjectSettings"])]
+        OpenProjectSettings,
         /// Opens the default keymap file.
         OpenDefaultKeymap,
         /// Opens the user keymap file.