From 9a6a2d9d2719660b3b5719f311f0643d0a8bbe26 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 9 May 2023 18:14:42 -0700 Subject: [PATCH] Start using the SettingsStore in the app --- 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 | 362 +++++---- 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, 530 insertions(+), 1049 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02370dc98b..266b0e6d41 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 2d42b192d9..77937e8bd0 100644 --- a/crates/copilot_button/Cargo.toml +++ b/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" } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 4b0c9b494a..fec48f1f34 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/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, language: Option>, path: Option>, + fs: Arc, } impl Entity for CopilotButton { @@ -143,7 +145,7 @@ impl View for CopilotButton { } impl CopilotButton { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(fs: Arc, cx: &mut ViewContext) -> 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) { 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) { let settings = cx.global::(); + 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,24 +328,26 @@ 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::() - .copilot - .disabled_globs - .clone() - .iter() - .map(|glob| glob.as_str().to_string()) - .collect::>() - }); + let edits = cx + .global::() + .update::(&text, |file| { + let copilot = file.copilot.get_or_insert_with(Default::default); + let globs = copilot.disabled_globs.get_or_insert_with(|| { + cx.global::() + .copilot + .disabled_globs + .clone() + .iter() + .map(|glob| glob.as_str().to_string()) + .collect::>() + }); - 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| { @@ -356,19 +364,19 @@ async fn configure_disabled_globs( anyhow::Ok(()) } -fn toggle_copilot_globally(cx: &mut AppContext) { +fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { let show_copilot_suggestions = cx.global::().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, cx: &mut AppContext) { +fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { let show_copilot_suggestions = cx .global::() .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, cx: &mut AppContext) { }); } -fn hide_copilot(cx: &mut AppContext) { - SettingsFile::update(cx, move |file_contents| { +fn hide_copilot(fs: Arc, cx: &mut AppContext) { + update_settings_file(fs, cx, move |file_contents| { file_contents.features.copilot = Some(false) }); } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index b38aa7c42d..6d6d303a82 100644 --- a/crates/settings/Cargo.toml +++ b/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 diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index a45a53145e..e0b3f547f9 100644 --- a/crates/settings/src/keymap_file.rs +++ b/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; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 9475d0205f..c3911f9254 100644 --- a/crates/settings/src/settings.rs +++ b/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::>(); + + 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(target: &mut T, value: Option) { *target = value; } } - -pub fn parse_json_with_comments(content: &str) -> Result { - 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, - new_object: &'a serde_json::Map, - text: &str, - syntax_tree: &Tree, - tab_size: usize, - key_path: &mut Vec<&'a str>, - edits: &mut Vec<(Range, 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, 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, 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 { - 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(), - ); - } -} diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 6402a07f5e..17294235ec 100644 --- a/crates/settings/src/settings_file.rs +++ b/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}; +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}; -// 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, - fs: Arc, +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()), + } } -impl SettingsFile { - pub fn new( - path: &'static Path, - settings_file_content: WatchedJsonFile, - fs: Arc, - ) -> Self { - SettingsFile { - path, - settings_file_content, - fs, - } - } +#[cfg(any(test, feature = "test-support"))] +pub fn test_settings() -> String { + let mut value = + parse_json_with_comments::(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() +} - async fn load_settings(path: &Path, fs: &Arc) -> Result { - match fs.load(path).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() { - if e.kind() == ErrorKind::NotFound { - return Ok(Settings::initial_user_settings_content(&Assets).to_string()); +pub fn watch_config_file( + executor: Arc, + fs: Arc, + path: PathBuf, +) -> mpsc::UnboundedReceiver { + 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; } } - return Err(err); + if events.next().await.is_none() { + break; + } + } + }) + .detach(); + rx +} + +pub fn handle_keymap_file_changes( + mut user_keymap_file_rx: mpsc::UnboundedReceiver, + 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::(&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::().base_keymap.clone()); + drop(settings_subscription); + settings_subscription = Some(cx.update(|cx| { + cx.observe_global::(move |cx| { + let settings = cx.global::(); + 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(); +} - pub fn update_unsaved( - text: &str, - cx: &AppContext, - update: impl FnOnce(&mut SettingsFileContent), - ) -> Vec<(Range, String)> { - let this = cx.global::(); - let tab_size = cx.global::().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 handle_settings_file_changes( + mut user_settings_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap(); + cx.update_global::(|store, cx| { + store + .set_user_settings(&user_settings_content, cx) + .log_err(); - pub fn update( - cx: &mut AppContext, - update: impl 'static + Send + FnOnce(&mut SettingsFileContent), - ) { - let this = cx.global::(); - let tab_size = cx.global::().tab_size(Some("JSON")); - let current_file_content = this.settings_file_content.current(); - let fs = this.fs.clone(); - let path = this.path.clone(); + // TODO - remove the Settings global, use the SettingsStore instead. + store.register_setting::(cx); + cx.set_global(store.get::(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::(|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::(None).clone()); + }); + }); + } + }) + .detach(); +} + +async fn load_settings(fs: &Arc) -> Result { + match fs.load(&paths::SETTINGS).await { + result @ Ok(_) => result, + Err(err) => { + if let Some(e) = err.downcast_ref::() { + if e.kind() == ErrorKind::NotFound { + return Ok(Settings::initial_user_settings_content(&Assets).to_string()); + } + } + return Err(err); + } + } +} + +pub fn update_settings_file( + fs: Arc, + 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?; + + let edits = cx.read(|cx| { + cx.global::() + .update::(&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::().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::().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::().clone()); - assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size); - } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 5191d768ea..394d457d3d 100644 --- a/crates/settings/src/settings_store.rs +++ b/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 { local_values: Vec<(Arc, 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; @@ -74,6 +79,7 @@ trait AnySettingValue: Debug { &self, default_value: &DeserializedSetting, custom: &[&DeserializedSetting], + cx: &AppContext, ) -> Box; fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; fn set_global_value(&mut self, value: Box); @@ -89,7 +95,7 @@ struct DeserializedSettingMap { impl SettingsStore { /// Add a new type of setting to the store. - pub fn register_setting(&mut self) { + pub fn register_setting(&mut self, cx: &AppContext) { let setting_type_id = TypeId::of::(); 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(&self, path: Option<&Path>) -> &T { self.setting_values .get(&TypeId::of::()) - .unwrap() + .expect("unregistered setting type") .value_for_path(path) .downcast_ref::() - .unwrap() + .expect("no default value for setting type") } /// Update the value of a setting. @@ -138,7 +146,7 @@ impl SettingsStore { pub fn update( &self, text: &str, - update: impl Fn(&mut T::FileContent), + update: impl FnOnce(&mut T::FileContent), ) -> Vec<(Range, String)> { let setting_type_id = TypeId::of::(); 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, 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, + 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 AnySettingValue for SettingValue { &self, default_value: &DeserializedSetting, user_values: &[&DeserializedSetting], + cx: &AppContext, ) -> Box { let default_value = default_value.0.downcast_ref::().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 { @@ -420,7 +441,9 @@ impl AnySettingValue for SettingValue { } } } - 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) { @@ -436,21 +459,21 @@ impl AnySettingValue for SettingValue { } } -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::>(), - ) - .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::>(), +// ) +// .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(content: &str) -> Result { +pub fn parse_json_with_comments(content: &str) -> Result { 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::(); - store.register_setting::(); - store.register_setting::(); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(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::(); - store.register_setting::(); + store + .set_user_settings(r#"{ "turbo": false }"#, cx) + .unwrap(); + store.register_setting::(cx); + store.register_setting::(cx); assert_eq!(store.get::(None), &TurboSetting(false)); assert_eq!( @@ -846,7 +879,7 @@ mod tests { } ); - store.register_setting::(); + store.register_setting::(cx); assert_eq!( store.get::(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::(); - store.register_setting::(); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); // entries added and updated check_settings_update::( @@ -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::( + &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::( + &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::(&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; - fn load(default_value: &Option, user_values: &[&Option]) -> Self { + fn load( + default_value: &Option, + user_values: &[&Option], + _: &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) } } diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 67a28397e2..d37ac3465b 100644 --- a/crates/theme/Cargo.toml +++ b/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 diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index f9f89b7adc..2bcdb4528c 100644 --- a/crates/theme/src/theme_registry.rs +++ b/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) -> Arc { - 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 + '_ { diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index a404e43f29..ac3a85d89a 100644 --- a/crates/theme_selector/Cargo.toml +++ b/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" } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 21332114e2..a35e546891 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/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.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, cx: &mut AppContext) { pub type ThemeSelector = Picker; pub struct ThemeSelectorDelegate { + fs: Arc, registry: Arc, theme_data: Vec, matches: Vec, @@ -49,7 +52,11 @@ pub struct ThemeSelectorDelegate { } impl ThemeSelectorDelegate { - fn new(registry: Arc, cx: &mut ViewContext) -> Self { + fn new( + fs: Arc, + registry: Arc, + cx: &mut ViewContext, + ) -> Self { let settings = cx.global::(); 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::().theme.meta.name.clone(); - SettingsFile::update(cx, |settings_content| { + update_settings_file(self.fs.clone(), cx, |settings_content| { settings_content.theme = Some(theme_name); }); diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index d35cced642..696a5c5e4e 100644 --- a/crates/welcome/Cargo.toml +++ b/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" } diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 260c279e18..c0e9e0a38d 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/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.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; pub struct BaseKeymapSelectorDelegate { matches: Vec, selected_index: usize, + fs: Arc, } impl BaseKeymapSelectorDelegate { - fn new(cx: &mut ViewContext) -> Self { + fn new(fs: Arc, cx: &mut ViewContext) -> Self { let base = cx.global::().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) { 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); } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index a3d91adc91..c2e65dc524 100644 --- a/crates/welcome/src/welcome.rs +++ b/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() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f498078b52..3d43109e6b 100644 --- a/crates/zed/src/main.rs +++ b/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, -) -> oneshot::Receiver<( - WatchedJsonFile, - WatchedJsonFile, -)> { - 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, IpcSender)> { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f687237bd2..26d2b50e0d 100644 --- a/crates/zed/src/zed.rs +++ b/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 =