Start using the SettingsStore in the app

Max Brunsfeld created

Change summary

Cargo.lock                                  |   3 
crates/copilot_button/Cargo.toml            |   1 
crates/copilot_button/src/copilot_button.rs |  66 +
crates/settings/Cargo.toml                  |   3 
crates/settings/src/keymap_file.rs          |   2 
crates/settings/src/settings.rs             | 794 ++--------------------
crates/settings/src/settings_file.rs        | 360 ++++-----
crates/settings/src/settings_store.rs       | 209 ++++-
crates/theme/Cargo.toml                     |   3 
crates/theme/src/theme_registry.rs          |  15 
crates/theme_selector/Cargo.toml            |   1 
crates/theme_selector/src/theme_selector.rs |  16 
crates/welcome/Cargo.toml                   |   1 
crates/welcome/src/base_keymap_picker.rs    |  19 
crates/welcome/src/welcome.rs               |  24 
crates/zed/src/main.rs                      |  58 -
crates/zed/src/zed.rs                       |   2 
17 files changed, 529 insertions(+), 1,048 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1404,6 +1404,7 @@ dependencies = [
  "context_menu",
  "copilot",
  "editor",
+ "fs",
  "futures 0.3.28",
  "gpui",
  "settings",
@@ -6847,6 +6848,7 @@ name = "theme_selector"
 version = "0.1.0"
 dependencies = [
  "editor",
+ "fs",
  "fuzzy",
  "gpui",
  "log",
@@ -8315,6 +8317,7 @@ dependencies = [
  "anyhow",
  "db",
  "editor",
+ "fs",
  "fuzzy",
  "gpui",
  "install_cli",

crates/copilot_button/Cargo.toml 🔗

@@ -12,6 +12,7 @@ doctest = false
 assets = { path = "../assets" }
 copilot = { path = "../copilot" }
 editor = { path = "../editor" }
+fs = { path = "../fs" }
 context_menu = { path = "../context_menu" }
 gpui = { path = "../gpui" }
 settings = { path = "../settings" }

crates/copilot_button/src/copilot_button.rs 🔗

@@ -2,13 +2,14 @@ use anyhow::Result;
 use context_menu::{ContextMenu, ContextMenuItem};
 use copilot::{Copilot, SignOut, Status};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
 use gpui::{
     elements::*,
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use settings::{settings_file::SettingsFile, Settings};
+use settings::{update_settings_file, Settings, SettingsStore};
 use std::{path::Path, sync::Arc};
 use util::{paths, ResultExt};
 use workspace::{
@@ -26,6 +27,7 @@ pub struct CopilotButton {
     editor_enabled: Option<bool>,
     language: Option<Arc<str>>,
     path: Option<Arc<Path>>,
+    fs: Arc<dyn Fs>,
 }
 
 impl Entity for CopilotButton {
@@ -143,7 +145,7 @@ impl View for CopilotButton {
 }
 
 impl CopilotButton {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
         let button_view_id = cx.view_id();
         let menu = cx.add_view(|cx| {
             let mut menu = ContextMenu::new(button_view_id, cx);
@@ -164,17 +166,19 @@ impl CopilotButton {
             editor_enabled: None,
             language: None,
             path: None,
+            fs,
         }
     }
 
     pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
         let mut menu_options = Vec::with_capacity(2);
+        let fs = self.fs.clone();
 
         menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
             initiate_sign_in(cx)
         }));
-        menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
-            hide_copilot(cx)
+        menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
+            hide_copilot(fs.clone(), cx)
         }));
 
         self.popup_menu.update(cx, |menu, cx| {
@@ -189,10 +193,12 @@ impl CopilotButton {
 
     pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
         let settings = cx.global::<Settings>();
+        let fs = self.fs.clone();
 
         let mut menu_options = Vec::with_capacity(8);
 
         if let Some(language) = self.language.clone() {
+            let fs = fs.clone();
             let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
             menu_options.push(ContextMenuItem::handler(
                 format!(
@@ -200,7 +206,7 @@ impl CopilotButton {
                     if language_enabled { "Hide" } else { "Show" },
                     language
                 ),
-                move |cx| toggle_copilot_for_language(language.clone(), cx),
+                move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
             ));
         }
 
@@ -235,7 +241,7 @@ impl CopilotButton {
             } else {
                 "Show Suggestions for All Files"
             },
-            |cx| toggle_copilot_globally(cx),
+            move |cx| toggle_copilot_globally(fs.clone(), cx),
         ));
 
         menu_options.push(ContextMenuItem::Separator);
@@ -322,25 +328,27 @@ async fn configure_disabled_globs(
     settings_editor.downgrade().update(&mut cx, |item, cx| {
         let text = item.buffer().read(cx).snapshot(cx).text();
 
-        let edits = SettingsFile::update_unsaved(&text, cx, |file| {
-            let copilot = file.copilot.get_or_insert_with(Default::default);
-            let globs = copilot.disabled_globs.get_or_insert_with(|| {
-                cx.global::<Settings>()
-                    .copilot
-                    .disabled_globs
-                    .clone()
-                    .iter()
-                    .map(|glob| glob.as_str().to_string())
-                    .collect::<Vec<_>>()
+        let edits = cx
+            .global::<SettingsStore>()
+            .update::<Settings>(&text, |file| {
+                let copilot = file.copilot.get_or_insert_with(Default::default);
+                let globs = copilot.disabled_globs.get_or_insert_with(|| {
+                    cx.global::<Settings>()
+                        .copilot
+                        .disabled_globs
+                        .clone()
+                        .iter()
+                        .map(|glob| glob.as_str().to_string())
+                        .collect::<Vec<_>>()
+                });
+
+                if let Some(path_to_disable) = &path_to_disable {
+                    globs.push(path_to_disable.to_string_lossy().into_owned());
+                } else {
+                    globs.clear();
+                }
             });
 
-            if let Some(path_to_disable) = &path_to_disable {
-                globs.push(path_to_disable.to_string_lossy().into_owned());
-            } else {
-                globs.clear();
-            }
-        });
-
         if !edits.is_empty() {
             item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
                 selections.select_ranges(edits.iter().map(|e| e.0.clone()));
@@ -356,19 +364,19 @@ async fn configure_disabled_globs(
     anyhow::Ok(())
 }
 
-fn toggle_copilot_globally(cx: &mut AppContext) {
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
     let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
-    SettingsFile::update(cx, move |file_contents| {
+    update_settings_file(fs, cx, move |file_contents| {
         file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
     });
 }
 
-fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
+fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
     let show_copilot_suggestions = cx
         .global::<Settings>()
         .show_copilot_suggestions(Some(&language), None);
 
-    SettingsFile::update(cx, move |file_contents| {
+    update_settings_file(fs, cx, move |file_contents| {
         file_contents.languages.insert(
             language,
             settings::EditorSettings {
@@ -379,8 +387,8 @@ fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
     });
 }
 
-fn hide_copilot(cx: &mut AppContext) {
-    SettingsFile::update(cx, move |file_contents| {
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    update_settings_file(fs, cx, move |file_contents| {
         file_contents.features.copilot = Some(false)
     });
 }

crates/settings/Cargo.toml 🔗

@@ -9,7 +9,7 @@ path = "src/settings.rs"
 doctest = false
 
 [features]
-test-support = []
+test-support = ["theme/test-support", "gpui/test-support", "fs/test-support"]
 
 [dependencies]
 assets = { path = "../assets" }
@@ -39,6 +39,7 @@ tree-sitter-json = "*"
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
 
 pretty_assertions = "1.3.0"
 unindent.workspace = true

crates/settings/src/keymap_file.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{parse_json_with_comments, Settings};
+use crate::{settings_store::parse_json_with_comments, Settings};
 use anyhow::{Context, Result};
 use assets::Assets;
 use collections::BTreeMap;

crates/settings/src/settings.rs 🔗

@@ -1,34 +1,31 @@
 mod keymap_file;
-pub mod settings_file;
-pub mod settings_store;
-pub mod watched_json;
+mod settings_file;
+mod settings_store;
 
-use anyhow::{bail, Result};
+use anyhow::bail;
 use gpui::{
     font_cache::{FamilyId, FontCache},
-    fonts, AssetSource,
+    fonts, AppContext, AssetSource,
 };
-use lazy_static::lazy_static;
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
     JsonSchema,
 };
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde::{Deserialize, Serialize};
 use serde_json::Value;
+use settings_store::Setting;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
-use std::{
-    borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
-};
+use std::{borrow::Cow, collections::HashMap, num::NonZeroU32, path::Path, str, sync::Arc};
 use theme::{Theme, ThemeRegistry};
-use tree_sitter::{Query, Tree};
-use util::{RangeExt, ResultExt as _};
+use util::ResultExt as _;
 
 pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
-pub use watched_json::watch_files;
+pub use settings_file::*;
+pub use settings_store::SettingsStore;
 
 pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
 pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
@@ -69,6 +66,92 @@ pub struct Settings {
     pub base_keymap: BaseKeymap,
 }
 
+impl Setting for Settings {
+    type FileContent = SettingsFileContent;
+
+    fn load(
+        defaults: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &AppContext,
+    ) -> Self {
+        let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
+        let themes = cx.global::<Arc<ThemeRegistry>>();
+
+        let mut this = Self {
+            buffer_font_family: cx
+                .font_cache()
+                .load_family(
+                    &[defaults.buffer_font_family.as_ref().unwrap()],
+                    &buffer_font_features,
+                )
+                .unwrap(),
+            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(),
+            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
+            confirm_quit: defaults.confirm_quit.unwrap(),
+            cursor_blink: defaults.cursor_blink.unwrap(),
+            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
+            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
+            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
+            vim_mode: defaults.vim_mode.unwrap(),
+            autosave: defaults.autosave.unwrap(),
+            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
+            editor_defaults: EditorSettings {
+                tab_size: defaults.editor.tab_size,
+                hard_tabs: defaults.editor.hard_tabs,
+                soft_wrap: defaults.editor.soft_wrap,
+                preferred_line_length: defaults.editor.preferred_line_length,
+                remove_trailing_whitespace_on_save: defaults
+                    .editor
+                    .remove_trailing_whitespace_on_save,
+                ensure_final_newline_on_save: defaults.editor.ensure_final_newline_on_save,
+                format_on_save: defaults.editor.format_on_save.clone(),
+                formatter: defaults.editor.formatter.clone(),
+                enable_language_server: defaults.editor.enable_language_server,
+                show_copilot_suggestions: defaults.editor.show_copilot_suggestions,
+                show_whitespaces: defaults.editor.show_whitespaces,
+            },
+            editor_overrides: Default::default(),
+            copilot: CopilotSettings {
+                disabled_globs: defaults
+                    .copilot
+                    .clone()
+                    .unwrap()
+                    .disabled_globs
+                    .unwrap()
+                    .into_iter()
+                    .map(|s| glob::Pattern::new(&s).unwrap())
+                    .collect(),
+            },
+            git: defaults.git.unwrap(),
+            git_overrides: Default::default(),
+            journal_defaults: defaults.journal.clone(),
+            journal_overrides: Default::default(),
+            terminal_defaults: defaults.terminal.clone(),
+            terminal_overrides: Default::default(),
+            language_defaults: defaults.languages.clone(),
+            language_overrides: Default::default(),
+            lsp: defaults.lsp.clone(),
+            theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
+            telemetry_defaults: defaults.telemetry,
+            telemetry_overrides: Default::default(),
+            auto_update: defaults.auto_update.unwrap(),
+            base_keymap: Default::default(),
+            features: Features {
+                copilot: defaults.features.copilot.unwrap(),
+            },
+        };
+
+        for value in user_values.into_iter().copied().cloned() {
+            this.set_user_settings(value, themes.as_ref(), cx.font_cache());
+        }
+
+        this
+    }
+}
+
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
 pub enum BaseKeymap {
     #[default]
@@ -477,7 +560,7 @@ impl Settings {
             value
         }
 
-        let defaults: SettingsFileContent = parse_json_with_comments(
+        let defaults: SettingsFileContent = settings_store::parse_json_with_comments(
             str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
         )
         .unwrap();
@@ -914,686 +997,3 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
         *target = value;
     }
 }
-
-pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
-    Ok(serde_json::from_reader(
-        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
-    )?)
-}
-
-lazy_static! {
-    static ref PAIR_QUERY: Query = Query::new(
-        tree_sitter_json::language(),
-        "
-            (pair
-                key: (string) @key
-                value: (_) @value)
-        ",
-    )
-    .unwrap();
-}
-
-fn update_object_in_settings_file<'a>(
-    old_object: &'a serde_json::Map<String, Value>,
-    new_object: &'a serde_json::Map<String, Value>,
-    text: &str,
-    syntax_tree: &Tree,
-    tab_size: usize,
-    key_path: &mut Vec<&'a str>,
-    edits: &mut Vec<(Range<usize>, String)>,
-) {
-    for (key, old_value) in old_object.iter() {
-        key_path.push(key);
-        let new_value = new_object.get(key).unwrap_or(&Value::Null);
-
-        // If the old and new values are both objects, then compare them key by key,
-        // preserving the comments and formatting of the unchanged parts. Otherwise,
-        // replace the old value with the new value.
-        if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
-            (old_value, new_value)
-        {
-            update_object_in_settings_file(
-                old_sub_object,
-                new_sub_object,
-                text,
-                syntax_tree,
-                tab_size,
-                key_path,
-                edits,
-            )
-        } else if old_value != new_value {
-            let (range, replacement) =
-                update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
-            edits.push((range, replacement));
-        }
-
-        key_path.pop();
-    }
-}
-
-fn update_key_in_settings_file(
-    text: &str,
-    syntax_tree: &Tree,
-    key_path: &[&str],
-    tab_size: usize,
-    new_value: impl Serialize,
-) -> (Range<usize>, String) {
-    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
-    const LANGUAGES: &'static str = "languages";
-
-    let mut cursor = tree_sitter::QueryCursor::new();
-
-    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
-
-    let mut depth = 0;
-    let mut last_value_range = 0..0;
-    let mut first_key_start = None;
-    let mut existing_value_range = 0..text.len();
-    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
-    for mat in matches {
-        if mat.captures.len() != 2 {
-            continue;
-        }
-
-        let key_range = mat.captures[0].node.byte_range();
-        let value_range = mat.captures[1].node.byte_range();
-
-        // Don't enter sub objects until we find an exact
-        // match for the current keypath
-        if last_value_range.contains_inclusive(&value_range) {
-            continue;
-        }
-
-        last_value_range = value_range.clone();
-
-        if key_range.start > existing_value_range.end {
-            break;
-        }
-
-        first_key_start.get_or_insert_with(|| key_range.start);
-
-        let found_key = text
-            .get(key_range.clone())
-            .map(|key_text| {
-                if key_path[depth] == LANGUAGES && has_language_overrides {
-                    return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
-                } else {
-                    return key_text == format!("\"{}\"", key_path[depth]);
-                }
-            })
-            .unwrap_or(false);
-
-        if found_key {
-            existing_value_range = value_range;
-            // Reset last value range when increasing in depth
-            last_value_range = existing_value_range.start..existing_value_range.start;
-            depth += 1;
-
-            if depth == key_path.len() {
-                break;
-            } else {
-                first_key_start = None;
-            }
-        }
-    }
-
-    // We found the exact key we want, insert the new value
-    if depth == key_path.len() {
-        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
-        (existing_value_range, new_val)
-    } else {
-        // We have key paths, construct the sub objects
-        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
-            LANGUAGE_OVERRIDES
-        } else {
-            key_path[depth]
-        };
-
-        // We don't have the key, construct the nested objects
-        let mut new_value = serde_json::to_value(new_value).unwrap();
-        for key in key_path[(depth + 1)..].iter().rev() {
-            if has_language_overrides && key == &LANGUAGES {
-                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
-            } else {
-                new_value = serde_json::json!({ key.to_string(): new_value });
-            }
-        }
-
-        if let Some(first_key_start) = first_key_start {
-            let mut row = 0;
-            let mut column = 0;
-            for (ix, char) in text.char_indices() {
-                if ix == first_key_start {
-                    break;
-                }
-                if char == '\n' {
-                    row += 1;
-                    column = 0;
-                } else {
-                    column += char.len_utf8();
-                }
-            }
-
-            if row > 0 {
-                // depth is 0 based, but division needs to be 1 based.
-                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
-                let space = ' ';
-                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
-                (first_key_start..first_key_start, content)
-            } else {
-                let new_val = serde_json::to_string(&new_value).unwrap();
-                let mut content = format!(r#""{new_key}": {new_val},"#);
-                content.push(' ');
-                (first_key_start..first_key_start, content)
-            }
-        } else {
-            new_value = serde_json::json!({ new_key.to_string(): new_value });
-            let indent_prefix_len = 4 * depth;
-            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
-            if depth == 0 {
-                new_val.push('\n');
-            }
-
-            (existing_value_range, new_val)
-        }
-    }
-}
-
-fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
-    const SPACES: [u8; 32] = [b' '; 32];
-
-    debug_assert!(indent_size <= SPACES.len());
-    debug_assert!(indent_prefix_len <= SPACES.len());
-
-    let mut output = Vec::new();
-    let mut ser = serde_json::Serializer::with_formatter(
-        &mut output,
-        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
-    );
-
-    value.serialize(&mut ser).unwrap();
-    let text = String::from_utf8(output).unwrap();
-
-    let mut adjusted_text = String::new();
-    for (i, line) in text.split('\n').enumerate() {
-        if i > 0 {
-            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
-        }
-        adjusted_text.push_str(line);
-        adjusted_text.push('\n');
-    }
-    adjusted_text.pop();
-    adjusted_text
-}
-
-/// Update the settings file with the given callback.
-///
-/// Returns a new JSON string and the offset where the first edit occurred.
-fn update_settings_file(
-    text: &str,
-    mut old_file_content: SettingsFileContent,
-    tab_size: NonZeroU32,
-    update: impl FnOnce(&mut SettingsFileContent),
-) -> Vec<(Range<usize>, String)> {
-    let mut new_file_content = old_file_content.clone();
-    update(&mut new_file_content);
-
-    if new_file_content.languages.len() != old_file_content.languages.len() {
-        for language in new_file_content.languages.keys() {
-            old_file_content
-                .languages
-                .entry(language.clone())
-                .or_default();
-        }
-        for language in old_file_content.languages.keys() {
-            new_file_content
-                .languages
-                .entry(language.clone())
-                .or_default();
-        }
-    }
-
-    let mut parser = tree_sitter::Parser::new();
-    parser.set_language(tree_sitter_json::language()).unwrap();
-    let tree = parser.parse(text, None).unwrap();
-
-    let old_object = to_json_object(old_file_content);
-    let new_object = to_json_object(new_file_content);
-    let mut key_path = Vec::new();
-    let mut edits = Vec::new();
-    update_object_in_settings_file(
-        &old_object,
-        &new_object,
-        &text,
-        &tree,
-        tab_size.get() as usize,
-        &mut key_path,
-        &mut edits,
-    );
-    edits.sort_unstable_by_key(|e| e.0.start);
-    return edits;
-}
-
-fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
-    let tmp = serde_json::to_value(settings_file).unwrap();
-    match tmp {
-        Value::Object(map) => map,
-        _ => unreachable!("SettingsFileContent represents a JSON map"),
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use unindent::Unindent;
-
-    fn assert_new_settings(
-        old_json: String,
-        update: fn(&mut SettingsFileContent),
-        expected_new_json: String,
-    ) {
-        let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
-        let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
-        let mut new_json = old_json;
-        for (range, replacement) in edits.into_iter().rev() {
-            new_json.replace_range(range, &replacement);
-        }
-        pretty_assertions::assert_eq!(new_json, expected_new_json);
-    }
-
-    #[test]
-    fn test_update_language_overrides_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "language_overrides": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.languages.insert(
-                    "Rust".into(),
-                    EditorSettings {
-                        show_copilot_suggestions: Some(true),
-                        ..Default::default()
-                    },
-                );
-            },
-            r#"
-                {
-                    "language_overrides": {
-                        "Rust": {
-                            "show_copilot_suggestions": true
-                        },
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_copilot_globs() {
-        assert_new_settings(
-            r#"
-                {
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.copilot = Some(CopilotSettingsContent {
-                    disabled_globs: Some(vec![]),
-                });
-            },
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": []
-                    }
-                }
-            "#
-            .unindent(),
-        );
-
-        assert_new_settings(
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": [
-                            "**/*.json"
-                        ]
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings
-                    .copilot
-                    .get_or_insert(Default::default())
-                    .disabled_globs
-                    .as_mut()
-                    .unwrap()
-                    .push(".env".into());
-            },
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": [
-                            "**/*.json",
-                            ".env"
-                        ]
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.editor.show_copilot_suggestions = Some(true);
-            },
-            r#"
-                {
-                    "show_copilot_suggestions": true,
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_language_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.languages.insert(
-                    "Rust".into(),
-                    EditorSettings {
-                        show_copilot_suggestions: Some(true),
-                        ..Default::default()
-                    },
-                );
-            },
-            r#"
-                {
-                    "languages": {
-                        "Rust": {
-                            "show_copilot_suggestions": true
-                        },
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_multiple_fields() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.telemetry.set_diagnostics(true);
-                settings.telemetry.set_metrics(true);
-            },
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": true,
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_weird_formatting() {
-        assert_new_settings(
-            r#"{
-                "telemetry":   { "metrics": false, "diagnostics": true }
-            }"#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"{
-                "telemetry":   { "metrics": false, "diagnostics": false }
-            }"#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_other_fields() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_empty_telemetry() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {}
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_pre_existing() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting() {
-        assert_new_settings(
-            "{}".into(),
-            |settings| settings.telemetry.set_diagnostics(true),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_object_empty_doc() {
-        assert_new_settings(
-            "".into(),
-            |settings| settings.telemetry.set_diagnostics(true),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_settings_with_theme() {
-        assert_new_settings(
-            r#"
-                {
-                    "theme": "One Dark"
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_empty_settings() {
-        assert_new_settings(
-            r#"
-                {
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn write_key_no_document() {
-        assert_new_settings(
-            "".to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_single_line_settings_without_theme() {
-        assert_new_settings(
-            r#"{ "a": "", "ok": true }"#.to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_pre_object_whitespace() {
-        assert_new_settings(
-            r#"          { "a": "", "ok": true }"#.to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_multi_line_settings_without_theme() {
-        assert_new_settings(
-            r#"
-                {
-                    "a": "b"
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light",
-                    "a": "b"
-                }
-            "#
-            .unindent(),
-        );
-    }
-}

crates/settings/src/settings_file.rs 🔗

@@ -1,88 +1,181 @@
-use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
+use crate::{
+    settings_store::parse_json_with_comments, settings_store::SettingsStore, KeymapFileContent,
+    Settings, SettingsFileContent, DEFAULT_SETTINGS_ASSET_PATH,
+};
 use anyhow::Result;
 use assets::Assets;
 use fs::Fs;
-use gpui::AppContext;
-use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
-
-// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
-//       And instant updates in the Zed editor
-#[derive(Clone)]
-pub struct SettingsFile {
-    path: &'static Path,
-    settings_file_content: WatchedJsonFile<SettingsFileContent>,
+use futures::{channel::mpsc, StreamExt};
+use gpui::{executor::Background, AppContext, AssetSource};
+use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use util::{paths, ResultExt};
+
+pub fn default_settings() -> Cow<'static, str> {
+    match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
+        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_settings() -> String {
+    let mut value =
+        parse_json_with_comments::<serde_json::Value>(default_settings().as_ref()).unwrap();
+    util::merge_non_null_json_value_into(
+        serde_json::json!({
+            "buffer_font_family": "Courier",
+            "buffer_font_features": {},
+            "default_buffer_font_size": 14,
+            "preferred_line_length": 80,
+            "theme": theme::EMPTY_THEME_NAME,
+        }),
+        &mut value,
+    );
+    serde_json::to_string(&value).unwrap()
+}
+
+pub fn watch_config_file(
+    executor: Arc<Background>,
     fs: Arc<dyn Fs>,
+    path: PathBuf,
+) -> mpsc::UnboundedReceiver<String> {
+    let (tx, rx) = mpsc::unbounded();
+    executor
+        .spawn(async move {
+            let events = fs.watch(&path, Duration::from_millis(100)).await;
+            futures::pin_mut!(events);
+            loop {
+                if let Ok(contents) = fs.load(&path).await {
+                    if !tx.unbounded_send(contents).is_ok() {
+                        break;
+                    }
+                }
+                if events.next().await.is_none() {
+                    break;
+                }
+            }
+        })
+        .detach();
+    rx
+}
+
+pub fn handle_keymap_file_changes(
+    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut AppContext,
+) {
+    cx.spawn(move |mut cx| async move {
+        let mut settings_subscription = None;
+        while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
+            if let Ok(keymap_content) =
+                parse_json_with_comments::<KeymapFileContent>(&user_keymap_content)
+            {
+                cx.update(|cx| {
+                    cx.clear_bindings();
+                    KeymapFileContent::load_defaults(cx);
+                    keymap_content.clone().add_to_cx(cx).log_err();
+                });
+
+                let mut old_base_keymap = cx.read(|cx| cx.global::<Settings>().base_keymap.clone());
+                drop(settings_subscription);
+                settings_subscription = Some(cx.update(|cx| {
+                    cx.observe_global::<Settings, _>(move |cx| {
+                        let settings = cx.global::<Settings>();
+                        if settings.base_keymap != old_base_keymap {
+                            old_base_keymap = settings.base_keymap.clone();
+
+                            cx.clear_bindings();
+                            KeymapFileContent::load_defaults(cx);
+                            keymap_content.clone().add_to_cx(cx).log_err();
+                        }
+                    })
+                    .detach();
+                }));
+            }
+        }
+    })
+    .detach();
 }
 
-impl SettingsFile {
-    pub fn new(
-        path: &'static Path,
-        settings_file_content: WatchedJsonFile<SettingsFileContent>,
-        fs: Arc<dyn Fs>,
-    ) -> Self {
-        SettingsFile {
-            path,
-            settings_file_content,
-            fs,
+pub fn handle_settings_file_changes(
+    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut AppContext,
+) {
+    let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
+    cx.update_global::<SettingsStore, _, _>(|store, cx| {
+        store
+            .set_user_settings(&user_settings_content, cx)
+            .log_err();
+
+        // TODO - remove the Settings global, use the SettingsStore instead.
+        store.register_setting::<Settings>(cx);
+        cx.set_global(store.get::<Settings>(None).clone());
+    });
+    cx.spawn(move |mut cx| async move {
+        while let Some(user_settings_content) = user_settings_file_rx.next().await {
+            cx.update(|cx| {
+                cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                    store
+                        .set_user_settings(&user_settings_content, cx)
+                        .log_err();
+
+                    // TODO - remove the Settings global, use the SettingsStore instead.
+                    cx.set_global(store.get::<Settings>(None).clone());
+                });
+            });
         }
-    }
+    })
+    .detach();
+}
 
-    async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
-        match fs.load(path).await {
-            result @ Ok(_) => result,
-            Err(err) => {
-                if let Some(e) = err.downcast_ref::<std::io::Error>() {
-                    if e.kind() == ErrorKind::NotFound {
-                        return Ok(Settings::initial_user_settings_content(&Assets).to_string());
-                    }
+async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
+    match fs.load(&paths::SETTINGS).await {
+        result @ Ok(_) => result,
+        Err(err) => {
+            if let Some(e) = err.downcast_ref::<std::io::Error>() {
+                if e.kind() == ErrorKind::NotFound {
+                    return Ok(Settings::initial_user_settings_content(&Assets).to_string());
                 }
-                return Err(err);
             }
+            return Err(err);
         }
     }
+}
 
-    pub fn update_unsaved(
-        text: &str,
-        cx: &AppContext,
-        update: impl FnOnce(&mut SettingsFileContent),
-    ) -> Vec<(Range<usize>, String)> {
-        let this = cx.global::<SettingsFile>();
-        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
-        let current_file_content = this.settings_file_content.current();
-        update_settings_file(&text, current_file_content, tab_size, update)
-    }
+pub fn update_settings_file(
+    fs: Arc<dyn Fs>,
+    cx: &mut AppContext,
+    update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
+) {
+    cx.spawn(|cx| async move {
+        let old_text = cx
+            .background()
+            .spawn({
+                let fs = fs.clone();
+                async move { load_settings(&fs).await }
+            })
+            .await?;
 
-    pub fn update(
-        cx: &mut AppContext,
-        update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
-    ) {
-        let this = cx.global::<SettingsFile>();
-        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
-        let current_file_content = this.settings_file_content.current();
-        let fs = this.fs.clone();
-        let path = this.path.clone();
+        let edits = cx.read(|cx| {
+            cx.global::<SettingsStore>()
+                .update::<Settings>(&old_text, update)
+        });
+
+        let mut new_text = old_text;
+        for (range, replacement) in edits.into_iter().rev() {
+            new_text.replace_range(range, &replacement);
+        }
 
         cx.background()
-            .spawn(async move {
-                let old_text = SettingsFile::load_settings(path, &fs).await?;
-                let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
-                let mut new_text = old_text;
-                for (range, replacement) in edits.into_iter().rev() {
-                    new_text.replace_range(range, &replacement);
-                }
-                fs.atomic_write(path.to_path_buf(), new_text).await?;
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx)
-    }
+            .spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
+            .await?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{
-        watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
-    };
     use fs::FakeFs;
     use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
     use theme::ThemeRegistry;
@@ -107,7 +200,6 @@ mod tests {
     async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
         let executor = cx.background();
         let fs = FakeFs::new(executor.clone());
-        let font_cache = cx.font_cache();
 
         actions!(test, [A, B]);
         // From the Atom keymap
@@ -145,25 +237,26 @@ mod tests {
         .await
         .unwrap();
 
-        let settings_file =
-            WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
-        let keymaps_file =
-            WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
-
-        let default_settings = cx.read(Settings::test);
-
         cx.update(|cx| {
+            let mut store = SettingsStore::default();
+            store.set_default_settings(&test_settings(), cx).unwrap();
+            cx.set_global(store);
+            cx.set_global(ThemeRegistry::new(Assets, cx.font_cache().clone()));
             cx.add_global_action(|_: &A, _cx| {});
             cx.add_global_action(|_: &B, _cx| {});
             cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
             cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-            watch_files(
-                default_settings,
-                settings_file,
-                ThemeRegistry::new((), font_cache),
-                keymaps_file,
-                cx,
-            )
+
+            let settings_rx = watch_config_file(
+                executor.clone(),
+                fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx =
+                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+            handle_keymap_file_changes(keymap_rx, cx);
+            handle_settings_file_changes(settings_rx, cx);
         });
 
         cx.foreground().run_until_parked();
@@ -255,113 +348,4 @@ mod tests {
             );
         }
     }
-
-    #[gpui::test]
-    async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-        let font_cache = cx.font_cache();
-
-        fs.save(
-            "/settings.json".as_ref(),
-            &r#"
-            {
-                "buffer_font_size": 24,
-                "soft_wrap": "editor_width",
-                "tab_size": 8,
-                "language_overrides": {
-                    "Markdown": {
-                        "tab_size": 2,
-                        "preferred_line_length": 100,
-                        "soft_wrap": "preferred_line_length"
-                    }
-                }
-            }
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-
-        let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
-
-        let default_settings = cx.read(Settings::test).with_language_defaults(
-            "JavaScript",
-            EditorSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        );
-        cx.update(|cx| {
-            watch_settings_file(
-                default_settings.clone(),
-                source,
-                ThemeRegistry::new((), font_cache),
-                cx,
-            )
-        });
-
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, 24.0);
-
-        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
-        assert_eq!(
-            settings.soft_wrap(Some("Markdown")),
-            SoftWrap::PreferredLineLength
-        );
-        assert_eq!(
-            settings.soft_wrap(Some("JavaScript")),
-            SoftWrap::EditorWidth
-        );
-
-        assert_eq!(settings.preferred_line_length(None), 80);
-        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
-        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
-        assert_eq!(settings.tab_size(None).get(), 8);
-        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
-        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
-        fs.save(
-            "/settings.json".as_ref(),
-            &"(garbage)".into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-        // fs.remove_file("/settings.json".as_ref(), Default::default())
-        //     .await
-        //     .unwrap();
-
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, 24.0);
-
-        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
-        assert_eq!(
-            settings.soft_wrap(Some("Markdown")),
-            SoftWrap::PreferredLineLength
-        );
-        assert_eq!(
-            settings.soft_wrap(Some("JavaScript")),
-            SoftWrap::EditorWidth
-        );
-
-        assert_eq!(settings.preferred_line_length(None), 80);
-        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
-        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
-        assert_eq!(settings.tab_size(None).get(), 8);
-        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
-        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
-        fs.remove_file("/settings.json".as_ref(), Default::default())
-            .await
-            .unwrap();
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
-    }
 }

crates/settings/src/settings_store.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Result};
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use gpui::AppContext;
 use lazy_static::lazy_static;
 use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
@@ -18,7 +19,7 @@ use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
 /// A value that can be defined as a user setting.
 ///
 /// Settings can be loaded from a combination of multiple JSON files.
-pub trait Setting: 'static + Debug {
+pub trait Setting: 'static {
     /// The name of a key within the JSON file from which this setting should
     /// be deserialized. If this is `None`, then the setting will be deserialized
     /// from the root object.
@@ -32,7 +33,11 @@ pub trait Setting: 'static + Debug {
     ///
     /// The user values are ordered from least specific (the global settings file)
     /// to most specific (the innermost local settings file).
-    fn load(default_value: &Self::FileContent, user_values: &[&Self::FileContent]) -> Self;
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &AppContext,
+    ) -> Self;
 
     fn load_via_json_merge(
         default_value: &Self::FileContent,
@@ -66,7 +71,7 @@ struct SettingValue<T> {
     local_values: Vec<(Arc<Path>, T)>,
 }
 
-trait AnySettingValue: Debug {
+trait AnySettingValue {
     fn key(&self) -> Option<&'static str>;
     fn setting_type_name(&self) -> &'static str;
     fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
@@ -74,6 +79,7 @@ trait AnySettingValue: Debug {
         &self,
         default_value: &DeserializedSetting,
         custom: &[&DeserializedSetting],
+        cx: &AppContext,
     ) -> Box<dyn Any>;
     fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
     fn set_global_value(&mut self, value: Box<dyn Any>);
@@ -89,7 +95,7 @@ struct DeserializedSettingMap {
 
 impl SettingsStore {
     /// Add a new type of setting to the store.
-    pub fn register_setting<T: Setting>(&mut self) {
+    pub fn register_setting<T: Setting>(&mut self, cx: &AppContext) {
         let setting_type_id = TypeId::of::<T>();
 
         let entry = self.setting_values.entry(setting_type_id);
@@ -112,24 +118,26 @@ impl SettingsStore {
                 }
             }
             if let Some(default_deserialized_value) = default_settings.typed.get(&setting_type_id) {
-                setting_value.set_global_value(
-                    setting_value.load_setting(default_deserialized_value, &user_values_stack),
-                );
+                setting_value.set_global_value(setting_value.load_setting(
+                    default_deserialized_value,
+                    &user_values_stack,
+                    cx,
+                ));
             }
         }
     }
 
     /// Get the value of a setting.
     ///
-    /// Panics if settings have not yet been loaded, or there is no default
+    /// Panics if the given setting type has not been registered, or if there is no
     /// value for this setting.
     pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
         self.setting_values
             .get(&TypeId::of::<T>())
-            .unwrap()
+            .expect("unregistered setting type")
             .value_for_path(path)
             .downcast_ref::<T>()
-            .unwrap()
+            .expect("no default value for setting type")
     }
 
     /// Update the value of a setting.
@@ -138,7 +146,7 @@ impl SettingsStore {
     pub fn update<T: Setting>(
         &self,
         text: &str,
-        update: impl Fn(&mut T::FileContent),
+        update: impl FnOnce(&mut T::FileContent),
     ) -> Vec<(Range<usize>, String)> {
         let setting_type_id = TypeId::of::<T>();
         let old_content = self
@@ -210,7 +218,11 @@ impl SettingsStore {
     /// Set the default settings via a JSON string.
     ///
     /// The string should contain a JSON object with a default value for every setting.
-    pub fn set_default_settings(&mut self, default_settings_content: &str) -> Result<()> {
+    pub fn set_default_settings(
+        &mut self,
+        default_settings_content: &str,
+        cx: &mut AppContext,
+    ) -> Result<()> {
         let deserialized_setting_map = self.load_setting_map(default_settings_content)?;
         if deserialized_setting_map.typed.len() != self.setting_values.len() {
             return Err(anyhow!(
@@ -223,16 +235,20 @@ impl SettingsStore {
             ));
         }
         self.default_deserialized_settings = Some(deserialized_setting_map);
-        self.recompute_values(false, None, None);
+        self.recompute_values(false, None, None, cx);
         Ok(())
     }
 
     /// Set the user settings via a JSON string.
-    pub fn set_user_settings(&mut self, user_settings_content: &str) -> Result<()> {
+    pub fn set_user_settings(
+        &mut self,
+        user_settings_content: &str,
+        cx: &mut AppContext,
+    ) -> Result<()> {
         let user_settings = self.load_setting_map(user_settings_content)?;
         let old_user_settings =
             mem::replace(&mut self.user_deserialized_settings, Some(user_settings));
-        self.recompute_values(true, None, old_user_settings);
+        self.recompute_values(true, None, old_user_settings, cx);
         Ok(())
     }
 
@@ -241,6 +257,7 @@ impl SettingsStore {
         &mut self,
         path: Arc<Path>,
         settings_content: Option<&str>,
+        cx: &mut AppContext,
     ) -> Result<()> {
         let removed_map = if let Some(settings_content) = settings_content {
             self.local_deserialized_settings
@@ -249,7 +266,7 @@ impl SettingsStore {
         } else {
             self.local_deserialized_settings.remove(&path)
         };
-        self.recompute_values(true, Some(&path), removed_map);
+        self.recompute_values(true, Some(&path), removed_map, cx);
         Ok(())
     }
 
@@ -258,6 +275,7 @@ impl SettingsStore {
         user_settings_changed: bool,
         changed_local_path: Option<&Path>,
         old_settings_map: Option<DeserializedSettingMap>,
+        cx: &AppContext,
     ) {
         // Identify all of the setting types that have changed.
         let new_settings_map = if let Some(changed_path) = changed_local_path {
@@ -300,9 +318,11 @@ impl SettingsStore {
 
             // If the global settings file changed, reload the global value for the field.
             if changed_local_path.is_none() {
-                setting_value.set_global_value(
-                    setting_value.load_setting(default_deserialized_value, &user_values_stack),
-                );
+                setting_value.set_global_value(setting_value.load_setting(
+                    default_deserialized_value,
+                    &user_values_stack,
+                    cx,
+                ));
             }
 
             // Reload the local values for the setting.
@@ -344,7 +364,7 @@ impl SettingsStore {
                 // Load the local value for the field.
                 setting_value.set_local_value(
                     path.clone(),
-                    setting_value.load_setting(default_deserialized_value, &user_values_stack),
+                    setting_value.load_setting(default_deserialized_value, &user_values_stack, cx),
                 );
             }
         }
@@ -398,13 +418,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
         &self,
         default_value: &DeserializedSetting,
         user_values: &[&DeserializedSetting],
+        cx: &AppContext,
     ) -> Box<dyn Any> {
         let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
         let values: SmallVec<[&T::FileContent; 6]> = user_values
             .iter()
             .map(|value| value.0.downcast_ref().unwrap())
             .collect();
-        Box::new(T::load(default_value, &values))
+        Box::new(T::load(default_value, &values, cx))
     }
 
     fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting> {
@@ -420,7 +441,9 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
                 }
             }
         }
-        self.global_value.as_ref().unwrap()
+        self.global_value
+            .as_ref()
+            .expect("no default value for setting")
     }
 
     fn set_global_value(&mut self, value: Box<dyn Any>) {
@@ -436,21 +459,21 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
     }
 }
 
-impl Debug for SettingsStore {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        return f
-            .debug_struct("SettingsStore")
-            .field(
-                "setting_value_sets_by_type",
-                &self
-                    .setting_values
-                    .values()
-                    .map(|set| (set.setting_type_name(), set))
-                    .collect::<HashMap<_, _>>(),
-            )
-            .finish_non_exhaustive();
-    }
-}
+// impl Debug for SettingsStore {
+//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+//         return f
+//             .debug_struct("SettingsStore")
+//             .field(
+//                 "setting_value_sets_by_type",
+//                 &self
+//                     .setting_values
+//                     .values()
+//                     .map(|set| (set.setting_type_name(), set))
+//                     .collect::<HashMap<_, _>>(),
+//             )
+//             .finish_non_exhaustive();
+//     }
+// }
 
 fn update_value_in_json_text<'a>(
     text: &str,
@@ -503,14 +526,6 @@ fn update_value_in_json_text<'a>(
     }
 }
 
-lazy_static! {
-    static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
-        tree_sitter_json::language(),
-        "(pair key: (string) @key value: (_) @value)",
-    )
-    .unwrap();
-}
-
 fn replace_value_in_json_text(
     text: &str,
     syntax_tree: &tree_sitter::Tree,
@@ -521,6 +536,14 @@ fn replace_value_in_json_text(
     const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
     const LANGUAGES: &'static str = "languages";
 
+    lazy_static! {
+        static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
+            tree_sitter_json::language(),
+            "(pair key: (string) @key value: (_) @value)",
+        )
+        .unwrap();
+    }
+
     let mut cursor = tree_sitter::QueryCursor::new();
 
     let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
@@ -666,7 +689,7 @@ fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len:
     adjusted_text
 }
 
-fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
     Ok(serde_json::from_reader(
         json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
     )?)
@@ -678,12 +701,12 @@ mod tests {
     use serde_derive::Deserialize;
     use unindent::Unindent;
 
-    #[test]
-    fn test_settings_store_basic() {
+    #[gpui::test]
+    fn test_settings_store_basic(cx: &mut AppContext) {
         let mut store = SettingsStore::default();
-        store.register_setting::<UserSettings>();
-        store.register_setting::<TurboSetting>();
-        store.register_setting::<MultiKeySettings>();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
+        store.register_setting::<MultiKeySettings>(cx);
 
         // error - missing required field in default settings
         store
@@ -695,6 +718,7 @@ mod tests {
                         "staff": false
                     }
                 }"#,
+                cx,
             )
             .unwrap_err();
 
@@ -709,6 +733,7 @@ mod tests {
                         "staff": false
                     }
                 }"#,
+                cx,
             )
             .unwrap_err();
 
@@ -723,6 +748,7 @@ mod tests {
                         "staff": false
                     }
                 }"#,
+                cx,
             )
             .unwrap();
 
@@ -750,6 +776,7 @@ mod tests {
                     "user": { "age": 31 },
                     "key1": "a"
                 }"#,
+                cx,
             )
             .unwrap();
 
@@ -767,12 +794,14 @@ mod tests {
             .set_local_settings(
                 Path::new("/root1").into(),
                 Some(r#"{ "user": { "staff": true } }"#),
+                cx,
             )
             .unwrap();
         store
             .set_local_settings(
                 Path::new("/root1/subdir").into(),
                 Some(r#"{ "user": { "name": "Jane Doe" } }"#),
+                cx,
             )
             .unwrap();
 
@@ -780,6 +809,7 @@ mod tests {
             .set_local_settings(
                 Path::new("/root2").into(),
                 Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
+                cx,
             )
             .unwrap();
 
@@ -816,8 +846,8 @@ mod tests {
         );
     }
 
-    #[test]
-    fn test_setting_store_assign_json_before_register() {
+    #[gpui::test]
+    fn test_setting_store_assign_json_before_register(cx: &mut AppContext) {
         let mut store = SettingsStore::default();
         store
             .set_default_settings(
@@ -830,11 +860,14 @@ mod tests {
                     },
                     "key1": "x"
                 }"#,
+                cx,
             )
             .unwrap();
-        store.set_user_settings(r#"{ "turbo": false }"#).unwrap();
-        store.register_setting::<UserSettings>();
-        store.register_setting::<TurboSetting>();
+        store
+            .set_user_settings(r#"{ "turbo": false }"#, cx)
+            .unwrap();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
 
         assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
         assert_eq!(
@@ -846,7 +879,7 @@ mod tests {
             }
         );
 
-        store.register_setting::<MultiKeySettings>();
+        store.register_setting::<MultiKeySettings>(cx);
         assert_eq!(
             store.get::<MultiKeySettings>(None),
             &MultiKeySettings {
@@ -856,11 +889,12 @@ mod tests {
         );
     }
 
-    #[test]
-    fn test_setting_store_update() {
+    #[gpui::test]
+    fn test_setting_store_update(cx: &mut AppContext) {
         let mut store = SettingsStore::default();
-        store.register_setting::<UserSettings>();
-        store.register_setting::<LanguageSettings>();
+        store.register_setting::<MultiKeySettings>(cx);
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<LanguageSettings>(cx);
 
         // entries added and updated
         check_settings_update::<LanguageSettings>(
@@ -890,6 +924,7 @@ mod tests {
                 }
             }"#
             .unindent(),
+            cx,
         );
 
         // weird formatting
@@ -904,6 +939,33 @@ mod tests {
                 "user":   { "age": 37, "name": "Max", "staff": true }
             }"#
             .unindent(),
+            cx,
+        );
+
+        // single-line formatting, other keys
+        check_settings_update::<MultiKeySettings>(
+            &mut store,
+            r#"{ "one": 1, "two": 2 }"#.unindent(),
+            |settings| settings.key1 = Some("x".into()),
+            r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(),
+            cx,
+        );
+
+        // empty object
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+                "user": {}
+            }"#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }"#
+            .unindent(),
+            cx,
         );
 
         // no content
@@ -918,6 +980,7 @@ mod tests {
             }
             "#
             .unindent(),
+            cx,
         );
     }
 
@@ -926,8 +989,9 @@ mod tests {
         old_json: String,
         update: fn(&mut T::FileContent),
         expected_new_json: String,
+        cx: &mut AppContext,
     ) {
-        store.set_user_settings(&old_json).ok();
+        store.set_user_settings(&old_json, cx).ok();
         let edits = store.update::<T>(&old_json, update);
         let mut new_json = old_json;
         for (range, replacement) in edits.into_iter().rev() {
@@ -954,7 +1018,11 @@ mod tests {
         const KEY: Option<&'static str> = Some("user");
         type FileContent = UserSettingsJson;
 
-        fn load(default_value: &UserSettingsJson, user_values: &[&UserSettingsJson]) -> Self {
+        fn load(
+            default_value: &UserSettingsJson,
+            user_values: &[&UserSettingsJson],
+            _: &AppContext,
+        ) -> Self {
             Self::load_via_json_merge(default_value, user_values)
         }
     }
@@ -966,7 +1034,11 @@ mod tests {
         const KEY: Option<&'static str> = Some("turbo");
         type FileContent = Option<bool>;
 
-        fn load(default_value: &Option<bool>, user_values: &[&Option<bool>]) -> Self {
+        fn load(
+            default_value: &Option<bool>,
+            user_values: &[&Option<bool>],
+            _: &AppContext,
+        ) -> Self {
             Self::load_via_json_merge(default_value, user_values)
         }
     }
@@ -991,6 +1063,7 @@ mod tests {
         fn load(
             default_value: &MultiKeySettingsJson,
             user_values: &[&MultiKeySettingsJson],
+            _: &AppContext,
         ) -> Self {
             Self::load_via_json_merge(default_value, user_values)
         }
@@ -1020,7 +1093,11 @@ mod tests {
 
         type FileContent = JournalSettingsJson;
 
-        fn load(default_value: &JournalSettingsJson, user_values: &[&JournalSettingsJson]) -> Self {
+        fn load(
+            default_value: &JournalSettingsJson,
+            user_values: &[&JournalSettingsJson],
+            _: &AppContext,
+        ) -> Self {
             Self::load_via_json_merge(default_value, user_values)
         }
     }
@@ -1041,7 +1118,7 @@ mod tests {
 
         type FileContent = Self;
 
-        fn load(default_value: &Self, user_values: &[&Self]) -> Self {
+        fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Self {
             Self::load_via_json_merge(default_value, user_values)
         }
     }

crates/theme/Cargo.toml 🔗

@@ -4,6 +4,9 @@ version = "0.1.0"
 edition = "2021"
 publish = false
 
+[features]
+test-support = ["gpui/test-support"]
+
 [lib]
 path = "src/theme.rs"
 doctest = false

crates/theme/src/theme_registry.rs 🔗

@@ -20,15 +20,26 @@ pub struct ThemeRegistry {
     next_theme_id: AtomicUsize,
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
+
 impl ThemeRegistry {
     pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
-        Arc::new(Self {
+        let this = Arc::new(Self {
             assets: Box::new(source),
             themes: Default::default(),
             theme_data: Default::default(),
             next_theme_id: Default::default(),
             font_cache,
-        })
+        });
+
+        #[cfg(any(test, feature = "test-support"))]
+        this.themes.lock().insert(
+            EMPTY_THEME_NAME.to_string(),
+            gpui::fonts::with_font_cache(this.font_cache.clone(), || Arc::new(Theme::default())),
+        );
+
+        this
     }
 
     pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {

crates/theme_selector/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 [dependencies]
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
+fs = { path = "../fs" }
 gpui = { path = "../gpui" }
 picker = { path = "../picker" }
 theme = { path = "../theme" }

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,7 +1,8 @@
+use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, Settings};
+use settings::{update_settings_file, Settings};
 use staff_mode::StaffMode;
 use std::sync::Arc;
 use theme::{Theme, ThemeMeta, ThemeRegistry};
@@ -18,7 +19,8 @@ pub fn init(cx: &mut AppContext) {
 pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
     workspace.toggle_modal(cx, |workspace, cx| {
         let themes = workspace.app_state().themes.clone();
-        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
+        let fs = workspace.app_state().fs.clone();
+        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, themes, cx), cx))
     });
 }
 
@@ -40,6 +42,7 @@ pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
 pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
 
 pub struct ThemeSelectorDelegate {
+    fs: Arc<dyn Fs>,
     registry: Arc<ThemeRegistry>,
     theme_data: Vec<ThemeMeta>,
     matches: Vec<StringMatch>,
@@ -49,7 +52,11 @@ pub struct ThemeSelectorDelegate {
 }
 
 impl ThemeSelectorDelegate {
-    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
+    fn new(
+        fs: Arc<dyn Fs>,
+        registry: Arc<ThemeRegistry>,
+        cx: &mut ViewContext<ThemeSelector>,
+    ) -> Self {
         let settings = cx.global::<Settings>();
 
         let original_theme = settings.theme.clone();
@@ -68,6 +75,7 @@ impl ThemeSelectorDelegate {
             })
             .collect();
         let mut this = Self {
+            fs,
             registry,
             theme_data: theme_names,
             matches,
@@ -121,7 +129,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         self.selection_completed = true;
 
         let theme_name = cx.global::<Settings>().theme.meta.name.clone();
-        SettingsFile::update(cx, |settings_content| {
+        update_settings_file(self.fs.clone(), cx, |settings_content| {
             settings_content.theme = Some(theme_name);
         });
 

crates/welcome/Cargo.toml 🔗

@@ -14,6 +14,7 @@ test-support = []
 anyhow.workspace = true
 log.workspace = true
 editor = { path = "../editor" }
+fs = { path = "../fs" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 db = { path = "../db" }

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -1,5 +1,3 @@
-use std::sync::Arc;
-
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -7,7 +5,9 @@ use gpui::{
     AppContext, Task, ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
+use project::Fs;
+use settings::{update_settings_file, BaseKeymap, Settings};
+use std::sync::Arc;
 use util::ResultExt;
 use workspace::Workspace;
 
@@ -23,8 +23,9 @@ pub fn toggle(
     _: &ToggleBaseKeymapSelector,
     cx: &mut ViewContext<Workspace>,
 ) {
-    workspace.toggle_modal(cx, |_, cx| {
-        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx))
+    workspace.toggle_modal(cx, |workspace, cx| {
+        let fs = workspace.app_state().fs.clone();
+        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
     });
 }
 
@@ -33,10 +34,11 @@ pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
 pub struct BaseKeymapSelectorDelegate {
     matches: Vec<StringMatch>,
     selected_index: usize,
+    fs: Arc<dyn Fs>,
 }
 
 impl BaseKeymapSelectorDelegate {
-    fn new(cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
+    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
         let base = cx.global::<Settings>().base_keymap;
         let selected_index = BaseKeymap::OPTIONS
             .iter()
@@ -45,6 +47,7 @@ impl BaseKeymapSelectorDelegate {
         Self {
             matches: Vec::new(),
             selected_index,
+            fs,
         }
     }
 }
@@ -119,7 +122,9 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
     fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
-            SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
+            update_settings_file(self.fs.clone(), cx, move |settings| {
+                settings.base_keymap = Some(base_keymap)
+            });
         }
         cx.emit(PickerEvent::Dismiss);
     }

crates/welcome/src/welcome.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     elements::{Flex, Label, ParentElement},
     AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
 };
-use settings::{settings_file::SettingsFile, Settings};
+use settings::{update_settings_file, Settings};
 
 use workspace::{
     item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
@@ -169,10 +169,13 @@ impl View for WelcomePage {
                                 metrics,
                                 0,
                                 cx,
-                                |_, checked, cx| {
-                                    SettingsFile::update(cx, move |file| {
-                                        file.telemetry.set_metrics(checked)
-                                    })
+                                |this, checked, cx| {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        let fs = workspace.read(cx).app_state().fs.clone();
+                                        update_settings_file(fs, cx, move |file| {
+                                            file.telemetry.set_metrics(checked)
+                                        })
+                                    }
                                 },
                             )
                             .contained()
@@ -185,10 +188,13 @@ impl View for WelcomePage {
                                 diagnostics,
                                 0,
                                 cx,
-                                |_, checked, cx| {
-                                    SettingsFile::update(cx, move |file| {
-                                        file.telemetry.set_diagnostics(checked)
-                                    })
+                                |this, checked, cx| {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        let fs = workspace.read(cx).app_state().fs.clone();
+                                        update_settings_file(fs, cx, move |file| {
+                                            file.telemetry.set_diagnostics(checked)
+                                        })
+                                    }
                                 },
                             )
                             .contained()

crates/zed/src/main.rs 🔗

@@ -24,8 +24,8 @@ use parking_lot::Mutex;
 use project::Fs;
 use serde::{Deserialize, Serialize};
 use settings::{
-    self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
-    WorkingDirectory,
+    default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file,
+    Settings, SettingsStore, WorkingDirectory,
 };
 use simplelog::ConfigBuilder;
 use smol::process::Command;
@@ -37,6 +37,7 @@ use std::{
     os::unix::prelude::OsStrExt,
     panic,
     path::PathBuf,
+    str,
     sync::{Arc, Weak},
     thread,
     time::Duration,
@@ -46,7 +47,6 @@ use util::http::{self, HttpClient};
 use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
-use settings::watched_json::WatchedJsonFile;
 #[cfg(debug_assertions)]
 use staff_mode::StaffMode;
 use theme::ThemeRegistry;
@@ -75,10 +75,11 @@ fn main() {
     load_embedded_fonts(&app);
 
     let fs = Arc::new(RealFs);
-
     let themes = ThemeRegistry::new(Assets, app.font_cache());
-    let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
-    let config_files = load_config_files(&app, fs.clone());
+    let user_settings_file_rx =
+        watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone());
+    let user_keymap_file_rx =
+        watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone());
 
     let login_shell_env_loaded = if stdout_is_a_pty() {
         Task::ready(())
@@ -126,26 +127,18 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(themes.clone());
 
         #[cfg(debug_assertions)]
         cx.set_global(StaffMode(true));
 
-        let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
-
-        //Setup settings global before binding actions
-        cx.set_global(SettingsFile::new(
-            &paths::SETTINGS,
-            settings_file_content.clone(),
-            fs.clone(),
-        ));
-
-        settings::watch_files(
-            default_settings,
-            settings_file_content,
-            themes.clone(),
-            keymap_file,
-            cx,
-        );
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        handle_settings_file_changes(user_settings_file_rx, cx);
+        handle_keymap_file_changes(user_keymap_file_rx, cx);
 
         if !stdout_is_a_pty() {
             upload_previous_panics(http.clone(), cx);
@@ -585,27 +578,6 @@ async fn watch_themes(
     None
 }
 
-fn load_config_files(
-    app: &App,
-    fs: Arc<dyn Fs>,
-) -> oneshot::Receiver<(
-    WatchedJsonFile<SettingsFileContent>,
-    WatchedJsonFile<KeymapFileContent>,
-)> {
-    let executor = app.background();
-    let (tx, rx) = oneshot::channel();
-    executor
-        .clone()
-        .spawn(async move {
-            let settings_file =
-                WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await;
-            let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await;
-            tx.send((settings_file, keymap_file)).ok()
-        })
-        .detach();
-    rx
-}
-
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {

crates/zed/src/zed.rs 🔗

@@ -323,7 +323,7 @@ pub fn initialize_workspace(
     });
 
     let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
-    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
+    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
     let activity_indicator =