From 9edebc58aa47e85f1be5deb650a9f038e3caa6be Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 14 Sep 2025 22:45:31 -0600 Subject: [PATCH] TEMP --- crates/languages/src/lib.rs | 7 +- crates/settings/src/base_keymap_setting.rs | 48 +- .../settings/src/editable_setting_control.rs | 10 +- crates/settings/src/settings.rs | 1 + crates/settings/src/settings_content.rs | 972 +++++++ crates/settings/src/settings_file.rs | 8 +- crates/settings/src/settings_store.rs | 2446 ++++++++--------- 7 files changed, 2115 insertions(+), 1377 deletions(-) create mode 100644 crates/settings/src/settings_content.rs diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index f71234a7d5c5219b679acdd12610da4fe6e857ad..f83e9b3b8620dc9887d49313522eec2603c4273e 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -311,7 +311,12 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime cx.update(|cx| { SettingsStore::update_global(cx, |settings, cx| { settings - .set_extension_settings(language_settings.clone(), cx) + .set_extension_settings( + ExtensionsSettingsContent { + languages: language_settings.clone(), + }, + cx, + ) .log_err(); }); })?; diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index a6bfeecbc3c01eb5309221443d1b9905b99dcd5b..599bdff9457c1b7eb947c28a1967fa2069ea11a5 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,9 +1,12 @@ use std::fmt::{Display, Formatter}; -use crate::{self as settings}; +use crate::{ + self as settings, + settings_content::{self, BaseKeymapContent, SettingsContent}, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings::{Settings, VsCodeSettings}; use settings_ui_macros::{SettingsKey, SettingsUi}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. @@ -24,6 +27,21 @@ pub enum BaseKeymap { None, } +impl From for BaseKeymap { + fn from(value: BaseKeymapContent) -> Self { + match value { + BaseKeymapContent::VSCode => Self::VSCode, + BaseKeymapContent::JetBrains => Self::JetBrains, + BaseKeymapContent::SublimeText => Self::SublimeText, + BaseKeymapContent::Atom => Self::Atom, + BaseKeymapContent::TextMate => Self::TextMate, + BaseKeymapContent::Emacs => Self::Emacs, + BaseKeymapContent::Cursor => Self::Cursor, + BaseKeymapContent::None => Self::None, + } + } +} + impl Display for BaseKeymap { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -120,25 +138,17 @@ pub struct BaseKeymapSetting { } impl Settings for BaseKeymap { - type FileContent = BaseKeymapSetting; + fn from_file(s: &crate::settings_content::SettingsContent) -> Option { + s.base_keymap.map(Into::into) + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - if let Some(Some(user_value)) = sources.user.map(|setting| setting.base_keymap) { - return Ok(user_value); - } - if let Some(Some(server_value)) = sources.server.map(|setting| setting.base_keymap) { - return Ok(server_value); - } - sources - .default - .base_keymap - .ok_or_else(Self::missing_default) + fn refine(&mut self, s: &settings_content::SettingsContent) { + if let Some(base_keymap) = s.base_keymap { + *self = base_keymap.into(); + }; } - fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) { - current.base_keymap = Some(BaseKeymap::VSCode); + fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut SettingsContent) { + current.base_keymap = Some(BaseKeymapContent::VSCode); } } diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs index 0a5b3e9be0f93e9e2c0a20386d68b644af883be9..40244c10c4e5789e56a8458e2cc0e7737f14a2d9 100644 --- a/crates/settings/src/editable_setting_control.rs +++ b/crates/settings/src/editable_setting_control.rs @@ -1,7 +1,7 @@ use fs::Fs; use gpui::{App, RenderOnce, SharedString}; -use crate::{Settings, update_settings_file}; +use crate::{Settings, settings_content::SettingsContent, update_settings_file}; /// A UI control that can be used to edit a setting. pub trait EditableSettingControl: RenderOnce { @@ -20,17 +20,13 @@ pub trait EditableSettingControl: RenderOnce { /// Applies the given setting file to the settings file contents. /// /// This will be called when writing the setting value back to the settings file. - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - cx: &App, - ); + fn apply(settings: &mut SettingsContent, value: Self::Value, cx: &App); /// Writes the given setting value to the settings files. fn write(value: Self::Value, cx: &App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, cx| { Self::apply(settings, value, cx); }); } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8a50b1afe5d0c68365efe0652421937f6dad2783..c02ea63e36aff49fc8b6b61423367cf862b9e544 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,6 +1,7 @@ mod base_keymap_setting; mod editable_setting_control; mod keymap_file; +mod settings_content; mod settings_file; mod settings_json; mod settings_store; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs new file mode 100644 index 0000000000000000000000000000000000000000..d5c11da66767a8016509d31d81a7f5b0754904bd --- /dev/null +++ b/crates/settings/src/settings_content.rs @@ -0,0 +1,972 @@ +use std::borrow::Cow; +use std::env; +use std::num::NonZeroU32; +use std::sync::Arc; + +use collections::{HashMap, HashSet}; +use gpui::{App, Modifiers, SharedString}; +use release_channel::ReleaseChannel; +use schemars::{JsonSchema, json_schema}; +use serde::de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use util::schemars::replace_subschema; +use util::serde::default_true; + +use crate::{ActiveSettingsProfileName, ParameterizedJsonSchema, Settings}; + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +pub struct SettingsContent { + #[serde(flatten)] + pub project: ProjectSettingsContent, + + pub base_keymap: Option, +} + +impl SettingsContent { + pub fn languages_mut(&mut self) -> &mut HashMap { + &mut self.project.all_languages.languages.0 + } +} + +// todo!() what should this be? +#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ServerSettingsContent { + #[serde(flatten)] + project: ProjectSettingsContent, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +pub(crate) struct UserSettingsContent { + #[serde(flatten)] + pub(crate) content: SettingsContent, + + pub(crate) dev: Option, + pub(crate) nightly: Option, + pub(crate) preview: Option, + pub(crate) stable: Option, + + pub(crate) macos: Option, + pub(crate) windows: Option, + pub(crate) linux: Option, + + #[serde(default)] + pub(crate) profiles: HashMap, +} + +pub struct ExtensionsSettingsContent { + pub(crate) all_languages: AllLanguageSettingsContent, +} + +impl UserSettingsContent { + pub(crate) fn for_release_channel(&self) -> Option<&SettingsContent> { + match *release_channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => self.dev.as_ref(), + ReleaseChannel::Nightly => self.nightly.as_ref(), + ReleaseChannel::Preview => self.preview.as_ref(), + ReleaseChannel::Stable => self.stable.as_ref(), + } + } + + pub(crate) fn for_os(&self) -> Option<&SettingsContent> { + match env::consts::OS { + "macos" => self.macos.as_ref(), + "linux" => self.linux.as_ref(), + "windows" => self.windows.as_ref(), + _ => None, + } + } + + pub(crate) fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { + let Some(active_profile) = cx.try_global::() else { + return None; + }; + self.profiles.get(&active_profile.0) + } +} + +/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. +/// +/// Default: VSCode +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymapContent { + #[default] + VSCode, + JetBrains, + SublimeText, + Atom, + TextMate, + Emacs, + Cursor, + None, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ProjectSettingsContent { + #[serde(flatten)] + pub(crate) all_languages: AllLanguageSettingsContent, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct AllLanguageSettingsContent { + /// The settings for enabling/disabling features. + #[serde(default)] + pub features: Option, + /// The edit prediction settings. + #[serde(default)] + pub edit_predictions: Option, + /// The default language settings. + #[serde(flatten)] + pub defaults: LanguageSettingsContent, + /// The settings for individual languages. + #[serde(default)] + pub languages: LanguageToSettingsMap, + /// Settings for associating file extensions and filenames + /// with languages. + #[serde(default)] + pub file_types: HashMap>, +} + +/// The settings for enabling/disabling features. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct FeaturesContent { + /// Determines which edit prediction provider to use. + pub edit_prediction_provider: Option, +} + +/// The provider that supplies edit predictions. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum EditPredictionProviderContent { + None, + #[default] + Copilot, + Supermaven, + Zed, +} + +/// The contents of the edit prediction settings. +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct EditPredictionSettingsContent { + /// A list of globs representing files that edit predictions should be disabled for. + /// This list adds to a pre-existing, sensible default set of globs. + /// Any additional ones you add are combined with them. + #[serde(default)] + pub disabled_globs: Option>, + /// The mode used to display edit predictions in the buffer. + /// Provider support required. + #[serde(default)] + pub mode: EditPredictionsModeContent, + /// Settings specific to GitHub Copilot. + #[serde(default)] + pub copilot: CopilotSettingsContent, + /// Whether edit predictions are enabled in the assistant prompt editor. + /// This has no effect if globally disabled. + #[serde(default = "default_true")] + pub enabled_in_text_threads: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct CopilotSettingsContent { + /// HTTP/HTTPS proxy to use for Copilot. + /// + /// Default: none + #[serde(default)] + pub proxy: Option, + /// Disable certificate verification for the proxy (not recommended). + /// + /// Default: false + #[serde(default)] + pub proxy_no_verify: Option, + /// Enterprise URI for Copilot. + /// + /// Default: none + #[serde(default)] + pub enterprise_uri: Option, +} + +/// The mode in which edit predictions should be displayed. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum EditPredictionsModeContent { + /// If provider supports it, display inline when holding modifier key (e.g., alt). + /// Otherwise, eager preview is used. + #[serde(alias = "auto")] + Subtle, + /// Display inline when there are no language server completions available. + #[default] + #[serde(alias = "eager_preview")] + Eager, +} + +/// Controls the soft-wrapping behavior in the editor. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SoftWrapContent { + /// Prefer a single line generally, unless an overly long line is encountered. + None, + /// Deprecated: use None instead. Left to avoid breaking existing users' configs. + /// Prefer a single line generally, unless an overly long line is encountered. + PreferLine, + /// Soft wrap lines that exceed the editor width. + EditorWidth, + /// Soft wrap lines at the preferred line length. + PreferredLineLength, + /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). + Bounded, +} + +/// The settings for a particular language. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct LanguageSettingsContent { + /// How many columns a tab should occupy. + /// + /// Default: 4 + #[serde(default)] + pub tab_size: Option, + /// Whether to indent lines using tab characters, as opposed to multiple + /// spaces. + /// + /// Default: false + #[serde(default)] + pub hard_tabs: Option, + /// How to soft-wrap long lines of text. + /// + /// Default: none + #[serde(default)] + pub soft_wrap: Option, + /// The column at which to soft-wrap lines, for buffers where soft-wrap + /// is enabled. + /// + /// Default: 80 + #[serde(default)] + pub preferred_line_length: Option, + /// Whether to show wrap guides in the editor. Setting this to true will + /// show a guide at the 'preferred_line_length' value if softwrap is set to + /// 'preferred_line_length', and will show any additional guides as specified + /// by the 'wrap_guides' setting. + /// + /// Default: true + #[serde(default)] + pub show_wrap_guides: Option, + /// Character counts at which to show wrap guides in the editor. + /// + /// Default: [] + #[serde(default)] + pub wrap_guides: Option>, + /// Indent guide related settings. + #[serde(default)] + pub indent_guides: Option, + /// Whether or not to perform a buffer format before saving. + /// + /// Default: on + #[serde(default)] + pub format_on_save: Option, + /// Whether or not to remove any trailing whitespace from lines of a buffer + /// before saving it. + /// + /// Default: true + #[serde(default)] + pub remove_trailing_whitespace_on_save: Option, + /// Whether or not to ensure there's a single newline at the end of a buffer + /// when saving it. + /// + /// Default: true + #[serde(default)] + pub ensure_final_newline_on_save: Option, + /// How to perform a buffer format. + /// + /// Default: auto + #[serde(default)] + pub formatter: Option, + /// Zed's Prettier integration settings. + /// Allows to enable/disable formatting with Prettier + /// and configure default Prettier, used when no project-level Prettier installation is found. + /// + /// Default: off + #[serde(default)] + pub prettier: Option, + /// Whether to automatically close JSX tags. + #[serde(default)] + pub jsx_tag_auto_close: Option, + /// Whether to use language servers to provide code intelligence. + /// + /// Default: true + #[serde(default)] + pub enable_language_server: Option, + /// The list of language servers to use (or disable) for this language. + /// + /// This array should consist of language server IDs, as well as the following + /// special tokens: + /// - `"!"` - A language server ID prefixed with a `!` will be disabled. + /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. + /// + /// Default: ["..."] + #[serde(default)] + pub language_servers: Option>, + /// Controls where the `editor::Rewrap` action is allowed for this language. + /// + /// Note: This setting has no effect in Vim mode, as rewrap is already + /// allowed everywhere. + /// + /// Default: "in_comments" + #[serde(default)] + pub allow_rewrap: Option, + /// Controls whether edit predictions are shown immediately (true) + /// or manually by triggering `editor::ShowEditPrediction` (false). + /// + /// Default: true + #[serde(default)] + pub show_edit_predictions: Option, + /// Controls whether edit predictions are shown in the given language + /// scopes. + /// + /// Example: ["string", "comment"] + /// + /// Default: [] + #[serde(default)] + pub edit_predictions_disabled_in: Option>, + /// Whether to show tabs and spaces in the editor. + #[serde(default)] + pub show_whitespaces: Option, + /// Visible characters used to render whitespace when show_whitespaces is enabled. + /// + /// Default: "•" for spaces, "→" for tabs. + #[serde(default)] + pub whitespace_map: Option, + /// Whether to start a new line with a comment when a previous line is a comment as well. + /// + /// Default: true + #[serde(default)] + pub extend_comment_on_newline: Option, + /// Inlay hint related settings. + #[serde(default)] + pub inlay_hints: Option, + /// Whether to automatically type closing characters for you. For example, + /// when you type (, Zed will automatically add a closing ) at the correct position. + /// + /// Default: true + pub use_autoclose: Option, + /// Whether to automatically surround text with characters for you. For example, + /// when you select text and type (, Zed will automatically surround text with (). + /// + /// Default: true + pub use_auto_surround: Option, + /// Controls how the editor handles the autoclosed characters. + /// When set to `false`(default), skipping over and auto-removing of the closing characters + /// happen only for auto-inserted characters. + /// Otherwise(when `true`), the closing characters are always skipped over and auto-removed + /// no matter how they were inserted. + /// + /// Default: false + pub always_treat_brackets_as_autoclosed: Option, + /// Whether to use additional LSP queries to format (and amend) the code after + /// every "trigger" symbol input, defined by LSP server capabilities. + /// + /// Default: true + pub use_on_type_format: Option, + /// Which code actions to run on save after the formatter. + /// These are not run if formatting is off. + /// + /// Default: {} (or {"source.organizeImports": true} for Go). + pub code_actions_on_format: Option>, + /// Whether to perform linked edits of associated ranges, if the language server supports it. + /// For example, when editing opening tag, the contents of the closing tag will be edited as well. + /// + /// Default: true + pub linked_edits: Option, + /// Whether indentation should be adjusted based on the context whilst typing. + /// + /// Default: true + pub auto_indent: Option, + /// Whether indentation of pasted content should be adjusted based on the context. + /// + /// Default: true + pub auto_indent_on_paste: Option, + /// Task configuration for this language. + /// + /// Default: {} + pub tasks: Option, + /// Whether to pop the completions menu while typing in an editor without + /// explicitly requesting it. + /// + /// Default: true + pub show_completions_on_input: Option, + /// Whether to display inline and alongside documentation for items in the + /// completions menu. + /// + /// Default: true + pub show_completion_documentation: Option, + /// Controls how completions are processed for this language. + pub completions: Option, + /// Preferred debuggers for this language. + /// + /// Default: [] + pub debuggers: Option>, +} + +/// Controls how whitespace should be displayedin the editor. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ShowWhitespaceSetting { + /// Draw whitespace only for the selected text. + Selection, + /// Do not draw any tabs or spaces. + None, + /// Draw all invisible symbols. + All, + /// Draw whitespaces at boundaries only. + /// + /// For a whitespace to be on a boundary, any of the following conditions need to be met: + /// - It is a tab + /// - It is adjacent to an edge (start or end) + /// - It is adjacent to a whitespace (left or right) + Boundary, + /// Draw whitespaces only after non-whitespace characters. + Trailing, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct WhitespaceMap { + #[serde(default)] + pub space: Option, + #[serde(default)] + pub tab: Option, +} + +impl WhitespaceMap { + pub fn space(&self) -> SharedString { + self.space + .as_ref() + .map_or_else(|| SharedString::from("•"), |s| SharedString::from(s)) + } + + pub fn tab(&self) -> SharedString { + self.tab + .as_ref() + .map_or_else(|| SharedString::from("→"), |s| SharedString::from(s)) + } +} + +/// The behavior of `editor::Rewrap`. +#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RewrapBehavior { + /// Only rewrap within comments. + #[default] + InComments, + /// Only rewrap within the current selection(s). + InSelections, + /// Allow rewrapping anywhere. + Anywhere, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct JsxTagAutoCloseSettings { + /// Enables or disables auto-closing of JSX tags. + #[serde(default)] + pub enabled: bool, +} + +/// The settings for inlay hints. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct InlayHintSettings { + /// Global switch to toggle hints on and off. + /// + /// Default: false + #[serde(default)] + pub enabled: bool, + /// Global switch to toggle inline values on and off when debugging. + /// + /// Default: true + #[serde(default = "default_true")] + pub show_value_hints: bool, + /// Whether type hints should be shown. + /// + /// Default: true + #[serde(default = "default_true")] + pub show_type_hints: bool, + /// Whether parameter hints should be shown. + /// + /// Default: true + #[serde(default = "default_true")] + pub show_parameter_hints: bool, + /// Whether other hints should be shown. + /// + /// Default: true + #[serde(default = "default_true")] + pub show_other_hints: bool, + /// Whether to show a background for inlay hints. + /// + /// If set to `true`, the background will use the `hint.background` color + /// from the current theme. + /// + /// Default: false + #[serde(default)] + pub show_background: bool, + /// Whether or not to debounce inlay hints updates after buffer edits. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 700 + #[serde(default = "edit_debounce_ms")] + pub edit_debounce_ms: u64, + /// Whether or not to debounce inlay hints updates after buffer scrolls. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 50 + #[serde(default = "scroll_debounce_ms")] + pub scroll_debounce_ms: u64, + /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified. + /// If only a subset of the modifiers specified is pressed, hints are not toggled. + /// If no modifiers are specified, this is equivalent to `None`. + /// + /// Default: None + #[serde(default)] + pub toggle_on_modifiers_press: Option, +} + +fn edit_debounce_ms() -> u64 { + 700 +} + +fn scroll_debounce_ms() -> u64 { + 50 +} + +/// Controls how completions are processed for this language. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct CompletionSettings { + /// Controls how words are completed. + /// For large documents, not all words may be fetched for completion. + /// + /// Default: `fallback` + #[serde(default = "default_words_completion_mode")] + pub words: WordsCompletionMode, + /// How many characters has to be in the completions query to automatically show the words-based completions. + /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. + /// + /// Default: 3 + #[serde(default = "default_3")] + pub words_min_length: usize, + /// Whether to fetch LSP completions or not. + /// + /// Default: true + #[serde(default = "default_true")] + pub lsp: bool, + /// When fetching LSP completions, determines how long to wait for a response of a particular server. + /// When set to 0, waits indefinitely. + /// + /// Default: 0 + #[serde(default)] + pub lsp_fetch_timeout_ms: u64, + /// Controls how LSP completions are inserted. + /// + /// Default: "replace_suffix" + #[serde(default = "default_lsp_insert_mode")] + pub lsp_insert_mode: LspInsertMode, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum LspInsertMode { + /// Replaces text before the cursor, using the `insert` range described in the LSP specification. + Insert, + /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification. + Replace, + /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, + /// and like `"insert"` otherwise. + ReplaceSubsequence, + /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like + /// `"insert"` otherwise. + ReplaceSuffix, +} + +/// Controls how document's words are completed. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WordsCompletionMode { + /// Always fetch document's words for completions along with LSP completions. + Enabled, + /// Only if LSP response errors or times out, + /// use document's words to show completions. + Fallback, + /// Never fetch or complete document's words for completions. + /// (Word-based completions can still be queried via a separate action) + Disabled, +} + +fn default_words_completion_mode() -> WordsCompletionMode { + WordsCompletionMode::Fallback +} + +fn default_lsp_insert_mode() -> LspInsertMode { + LspInsertMode::ReplaceSuffix +} + +fn default_3() -> usize { + 3 +} + +/// Allows to enable/disable formatting with Prettier +/// and configure default Prettier, used when no project-level Prettier installation is found. +/// Prettier formatting is disabled by default. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct PrettierSettings { + /// Enables or disables formatting with Prettier for a given language. + #[serde(default)] + pub allowed: bool, + + /// Forces Prettier integration to use a specific parser name when formatting files with the language. + #[serde(default)] + pub parser: Option, + + /// Forces Prettier integration to use specific plugins when formatting files with the language. + /// The default Prettier will be installed with these plugins. + #[serde(default)] + pub plugins: HashSet, + + /// Default Prettier options, in the format as in package.json section for Prettier. + /// If project installs Prettier via its package.json, these options will be ignored. + #[serde(flatten)] + pub options: HashMap, +} +/// Controls the behavior of formatting files when they are saved. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FormatOnSave { + /// Files should be formatted on save. + On, + /// Files should not be formatted on save. + Off, + List(FormatterList), +} + +impl JsonSchema for FormatOnSave { + fn schema_name() -> Cow<'static, str> { + "OnSaveFormatter".into() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let formatter_schema = Formatter::json_schema(generator); + + json_schema!({ + "oneOf": [ + { + "type": "array", + "items": formatter_schema + }, + { + "type": "string", + "enum": ["on", "off", "language_server"] + }, + formatter_schema + ] + }) + } +} + +impl Serialize for FormatOnSave { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + match self { + Self::On => serializer.serialize_str("on"), + Self::Off => serializer.serialize_str("off"), + Self::List(list) => list.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for FormatOnSave { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + struct FormatDeserializer; + + impl<'d> Visitor<'d> for FormatDeserializer { + type Value = FormatOnSave; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid on-save formatter kind") + } + fn visit_str(self, v: &str) -> std::result::Result + where + E: serde::de::Error, + { + if v == "on" { + Ok(Self::Value::On) + } else if v == "off" { + Ok(Self::Value::Off) + } else if v == "language_server" { + Ok(Self::Value::List(FormatterList::Single( + Formatter::LanguageServer { name: None }, + ))) + } else { + let ret: Result = + Deserialize::deserialize(v.into_deserializer()); + ret.map(Self::Value::List) + } + } + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); + ret.map(Self::Value::List) + } + fn visit_seq(self, map: A) -> Result + where + A: SeqAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map)); + ret.map(Self::Value::List) + } + } + deserializer.deserialize_any(FormatDeserializer) + } +} + +/// Controls which formatter should be used when formatting code. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum SelectedFormatter { + /// Format files using Zed's Prettier integration (if applicable), + /// or falling back to formatting via language server. + #[default] + Auto, + List(FormatterList), +} + +impl JsonSchema for SelectedFormatter { + fn schema_name() -> Cow<'static, str> { + "Formatter".into() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let formatter_schema = Formatter::json_schema(generator); + + json_schema!({ + "oneOf": [ + { + "type": "array", + "items": formatter_schema + }, + { + "type": "string", + "enum": ["auto", "language_server"] + }, + formatter_schema + ] + }) + } +} + +impl Serialize for SelectedFormatter { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + match self { + SelectedFormatter::Auto => serializer.serialize_str("auto"), + SelectedFormatter::List(list) => list.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for SelectedFormatter { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + struct FormatDeserializer; + + impl<'d> Visitor<'d> for FormatDeserializer { + type Value = SelectedFormatter; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid formatter kind") + } + fn visit_str(self, v: &str) -> std::result::Result + where + E: serde::de::Error, + { + if v == "auto" { + Ok(Self::Value::Auto) + } else if v == "language_server" { + Ok(Self::Value::List(FormatterList::Single( + Formatter::LanguageServer { name: None }, + ))) + } else { + let ret: Result = + Deserialize::deserialize(v.into_deserializer()); + ret.map(SelectedFormatter::List) + } + } + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); + ret.map(SelectedFormatter::List) + } + fn visit_seq(self, map: A) -> Result + where + A: SeqAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map)); + ret.map(SelectedFormatter::List) + } + } + deserializer.deserialize_any(FormatDeserializer) + } +} + +/// Controls which formatters should be used when formatting code. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(untagged)] +pub enum FormatterList { + Single(Formatter), + Vec(Vec), +} + +impl Default for FormatterList { + fn default() -> Self { + Self::Single(Formatter::default()) + } +} + +/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration. +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Formatter { + /// Format code using the current language server. + LanguageServer { name: Option }, + /// Format code using Zed's Prettier integration. + #[default] + Prettier, + /// Format code using an external command. + External { + /// The external program to run. + command: Arc, + /// The arguments to pass to the program. + arguments: Option>, + }, + /// Files should be formatted using code actions executed by language servers. + CodeActions(HashMap), +} + +/// The settings for indent guides. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct IndentGuideSettingsContent { + /// Whether to display indent guides in the editor. + /// + /// Default: true + #[serde(default = "default_true")] + pub enabled: bool, + /// The width of the indent guides in pixels, between 1 and 10. + /// + /// Default: 1 + #[serde(default = "line_width")] + pub line_width: u32, + /// The width of the active indent guide in pixels, between 1 and 10. + /// + /// Default: 1 + #[serde(default = "active_line_width")] + pub active_line_width: u32, + /// Determines how indent guides are colored. + /// + /// Default: Fixed + #[serde(default)] + pub coloring: IndentGuideColoring, + /// Determines how indent guide backgrounds are colored. + /// + /// Default: Disabled + #[serde(default)] + pub background_coloring: IndentGuideBackgroundColoring, +} + +fn line_width() -> u32 { + 1 +} + +fn active_line_width() -> u32 { + line_width() +} + +/// The task settings for a particular language. +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct LanguageTaskConfig { + /// Extra task variables to set for a particular language. + #[serde(default)] + pub variables: HashMap, + #[serde(default = "default_true")] + pub enabled: bool, + /// Use LSP tasks over Zed language extension ones. + /// If no LSP tasks are returned due to error/timeout or regular execution, + /// Zed language extension tasks will be used instead. + /// + /// Other Zed tasks will still be shown: + /// * Zed task from either of the task config file + /// * Zed task from history (e.g. one-off task was spawned before) + #[serde(default = "default_true")] + pub prefer_lsp: bool, +} + +/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language +/// names in the keys. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct LanguageToSettingsMap(pub HashMap); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + let language_settings_content_ref = generator + .subschema_for::() + .to_value(); + replace_subschema::(generator, || json_schema!({ + "type": "object", + "properties": params + .language_names + .iter() + .map(|name| { + ( + name.clone(), + language_settings_content_ref.clone(), + ) + }) + .collect::>() + })) + } + } +} + +/// Determines how indent guides are colored. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IndentGuideColoring { + /// Do not render any lines for indent guides. + Disabled, + /// Use the same color for all indentation levels. + #[default] + Fixed, + /// Use a different color for each indentation level. + IndentAware, +} + +/// Determines how indent guide backgrounds are colored. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum IndentGuideBackgroundColoring { + /// Do not render any background for indent guides. + #[default] + Disabled, + /// Use a different color for each indentation level. + IndentAware, +} diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index d31dd82da475744d9658bc8ecdc6ec2ad17732fb..af8e3d6d4b8b8c06eeed2c1866959de6ee4b8f6d 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,4 +1,4 @@ -use crate::{Settings, settings_store::SettingsStore}; +use crate::{Settings, settings_content::SettingsContent, settings_store::SettingsStore}; use collections::HashSet; use fs::{Fs, PathEventKind}; use futures::{StreamExt, channel::mpsc}; @@ -126,10 +126,10 @@ pub fn watch_config_dir( rx } -pub fn update_settings_file( +pub fn update_settings_file( fs: Arc, cx: &App, - update: impl 'static + Send + FnOnce(&mut T::FileContent, &App), + update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), ) { - SettingsStore::global(cx).update_settings_file::(fs, update); + SettingsStore::global(cx).update_settings_file(fs, update); } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index abce84daf7cf2b0adc304894476d9c763e5e1a3d..5c2f3b8e73a46ba5033919c27a709ad6839136ab 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -33,7 +33,11 @@ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, - settings_ui_core::SettingsUi, update_value_in_json_text, + settings_content::{ + ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, + UserSettingsContent, + }, + update_value_in_json_text, }; pub trait SettingsKey: 'static + Send + Sync { @@ -48,7 +52,7 @@ pub trait SettingsKey: 'static + Send + Sync { /// A value that can be defined as a user setting. /// /// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: 'static + Send + Sync { +pub trait Settings: 'static + Send + Sync + Sized { /// The name of the keys in the [`FileContent`](Self::FileContent) that should /// always be written to a settings file, even if their value matches the default /// value. @@ -58,30 +62,9 @@ pub trait Settings: 'static + Send + Sync { /// user settings match the current version of the settings. const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - /// The type that is stored in an individual JSON file. - type FileContent: Clone - + Default - + Serialize - + DeserializeOwned - + JsonSchema - + SettingsUi - + SettingsKey; - - /* - * let path = Settings - * - * - */ - /// The logic for combining together values from one or more JSON files into the - /// final value for this setting. - /// - /// # Warning - /// `Self::FileContent` deserialized field names should match with `Self` deserialized field names - /// otherwise the field won't be deserialized properly and you will get the error: - /// "A default setting must be added to the `default.json` file" - fn load(sources: SettingsSources, cx: &mut App) -> Result - where - Self: Sized; + fn from_file(content: &SettingsContent) -> Option; + + fn refine(&mut self, content: &SettingsContent); fn missing_default() -> anyhow::Error { anyhow::anyhow!("missing default for: {}", std::any::type_name::()) @@ -89,7 +72,7 @@ pub trait Settings: 'static + Send + Sync { /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known /// equivalent settings from a vscode config to our config - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent); + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent); #[track_caller] fn register(cx: &mut App) @@ -218,17 +201,15 @@ pub struct SettingsLocation<'a> { /// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, - raw_default_settings: Value, - raw_global_settings: Option, - raw_user_settings: Value, - raw_server_settings: Option, - raw_extension_settings: Value, - raw_local_settings: BTreeMap<(WorktreeId, Arc), Value>, + default_settings: Option, + user_settings: Option, + global_settings: Option, + + extension_settings: Option, + server_settings: Option, + local_settings: BTreeMap<(WorktreeId, Arc), SettingsContent>, raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, - tab_size_callback: Option<( - TypeId, - Box Option + Send + Sync + 'static>, - )>, + _setting_file_updates: Task<()>, setting_file_updates_tx: mpsc::UnboundedSender LocalBoxFuture<'static, Result<()>>>>, @@ -271,20 +252,11 @@ struct SettingValue { } trait AnySettingValue: 'static + Send + Sync { - fn key(&self) -> Option<&'static str>; fn setting_type_name(&self) -> &'static str; - fn deserialize_setting(&self, json: &Value) -> Result { - self.deserialize_setting_with_key(json).1 - } - fn deserialize_setting_with_key( - &self, - json: &Value, - ) -> (Option<&'static str>, Result); - fn load_setting( - &self, - sources: SettingsSources, - cx: &mut App, - ) -> Result>; + + fn from_file(&self, s: &SettingsContent) -> Option>; + fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent]); + fn value_for_path(&self, path: Option) -> &dyn Any; fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); @@ -308,14 +280,13 @@ impl SettingsStore { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); Self { setting_values: Default::default(), - raw_default_settings: json!({}), - raw_global_settings: None, - raw_user_settings: json!({}), - raw_server_settings: None, - raw_extension_settings: json!({}), - raw_local_settings: Default::default(), + default_settings: Some(Default::default()), // todo!() + global_settings: None, + server_settings: None, + user_settings: Some(Default::default()), // todo!() + extension_settings: None, + local_settings: BTreeMap::default(), raw_editorconfig_settings: BTreeMap::default(), - tab_size_callback: Default::default(), setting_file_updates_tx, _setting_file_updates: cx.spawn(async move |cx| { while let Some(setting_file_update) = setting_file_updates_rx.next().await { @@ -354,71 +325,39 @@ impl SettingsStore { local_values: Vec::new(), })); - if let Some(default_settings) = setting_value - .deserialize_setting(&self.raw_default_settings) - .log_err() - { - let user_value = setting_value - .deserialize_setting(&self.raw_user_settings) - .log_err(); - - let mut release_channel_value = None; - if let Some(release_settings) = &self - .raw_user_settings - .get(release_channel::RELEASE_CHANNEL.dev_name()) - { - release_channel_value = setting_value - .deserialize_setting(release_settings) - .log_err(); - } + let mut refinements = Vec::default(); - let mut os_settings_value = None; - if let Some(os_settings) = &self.raw_user_settings.get(env::consts::OS) { - os_settings_value = setting_value.deserialize_setting(os_settings).log_err(); - } + if let Some(extension_settings) = self.extension_settings.as_ref() { + refinements.push(extension_settings) + } - let mut profile_value = None; - if let Some(active_profile) = cx.try_global::() - && let Some(profiles) = self.raw_user_settings.get("profiles") - && let Some(profile_settings) = profiles.get(&active_profile.0) - { - profile_value = setting_value - .deserialize_setting(profile_settings) - .log_err(); + if let Some(user_settings) = self.user_settings.as_ref() { + refinements.push(&user_settings.content); + if let Some(release_channel) = user_settings.for_release_channel() { + refinements.push(release_channel) } - - let server_value = self - .raw_server_settings - .as_ref() - .and_then(|server_setting| { - setting_value.deserialize_setting(server_setting).log_err() - }); - - let extension_value = setting_value - .deserialize_setting(&self.raw_extension_settings) - .log_err(); - - if let Some(setting) = setting_value - .load_setting( - SettingsSources { - default: &default_settings, - global: None, - extensions: extension_value.as_ref(), - user: user_value.as_ref(), - release_channel: release_channel_value.as_ref(), - operating_system: os_settings_value.as_ref(), - profile: profile_value.as_ref(), - server: server_value.as_ref(), - project: &[], - }, - cx, - ) - .context("A default setting must be added to the `default.json` file") - .log_err() - { - setting_value.set_global_value(setting); + if let Some(os) = user_settings.for_os() { + refinements.push(os) } + if let Some(profile) = user_settings.for_profile(cx) { + refinements.push(profile) + } + } + + if let Some(server_settings) = self.server_settings.as_ref() { + refinements.push(server_settings) } + let default = self.default_settings.as_ref().unwrap(); + // todo!() unwrap... + let mut value = T::from_file(default).unwrap(); + for refinement in refinements { + value.refine(refinement) + } + + setting_value.set_global_value(Box::new(value)); + + // todo!() local settings + // (they weren't handled before...) } /// Get the value of a setting. @@ -476,35 +415,31 @@ impl SettingsStore { /// /// For user-facing functionality use the typed setting interface. /// (e.g. ProjectSettings::get_global(cx)) - pub fn raw_user_settings(&self) -> &Value { - &self.raw_user_settings + pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> { + self.user_settings.as_ref() } /// Replaces current settings with the values from the given JSON. - pub fn set_raw_user_settings(&mut self, new_settings: Value, cx: &mut App) -> Result<()> { - self.raw_user_settings = new_settings; + pub fn set_raw_user_settings( + &mut self, + new_settings: UserSettingsContent, + cx: &mut App, + ) -> Result<()> { + self.user_settings = Some(new_settings); self.recompute_values(None, cx)?; Ok(()) } /// Get the configured settings profile names. pub fn configured_settings_profiles(&self) -> impl Iterator { - self.raw_user_settings - .get("profiles") - .and_then(|v| v.as_object()) - .into_iter() - .flat_map(|obj| obj.keys()) - .map(|s| s.as_str()) - } - - /// Access the raw JSON value of the global settings. - pub fn raw_global_settings(&self) -> Option<&Value> { - self.raw_global_settings.as_ref() + self.user_settings + .iter() + .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str())) } /// Access the raw JSON value of the default settings. - pub fn raw_default_settings(&self) -> &Value { - &self.raw_default_settings + pub fn raw_default_settings(&self) -> Option<&SettingsContent> { + self.default_settings.as_ref() } #[cfg(any(test, feature = "test-support"))] @@ -521,13 +456,18 @@ impl SettingsStore { /// This is only for tests. Normally, settings are only loaded from /// JSON files. #[cfg(any(test, feature = "test-support"))] - pub fn update_user_settings( + pub fn update_user_settings( &mut self, cx: &mut App, - update: impl FnOnce(&mut T::FileContent), + update: impl FnOnce(&mut SettingsContent), ) { - let old_text = serde_json::to_string(&self.raw_user_settings).unwrap(); - let new_text = self.new_text_for_update::(old_text, update); + let mut content = self.user_settings.as_ref().unwrap().content.clone(); + update(&mut content); + let new_text = serde_json::to_string(&UserSettingsContent { + content, + ..Default::default() + }) + .unwrap(); self.set_user_settings(&new_text, cx).unwrap(); } @@ -626,14 +566,14 @@ impl SettingsStore { self.update_settings_file_inner(fs, update) } - pub fn update_settings_file( + pub fn update_settings_file( &self, fs: Arc, - update: impl 'static + Send + FnOnce(&mut T::FileContent, &App), + update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), ) { _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { cx.read_global(|store: &SettingsStore, cx| { - store.new_text_for_update::(old_text, |content| update(content, cx)) + store.new_text_for_update(old_text, |content| update(content, cx)) }) }); } @@ -660,12 +600,12 @@ impl SettingsStore { impl SettingsStore { /// Updates the value of a setting in a JSON file, returning the new text /// for that JSON file. - pub fn new_text_for_update( + pub fn new_text_for_update( &self, old_text: String, - update: impl FnOnce(&mut T::FileContent), + update: impl FnOnce(&mut SettingsContent), ) -> String { - let edits = self.edits_for_update::(&old_text, update); + let edits = self.edits_for_update(&old_text, update); let mut new_text = old_text; for (range, replacement) in edits.into_iter() { new_text.replace_range(range, &replacement); @@ -689,36 +629,19 @@ impl SettingsStore { /// Updates the value of a setting in a JSON file, returning a list /// of edits to apply to the JSON file. - pub fn edits_for_update( + pub fn edits_for_update( &self, text: &str, - update: impl FnOnce(&mut T::FileContent), + update: impl FnOnce(&mut SettingsContent), ) -> Vec<(Range, String)> { - let setting_type_id = TypeId::of::(); - - let preserved_keys = T::PRESERVED_KEYS.unwrap_or_default(); - - let setting = self - .setting_values - .get(&setting_type_id) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())); - let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); - let (key, deserialized_setting) = setting.deserialize_setting_with_key(&raw_settings); - let old_content = match deserialized_setting { - Ok(content) => content.0.downcast::().unwrap(), - Err(_) => Box::<::FileContent>::default(), - }; + let old_content: UserSettingsContent = serde_json::from_str(text).unwrap_or_default(); let mut new_content = old_content.clone(); - update(&mut new_content); + update(&mut new_content.content); let old_value = serde_json::to_value(&old_content).unwrap(); let new_value = serde_json::to_value(new_content).unwrap(); let mut key_path = Vec::new(); - if let Some(key) = key { - key_path.push(key); - } - let mut edits = Vec::new(); let tab_size = self.json_tab_size(); let mut text = text.to_string(); @@ -728,35 +651,14 @@ impl SettingsStore { tab_size, &old_value, &new_value, - preserved_keys, + &[], // todo!() is this still needed? &mut edits, ); edits } - /// Configure the tab sized when updating JSON files. - pub fn set_json_tab_size_callback( - &mut self, - get_tab_size: fn(&T) -> Option, - ) { - self.tab_size_callback = Some(( - TypeId::of::(), - Box::new(move |value| get_tab_size(value.downcast_ref::().unwrap())), - )); - } - pub fn json_tab_size(&self) -> usize { - const DEFAULT_JSON_TAB_SIZE: usize = 2; - - if let Some((setting_type_id, callback)) = &self.tab_size_callback { - let setting_value = self.setting_values.get(setting_type_id).unwrap(); - let value = setting_value.value_for_path(None); - if let Some(value) = callback(value) { - return value; - } - } - - DEFAULT_JSON_TAB_SIZE + 2 } /// Sets the default settings via a JSON string. @@ -767,29 +669,22 @@ impl SettingsStore { default_settings_content: &str, cx: &mut App, ) -> Result<()> { - let settings: Value = parse_json_with_comments(default_settings_content)?; - anyhow::ensure!(settings.is_object(), "settings must be an object"); - self.raw_default_settings = settings; + self.default_settings = parse_json_with_comments(default_settings_content)?; self.recompute_values(None, cx)?; Ok(()) } /// Sets the user settings via a JSON string. - pub fn set_user_settings( - &mut self, - user_settings_content: &str, - cx: &mut App, - ) -> Result { - let settings: Value = if user_settings_content.is_empty() { + pub fn set_user_settings(&mut self, user_settings_content: &str, cx: &mut App) -> Result<()> { + let settings: UserSettingsContent = if user_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(user_settings_content)? }; - anyhow::ensure!(settings.is_object(), "settings must be an object"); - self.raw_user_settings = settings.clone(); + self.user_settings = Some(settings); self.recompute_values(None, cx)?; - Ok(settings) + Ok(()) } /// Sets the global settings via a JSON string. @@ -797,17 +692,16 @@ impl SettingsStore { &mut self, global_settings_content: &str, cx: &mut App, - ) -> Result { - let settings: Value = if global_settings_content.is_empty() { + ) -> Result<()> { + let settings: SettingsContent = if global_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(global_settings_content)? }; - anyhow::ensure!(settings.is_object(), "settings must be an object"); - self.raw_global_settings = Some(settings.clone()); + self.global_settings = Some(settings); self.recompute_values(None, cx)?; - Ok(settings) + Ok(()) } pub fn set_server_settings( @@ -815,20 +709,14 @@ impl SettingsStore { server_settings_content: &str, cx: &mut App, ) -> Result<()> { - let settings: Option = if server_settings_content.is_empty() { + let settings: Option = if server_settings_content.is_empty() { None } else { parse_json_with_comments(server_settings_content)? }; - anyhow::ensure!( - settings - .as_ref() - .map(|value| value.is_object()) - .unwrap_or(true), - "settings must be an object" - ); - self.raw_server_settings = settings; + todo!(); + // self.server_settings = Some(settings); self.recompute_values(None, cx)?; Ok(()) } @@ -864,7 +752,7 @@ impl SettingsStore { } (LocalSettingsKind::Settings, None) => { zed_settings_changed = self - .raw_local_settings + .local_settings .remove(&(root_id, directory_path.clone())) .is_some() } @@ -873,24 +761,27 @@ impl SettingsStore { .remove(&(root_id, directory_path.clone())); } (LocalSettingsKind::Settings, Some(settings_contents)) => { - let new_settings = - parse_json_with_comments::(settings_contents).map_err(|e| { - InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: e.to_string(), - } - })?; - match self - .raw_local_settings - .entry((root_id, directory_path.clone())) - { + let new_settings = parse_json_with_comments::( + settings_contents, + ) + .map_err(|e| InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), + })?; + match self.local_settings.entry((root_id, directory_path.clone())) { btree_map::Entry::Vacant(v) => { - v.insert(new_settings); + v.insert(SettingsContent { + project: new_settings, + ..Default::default() + }); zed_settings_changed = true; } btree_map::Entry::Occupied(mut o) => { - if o.get() != &new_settings { - o.insert(new_settings); + if &o.get().project != &new_settings { + o.insert(SettingsContent { + project: new_settings, + ..Default::default() + }); zed_settings_changed = true; } } @@ -942,17 +833,24 @@ impl SettingsStore { Ok(()) } - pub fn set_extension_settings(&mut self, content: T, cx: &mut App) -> Result<()> { - let settings: Value = serde_json::to_value(content)?; - anyhow::ensure!(settings.is_object(), "settings must be an object"); - self.raw_extension_settings = settings; + pub fn set_extension_settings( + &mut self, + content: ExtensionsSettingsContent, + cx: &mut App, + ) -> Result<()> { + self.extension_settings = Some(SettingsContent { + project: ProjectSettingsContent { + all_languages: content.all_languages, + }, + ..Default::default() + }); self.recompute_values(None, cx)?; Ok(()) } /// Add or remove a set of local settings via a JSON string. pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> { - self.raw_local_settings + self.local_settings .retain(|(worktree_id, _), _| worktree_id != &root_id); self.recompute_values(Some((root_id, "".as_ref())), cx)?; Ok(()) @@ -962,7 +860,7 @@ impl SettingsStore { &self, root_id: WorktreeId, ) -> impl '_ + Iterator, String)> { - self.raw_local_settings + self.local_settings .range( (root_id, Path::new("").into()) ..( @@ -991,193 +889,194 @@ impl SettingsStore { } pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .into_generator(); - let mut combined_schema = json!({ - "type": "object", - "properties": {} - }); - - // Merge together settings schemas, similarly to json schema's "allOf". This merging is - // recursive, though at time of writing this recursive nature isn't used very much. An - // example of it is the schema for `jupyter` having contribution from both `EditorSettings` - // and `JupyterSettings`. - // - // This logic could be removed in favor of "allOf", but then there isn't the opportunity to - // validate and fully control the merge. - for setting_value in self.setting_values.values() { - let mut setting_schema = setting_value.json_schema(&mut generator); - - if let Some(key) = setting_value.key() { - if let Some(properties) = combined_schema.get_mut("properties") - && let Some(properties_obj) = properties.as_object_mut() - { - if let Some(target) = properties_obj.get_mut(key) { - merge_schema(target, setting_schema.to_value()); - } else { - properties_obj.insert(key.to_string(), setting_schema.to_value()); - } - } - } else { - setting_schema.remove("description"); - setting_schema.remove("additionalProperties"); - merge_schema(&mut combined_schema, setting_schema.to_value()); - } - } - - fn merge_schema(target: &mut serde_json::Value, source: serde_json::Value) { - let (Some(target_obj), serde_json::Value::Object(source_obj)) = - (target.as_object_mut(), source) - else { - return; - }; - - for (source_key, source_value) in source_obj { - match source_key.as_str() { - "properties" => { - let serde_json::Value::Object(source_properties) = source_value else { - log::error!( - "bug: expected object for `{}` json schema field, but got: {}", - source_key, - source_value - ); - continue; - }; - let target_properties = - target_obj.entry(source_key.clone()).or_insert(json!({})); - let Some(target_properties) = target_properties.as_object_mut() else { - log::error!( - "bug: expected object for `{}` json schema field, but got: {}", - source_key, - target_properties - ); - continue; - }; - for (key, value) in source_properties { - if let Some(existing) = target_properties.get_mut(&key) { - merge_schema(existing, value); - } else { - target_properties.insert(key, value); - } - } - } - "allOf" | "anyOf" | "oneOf" => { - let serde_json::Value::Array(source_array) = source_value else { - log::error!( - "bug: expected array for `{}` json schema field, but got: {}", - source_key, - source_value, - ); - continue; - }; - let target_array = - target_obj.entry(source_key.clone()).or_insert(json!([])); - let Some(target_array) = target_array.as_array_mut() else { - log::error!( - "bug: expected array for `{}` json schema field, but got: {}", - source_key, - target_array, - ); - continue; - }; - target_array.extend(source_array); - } - "type" - | "$ref" - | "enum" - | "minimum" - | "maximum" - | "pattern" - | "description" - | "additionalProperties" => { - if let Some(old_value) = - target_obj.insert(source_key.clone(), source_value.clone()) - && old_value != source_value - { - log::error!( - "bug: while merging JSON schemas, \ - mismatch `\"{}\": {}` (before was `{}`)", - source_key, - old_value, - source_value - ); - } - } - _ => { - log::error!( - "bug: while merging settings JSON schemas, \ - encountered unexpected `\"{}\": {}`", - source_key, - source_value - ); - } - } - } - } - - // add schemas which are determined at runtime - for parameterized_json_schema in inventory::iter::() { - (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx); - } - - // add merged settings schema to the definitions - const ZED_SETTINGS: &str = "ZedSettings"; - let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema); - - // add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown - // fields are rejected. This is used for release stage settings and profiles. - let mut zed_settings_override = zed_settings_ref.clone(); - zed_settings_override.insert("unevaluatedProperties".to_string(), false.into()); - let zed_settings_override_ref = add_new_subschema( - &mut generator, - "ZedSettingsOverride", - zed_settings_override.to_value(), - ); - - // Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that - // unknown fields can be handled by the root schema and `ZedSettingsOverride`. - let mut definitions = generator.take_definitions(true); - definitions - .get_mut(ZED_SETTINGS) - .unwrap() - .as_object_mut() - .unwrap() - .remove("additionalProperties"); - - let meta_schema = generator - .settings() - .meta_schema - .as_ref() - .expect("meta_schema should be present in schemars settings") - .to_string(); - - json!({ - "$schema": meta_schema, - "title": "Zed Settings", - "unevaluatedProperties": false, - // ZedSettings + settings overrides for each release stage / OS / profiles - "allOf": [ - zed_settings_ref, - { - "properties": { - "dev": zed_settings_override_ref, - "nightly": zed_settings_override_ref, - "stable": zed_settings_override_ref, - "preview": zed_settings_override_ref, - "linux": zed_settings_override_ref, - "macos": zed_settings_override_ref, - "windows": zed_settings_override_ref, - "profiles": { - "type": "object", - "description": "Configures any number of settings profiles.", - "additionalProperties": zed_settings_override_ref - } - } - } - ], - "$defs": definitions, - }) + todo!() + // let mut generator = schemars::generate::SchemaSettings::draft2019_09() + // .with_transform(DefaultDenyUnknownFields) + // .into_generator(); + // let mut combined_schema = json!({ + // "type": "object", + // "properties": {} + // }); + + // // Merge together settings schemas, similarly to json schema's "allOf". This merging is + // // recursive, though at time of writing this recursive nature isn't used very much. An + // // example of it is the schema for `jupyter` having contribution from both `EditorSettings` + // // and `JupyterSettings`. + // // + // // This logic could be removed in favor of "allOf", but then there isn't the opportunity to + // // validate and fully control the merge. + // for setting_value in self.setting_values.values() { + // let mut setting_schema = setting_value.json_schema(&mut generator); + + // if let Some(key) = setting_value.key() { + // if let Some(properties) = combined_schema.get_mut("properties") + // && let Some(properties_obj) = properties.as_object_mut() + // { + // if let Some(target) = properties_obj.get_mut(key) { + // merge_schema(target, setting_schema.to_value()); + // } else { + // properties_obj.insert(key.to_string(), setting_schema.to_value()); + // } + // } + // } else { + // setting_schema.remove("description"); + // setting_schema.remove("additionalProperties"); + // merge_schema(&mut combined_schema, setting_schema.to_value()); + // } + // } + + // fn merge_schema(target: &mut serde_json::Value, source: serde_json::Value) { + // let (Some(target_obj), serde_json::Value::Object(source_obj)) = + // (target.as_object_mut(), source) + // else { + // return; + // }; + + // for (source_key, source_value) in source_obj { + // match source_key.as_str() { + // "properties" => { + // let serde_json::Value::Object(source_properties) = source_value else { + // log::error!( + // "bug: expected object for `{}` json schema field, but got: {}", + // source_key, + // source_value + // ); + // continue; + // }; + // let target_properties = + // target_obj.entry(source_key.clone()).or_insert(json!({})); + // let Some(target_properties) = target_properties.as_object_mut() else { + // log::error!( + // "bug: expected object for `{}` json schema field, but got: {}", + // source_key, + // target_properties + // ); + // continue; + // }; + // for (key, value) in source_properties { + // if let Some(existing) = target_properties.get_mut(&key) { + // merge_schema(existing, value); + // } else { + // target_properties.insert(key, value); + // } + // } + // } + // "allOf" | "anyOf" | "oneOf" => { + // let serde_json::Value::Array(source_array) = source_value else { + // log::error!( + // "bug: expected array for `{}` json schema field, but got: {}", + // source_key, + // source_value, + // ); + // continue; + // }; + // let target_array = + // target_obj.entry(source_key.clone()).or_insert(json!([])); + // let Some(target_array) = target_array.as_array_mut() else { + // log::error!( + // "bug: expected array for `{}` json schema field, but got: {}", + // source_key, + // target_array, + // ); + // continue; + // }; + // target_array.extend(source_array); + // } + // "type" + // | "$ref" + // | "enum" + // | "minimum" + // | "maximum" + // | "pattern" + // | "description" + // | "additionalProperties" => { + // if let Some(old_value) = + // target_obj.insert(source_key.clone(), source_value.clone()) + // && old_value != source_value + // { + // log::error!( + // "bug: while merging JSON schemas, \ + // mismatch `\"{}\": {}` (before was `{}`)", + // source_key, + // old_value, + // source_value + // ); + // } + // } + // _ => { + // log::error!( + // "bug: while merging settings JSON schemas, \ + // encountered unexpected `\"{}\": {}`", + // source_key, + // source_value + // ); + // } + // } + // } + // } + + // // add schemas which are determined at runtime + // for parameterized_json_schema in inventory::iter::() { + // (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx); + // } + + // // add merged settings schema to the definitions + // const ZED_SETTINGS: &str = "ZedSettings"; + // let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema); + + // // add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown + // // fields are rejected. This is used for release stage settings and profiles. + // let mut zed_settings_override = zed_settings_ref.clone(); + // zed_settings_override.insert("unevaluatedProperties".to_string(), false.into()); + // let zed_settings_override_ref = add_new_subschema( + // &mut generator, + // "ZedSettingsOverride", + // zed_settings_override.to_value(), + // ); + + // // Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that + // // unknown fields can be handled by the root schema and `ZedSettingsOverride`. + // let mut definitions = generator.take_definitions(true); + // definitions + // .get_mut(ZED_SETTINGS) + // .unwrap() + // .as_object_mut() + // .unwrap() + // .remove("additionalProperties"); + + // let meta_schema = generator + // .settings() + // .meta_schema + // .as_ref() + // .expect("meta_schema should be present in schemars settings") + // .to_string(); + + // json!({ + // "$schema": meta_schema, + // "title": "Zed Settings", + // "unevaluatedProperties": false, + // // ZedSettings + settings overrides for each release stage / OS / profiles + // "allOf": [ + // zed_settings_ref, + // { + // "properties": { + // "dev": zed_settings_override_ref, + // "nightly": zed_settings_override_ref, + // "stable": zed_settings_override_ref, + // "preview": zed_settings_override_ref, + // "linux": zed_settings_override_ref, + // "macos": zed_settings_override_ref, + // "windows": zed_settings_override_ref, + // "profiles": { + // "type": "object", + // "description": "Configures any number of settings profiles.", + // "additionalProperties": zed_settings_override_ref + // } + // } + // } + // ], + // "$defs": definitions, + // }) } fn recompute_values( @@ -1186,90 +1085,45 @@ impl SettingsStore { cx: &mut App, ) -> std::result::Result<(), InvalidSettingsError> { // Reload the global and local values for every setting. - let mut project_settings_stack = Vec::::new(); + let mut project_settings_stack = Vec::<&SettingsContent>::new(); let mut paths_stack = Vec::>::new(); - for setting_value in self.setting_values.values_mut() { - let default_settings = setting_value - .deserialize_setting(&self.raw_default_settings) - .map_err(|e| InvalidSettingsError::DefaultSettings { - message: e.to_string(), - })?; - let global_settings = self - .raw_global_settings - .as_ref() - .and_then(|setting| setting_value.deserialize_setting(setting).log_err()); - - let extension_settings = setting_value - .deserialize_setting(&self.raw_extension_settings) - .log_err(); - - let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) { - Ok(settings) => Some(settings), - Err(error) => { - return Err(InvalidSettingsError::UserSettings { - message: error.to_string(), - }); - } - }; - - let server_settings = self - .raw_server_settings - .as_ref() - .and_then(|setting| setting_value.deserialize_setting(setting).log_err()); - - let mut release_channel_settings = None; - if let Some(release_settings) = &self - .raw_user_settings - .get(release_channel::RELEASE_CHANNEL.dev_name()) - && let Some(release_settings) = setting_value - .deserialize_setting(release_settings) - .log_err() - { - release_channel_settings = Some(release_settings); - } + let mut refinements = Vec::default(); - let mut os_settings = None; - if let Some(settings) = &self.raw_user_settings.get(env::consts::OS) - && let Some(settings) = setting_value.deserialize_setting(settings).log_err() - { - os_settings = Some(settings); - } + if let Some(extension_settings) = self.extension_settings.as_ref() { + refinements.push(extension_settings) + } - let mut profile_settings = None; - if let Some(active_profile) = cx.try_global::() - && let Some(profiles) = self.raw_user_settings.get("profiles") - && let Some(profile_json) = profiles.get(&active_profile.0) - { - profile_settings = setting_value.deserialize_setting(profile_json).log_err(); + if let Some(user_settings) = self.user_settings.as_ref() { + refinements.push(&user_settings.content); + if let Some(release_channel) = user_settings.for_release_channel() { + refinements.push(release_channel) + } + if let Some(os) = user_settings.for_os() { + refinements.push(os) } + if let Some(profile) = user_settings.for_profile(cx) { + refinements.push(profile) + } + } + + if let Some(server_settings) = self.server_settings.as_ref() { + refinements.push(server_settings) + } + let default = self.default_settings.as_ref().unwrap(); + for setting_value in self.setting_values.values_mut() { // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() - && let Some(value) = setting_value - .load_setting( - SettingsSources { - default: &default_settings, - global: global_settings.as_ref(), - extensions: extension_settings.as_ref(), - user: user_settings.as_ref(), - release_channel: release_channel_settings.as_ref(), - operating_system: os_settings.as_ref(), - profile: profile_settings.as_ref(), - server: server_settings.as_ref(), - project: &[], - }, - cx, - ) - .log_err() - { + if changed_local_path.is_none() { + let mut value = setting_value.from_file(&default).unwrap(); + setting_value.refine(&mut value, &refinements); setting_value.set_global_value(value); } // Reload the local values for the setting. paths_stack.clear(); project_settings_stack.clear(); - for ((root_id, directory_path), local_settings) in &self.raw_local_settings { + for ((root_id, directory_path), local_settings) in &self.local_settings { // Build a stack of all of the local values for that setting. while let Some(prev_entry) = paths_stack.last() { if let Some((prev_root_id, prev_path)) = prev_entry @@ -1282,48 +1136,25 @@ impl SettingsStore { break; } - match setting_value.deserialize_setting(local_settings) { - Ok(local_settings) => { - paths_stack.push(Some((*root_id, directory_path.as_ref()))); - project_settings_stack.push(local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.is_some_and( - |(changed_root_id, changed_local_path)| { - *root_id != changed_root_id - || !directory_path.starts_with(changed_local_path) - }, - ) { - continue; - } - - if let Some(value) = setting_value - .load_setting( - SettingsSources { - default: &default_settings, - global: global_settings.as_ref(), - extensions: extension_settings.as_ref(), - user: user_settings.as_ref(), - release_channel: release_channel_settings.as_ref(), - operating_system: os_settings.as_ref(), - profile: profile_settings.as_ref(), - server: server_settings.as_ref(), - project: &project_settings_stack.iter().collect::>(), - }, - cx, - ) - .log_err() - { - setting_value.set_local_value(*root_id, directory_path.clone(), value); - } - } - Err(error) => { - return Err(InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: error.to_string(), - }); + // NOTE: this kind of condition existing in the old code too, + // but is there a problem when a setting is removed from a file? + if setting_value.from_file(local_settings).is_some() { + paths_stack.push(Some((*root_id, directory_path.as_ref()))); + project_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| { + *root_id != changed_root_id + || !directory_path.starts_with(changed_local_path) + }) { + continue; } + + let mut value = setting_value.from_file(&default).unwrap(); + setting_value.refine(&mut value, &refinements); + setting_value.refine(&mut value, &project_settings_stack); + setting_value.set_local_value(*root_id, directory_path.clone(), value); } } } @@ -1397,108 +1228,27 @@ impl Debug for SettingsStore { .map(|value| value.setting_type_name()) .collect::>(), ) - .field("default_settings", &self.raw_default_settings) - .field("user_settings", &self.raw_user_settings) - .field("local_settings", &self.raw_local_settings) + .field("default_settings", &self.default_settings) + .field("user_settings", &self.user_settings) + .field("local_settings", &self.local_settings) .finish_non_exhaustive() } } impl AnySettingValue for SettingValue { - fn key(&self) -> Option<&'static str> { - T::FileContent::KEY - } - - fn setting_type_name(&self) -> &'static str { - type_name::() + fn from_file(&self, s: &SettingsContent) -> Option> { + T::from_file(s).map(|result| Box::new(result) as _) } - fn load_setting( - &self, - values: SettingsSources, - cx: &mut App, - ) -> Result> { - Ok(Box::new(T::load( - SettingsSources { - default: values.default.0.downcast_ref::().unwrap(), - global: values - .global - .map(|value| value.0.downcast_ref::().unwrap()), - extensions: values - .extensions - .map(|value| value.0.downcast_ref::().unwrap()), - user: values - .user - .map(|value| value.0.downcast_ref::().unwrap()), - release_channel: values - .release_channel - .map(|value| value.0.downcast_ref::().unwrap()), - operating_system: values - .operating_system - .map(|value| value.0.downcast_ref::().unwrap()), - profile: values - .profile - .map(|value| value.0.downcast_ref::().unwrap()), - server: values - .server - .map(|value| value.0.downcast_ref::().unwrap()), - project: values - .project - .iter() - .map(|value| value.0.downcast_ref().unwrap()) - .collect::>() - .as_slice(), - }, - cx, - )?)) + fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent]) { + let value = value.downcast_mut::().unwrap(); + for refinement in refinements { + value.refine(refinement) + } } - fn deserialize_setting_with_key( - &self, - mut json: &Value, - ) -> (Option<&'static str>, Result) { - let mut key = None; - if let Some(k) = T::FileContent::KEY { - if let Some(value) = json.get(k) { - json = value; - key = Some(k); - } else if let Some((k, value)) = - T::FileContent::FALLBACK_KEY.and_then(|k| Some((k, json.get(k)?))) - { - json = value; - key = Some(k); - } else { - let value = T::FileContent::default(); - return ( - T::FileContent::KEY, - Ok(DeserializedSetting(Box::new(value))), - ); - } - } - let value = serde_path_to_error::deserialize::<_, T::FileContent>(json) - .map(|value| DeserializedSetting(Box::new(value))) - .map_err(|err| { - // construct a path using the key and reported error path if possible. - // Unfortunately, serde_path_to_error does not expose the necessary - // methods and data to simply add the key to the path - let mut path = String::new(); - if let Some(key) = key { - path.push_str(key); - } - let err_path = err.path().to_string(); - // when the path is empty, serde_path_to_error stringifies the path as ".", - // when the path is unknown, serde_path_to_error stringifies the path as an empty string - if !err_path.is_empty() && !err_path.starts_with(".") { - path.push('.'); - path.push_str(&err_path); - } - if path.is_empty() { - anyhow::Error::from(err.into_inner()) - } else { - anyhow::anyhow!("'{}': {}", err.into_inner(), path) - } - }); - (key, value) + fn setting_type_name(&self) -> &'static str { + type_name::() } fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)> { @@ -1538,7 +1288,8 @@ impl AnySettingValue for SettingValue { } fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - T::FileContent::json_schema(generator) + todo!() + // T::FileContent::json_schema(generator) } fn edits_for_update( @@ -1549,41 +1300,43 @@ impl AnySettingValue for SettingValue { text: &mut String, edits: &mut Vec<(Range, String)>, ) { - let (key, deserialized_setting) = self.deserialize_setting_with_key(raw_settings); - let old_content = match deserialized_setting { - Ok(content) => content.0.downcast::().unwrap(), - Err(_) => Box::<::FileContent>::default(), - }; - let mut new_content = old_content.clone(); - T::import_from_vscode(vscode_settings, &mut new_content); - - let old_value = serde_json::to_value(&old_content).unwrap(); - let new_value = serde_json::to_value(new_content).unwrap(); - - let mut key_path = Vec::new(); - if let Some(key) = key { - key_path.push(key); - } - - update_value_in_json_text( - text, - &mut key_path, - tab_size, - &old_value, - &new_value, - T::PRESERVED_KEYS.unwrap_or_default(), - edits, - ); + todo!() + // let (key, deserialized_setting) = self.deserialize_setting_with_key(raw_settings); + // let old_content = match deserialized_setting { + // Ok(content) => content.0.downcast::().unwrap(), + // Err(_) => Box::<::FileContent>::default(), + // }; + // let mut new_content = old_content.clone(); + // T::import_from_vscode(vscode_settings, &mut new_content); + + // let old_value = serde_json::to_value(&old_content).unwrap(); + // let new_value = serde_json::to_value(new_content).unwrap(); + + // let mut key_path = Vec::new(); + // if let Some(key) = key { + // key_path.push(key); + // } + + // update_value_in_json_text( + // text, + // &mut key_path, + // tab_size, + // &old_value, + // &new_value, + // T::PRESERVED_KEYS.unwrap_or_default(), + // edits, + // ); } fn settings_ui_item(&self) -> SettingsUiEntry { - <::FileContent as SettingsUi>::settings_ui_entry() + todo!() + // <::FileContent as SettingsUi>::settings_ui_entry() } } #[cfg(test)] mod tests { - use crate::VsCodeSettingsSource; + use crate::{VsCodeSettingsSource, settings_content::LanguageSettingsContent}; use super::*; // This is so the SettingsUi macro can still work properly @@ -1592,190 +1345,190 @@ mod tests { use settings_ui_macros::{SettingsKey, SettingsUi}; use unindent::Unindent; - #[gpui::test] - fn test_settings_store_basic(cx: &mut App) { - let mut store = SettingsStore::new(cx); - store.register_setting::(cx); - store.register_setting::(cx); - store.register_setting::(cx); - store - .set_default_settings( - r#"{ - "turbo": false, - "user": { - "name": "John Doe", - "age": 30, - "staff": false - } - }"#, - cx, - ) - .unwrap(); - - assert_eq!(store.get::(None), &TurboSetting(false)); - assert_eq!( - store.get::(None), - &UserSettings { - name: "John Doe".to_string(), - age: 30, - staff: false, - } - ); - assert_eq!( - store.get::(None), - &MultiKeySettings { - key1: String::new(), - key2: String::new(), - } - ); - - store - .set_user_settings( - r#"{ - "turbo": true, - "user": { "age": 31 }, - "key1": "a" - }"#, - cx, - ) - .unwrap(); - - assert_eq!(store.get::(None), &TurboSetting(true)); - assert_eq!( - store.get::(None), - &UserSettings { - name: "John Doe".to_string(), - age: 31, - staff: false - } - ); - - store - .set_local_settings( - WorktreeId::from_usize(1), - Path::new("/root1").into(), - LocalSettingsKind::Settings, - Some(r#"{ "user": { "staff": true } }"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - WorktreeId::from_usize(1), - Path::new("/root1/subdir").into(), - LocalSettingsKind::Settings, - Some(r#"{ "user": { "name": "Jane Doe" } }"#), - cx, - ) - .unwrap(); - - store - .set_local_settings( - WorktreeId::from_usize(1), - Path::new("/root2").into(), - LocalSettingsKind::Settings, - Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), - cx, - ) - .unwrap(); - - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: Path::new("/root1/something"), - })), - &UserSettings { - name: "John Doe".to_string(), - age: 31, - staff: true - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: Path::new("/root1/subdir/something") - })), - &UserSettings { - name: "Jane Doe".to_string(), - age: 31, - staff: true - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: Path::new("/root2/something") - })), - &UserSettings { - name: "John Doe".to_string(), - age: 42, - staff: false - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: Path::new("/root2/something") - })), - &MultiKeySettings { - key1: "a".to_string(), - key2: "b".to_string(), - } - ); - } - - #[gpui::test] - fn test_setting_store_assign_json_before_register(cx: &mut App) { - let mut store = SettingsStore::new(cx); - store - .set_default_settings( - r#"{ - "turbo": true, - "user": { - "name": "John Doe", - "age": 30, - "staff": false - }, - "key1": "x" - }"#, - cx, - ) - .unwrap(); - 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!( - store.get::(None), - &UserSettings { - name: "John Doe".to_string(), - age: 30, - staff: false, - } - ); - - store.register_setting::(cx); - assert_eq!( - store.get::(None), - &MultiKeySettings { - key1: "x".into(), - key2: String::new(), - } - ); - } - - fn check_settings_update( + // #[gpui::test] + // fn test_settings_store_basic(cx: &mut App) { + // let mut store = SettingsStore::new(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + // store + // .set_default_settings( + // r#"{ + // "turbo": false, + // "user": { + // "name": "John Doe", + // "age": 30, + // "staff": false + // } + // }"#, + // cx, + // ) + // .unwrap(); + + // assert_eq!(store.get::(None), &TurboSetting(false)); + // assert_eq!( + // store.get::(None), + // &UserSettings { + // name: "John Doe".to_string(), + // age: 30, + // staff: false, + // } + // ); + // assert_eq!( + // store.get::(None), + // &MultiKeySettings { + // key1: String::new(), + // key2: String::new(), + // } + // ); + + // store + // .set_user_settings( + // r#"{ + // "turbo": true, + // "user": { "age": 31 }, + // "key1": "a" + // }"#, + // cx, + // ) + // .unwrap(); + + // assert_eq!(store.get::(None), &TurboSetting(true)); + // assert_eq!( + // store.get::(None), + // &UserSettings { + // name: "John Doe".to_string(), + // age: 31, + // staff: false + // } + // ); + + // store + // .set_local_settings( + // WorktreeId::from_usize(1), + // Path::new("/root1").into(), + // LocalSettingsKind::Settings, + // Some(r#"{ "user": { "staff": true } }"#), + // cx, + // ) + // .unwrap(); + // store + // .set_local_settings( + // WorktreeId::from_usize(1), + // Path::new("/root1/subdir").into(), + // LocalSettingsKind::Settings, + // Some(r#"{ "user": { "name": "Jane Doe" } }"#), + // cx, + // ) + // .unwrap(); + + // store + // .set_local_settings( + // WorktreeId::from_usize(1), + // Path::new("/root2").into(), + // LocalSettingsKind::Settings, + // Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), + // cx, + // ) + // .unwrap(); + + // assert_eq!( + // store.get::(Some(SettingsLocation { + // worktree_id: WorktreeId::from_usize(1), + // path: Path::new("/root1/something"), + // })), + // &UserSettings { + // name: "John Doe".to_string(), + // age: 31, + // staff: true + // } + // ); + // assert_eq!( + // store.get::(Some(SettingsLocation { + // worktree_id: WorktreeId::from_usize(1), + // path: Path::new("/root1/subdir/something") + // })), + // &UserSettings { + // name: "Jane Doe".to_string(), + // age: 31, + // staff: true + // } + // ); + // assert_eq!( + // store.get::(Some(SettingsLocation { + // worktree_id: WorktreeId::from_usize(1), + // path: Path::new("/root2/something") + // })), + // &UserSettings { + // name: "John Doe".to_string(), + // age: 42, + // staff: false + // } + // ); + // assert_eq!( + // store.get::(Some(SettingsLocation { + // worktree_id: WorktreeId::from_usize(1), + // path: Path::new("/root2/something") + // })), + // &MultiKeySettings { + // key1: "a".to_string(), + // key2: "b".to_string(), + // } + // ); + // } + + // #[gpui::test] + // fn test_setting_store_assign_json_before_register(cx: &mut App) { + // let mut store = SettingsStore::new(cx); + // store + // .set_default_settings( + // r#"{ + // "turbo": true, + // "user": { + // "name": "John Doe", + // "age": 30, + // "staff": false + // }, + // "key1": "x" + // }"#, + // cx, + // ) + // .unwrap(); + // 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!( + // store.get::(None), + // &UserSettings { + // name: "John Doe".to_string(), + // age: 30, + // staff: false, + // } + // ); + + // store.register_setting::(cx); + // assert_eq!( + // store.get::(None), + // &MultiKeySettings { + // key1: "x".into(), + // key2: String::new(), + // } + // ); + // } + + fn check_settings_update( store: &mut SettingsStore, old_json: String, - update: fn(&mut T::FileContent), + update: fn(&mut SettingsContent), expected_new_json: String, cx: &mut App, ) { store.set_user_settings(&old_json, cx).ok(); - let edits = store.edits_for_update::(&old_json, update); + let edits = store.edits_for_update(&old_json, update); let mut new_json = old_json; for (range, replacement) in edits.into_iter() { new_json.replace_range(range, &replacement); @@ -1786,31 +1539,32 @@ mod tests { #[gpui::test] fn test_setting_store_update(cx: &mut App) { let mut store = SettingsStore::new(cx); - store.register_setting::(cx); - store.register_setting::(cx); - store.register_setting::(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); // entries added and updated - check_settings_update::( + check_settings_update( &mut store, r#"{ "languages": { "JSON": { - "language_setting_1": true + "auto_indent": true } } }"# .unindent(), |settings| { settings - .languages + .languages_mut() .get_mut("JSON") .unwrap() - .language_setting_1 = Some(false); - settings.languages.insert( + .auto_indent = Some(false); + + settings.languages_mut().insert( "Rust".into(), - LanguageSettingEntry { - language_setting_2: Some(true), + LanguageSettingsContent { + auto_indent: Some(true), ..Default::default() }, ); @@ -1818,10 +1572,10 @@ mod tests { r#"{ "languages": { "Rust": { - "language_setting_2": true + "auto_indent": true }, "JSON": { - "language_setting_1": false + "auto_indent": false } } }"# @@ -1830,7 +1584,7 @@ mod tests { ); // entries removed - check_settings_update::( + check_settings_update( &mut store, r#"{ "languages": { @@ -1844,7 +1598,7 @@ mod tests { }"# .unindent(), |settings| { - settings.languages.remove("JSON").unwrap(); + settings.languages_mut().remove("JSON").unwrap(); }, r#"{ "languages": { @@ -1857,7 +1611,7 @@ mod tests { cx, ); - check_settings_update::( + check_settings_update( &mut store, r#"{ "languages": { @@ -1871,7 +1625,7 @@ mod tests { }"# .unindent(), |settings| { - settings.languages.remove("Rust").unwrap(); + settings.languages_mut().remove("Rust").unwrap(); }, r#"{ "languages": { @@ -1884,501 +1638,501 @@ mod tests { cx, ); - // weird formatting - check_settings_update::( - &mut store, - r#"{ - "user": { "age": 36, "name": "Max", "staff": true } - }"# - .unindent(), - |settings| settings.age = Some(37), - r#"{ - "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 - check_settings_update::( - &mut store, - r#""#.unindent(), - |settings| settings.age = Some(37), - r#"{ - "user": { - "age": 37 - } - } - "# - .unindent(), - cx, - ); - - check_settings_update::( - &mut store, - r#"{ - } - "# - .unindent(), - |settings| settings.age = Some(37), - r#"{ - "user": { - "age": 37 - } - } - "# - .unindent(), - cx, - ); - } - - #[gpui::test] - fn test_vscode_import(cx: &mut App) { - let mut store = SettingsStore::new(cx); - store.register_setting::(cx); - store.register_setting::(cx); - store.register_setting::(cx); - store.register_setting::(cx); - - // create settings that werent present - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#" { "user.age": 37 } "#.to_owned(), - r#"{ - "user": { - "age": 37 - } - } - "# - .unindent(), - cx, - ); - - // persist settings that were present - check_vscode_import( - &mut store, - r#"{ - "user": { - "staff": true, - "age": 37 - } - } - "# - .unindent(), - r#"{ "user.age": 42 }"#.to_owned(), - r#"{ - "user": { - "staff": true, - "age": 42 - } - } - "# - .unindent(), - cx, - ); - - // don't clobber settings that aren't present in vscode - check_vscode_import( - &mut store, - r#"{ - "user": { - "staff": true, - "age": 37 - } - } - "# - .unindent(), - r#"{}"#.to_owned(), - r#"{ - "user": { - "staff": true, - "age": 37 - } - } - "# - .unindent(), - cx, - ); - - // custom enum - check_vscode_import( - &mut store, - r#"{ - "journal": { - "hour_format": "hour12" - } - } - "# - .unindent(), - r#"{ "time_format": "24" }"#.to_owned(), - r#"{ - "journal": { - "hour_format": "hour24" - } - } - "# - .unindent(), - cx, - ); - - // Multiple keys for one setting - check_vscode_import( - &mut store, - r#"{ - "key1": "value" - } - "# - .unindent(), - r#"{ - "key_1_first": "hello", - "key_1_second": "world" - }"# - .to_owned(), - r#"{ - "key1": "hello world" - } - "# - .unindent(), - cx, - ); - - // Merging lists together entries added and updated - check_vscode_import( - &mut store, - r#"{ - "languages": { - "JSON": { - "language_setting_1": true - }, - "Rust": { - "language_setting_2": true - } - } - }"# - .unindent(), - r#"{ - "vscode_languages": [ - { - "name": "JavaScript", - "language_setting_1": true - }, - { - "name": "Rust", - "language_setting_2": false - } - ] - }"# - .to_owned(), - r#"{ - "languages": { - "JavaScript": { - "language_setting_1": true - }, - "JSON": { - "language_setting_1": true - }, - "Rust": { - "language_setting_2": false - } - } - }"# - .unindent(), - cx, - ); - } - - fn check_vscode_import( - store: &mut SettingsStore, - old: String, - vscode: String, - expected: String, - cx: &mut App, - ) { - store.set_user_settings(&old, cx).ok(); - let new = store.get_vscode_edits( - old, - &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(), - ); - pretty_assertions::assert_eq!(new, expected); - } - - #[derive(Debug, PartialEq, Deserialize, SettingsUi)] - struct UserSettings { - name: String, - age: u32, - staff: bool, - } - - #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] - #[settings_key(key = "user")] - struct UserSettingsContent { - name: Option, - age: Option, - staff: Option, - } - - impl Settings for UserSettings { - type FileContent = UserSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { - vscode.u32_setting("user.age", &mut current.age); - } - } - - #[derive(Debug, Deserialize, PartialEq)] - struct TurboSetting(bool); - - #[derive( - Copy, - Clone, - PartialEq, - Eq, - Debug, - Default, - serde::Serialize, - serde::Deserialize, - SettingsUi, - SettingsKey, - JsonSchema, - )] - #[serde(default)] - #[settings_key(None)] - pub struct TurboSettingContent { - turbo: Option, - } - - impl Settings for TurboSetting { - type FileContent = TurboSettingContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self( - sources - .user - .or(sources.server) - .unwrap_or(sources.default) - .turbo - .unwrap_or_default(), - )) - } - - fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} - } - - #[derive(Clone, Debug, PartialEq, Deserialize)] - struct MultiKeySettings { - #[serde(default)] - key1: String, - #[serde(default)] - key2: String, - } - - #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] - #[settings_key(None)] - struct MultiKeySettingsJson { - key1: Option, - key2: Option, - } - - impl Settings for MultiKeySettings { - type FileContent = MultiKeySettingsJson; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { - let first_value = vscode.read_string("key_1_first"); - let second_value = vscode.read_string("key_1_second"); - - if let Some((first, second)) = first_value.zip(second_value) { - current.key1 = Some(format!("{} {}", first, second)); - } - } - } - - #[derive(Debug, Deserialize)] - struct JournalSettings { - #[expect(unused)] - pub path: String, - #[expect(unused)] - pub hour_format: HourFormat, - } - - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] - #[serde(rename_all = "snake_case")] - enum HourFormat { - Hour12, - Hour24, - } - - #[derive( - Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, - )] - #[settings_key(key = "journal")] - struct JournalSettingsJson { - pub path: Option, - pub hour_format: Option, - } - - impl Settings for JournalSettings { - type FileContent = JournalSettingsJson; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { - vscode.enum_setting("time_format", &mut current.hour_format, |s| match s { - "12" => Some(HourFormat::Hour12), - "24" => Some(HourFormat::Hour24), - _ => None, - }); - } - } - - #[gpui::test] - fn test_global_settings(cx: &mut App) { - let mut store = SettingsStore::new(cx); - store.register_setting::(cx); - store - .set_default_settings( - r#"{ - "user": { - "name": "John Doe", - "age": 30, - "staff": false - } - }"#, - cx, - ) - .unwrap(); - - // Set global settings - these should override defaults but not user settings - store - .set_global_settings( - r#"{ - "user": { - "name": "Global User", - "age": 35, - "staff": true - } - }"#, - cx, - ) - .unwrap(); - - // Before user settings, global settings should apply - assert_eq!( - store.get::(None), - &UserSettings { - name: "Global User".to_string(), - age: 35, - staff: true, - } - ); - - // Set user settings - these should override both defaults and global - store - .set_user_settings( - r#"{ - "user": { - "age": 40 - } - }"#, - cx, - ) - .unwrap(); - - // User settings should override global settings - assert_eq!( - store.get::(None), - &UserSettings { - name: "Global User".to_string(), // Name from global settings - age: 40, // Age from user settings - staff: true, // Staff from global settings - } - ); - } - - #[derive( - Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, - )] - #[settings_key(None)] - struct LanguageSettings { - #[serde(default)] - languages: HashMap, - } - - #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] - struct LanguageSettingEntry { - language_setting_1: Option, - language_setting_2: Option, - } - - impl Settings for LanguageSettings { - type FileContent = Self; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { - current.languages.extend( - vscode - .read_value("vscode_languages") - .and_then(|value| value.as_array()) - .map(|languages| { - languages - .iter() - .filter_map(|value| value.as_object()) - .filter_map(|item| { - let mut rest = item.clone(); - let name = rest.remove("name")?.as_str()?.to_string(); - let entry = serde_json::from_value::( - serde_json::Value::Object(rest), - ) - .ok()?; - - Some((name, entry)) - }) - }) - .into_iter() - .flatten(), - ); - } - } + // // weird formatting + // check_settings_update( + // &mut store, + // r#"{ + // "user": { "age": 36, "name": "Max", "staff": true } + // }"# + // .unindent(), + // |settings| settings.age = Some(37), + // r#"{ + // "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 + // check_settings_update::( + // &mut store, + // r#""#.unindent(), + // |settings| settings.age = Some(37), + // r#"{ + // "user": { + // "age": 37 + // } + // } + // "# + // .unindent(), + // cx, + // ); + + // check_settings_update::( + // &mut store, + // r#"{ + // } + // "# + // .unindent(), + // |settings| settings.age = Some(37), + // r#"{ + // "user": { + // "age": 37 + // } + // } + // "# + // .unindent(), + // cx, + // ); + } + + // #[gpui::test] + // fn test_vscode_import(cx: &mut App) { + // let mut store = SettingsStore::new(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + // store.register_setting::(cx); + + // // create settings that werent present + // check_vscode_import( + // &mut store, + // r#"{ + // } + // "# + // .unindent(), + // r#" { "user.age": 37 } "#.to_owned(), + // r#"{ + // "user": { + // "age": 37 + // } + // } + // "# + // .unindent(), + // cx, + // ); + + // // persist settings that were present + // check_vscode_import( + // &mut store, + // r#"{ + // "user": { + // "staff": true, + // "age": 37 + // } + // } + // "# + // .unindent(), + // r#"{ "user.age": 42 }"#.to_owned(), + // r#"{ + // "user": { + // "staff": true, + // "age": 42 + // } + // } + // "# + // .unindent(), + // cx, + // ); + + // // don't clobber settings that aren't present in vscode + // check_vscode_import( + // &mut store, + // r#"{ + // "user": { + // "staff": true, + // "age": 37 + // } + // } + // "# + // .unindent(), + // r#"{}"#.to_owned(), + // r#"{ + // "user": { + // "staff": true, + // "age": 37 + // } + // } + // "# + // .unindent(), + // cx, + // ); + + // // custom enum + // check_vscode_import( + // &mut store, + // r#"{ + // "journal": { + // "hour_format": "hour12" + // } + // } + // "# + // .unindent(), + // r#"{ "time_format": "24" }"#.to_owned(), + // r#"{ + // "journal": { + // "hour_format": "hour24" + // } + // } + // "# + // .unindent(), + // cx, + // ); + + // // Multiple keys for one setting + // check_vscode_import( + // &mut store, + // r#"{ + // "key1": "value" + // } + // "# + // .unindent(), + // r#"{ + // "key_1_first": "hello", + // "key_1_second": "world" + // }"# + // .to_owned(), + // r#"{ + // "key1": "hello world" + // } + // "# + // .unindent(), + // cx, + // ); + + // // Merging lists together entries added and updated + // check_vscode_import( + // &mut store, + // r#"{ + // "languages": { + // "JSON": { + // "language_setting_1": true + // }, + // "Rust": { + // "language_setting_2": true + // } + // } + // }"# + // .unindent(), + // r#"{ + // "vscode_languages": [ + // { + // "name": "JavaScript", + // "language_setting_1": true + // }, + // { + // "name": "Rust", + // "language_setting_2": false + // } + // ] + // }"# + // .to_owned(), + // r#"{ + // "languages": { + // "JavaScript": { + // "language_setting_1": true + // }, + // "JSON": { + // "language_setting_1": true + // }, + // "Rust": { + // "language_setting_2": false + // } + // } + // }"# + // .unindent(), + // cx, + // ); + // } + + // fn check_vscode_import( + // store: &mut SettingsStore, + // old: String, + // vscode: String, + // expected: String, + // cx: &mut App, + // ) { + // store.set_user_settings(&old, cx).ok(); + // let new = store.get_vscode_edits( + // old, + // &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(), + // ); + // pretty_assertions::assert_eq!(new, expected); + // } + + // #[derive(Debug, PartialEq, Deserialize, SettingsUi)] + // struct UserSettings { + // name: String, + // age: u32, + // staff: bool, + // } + + // #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] + // #[settings_key(key = "user")] + // struct UserSettingsContent { + // name: Option, + // age: Option, + // staff: Option, + // } + + // impl Settings for UserSettings { + // type FileContent = UserSettingsContent; + + // fn load(sources: SettingsSources, _: &mut App) -> Result { + // sources.json_merge() + // } + + // fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + // vscode.u32_setting("user.age", &mut current.age); + // } + // } + + // #[derive(Debug, Deserialize, PartialEq)] + // struct TurboSetting(bool); + + // #[derive( + // Copy, + // Clone, + // PartialEq, + // Eq, + // Debug, + // Default, + // serde::Serialize, + // serde::Deserialize, + // SettingsUi, + // SettingsKey, + // JsonSchema, + // )] + // #[serde(default)] + // #[settings_key(None)] + // pub struct TurboSettingContent { + // turbo: Option, + // } + + // impl Settings for TurboSetting { + // type FileContent = TurboSettingContent; + + // fn load(sources: SettingsSources, _: &mut App) -> Result { + // Ok(Self( + // sources + // .user + // .or(sources.server) + // .unwrap_or(sources.default) + // .turbo + // .unwrap_or_default(), + // )) + // } + + // fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} + // } + + // #[derive(Clone, Debug, PartialEq, Deserialize)] + // struct MultiKeySettings { + // #[serde(default)] + // key1: String, + // #[serde(default)] + // key2: String, + // } + + // #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] + // #[settings_key(None)] + // struct MultiKeySettingsJson { + // key1: Option, + // key2: Option, + // } + + // impl Settings for MultiKeySettings { + // type FileContent = MultiKeySettingsJson; + + // fn load(sources: SettingsSources, _: &mut App) -> Result { + // sources.json_merge() + // } + + // fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + // let first_value = vscode.read_string("key_1_first"); + // let second_value = vscode.read_string("key_1_second"); + + // if let Some((first, second)) = first_value.zip(second_value) { + // current.key1 = Some(format!("{} {}", first, second)); + // } + // } + // } + + // #[derive(Debug, Deserialize)] + // struct JournalSettings { + // #[expect(unused)] + // pub path: String, + // #[expect(unused)] + // pub hour_format: HourFormat, + // } + + // #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + // #[serde(rename_all = "snake_case")] + // enum HourFormat { + // Hour12, + // Hour24, + // } + + // #[derive( + // Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, + // )] + // #[settings_key(key = "journal")] + // struct JournalSettingsJson { + // pub path: Option, + // pub hour_format: Option, + // } + + // impl Settings for JournalSettings { + // type FileContent = JournalSettingsJson; + + // fn load(sources: SettingsSources, _: &mut App) -> Result { + // sources.json_merge() + // } + + // fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + // vscode.enum_setting("time_format", &mut current.hour_format, |s| match s { + // "12" => Some(HourFormat::Hour12), + // "24" => Some(HourFormat::Hour24), + // _ => None, + // }); + // } + // } + + // #[gpui::test] + // fn test_global_settings(cx: &mut App) { + // let mut store = SettingsStore::new(cx); + // store.register_setting::(cx); + // store + // .set_default_settings( + // r#"{ + // "user": { + // "name": "John Doe", + // "age": 30, + // "staff": false + // } + // }"#, + // cx, + // ) + // .unwrap(); + + // // Set global settings - these should override defaults but not user settings + // store + // .set_global_settings( + // r#"{ + // "user": { + // "name": "Global User", + // "age": 35, + // "staff": true + // } + // }"#, + // cx, + // ) + // .unwrap(); + + // // Before user settings, global settings should apply + // assert_eq!( + // store.get::(None), + // &UserSettings { + // name: "Global User".to_string(), + // age: 35, + // staff: true, + // } + // ); + + // // Set user settings - these should override both defaults and global + // store + // .set_user_settings( + // r#"{ + // "user": { + // "age": 40 + // } + // }"#, + // cx, + // ) + // .unwrap(); + + // // User settings should override global settings + // assert_eq!( + // store.get::(None), + // &UserSettings { + // name: "Global User".to_string(), // Name from global settings + // age: 40, // Age from user settings + // staff: true, // Staff from global settings + // } + // ); + // } + + // #[derive( + // Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, + // )] + // #[settings_key(None)] + // struct LanguageSettings { + // #[serde(default)] + // languages: HashMap, + // } + + // #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] + // struct LanguageSettingEntry { + // language_setting_1: Option, + // language_setting_2: Option, + // } + + // impl Settings for LanguageSettings { + // type FileContent = Self; + + // fn load(sources: SettingsSources, _: &mut App) -> Result { + // sources.json_merge() + // } + + // fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + // current.languages.extend( + // vscode + // .read_value("vscode_languages") + // .and_then(|value| value.as_array()) + // .map(|languages| { + // languages + // .iter() + // .filter_map(|value| value.as_object()) + // .filter_map(|item| { + // let mut rest = item.clone(); + // let name = rest.remove("name")?.as_str()?.to_string(); + // let entry = serde_json::from_value::( + // serde_json::Value::Object(rest), + // ) + // .ok()?; + + // Some((name, entry)) + // }) + // }) + // .into_iter() + // .flatten(), + // ); + // } + // } }