Define workspace settings in workspace crate

Max Brunsfeld created

Change summary

Cargo.lock                                        |   1 
crates/collab_ui/src/sharing_status_indicator.rs  |   6 
crates/diagnostics/src/diagnostics.rs             |   1 
crates/editor/src/editor_tests.rs                 |   1 
crates/editor/src/element.rs                      |  10 
crates/editor/src/test/editor_lsp_test_context.rs |   1 
crates/file_finder/src/file_finder.rs             |   1 
crates/project_panel/src/project_panel.rs         |   1 
crates/project_symbols/src/project_symbols.rs     |   1 
crates/search/src/project_search.rs               |   1 
crates/settings/src/settings.rs                   | 128 ----------------
crates/terminal_view/src/terminal_view.rs         |  47 ++---
crates/workspace/Cargo.toml                       |   1 
crates/workspace/src/dock.rs                      |  18 +-
crates/workspace/src/item.rs                      |  17 +
crates/workspace/src/pane.rs                      |  38 +++-
crates/workspace/src/pane_group.rs                |   6 
crates/workspace/src/persistence.rs               |   9 
crates/workspace/src/persistence/model.rs         |   9 
crates/workspace/src/workspace.rs                 |  92 +++++++----
crates/workspace/src/workspace_settings.rs        | 103 +++++++++++++
crates/zed/src/zed.rs                             |   4 
22 files changed, 253 insertions(+), 243 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8676,6 +8676,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "project",
+ "schemars",
  "serde",
  "serde_derive",
  "serde_json",

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     platform::{Appearance, MouseButton},
     AnyElement, AppContext, Element, Entity, View, ViewContext,
 };
-use settings::Settings;
+use workspace::WorkspaceSettings;
 
 pub fn init(cx: &mut AppContext) {
     let active_call = ActiveCall::global(cx);
@@ -15,7 +15,9 @@ pub fn init(cx: &mut AppContext) {
     cx.observe(&active_call, move |call, cx| {
         if let Some(room) = call.read(cx).room() {
             if room.read(cx).is_screen_sharing() {
-                if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
+                if status_indicator.is_none()
+                    && settings::get_setting::<WorkspaceSettings>(None, cx).show_call_status_icon
+                {
                     status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
                 }
             } else if let Some((window_id, _)) = status_indicator.take() {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1499,6 +1499,7 @@ mod tests {
             cx.set_global(Settings::test(cx));
             cx.set_global(SettingsStore::test(cx));
             language::init(cx);
+            workspace::init_settings(cx);
         });
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -6685,6 +6685,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
         language::init(cx);
         crate::init(cx);
         Project::init_settings(cx);
+        workspace::init_settings(cx);
     });
 
     update_test_settings(cx, f);

crates/editor/src/element.rs 🔗

@@ -40,7 +40,7 @@ use language::{
     Selection,
 };
 use project::ProjectPath;
-use settings::{GitGutter, Settings};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -50,7 +50,7 @@ use std::{
     ops::Range,
     sync::Arc,
 };
-use workspace::item::Item;
+use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
 
 enum FoldMarkers {}
 
@@ -550,11 +550,11 @@ impl EditorElement {
         let scroll_top = scroll_position.y() * line_height;
 
         let show_gutter = matches!(
-            &cx.global::<Settings>()
-                .git_overrides
+            settings::get_setting::<WorkspaceSettings>(None, cx)
+                .git
                 .git_gutter
                 .unwrap_or_default(),
-            GitGutter::TrackedFiles
+            GitGutterSetting::TrackedFiles
         );
 
         if show_gutter {

crates/settings/src/settings.rs 🔗

@@ -3,7 +3,7 @@ mod keymap_file;
 mod settings_file;
 mod settings_store;
 
-use anyhow::{bail, Result};
+use anyhow::Result;
 use gpui::{
     font_cache::{FamilyId, FontCache},
     fonts, AppContext, AssetSource,
@@ -15,10 +15,6 @@ use schemars::{
 };
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use sqlez::{
-    bindable::{Bind, Column, StaticColumnCount},
-    statement::Statement,
-};
 use std::{borrow::Cow, str, sync::Arc};
 use theme::{Theme, ThemeRegistry};
 use util::ResultExt as _;
@@ -37,13 +33,6 @@ pub struct Settings {
     pub buffer_font_features: fonts::Features,
     pub buffer_font_family: FamilyId,
     pub buffer_font_size: f32,
-    pub active_pane_magnification: f32,
-    pub confirm_quit: bool,
-    pub show_call_status_icon: bool,
-    pub autosave: Autosave,
-    pub default_dock_anchor: DockAnchor,
-    pub git: GitSettings,
-    pub git_overrides: GitSettings,
     pub theme: Arc<Theme>,
     pub base_keymap: BaseKeymap,
 }
@@ -72,13 +61,6 @@ impl Setting for Settings {
             buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
             buffer_font_features,
             buffer_font_size: defaults.buffer_font_size.unwrap(),
-            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
-            confirm_quit: defaults.confirm_quit.unwrap(),
-            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
-            autosave: defaults.autosave.unwrap(),
-            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
-            git: defaults.git.unwrap(),
-            git_overrides: Default::default(),
             theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
             base_keymap: Default::default(),
         };
@@ -201,65 +183,6 @@ impl BaseKeymap {
             .unwrap_or_default()
     }
 }
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct GitSettings {
-    pub git_gutter: Option<GitGutter>,
-    pub gutter_debounce: Option<u64>,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutter {
-    #[default]
-    TrackedFiles,
-    Hide,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Autosave {
-    Off,
-    AfterDelay { milliseconds: u64 },
-    OnFocusChange,
-    OnWindowChange,
-}
-
-#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DockAnchor {
-    #[default]
-    Bottom,
-    Right,
-    Expanded,
-}
-
-impl StaticColumnCount for DockAnchor {}
-impl Bind for DockAnchor {
-    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
-        match self {
-            DockAnchor::Bottom => "Bottom",
-            DockAnchor::Right => "Right",
-            DockAnchor::Expanded => "Expanded",
-        }
-        .bind(statement, start_index)
-    }
-}
-
-impl Column for DockAnchor {
-    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
-        String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
-            Ok((
-                match anchor_text.as_ref() {
-                    "Bottom" => DockAnchor::Bottom,
-                    "Right" => DockAnchor::Right,
-                    "Expanded" => DockAnchor::Expanded,
-                    _ => bail!("Stored dock anchor is incorrect"),
-                },
-                next_index,
-            ))
-        })
-    }
-}
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct SettingsFileContent {
@@ -270,24 +193,6 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub buffer_font_features: Option<fonts::Features>,
     #[serde(default)]
-    pub active_pane_magnification: Option<f32>,
-    #[serde(default)]
-    pub cursor_blink: Option<bool>,
-    #[serde(default)]
-    pub confirm_quit: Option<bool>,
-    #[serde(default)]
-    pub hover_popover_enabled: Option<bool>,
-    #[serde(default)]
-    pub show_completions_on_input: Option<bool>,
-    #[serde(default)]
-    pub show_call_status_icon: Option<bool>,
-    #[serde(default)]
-    pub autosave: Option<Autosave>,
-    #[serde(default)]
-    pub default_dock_anchor: Option<DockAnchor>,
-    #[serde(default)]
-    pub git: Option<GitSettings>,
-    #[serde(default)]
     pub theme: Option<String>,
     #[serde(default)]
     pub base_keymap: Option<BaseKeymap>,
@@ -323,13 +228,6 @@ impl Settings {
             buffer_font_family_name: defaults.buffer_font_family.unwrap(),
             buffer_font_features,
             buffer_font_size: defaults.buffer_font_size.unwrap(),
-            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
-            confirm_quit: defaults.confirm_quit.unwrap(),
-            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
-            autosave: defaults.autosave.unwrap(),
-            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
-            git: defaults.git.unwrap(),
-            git_overrides: Default::default(),
             theme: themes.get(&defaults.theme.unwrap()).unwrap(),
             base_keymap: Default::default(),
         }
@@ -367,24 +265,7 @@ impl Settings {
         }
 
         merge(&mut self.buffer_font_size, data.buffer_font_size);
-        merge(
-            &mut self.active_pane_magnification,
-            data.active_pane_magnification,
-        );
-        merge(&mut self.confirm_quit, data.confirm_quit);
-        merge(&mut self.autosave, data.autosave);
-        merge(&mut self.default_dock_anchor, data.default_dock_anchor);
         merge(&mut self.base_keymap, data.base_keymap);
-
-        self.git_overrides = data.git.unwrap_or_default();
-    }
-
-    pub fn git_gutter(&self) -> GitGutter {
-        self.git_overrides.git_gutter.unwrap_or_else(|| {
-            self.git
-                .git_gutter
-                .expect("git_gutter should be some by setting setup")
-        })
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -397,13 +278,6 @@ impl Settings {
                 .load_family(&["Monaco"], &Default::default())
                 .unwrap(),
             buffer_font_size: 14.,
-            active_pane_magnification: 1.,
-            confirm_quit: false,
-            show_call_status_icon: true,
-            autosave: Autosave::Off,
-            default_dock_anchor: DockAnchor::Bottom,
-            git: Default::default(),
-            git_overrides: Default::default(),
             theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
             base_keymap: Default::default(),
         }

crates/terminal_view/src/terminal_view.rs 🔗

@@ -787,22 +787,18 @@ fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
 
 #[cfg(test)]
 mod tests {
-
     use super::*;
     use gpui::TestAppContext;
     use project::{Entry, Project, ProjectPath, Worktree};
-    use workspace::AppState;
-
     use std::path::Path;
+    use workspace::AppState;
 
-    ///Working directory calculation tests
+    // Working directory calculation tests
 
-    ///No Worktrees in project -> home_dir()
+    // No Worktrees in project -> home_dir()
     #[gpui::test]
     async fn no_worktree(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
-        //Test
+        let (project, workspace) = init_test(cx).await;
         cx.read(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -818,14 +814,12 @@ mod tests {
         });
     }
 
-    ///No active entry, but a worktree, worktree is a file -> home_dir()
+    // No active entry, but a worktree, worktree is a file -> home_dir()
     #[gpui::test]
     async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
+        let (project, workspace) = init_test(cx).await;
 
-        let (project, workspace) = blank_workspace(cx).await;
         create_file_wt(project.clone(), "/root.txt", cx).await;
-
         cx.read(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -841,14 +835,12 @@ mod tests {
         });
     }
 
-    //No active entry, but a worktree, worktree is a folder -> worktree_folder
+    // No active entry, but a worktree, worktree is a folder -> worktree_folder
     #[gpui::test]
     async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
-        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
+        let (project, workspace) = init_test(cx).await;
 
-        //Test
+        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -863,17 +855,15 @@ mod tests {
         });
     }
 
-    //Active entry with a work tree, worktree is a file -> home_dir()
+    // Active entry with a work tree, worktree is a file -> home_dir()
     #[gpui::test]
     async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
+        let (project, workspace) = init_test(cx).await;
 
-        let (project, workspace) = blank_workspace(cx).await;
         let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
         let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
         insert_active_entry_for(wt2, entry2, project.clone(), cx);
 
-        //Test
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -887,16 +877,15 @@ mod tests {
         });
     }
 
-    //Active entry, with a worktree, worktree is a folder -> worktree_folder
+    // Active entry, with a worktree, worktree is a folder -> worktree_folder
     #[gpui::test]
     async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
+        let (project, workspace) = init_test(cx).await;
+
         let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
         let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
         insert_active_entry_for(wt2, entry2, project.clone(), cx);
 
-        //Test
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -910,8 +899,8 @@ mod tests {
         });
     }
 
-    ///Creates a worktree with 1 file: /root.txt
-    pub async fn blank_workspace(
+    /// Creates a worktree with 1 file: /root.txt
+    pub async fn init_test(
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
         let params = cx.update(AppState::test);
@@ -922,7 +911,7 @@ mod tests {
         (project, workspace)
     }
 
-    ///Creates a worktree with 1 folder: /root{suffix}/
+    /// Creates a worktree with 1 folder: /root{suffix}/
     async fn create_folder_wt(
         project: ModelHandle<Project>,
         path: impl AsRef<Path>,
@@ -931,7 +920,7 @@ mod tests {
         create_wt(project, true, path, cx).await
     }
 
-    ///Creates a worktree with 1 file: /root{suffix}.txt
+    /// Creates a worktree with 1 file: /root{suffix}.txt
     async fn create_file_wt(
         project: ModelHandle<Project>,
         path: impl AsRef<Path>,

crates/workspace/Cargo.toml 🔗

@@ -45,6 +45,7 @@ lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/workspace/src/dock.rs 🔗

@@ -1,5 +1,9 @@
 mod toggle_dock_button;
 
+use crate::{
+    sidebar::SidebarSide, BackgroundActions, DockAnchor, ItemHandle, Pane, Workspace,
+    WorkspaceSettings,
+};
 use collections::HashMap;
 use gpui::{
     actions,
@@ -8,10 +12,7 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
 };
-use settings::{DockAnchor, Settings};
 use theme::Theme;
-
-use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
 pub use toggle_dock_button::ToggleDockButton;
 
 actions!(
@@ -171,7 +172,9 @@ impl Dock {
         background_actions: BackgroundActions,
         cx: &mut ViewContext<Workspace>,
     ) -> Self {
-        let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
+        let position = DockPosition::Hidden(
+            settings::get_setting::<WorkspaceSettings>(None, cx).default_dock_anchor,
+        );
         let workspace = cx.weak_handle();
         let pane =
             cx.add_view(|cx| Pane::new(workspace, Some(position.anchor()), background_actions, cx));
@@ -405,7 +408,6 @@ mod tests {
 
     use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
     use project::{FakeFs, Project};
-    use settings::Settings;
     use theme::ThemeRegistry;
 
     use super::*;
@@ -417,6 +419,7 @@ mod tests {
         },
         register_deserializable_item,
         sidebar::Sidebar,
+        tests::init_test,
         AppState, ItemHandle, Workspace,
     };
 
@@ -429,8 +432,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
 
         cx.update(|cx| {
             register_deserializable_item::<item::test::TestItem>(cx);
@@ -598,7 +600,7 @@ mod tests {
 
     impl<'a> DockTestContext<'a> {
         pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
-            Settings::test_async(cx);
+            init_test(cx);
             let fs = FakeFs::new(cx.background());
 
             cx.update(|cx| init(cx));

crates/workspace/src/item.rs 🔗

@@ -3,6 +3,7 @@ use crate::{
     FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
     WorkspaceId,
 };
+use crate::{AutosaveSetting, WorkspaceSettings};
 use anyhow::Result;
 use client::{proto, Client};
 use gpui::{
@@ -10,7 +11,6 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
-use settings::{Autosave, Settings};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -450,8 +450,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                             }
 
                             ItemEvent::Edit => {
-                                if let Autosave::AfterDelay { milliseconds } =
-                                    cx.global::<Settings>().autosave
+                                let settings = settings::get_setting::<WorkspaceSettings>(None, cx);
+                                let debounce_delay = settings.git.gutter_debounce;
+
+                                if let AutosaveSetting::AfterDelay { milliseconds } =
+                                    settings.autosave
                                 {
                                     let delay = Duration::from_millis(milliseconds);
                                     let item = item.clone();
@@ -460,9 +463,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     });
                                 }
 
-                                let settings = cx.global::<Settings>();
-                                let debounce_delay = settings.git_overrides.gutter_debounce;
-
                                 let item = item.clone();
 
                                 if let Some(delay) = debounce_delay {
@@ -500,7 +500,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                 }));
 
             cx.observe_focus(self, move |workspace, item, focused, cx| {
-                if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
+                if !focused
+                    && settings::get_setting::<WorkspaceSettings>(None, cx).autosave
+                        == AutosaveSetting::OnFocusChange
+                {
                     Pane::autosave_item(&item, workspace.project.clone(), cx)
                         .detach_and_log_err(cx);
                 }

crates/workspace/src/pane.rs 🔗

@@ -5,7 +5,8 @@ use crate::{
     dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
     item::WeakItemHandle,
     toolbar::Toolbar,
-    Item, NewFile, NewSearch, NewTerminal, Workspace,
+    AutosaveSetting, DockAnchor, Item, NewFile, NewSearch, NewTerminal, Workspace,
+    WorkspaceSettings,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -29,7 +30,7 @@ use gpui::{
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
-use settings::{Autosave, DockAnchor, Settings};
+use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use theme::Theme;
 use util::ResultExt;
@@ -1024,8 +1025,8 @@ impl Pane {
         } else if is_dirty && (can_save || is_singleton) {
             let will_autosave = cx.read(|cx| {
                 matches!(
-                    cx.global::<Settings>().autosave,
-                    Autosave::OnFocusChange | Autosave::OnWindowChange
+                    settings::get_setting::<WorkspaceSettings>(None, cx).autosave,
+                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
                 ) && Self::can_autosave_item(&*item, cx)
             });
             let should_save = if should_prompt_for_save && !will_autosave {
@@ -2087,10 +2088,11 @@ mod tests {
     use crate::item::test::{TestItem, TestProjectItem};
     use gpui::{executor::Deterministic, TestAppContext};
     use project::FakeFs;
+    use settings::SettingsStore;
 
     #[gpui::test]
     async fn test_remove_active_empty(cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2104,7 +2106,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2192,7 +2194,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2268,7 +2270,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2377,7 +2379,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2424,7 +2426,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2443,7 +2445,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2470,7 +2472,7 @@ mod tests {
         deterministic: Arc<Deterministic>,
         cx: &mut TestAppContext,
     ) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2492,7 +2494,7 @@ mod tests {
         deterministic: Arc<Deterministic>,
         cx: &mut TestAppContext,
     ) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2511,7 +2513,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2531,6 +2533,14 @@ mod tests {
         assert_item_labels(&pane, [], cx);
     }
 
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            cx.set_global(settings::Settings::test(cx));
+            crate::init_settings(cx);
+        });
+    }
+
     fn add_labeled_item(
         workspace: &ViewHandle<Workspace>,
         pane: &ViewHandle<Pane>,

crates/workspace/src/pane_group.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
+use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
@@ -11,7 +11,6 @@ use gpui::{
 };
 use project::Project;
 use serde::Deserialize;
-use settings::Settings;
 use theme::Theme;
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -380,7 +379,8 @@ impl PaneAxis {
             .with_children(self.members.iter().enumerate().map(|(ix, member)| {
                 let mut flex = 1.0;
                 if member.contains(active_pane) {
-                    flex = cx.global::<Settings>().active_pane_magnification;
+                    flex = settings::get_setting::<WorkspaceSettings>(None, cx)
+                        .active_pane_magnification;
                 }
 
                 let mut member = member.render(

crates/workspace/src/persistence.rs 🔗

@@ -497,13 +497,10 @@ impl WorkspaceDb {
 
 #[cfg(test)]
 mod tests {
-
-    use std::sync::Arc;
-
-    use db::open_test_db;
-    use settings::DockAnchor;
-
     use super::*;
+    use crate::DockAnchor;
+    use db::open_test_db;
+    use std::sync::Arc;
 
     #[gpui::test]
     async fn test_next_id_stability() {

crates/workspace/src/persistence/model.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
-    dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
+    dock::DockPosition, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis, Workspace,
+    WorkspaceId,
 };
 use anyhow::{anyhow, Context, Result};
 use async_recursion::async_recursion;
@@ -11,7 +12,6 @@ use gpui::{
     platform::WindowBounds, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WeakViewHandle,
 };
 use project::Project;
-use settings::DockAnchor;
 use std::{
     path::{Path, PathBuf},
     sync::Arc,
@@ -305,10 +305,9 @@ impl Column for DockPosition {
 
 #[cfg(test)]
 mod tests {
-    use db::sqlez::connection::Connection;
-    use settings::DockAnchor;
-
     use super::WorkspaceLocation;
+    use crate::DockAnchor;
+    use db::sqlez::connection::Connection;
 
     #[test]
     fn test_workspace_round_trips() {

crates/workspace/src/workspace.rs 🔗

@@ -13,6 +13,7 @@ pub mod shared_screen;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
+mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use assets::Assets;
@@ -75,7 +76,7 @@ pub use persistence::{
 use postage::prelude::Stream;
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
-use settings::{Autosave, DockAnchor, Settings};
+use settings::Settings;
 use shared_screen::SharedScreen;
 use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
 use status_bar::StatusBar;
@@ -83,6 +84,7 @@ pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::{paths, ResultExt};
+pub use workspace_settings::{AutosaveSetting, DockAnchor, GitGutterSetting, WorkspaceSettings};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -183,7 +185,12 @@ pub type WorkspaceId = i64;
 
 impl_actions!(workspace, [ActivatePane]);
 
+pub fn init_settings(cx: &mut AppContext) {
+    settings::register_setting::<WorkspaceSettings>(cx);
+}
+
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+    init_settings(cx);
     pane::init(cx);
     dock::init(cx);
     notifications::init(cx);
@@ -384,6 +391,7 @@ impl AppState {
         let themes = ThemeRegistry::new((), cx.font_cache().clone());
 
         client::init(&client, cx);
+        crate::init_settings(cx);
 
         Arc::new(Self {
             client,
@@ -672,7 +680,9 @@ impl Workspace {
                 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
             });
         } else if project.read(cx).is_local() {
-            if cx.global::<Settings>().default_dock_anchor != DockAnchor::Expanded {
+            if settings::get_setting::<WorkspaceSettings>(None, cx).default_dock_anchor
+                != DockAnchor::Expanded
+            {
                 Dock::show(&mut this, false, cx);
             }
         }
@@ -2406,8 +2416,8 @@ impl Workspace {
                         item.workspace_deactivated(cx);
                     }
                     if matches!(
-                        cx.global::<Settings>().autosave,
-                        Autosave::OnWindowChange | Autosave::OnFocusChange
+                        settings::get_setting::<WorkspaceSettings>(None, cx).autosave,
+                        AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
                     ) {
                         for item in pane.items() {
                             Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
@@ -3067,7 +3077,7 @@ pub fn join_remote_project(
 }
 
 pub fn restart(_: &Restart, cx: &mut AppContext) {
-    let should_confirm = cx.global::<Settings>().confirm_quit;
+    let should_confirm = settings::get_setting::<WorkspaceSettings>(None, cx).confirm_quit;
     cx.spawn(|mut cx| async move {
         let mut workspaces = cx
             .window_ids()
@@ -3128,20 +3138,18 @@ fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
 
 #[cfg(test)]
 mod tests {
-    use std::{cell::RefCell, rc::Rc};
-
-    use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
-
     use super::*;
+    use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
     use fs::FakeFs;
     use gpui::{executor::Deterministic, TestAppContext};
     use project::{Project, ProjectEntryId};
     use serde_json::json;
+    use settings::SettingsStore;
+    use std::{cell::RefCell, rc::Rc};
 
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
@@ -3189,8 +3197,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_tracking_active_path(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/root1",
@@ -3293,8 +3301,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_window(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
@@ -3329,8 +3337,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_pane_items(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -3436,8 +3444,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3542,9 +3550,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
-        deterministic.forbid_parking();
+        init_test(cx);
 
-        Settings::test_async(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3560,8 +3567,10 @@ mod tests {
 
         // Autosave on window change.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnWindowChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3574,8 +3583,10 @@ mod tests {
         // Autosave on focus change.
         item.update(cx, |item, cx| {
             cx.focus_self();
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnFocusChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3598,8 +3609,10 @@ mod tests {
 
         // Autosave after delay.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+                })
             });
             item.is_dirty = true;
             cx.emit(TestItemEvent::Edit);
@@ -3615,8 +3628,10 @@ mod tests {
 
         // Autosave on focus change, ensuring closing the tab counts as such.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnFocusChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3656,12 +3671,9 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_pane_navigation(
-        deterministic: Arc<Deterministic>,
-        cx: &mut gpui::TestAppContext,
-    ) {
-        deterministic.forbid_parking();
-        Settings::test_async(cx);
+    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3713,4 +3725,14 @@ mod tests {
             assert!(pane.can_navigate_forward());
         });
     }
+
+    pub fn init_test(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            cx.set_global(Settings::test(cx));
+            language::init(cx);
+            crate::init_settings(cx);
+        });
+    }
 }

crates/workspace/src/workspace_settings.rs 🔗

@@ -0,0 +1,103 @@
+use anyhow::bail;
+use db::sqlez::{
+    bindable::{Bind, Column, StaticColumnCount},
+    statement::Statement,
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct WorkspaceSettings {
+    pub active_pane_magnification: f32,
+    pub confirm_quit: bool,
+    pub show_call_status_icon: bool,
+    pub autosave: AutosaveSetting,
+    pub default_dock_anchor: DockAnchor,
+    pub git: GitSettings,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct WorkspaceSettingsContent {
+    pub active_pane_magnification: Option<f32>,
+    pub confirm_quit: Option<bool>,
+    pub show_call_status_icon: Option<bool>,
+    pub autosave: Option<AutosaveSetting>,
+    pub default_dock_anchor: Option<DockAnchor>,
+    pub git: Option<GitSettings>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AutosaveSetting {
+    Off,
+    AfterDelay { milliseconds: u64 },
+    OnFocusChange,
+    OnWindowChange,
+}
+
+#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DockAnchor {
+    #[default]
+    Bottom,
+    Right,
+    Expanded,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+    pub git_gutter: Option<GitGutterSetting>,
+    pub gutter_debounce: Option<u64>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+    #[default]
+    TrackedFiles,
+    Hide,
+}
+
+impl StaticColumnCount for DockAnchor {}
+
+impl Bind for DockAnchor {
+    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
+        match self {
+            DockAnchor::Bottom => "Bottom",
+            DockAnchor::Right => "Right",
+            DockAnchor::Expanded => "Expanded",
+        }
+        .bind(statement, start_index)
+    }
+}
+
+impl Column for DockAnchor {
+    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
+        String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
+            Ok((
+                match anchor_text.as_ref() {
+                    "Bottom" => DockAnchor::Bottom,
+                    "Right" => DockAnchor::Right,
+                    "Expanded" => DockAnchor::Expanded,
+                    _ => bail!("Stored dock anchor is incorrect"),
+                },
+                next_index,
+            ))
+        })
+    }
+}
+
+impl Setting for WorkspaceSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = WorkspaceSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/zed/src/zed.rs 🔗

@@ -37,7 +37,7 @@ use uuid::Uuid;
 pub use workspace;
 use workspace::{
     create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
-    Workspace,
+    Workspace, WorkspaceSettings,
 };
 
 #[derive(Deserialize, Clone, PartialEq)]
@@ -367,7 +367,7 @@ pub fn build_window_options(
 }
 
 fn quit(_: &Quit, cx: &mut gpui::AppContext) {
-    let should_confirm = cx.global::<Settings>().confirm_quit;
+    let should_confirm = settings::get_setting::<WorkspaceSettings>(None, cx).confirm_quit;
     cx.spawn(|mut cx| async move {
         let mut workspaces = cx
             .window_ids()