Move Settings to its own crate

Max Brunsfeld and Keith Simmons created

Co-authored-by: Keith Simmons <keith@zed.dev>

Change summary

Cargo.lock                                    |  32 ++
crates/breadcrumbs/Cargo.toml                 |   1 
crates/breadcrumbs/src/breadcrumbs.rs         |   3 
crates/chat_panel/Cargo.toml                  |   1 
crates/chat_panel/src/chat_panel.rs           |   2 
crates/contacts_panel/Cargo.toml              |   1 
crates/contacts_panel/src/contacts_panel.rs   |   3 
crates/diagnostics/Cargo.toml                 |   1 
crates/diagnostics/src/diagnostics.rs         |   3 
crates/diagnostics/src/items.rs               |   3 
crates/editor/Cargo.toml                      |   2 
crates/editor/src/display_map.rs              |   3 
crates/editor/src/editor.rs                   |   9 
crates/editor/src/element.rs                  |   2 
crates/editor/src/items.rs                    |   5 
crates/file_finder/Cargo.toml                 |   1 
crates/file_finder/src/file_finder.rs         |   3 
crates/go_to_line/Cargo.toml                  |   1 
crates/go_to_line/src/go_to_line.rs           |   3 
crates/outline/Cargo.toml                     |   1 
crates/outline/src/outline.rs                 |   3 
crates/project_panel/Cargo.toml               |   1 
crates/project_panel/src/project_panel.rs     |   3 
crates/project_symbols/Cargo.toml             |   1 
crates/project_symbols/src/project_symbols.rs |   3 
crates/search/Cargo.toml                      |   1 
crates/search/src/buffer_search.rs            |   3 
crates/search/src/project_search.rs           |   5 
crates/server/Cargo.toml                      |   1 
crates/server/src/rpc.rs                      |   3 
crates/settings/Cargo.toml                    |  22 +
crates/settings/src/settings.rs               | 171 +++++++++++
crates/theme_selector/Cargo.toml              |   1 
crates/theme_selector/src/theme_selector.rs   |   3 
crates/vim/Cargo.toml                         |   4 
crates/vim/src/vim.rs                         |   3 
crates/workspace/Cargo.toml                   |   3 
crates/workspace/src/lsp_status.rs            |   3 
crates/workspace/src/pane.rs                  |   5 
crates/workspace/src/settings.rs              | 325 ---------------------
crates/workspace/src/status_bar.rs            |   3 
crates/workspace/src/toolbar.rs               |   3 
crates/workspace/src/workspace.rs             |   3 
crates/zed/Cargo.toml                         |   2 
crates/zed/src/main.rs                        |  16 
crates/zed/src/settings_file.rs               | 157 ++++++++++
crates/zed/src/test.rs                        |   2 
crates/zed/src/zed.rs                         |   4 
48 files changed, 465 insertions(+), 369 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -729,6 +729,7 @@ dependencies = [
  "language",
  "project",
  "search",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -881,6 +882,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "time 0.3.7",
  "util",
@@ -1131,6 +1133,7 @@ dependencies = [
  "client",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -1480,6 +1483,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",
@@ -1646,6 +1650,7 @@ dependencies = [
  "rand 0.8.3",
  "rpc",
  "serde",
+ "settings",
  "smallvec",
  "smol",
  "snippet",
@@ -1806,6 +1811,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -2206,6 +2212,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "text",
  "workspace",
 ]
@@ -3255,6 +3262,7 @@ dependencies = [
  "language",
  "ordered-float",
  "postage",
+ "settings",
  "smol",
  "text",
  "workspace",
@@ -3643,6 +3651,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -3659,6 +3668,7 @@ dependencies = [
  "ordered-float",
  "postage",
  "project",
+ "settings",
  "smol",
  "text",
  "util",
@@ -4258,6 +4268,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",
@@ -4406,6 +4417,21 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "settings"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "schemars",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "theme",
+ "toml",
+ "util",
+]
+
 [[package]]
 name = "sha-1"
 version = "0.8.2"
@@ -5142,6 +5168,7 @@ dependencies = [
  "log",
  "parking_lot",
  "postage",
+ "settings",
  "smol",
  "theme",
  "workspace",
@@ -5719,6 +5746,7 @@ dependencies = [
  "language",
  "log",
  "project",
+ "settings",
  "util",
  "workspace",
 ]
@@ -5948,9 +5976,9 @@ dependencies = [
  "parking_lot",
  "postage",
  "project",
- "schemars",
  "serde",
  "serde_json",
+ "settings",
  "smallvec",
  "theme",
  "util",
@@ -6034,6 +6062,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_path_to_error",
+ "settings",
  "simplelog",
  "smallvec",
  "smol",
@@ -6099,6 +6128,7 @@ dependencies = [
  "scrypt",
  "serde",
  "serde_json",
+ "settings",
  "sha-1 0.9.6",
  "sqlx 0.5.5",
  "surf",

crates/breadcrumbs/Cargo.toml 🔗

@@ -14,6 +14,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
 search = { path = "../search" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -6,8 +6,9 @@ use gpui::{
 use language::{Buffer, OutlineItem};
 use project::Project;
 use search::ProjectSearchView;
+use settings::Settings;
 use theme::SyntaxTheme;
-use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub enum Event {
     UpdateLocation,

crates/chat_panel/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 client = { path = "../client" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/chat_panel/src/chat_panel.rs 🔗

@@ -13,10 +13,10 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use postage::prelude::Stream;
+use settings::{Settings, SoftWrap};
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
-use workspace::{settings::SoftWrap, Settings};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 

crates/contacts_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 client = { path = "../client" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4.1", features = ["futures-traits"] }

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -8,7 +8,8 @@ use gpui::{
     Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
     ViewContext,
 };
-use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
+use workspace::{AppState, JoinProject, JoinProjectParams};
+use settings::Settings;
 
 pub struct ContactsPanel {
     contacts: ListState,

crates/diagnostics/Cargo.toml 🔗

@@ -14,6 +14,7 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -25,7 +25,8 @@ use std::{
     sync::Arc,
 };
 use util::TryFutureExt;
-use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
+use settings::Settings;
 
 action!(Deploy);
 

crates/diagnostics/src/items.rs 🔗

@@ -3,7 +3,8 @@ use gpui::{
     elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
 };
 use project::Project;
-use workspace::{Settings, StatusItemView};
+use workspace::{StatusItemView};
+use settings::Settings;
 
 pub struct DiagnosticSummary {
     summary: project::DiagnosticSummary,

crates/editor/Cargo.toml 🔗

@@ -28,6 +28,7 @@ language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }
@@ -54,6 +55,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/editor/src/display_map.rs 🔗

@@ -46,6 +46,7 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<MultiBuffer>,
+        // TODO - remove. read tab_size from settings inside
         tab_size: usize,
         font_id: FontId,
         font_size: f32,
@@ -76,6 +77,8 @@ impl DisplayMap {
         let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
+
+        // TODO: Pull tabsize out of cx and pass it to sync
         let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
         let (wraps_snapshot, edits) = self
             .wrap_map

crates/editor/src/editor.rs 🔗

@@ -41,6 +41,7 @@ pub use multi_buffer::{
 use ordered_float::OrderedFloat;
 use project::{Project, ProjectTransaction};
 use serde::{Deserialize, Serialize};
+use settings::Settings;
 use smallvec::SmallVec;
 use smol::Timer;
 use snippet::Snippet;
@@ -57,7 +58,7 @@ pub use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::DiagnosticStyle;
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{settings, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemNavHistory, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -5669,16 +5670,16 @@ impl Editor {
     }
 
     pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
-        let language = self.language(cx);
+        let language = self.language(cx).map(|language| language.name());
         let settings = cx.global::<Settings>();
         let mode = self
             .soft_wrap_mode_override
-            .unwrap_or_else(|| settings.soft_wrap(language));
+            .unwrap_or_else(|| settings.soft_wrap(language.as_deref()));
         match mode {
             settings::SoftWrap::None => SoftWrap::None,
             settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
             settings::SoftWrap::PreferredLineLength => {
-                SoftWrap::Column(settings.preferred_line_length(language))
+                SoftWrap::Column(settings.preferred_line_length(language.as_deref()))
             }
         }
     }

crates/editor/src/element.rs 🔗

@@ -1494,8 +1494,8 @@ mod tests {
         display_map::{BlockDisposition, BlockProperties},
         Editor, MultiBuffer,
     };
+    use settings::Settings;
     use util::test::sample_text;
-    use workspace::Settings;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {

crates/editor/src/items.rs 🔗

@@ -8,12 +8,11 @@ use gpui::{
 use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
 use project::{File, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
+use settings::Settings;
 use std::{fmt::Write, path::PathBuf, time::Duration};
 use text::{Point, Selection};
 use util::TryFutureExt;
-use workspace::{
-    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
-};
+use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 

crates/file_finder/Cargo.toml 🔗

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }

crates/file_finder/src/file_finder.rs 🔗

@@ -19,8 +19,9 @@ use std::{
 use util::post_inc;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
+use settings::Settings;
 
 pub struct FileFinder {
     handle: WeakViewHandle<Self>,

crates/go_to_line/Cargo.toml 🔗

@@ -11,5 +11,6 @@ doctest = false
 text = { path = "../text" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4", features = ["futures-traits"] }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -3,8 +3,9 @@ use gpui::{
     action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 use text::{Bias, Point};
-use workspace::{Settings, Workspace};
+use workspace::Workspace;
 
 action!(Toggle);
 action!(Confirm);

crates/outline/Cargo.toml 🔗

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"

crates/outline/src/outline.rs 🔗

@@ -13,10 +13,11 @@ use gpui::{
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
+use settings::Settings;
 use std::cmp::{self, Reverse};
 use workspace::{
     menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 action!(Toggle);

crates/project_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/project_panel/src/project_panel.rs 🔗

@@ -10,6 +10,7 @@ use gpui::{
     ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use settings::Settings;
 use std::{
     collections::{hash_map, HashMap},
     ffi::OsStr,
@@ -17,7 +18,7 @@ use std::{
 };
 use workspace::{
     menu::{SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct ProjectPanel {

crates/project_symbols/Cargo.toml 🔗

@@ -13,6 +13,7 @@ fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
 text = { path = "../text" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
 anyhow = "1.0.38"

crates/project_symbols/src/project_symbols.rs 🔗

@@ -11,6 +11,7 @@ use gpui::{
 };
 use ordered_float::OrderedFloat;
 use project::{Project, Symbol};
+use settings::Settings;
 use std::{
     borrow::Cow,
     cmp::{self, Reverse},
@@ -18,7 +19,7 @@ use std::{
 use util::ResultExt;
 use workspace::{
     menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 action!(Toggle);

crates/search/Cargo.toml 🔗

@@ -13,6 +13,7 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/search/src/buffer_search.rs 🔗

@@ -9,7 +9,8 @@ use gpui::{
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
+use settings::Settings;
 
 action!(Deploy, bool);
 action!(Dismiss);

crates/search/src/project_search.rs 🔗

@@ -10,15 +10,14 @@ use gpui::{
     ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use project::{search::SearchQuery, Project};
+use settings::Settings;
 use std::{
     any::{Any, TypeId},
     ops::Range,
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{
-    Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
+use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
 
 action!(Deploy);
 action!(Search);

crates/server/Cargo.toml 🔗

@@ -14,6 +14,7 @@ required-features = ["seed-support"]
 
 [dependencies]
 collections = { path = "../collections" }
+settings = { path = "../settings" }
 rpc = { path = "../rpc" }
 anyhow = "1.0.40"
 async-io = "1.3"

crates/server/src/rpc.rs 🔗

@@ -1104,6 +1104,7 @@ mod tests {
     use rand::prelude::*;
     use rpc::PeerId;
     use serde_json::json;
+    use settings::Settings;
     use sqlx::types::time::OffsetDateTime;
     use std::{
         cell::Cell,
@@ -1117,7 +1118,7 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
+    use workspace::{Item, SplitDirection, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]

crates/settings/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "settings"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/settings.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+anyhow = "1.0.38"
+schemars = "0.8"
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_path_to_error = "0.1.4"
+toml = "0.5"

crates/settings/src/settings.rs 🔗

@@ -0,0 +1,171 @@
+use anyhow::Result;
+use gpui::font_cache::{FamilyId, FontCache};
+use schemars::{schema_for, JsonSchema};
+use serde::Deserialize;
+use std::{collections::HashMap, sync::Arc};
+use theme::{Theme, ThemeRegistry};
+use util::ResultExt as _;
+
+#[derive(Clone)]
+pub struct Settings {
+    pub buffer_font_family: FamilyId,
+    pub buffer_font_size: f32,
+    pub vim_mode: bool,
+    pub tab_size: usize,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    pub theme: Arc<Theme>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct LanguageOverride {
+    pub tab_size: Option<usize>,
+    pub soft_wrap: Option<SoftWrap>,
+    pub preferred_line_length: Option<u32>,
+}
+
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct SettingsFileContent {
+    #[serde(default)]
+    pub buffer_font_family: Option<String>,
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    #[serde(default)]
+    pub vim_mode: Option<bool>,
+    #[serde(flatten)]
+    pub editor: LanguageOverride,
+    #[serde(default)]
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    #[serde(default)]
+    pub theme: Option<String>,
+}
+
+impl Settings {
+    pub fn new(
+        buffer_font_family: &str,
+        font_cache: &FontCache,
+        theme: Arc<Theme>,
+    ) -> Result<Self> {
+        Ok(Self {
+            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
+            buffer_font_size: 15.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme,
+        })
+    }
+
+    pub fn file_json_schema() -> serde_json::Value {
+        serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
+    }
+
+    pub fn with_overrides(
+        mut self,
+        language_name: impl Into<Arc<str>>,
+        overrides: LanguageOverride,
+    ) -> Self {
+        self.language_overrides
+            .insert(language_name.into(), overrides);
+        self
+    }
+
+    pub fn tab_size(&self, language: Option<&str>) -> usize {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.tab_size)
+            .unwrap_or(self.tab_size)
+    }
+
+    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.soft_wrap)
+            .unwrap_or(self.soft_wrap)
+    }
+
+    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.preferred_line_length)
+            .unwrap_or(self.preferred_line_length)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &gpui::AppContext) -> Settings {
+        Settings {
+            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+            buffer_font_size: 14.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
+        }
+    }
+
+    pub fn merge(
+        &mut self,
+        data: &SettingsFileContent,
+        theme_registry: &ThemeRegistry,
+        font_cache: &FontCache,
+    ) {
+        if let Some(value) = &data.buffer_font_family {
+            if let Some(id) = font_cache.load_family(&[value]).log_err() {
+                self.buffer_font_family = id;
+            }
+        }
+        if let Some(value) = &data.theme {
+            if let Some(theme) = theme_registry.get(value).log_err() {
+                self.theme = theme;
+            }
+        }
+
+        merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.vim_mode, data.vim_mode);
+        merge(&mut self.soft_wrap, data.editor.soft_wrap);
+        merge(&mut self.tab_size, data.editor.tab_size);
+        merge(
+            &mut self.preferred_line_length,
+            data.editor.preferred_line_length,
+        );
+
+        for (language_name, settings) in &data.language_overrides {
+            let target = self
+                .language_overrides
+                .entry(language_name.clone())
+                .or_default();
+
+            merge_option(&mut target.tab_size, settings.tab_size);
+            merge_option(&mut target.soft_wrap, settings.soft_wrap);
+            merge_option(
+                &mut target.preferred_line_length,
+                settings.preferred_line_length,
+            );
+        }
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}
+
+fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
+    if value.is_some() {
+        *target = value;
+    }
+}

crates/theme_selector/Cargo.toml 🔗

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 theme = { path = "../theme" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 log = "0.4"
 parking_lot = "0.11.1"

crates/theme_selector/src/theme_selector.rs 🔗

@@ -9,9 +9,10 @@ use gpui::{
 };
 use std::{cmp, sync::Arc};
 use theme::{Theme, ThemeRegistry};
+use settings::Settings;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct ThemeSelector {

crates/vim/Cargo.toml 🔗

@@ -12,6 +12,7 @@ collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 log = "0.4"
 
@@ -19,7 +20,8 @@ log = "0.4"
 indoc = "1.0.4"
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
+settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/vim/src/vim.rs 🔗

@@ -10,7 +10,8 @@ use editor::{CursorShape, Editor};
 use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
 
 use mode::Mode;
-use workspace::{self, Settings, Workspace};
+use settings::Settings;
+use workspace::{self, Workspace};
 
 action!(SwitchMode, Mode);
 

crates/workspace/Cargo.toml 🔗

@@ -17,6 +17,7 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
@@ -24,7 +25,6 @@ futures = "0.3"
 log = "0.4"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
-schemars = "0.8"
 serde = { version = "1", features = ["derive", "rc"] }
 serde_json = { version = "1", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
@@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] }
 client = { path = "../client", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }

crates/workspace/src/lsp_status.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ItemHandle, Settings, StatusItemView};
+use crate::{ItemHandle, StatusItemView};
 use futures::StreamExt;
 use gpui::AppContext;
 use gpui::{
@@ -7,6 +7,7 @@ use gpui::{
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::cmp::Reverse;
 use std::fmt::Write;

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,5 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
 use anyhow::Result;
 use collections::{HashMap, VecDeque};
 use futures::StreamExt;
@@ -12,7 +12,8 @@ use gpui::{
     AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
-use project::{ProjectEntryId, ProjectPath};
+use project::{Project, ProjectEntryId, ProjectPath};
+use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use util::ResultExt;
 

crates/workspace/src/settings.rs 🔗

@@ -1,325 +0,0 @@
-use anyhow::Result;
-use futures::{stream, SinkExt, StreamExt as _};
-use gpui::{
-    executor,
-    font_cache::{FamilyId, FontCache},
-};
-use language::Language;
-use postage::{prelude::Stream, watch};
-use project::Fs;
-use schemars::{schema_for, JsonSchema};
-use serde::Deserialize;
-use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
-use theme::{Theme, ThemeRegistry};
-use util::ResultExt;
-
-#[derive(Clone)]
-pub struct Settings {
-    pub buffer_font_family: FamilyId,
-    pub buffer_font_size: f32,
-    pub vim_mode: bool,
-    pub tab_size: usize,
-    pub soft_wrap: SoftWrap,
-    pub preferred_line_length: u32,
-    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    pub theme: Arc<Theme>,
-}
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-pub struct LanguageOverride {
-    pub tab_size: Option<usize>,
-    pub soft_wrap: Option<SoftWrap>,
-    pub preferred_line_length: Option<u32>,
-}
-
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
-    None,
-    EditorWidth,
-    PreferredLineLength,
-}
-
-#[derive(Clone)]
-pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-struct SettingsFileContent {
-    #[serde(default)]
-    buffer_font_family: Option<String>,
-    #[serde(default)]
-    buffer_font_size: Option<f32>,
-    #[serde(default)]
-    vim_mode: Option<bool>,
-    #[serde(flatten)]
-    editor: LanguageOverride,
-    #[serde(default)]
-    language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    #[serde(default)]
-    theme: Option<String>,
-}
-
-impl SettingsFile {
-    pub async fn new(
-        fs: Arc<dyn Fs>,
-        executor: &executor::Background,
-        path: impl Into<Arc<Path>>,
-    ) -> Self {
-        let path = path.into();
-        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
-        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
-        let (mut tx, rx) = watch::channel_with(settings);
-        executor
-            .spawn(async move {
-                while events.next().await.is_some() {
-                    if let Some(settings) = Self::load(fs.clone(), &path).await {
-                        if tx.send(settings).await.is_err() {
-                            break;
-                        }
-                    }
-                }
-            })
-            .detach();
-        Self(rx)
-    }
-
-    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
-        if fs.is_file(&path).await {
-            fs.load(&path)
-                .await
-                .log_err()
-                .and_then(|data| serde_json::from_str(&data).log_err())
-        } else {
-            Some(SettingsFileContent::default())
-        }
-    }
-}
-
-impl Settings {
-    pub fn file_json_schema() -> serde_json::Value {
-        serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
-    }
-
-    pub fn from_files(
-        defaults: Self,
-        sources: Vec<SettingsFile>,
-        theme_registry: Arc<ThemeRegistry>,
-        font_cache: Arc<FontCache>,
-    ) -> impl futures::stream::Stream<Item = Self> {
-        stream::select_all(sources.iter().enumerate().map(|(i, source)| {
-            let mut rx = source.0.clone();
-            // Consume the initial item from all of the constituent file watches but one.
-            // This way, the stream will yield exactly one item for the files' initial
-            // state, and won't return any more items until the files change.
-            if i > 0 {
-                rx.try_recv().ok();
-            }
-            rx
-        }))
-        .map(move |_| {
-            let mut settings = defaults.clone();
-            for source in &sources {
-                settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
-            }
-            settings
-        })
-    }
-
-    pub fn new(
-        buffer_font_family: &str,
-        font_cache: &FontCache,
-        theme: Arc<Theme>,
-    ) -> Result<Self> {
-        Ok(Self {
-            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
-            buffer_font_size: 15.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme,
-        })
-    }
-
-    pub fn with_overrides(
-        mut self,
-        language_name: impl Into<Arc<str>>,
-        overrides: LanguageOverride,
-    ) -> Self {
-        self.language_overrides
-            .insert(language_name.into(), overrides);
-        self
-    }
-
-    pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.tab_size)
-            .unwrap_or(self.tab_size)
-    }
-
-    pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.soft_wrap)
-            .unwrap_or(self.soft_wrap)
-    }
-
-    pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.preferred_line_length)
-            .unwrap_or(self.preferred_line_length)
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &gpui::AppContext) -> Settings {
-        Settings {
-            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
-            buffer_font_size: 14.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
-        }
-    }
-
-    fn merge(
-        &mut self,
-        data: &SettingsFileContent,
-        theme_registry: &ThemeRegistry,
-        font_cache: &FontCache,
-    ) {
-        if let Some(value) = &data.buffer_font_family {
-            if let Some(id) = font_cache.load_family(&[value]).log_err() {
-                self.buffer_font_family = id;
-            }
-        }
-        if let Some(value) = &data.theme {
-            if let Some(theme) = theme_registry.get(value).log_err() {
-                self.theme = theme;
-            }
-        }
-
-        merge(&mut self.buffer_font_size, data.buffer_font_size);
-        merge(&mut self.vim_mode, data.vim_mode);
-        merge(&mut self.soft_wrap, data.editor.soft_wrap);
-        merge(&mut self.tab_size, data.editor.tab_size);
-        merge(
-            &mut self.preferred_line_length,
-            data.editor.preferred_line_length,
-        );
-
-        for (language_name, settings) in &data.language_overrides {
-            let target = self
-                .language_overrides
-                .entry(language_name.clone())
-                .or_default();
-
-            merge_option(&mut target.tab_size, settings.tab_size);
-            merge_option(&mut target.soft_wrap, settings.soft_wrap);
-            merge_option(
-                &mut target.preferred_line_length,
-                settings.preferred_line_length,
-            );
-        }
-    }
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
-
-fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
-    if value.is_some() {
-        *target = value;
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use project::FakeFs;
-
-    #[gpui::test]
-    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-
-        fs.save(
-            "/settings1.json".as_ref(),
-            &r#"
-            {
-                "buffer_font_size": 24,
-                "soft_wrap": "editor_width",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 100,
-                        "soft_wrap": "preferred_line_length"
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
-        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
-        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
-
-        let mut settings_rx = Settings::from_files(
-            cx.read(Settings::test),
-            vec![source1, source2, source3],
-            ThemeRegistry::new((), cx.font_cache()),
-            cx.font_cache(),
-        );
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 4);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(100));
-
-        fs.save(
-            "/settings2.json".as_ref(),
-            &r#"
-            {
-                "tab_size": 2,
-                "soft_wrap": "none",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 120
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::None);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 2);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(120));
-
-        fs.remove_file("/settings2.json".as_ref(), Default::default())
-            .await
-            .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        assert_eq!(settings.tab_size, 4);
-    }
-}

crates/workspace/src/status_bar.rs 🔗

@@ -1,4 +1,5 @@
-use crate::{ItemHandle, Pane, Settings};
+use crate::{ItemHandle, Pane};
+use settings::Settings;
 use gpui::{
     elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
     View, ViewContext, ViewHandle,

crates/workspace/src/toolbar.rs 🔗

@@ -1,8 +1,9 @@
-use crate::{ItemHandle, Settings};
+use crate::ItemHandle;
 use gpui::{
     elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
     View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 
 pub trait ToolbarItemView: View {
     fn set_active_pane_item(

crates/workspace/src/workspace.rs 🔗

@@ -2,7 +2,6 @@ pub mod lsp_status;
 pub mod menu;
 pub mod pane;
 pub mod pane_group;
-pub mod settings;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
@@ -31,7 +30,7 @@ pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
-pub use settings::Settings;
+use settings::Settings;
 use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;

crates/zed/Cargo.toml 🔗

@@ -51,6 +51,7 @@ project = { path = "../project" }
 project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
 theme = { path = "../theme" }
@@ -111,6 +112,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 env_logger = "0.8"

crates/zed/src/main.rs 🔗

@@ -9,17 +9,19 @@ use gpui::{App, AssetSource, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
+use settings::{self, Settings};
 use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
-use workspace::{
-    self,
-    settings::{self, SettingsFile},
-    AppState, OpenNew, OpenParams, OpenPaths, Settings,
-};
+use workspace::{self, AppState, OpenNew, OpenParams, OpenPaths};
 use zed::{
-    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
+    self,
+    assets::Assets,
+    build_window_options, build_workspace,
+    fs::RealFs,
+    languages, menus,
+    settings_file::{settings_from_files, SettingsFile},
 };
 
 fn main() {
@@ -97,7 +99,7 @@ fn main() {
         .detach_and_log_err(cx);
 
         let settings_file = cx.background().block(settings_file).unwrap();
-        let mut settings_rx = Settings::from_files(
+        let mut settings_rx = settings_from_files(
             default_settings,
             vec![settings_file],
             themes.clone(),

crates/zed/src/settings_file.rs 🔗

@@ -0,0 +1,157 @@
+use futures::{stream, StreamExt};
+use gpui::{executor, FontCache};
+use postage::sink::Sink as _;
+use postage::{prelude::Stream, watch};
+use project::Fs;
+use settings::{Settings, SettingsFileContent};
+use std::{path::Path, sync::Arc, time::Duration};
+use theme::ThemeRegistry;
+use util::ResultExt;
+
+#[derive(Clone)]
+pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
+
+impl SettingsFile {
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        executor: &executor::Background,
+        path: impl Into<Arc<Path>>,
+    ) -> Self {
+        let path = path.into();
+        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
+        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
+        let (mut tx, rx) = watch::channel_with(settings);
+        executor
+            .spawn(async move {
+                while events.next().await.is_some() {
+                    if let Some(settings) = Self::load(fs.clone(), &path).await {
+                        if tx.send(settings).await.is_err() {
+                            break;
+                        }
+                    }
+                }
+            })
+            .detach();
+        Self(rx)
+    }
+
+    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
+        if fs.is_file(&path).await {
+            fs.load(&path)
+                .await
+                .log_err()
+                .and_then(|data| serde_json::from_str(&data).log_err())
+        } else {
+            Some(SettingsFileContent::default())
+        }
+    }
+}
+
+pub fn settings_from_files(
+    defaults: Settings,
+    sources: Vec<SettingsFile>,
+    theme_registry: Arc<ThemeRegistry>,
+    font_cache: Arc<FontCache>,
+) -> impl futures::stream::Stream<Item = Settings> {
+    stream::select_all(sources.iter().enumerate().map(|(i, source)| {
+        let mut rx = source.0.clone();
+        // Consume the initial item from all of the constituent file watches but one.
+        // This way, the stream will yield exactly one item for the files' initial
+        // state, and won't return any more items until the files change.
+        if i > 0 {
+            rx.try_recv().ok();
+        }
+        rx
+    }))
+    .map(move |_| {
+        let mut settings = defaults.clone();
+        for source in &sources {
+            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
+        }
+        settings
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use project::FakeFs;
+    use settings::SoftWrap;
+
+    #[gpui::test]
+    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        fs.save(
+            "/settings1.json".as_ref(),
+            &r#"
+            {
+                "buffer_font_size": 24,
+                "soft_wrap": "editor_width",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 100,
+                        "soft_wrap": "preferred_line_length"
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
+        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
+        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
+
+        let mut settings_rx = settings_from_files(
+            cx.read(Settings::test),
+            vec![source1, source2, source3],
+            ThemeRegistry::new((), cx.font_cache()),
+            cx.font_cache(),
+        );
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 4);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(100));
+
+        fs.save(
+            "/settings2.json".as_ref(),
+            &r#"
+            {
+                "tab_size": 2,
+                "soft_wrap": "none",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 120
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::None);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 2);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(120));
+
+        fs.remove_file("/settings2.json".as_ref(), Default::default())
+            .await
+            .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        assert_eq!(settings.tab_size, 4);
+    }
+}

crates/zed/src/test.rs 🔗

@@ -3,9 +3,9 @@ use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
 use gpui::MutableAppContext;
 use language::LanguageRegistry;
 use project::fs::FakeFs;
+use settings::Settings;
 use std::sync::Arc;
 use theme::ThemeRegistry;
-use workspace::Settings;
 
 #[cfg(test)]
 #[ctor::ctor]

crates/zed/src/zed.rs 🔗

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod languages;
 pub mod menus;
+pub mod settings_file;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
@@ -23,9 +24,10 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
+use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
 pub use workspace;
-use workspace::{AppState, Settings, Workspace, WorkspaceParams};
+use workspace::{AppState, Workspace, WorkspaceParams};
 
 action!(About);
 action!(Quit);