From 9edebc58aa47e85f1be5deb650a9f038e3caa6be Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 14 Sep 2025 22:45:31 -0600 Subject: [PATCH 001/117] 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(), + // ); + // } + // } } From b2bd97c16e507627f4345eb0e339cdb939d9590d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 12:31:15 -0600 Subject: [PATCH 002/117] TEMP --- crates/eval/src/eval.rs | 5 +- crates/settings/src/settings.rs | 7 +- crates/settings/src/settings_content.rs | 367 ++++++++++++++++++++- crates/settings/src/settings_store.rs | 350 +++++++++++--------- crates/theme/src/settings.rs | 2 - crates/title_bar/src/title_bar_settings.rs | 76 ++--- crates/util/src/util.rs | 18 + crates/zeta_cli/src/headless.rs | 5 +- 8 files changed, 618 insertions(+), 212 deletions(-) diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 32399e26c020b315f948e7e9013c7000ef9bd899..17b9486a9f5cbbc8f0cafe86f55a6ac00d1c3023 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -340,10 +340,7 @@ pub fn init(cx: &mut App) -> Arc { release_channel::init(app_version, cx); gpui_tokio::init(cx); - let mut settings_store = SettingsStore::new(cx); - settings_store - .set_default_settings(settings::default_settings().as_ref(), cx) - .unwrap(); + let mut settings_store = SettingsStore::new(cx, &settings::default_settings()); cx.set_global(settings_store); client::init_settings(cx); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index c02ea63e36aff49fc8b6b61423367cf862b9e544..41302a430441107dd7c14f9c9c121be84a0012ba 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -8,6 +8,8 @@ mod settings_store; mod settings_ui_core; mod vscode_import; +pub use settings_content::*; + use gpui::{App, Global}; use rust_embed::RustEmbed; use std::{borrow::Cow, fmt, str}; @@ -76,10 +78,7 @@ impl fmt::Display for WorktreeId { pub struct SettingsAssets; pub fn init(cx: &mut App) { - let mut settings = SettingsStore::new(cx); - settings - .set_default_settings(&default_settings(), cx) - .unwrap(); + let settings = SettingsStore::new(cx, &default_settings()); cx.set_global(settings); BaseKeymap::register(cx); SettingsStore::observe_active_settings_profile_name(cx).detach(); diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index d5c11da66767a8016509d31d81a7f5b0754904bd..03510564d117d73db97b32d30b634c9f6c253b71 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -3,8 +3,9 @@ use std::env; use std::num::NonZeroU32; use std::sync::Arc; +use anyhow::Result; use collections::{HashMap, HashSet}; -use gpui::{App, Modifiers, SharedString}; +use gpui::{App, FontFallbacks, FontFeatures, HighlightStyle, Hsla, Modifiers, SharedString}; use release_channel::ReleaseChannel; use schemars::{JsonSchema, json_schema}; use serde::de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}; @@ -20,6 +21,10 @@ pub struct SettingsContent { pub project: ProjectSettingsContent, pub base_keymap: Option, + + pub auto_update: Option, + + pub title_bar: Option, } impl SettingsContent { @@ -32,7 +37,7 @@ impl SettingsContent { #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ServerSettingsContent { #[serde(flatten)] - project: ProjectSettingsContent, + pub project: ProjectSettingsContent, } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] @@ -970,3 +975,361 @@ pub enum IndentGuideBackgroundColoring { /// Use a different color for each indentation level. IndentAware, } + +#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct TitleBarSettingsContent { + /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". + /// + /// Default: "always" + pub show: Option, + /// Whether to show the branch icon beside branch switcher in the title bar. + /// + /// Default: false + pub show_branch_icon: Option, + /// Whether to show onboarding banners in the title bar. + /// + /// Default: true + pub show_onboarding_banner: Option, + /// Whether to show user avatar in the title bar. + /// + /// Default: true + pub show_user_picture: Option, + /// Whether to show the branch name button in the titlebar. + /// + /// Default: true + pub show_branch_name: Option, + /// Whether to show the project host and name in the titlebar. + /// + /// Default: true + pub show_project_items: Option, + /// Whether to show the sign in button in the title bar. + /// + /// Default: true + pub show_sign_in: Option, + /// Whether to show the menus in the title bar. + /// + /// Default: false + pub show_menus: Option, +} + +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TitleBarVisibilityContent { + Always, + Never, + HideInFullScreen, +} + +/// Settings for rendering text in UI and text buffers. +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ThemeSettingsContent { + /// The default font size for text in the UI. + #[serde(default)] + pub ui_font_size: Option, + /// The name of a font to use for rendering in the UI. + #[serde(default)] + pub ui_font_family: Option, + /// The font fallbacks to use for rendering in the UI. + #[serde(default)] + #[schemars(default = "default_font_fallbacks")] + #[schemars(extend("uniqueItems" = true))] + pub ui_font_fallbacks: Option>, + /// The OpenType features to enable for text in the UI. + #[serde(default)] + #[schemars(default = "default_font_features")] + pub ui_font_features: Option, + /// The weight of the UI font in CSS units from 100 to 900. + #[serde(default)] + pub ui_font_weight: Option, + /// The name of a font to use for rendering in text buffers. + #[serde(default)] + pub buffer_font_family: Option, + /// The font fallbacks to use for rendering in text buffers. + #[serde(default)] + #[schemars(extend("uniqueItems" = true))] + pub buffer_font_fallbacks: Option>, + /// The default font size for rendering in text buffers. + #[serde(default)] + pub buffer_font_size: Option, + /// The weight of the editor font in CSS units from 100 to 900. + #[serde(default)] + pub buffer_font_weight: Option, + /// The buffer's line height. + #[serde(default)] + pub buffer_line_height: Option, + /// The OpenType features to enable for rendering in text buffers. + #[serde(default)] + #[schemars(default = "default_font_features")] + pub buffer_font_features: Option, + /// The font size for the agent panel. Falls back to the UI font size if unset. + #[serde(default)] + pub agent_font_size: Option>, + /// The name of the Zed theme to use. + #[serde(default)] + pub theme: Option, + /// The name of the icon theme to use. + #[serde(default)] + pub icon_theme: Option, + + /// UNSTABLE: Expect many elements to be broken. + /// + // Controls the density of the UI. + #[serde(rename = "unstable.ui_density", default)] + pub ui_density: Option, + + /// How much to fade out unused code. + #[serde(default)] + pub unnecessary_code_fade: Option, + + /// EXPERIMENTAL: Overrides for the current theme. + /// + /// These values will override the ones on the current theme specified in `theme`. + #[serde(rename = "experimental.theme_overrides", default)] + pub experimental_theme_overrides: Option, + + /// Overrides per theme + /// + /// These values will override the ones on the specified theme + #[serde(default)] + pub theme_overrides: HashMap, +} + +fn default_font_features() -> Option { + Some(FontFeatures::default()) +} + +fn default_font_fallbacks() -> Option { + Some(FontFallbacks::default()) +} + +/// Represents the selection of a theme, which can be either static or dynamic. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(untagged)] +pub enum ThemeSelection { + /// A static theme selection, represented by a single theme name. + Static(ThemeName), + /// A dynamic theme selection, which can change based the [ThemeMode]. + Dynamic { + /// The mode used to determine which theme to use. + #[serde(default)] + mode: ThemeMode, + /// The theme to use for light mode. + light: ThemeName, + /// The theme to use for dark mode. + dark: ThemeName, + }, +} + +/// Represents the selection of an icon theme, which can be either static or dynamic. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(untagged)] +pub enum IconThemeSelection { + /// A static icon theme selection, represented by a single icon theme name. + Static(IconThemeName), + /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. + Dynamic { + /// The mode used to determine which theme to use. + #[serde(default)] + mode: ThemeMode, + /// The icon theme to use for light mode. + light: IconThemeName, + /// The icon theme to use for dark mode. + dark: IconThemeName, + }, +} + +// TODO: Rename ThemeMode -> ThemeAppearanceMode +/// The mode use to select a theme. +/// +/// `Light` and `Dark` will select their respective themes. +/// +/// `System` will select the theme based on the system's appearance. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ThemeMode { + /// Use the specified `light` theme. + Light, + + /// Use the specified `dark` theme. + Dark, + + /// Use the theme based on the system's appearance. + #[default] + System, +} + +/// Specifies the density of the UI. +/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) +#[derive( + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum UiDensity { + /// A denser UI with tighter spacing and smaller elements. + #[serde(alias = "compact")] + Compact, + #[default] + #[serde(alias = "default")] + /// The default UI density. + Default, + #[serde(alias = "comfortable")] + /// A looser UI with more spacing and larger elements. + Comfortable, +} + +impl UiDensity { + /// The spacing ratio of a given density. + /// TODO: Standardize usage throughout the app or remove + pub fn spacing_ratio(self) -> f32 { + match self { + UiDensity::Compact => 0.75, + UiDensity::Default => 1.0, + UiDensity::Comfortable => 1.25, + } + } +} + +/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct FontFamilyName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + replace_subschema::(generator, || { + json_schema!({ + "type": "string", + "enum": params.font_names, + }) + }) + } + } +} + +/// The buffer's line height. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum BufferLineHeight { + /// A less dense line height. + #[default] + Comfortable, + /// The default line height. + Standard, + /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. + Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), +} + +fn deserialize_line_height<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = f32::deserialize(deserializer)?; + if value < 1.0 { + return Err(serde::de::Error::custom( + "buffer_line_height.custom must be at least 1.0", + )); + } + + Ok(value) +} + +/// The content of a serialized theme. +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(default)] +pub struct ThemeStyleContent { + #[serde(default, rename = "background.appearance")] + pub window_background_appearance: Option, + + #[serde(default)] + pub accents: Vec, + + #[serde(flatten, default)] + pub colors: ThemeColorsContent, + + #[serde(flatten, default)] + pub status: StatusColorsContent, + + #[serde(default)] + pub players: Vec, + + /// The styles for syntax nodes. + #[serde(default)] + pub syntax: IndexMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct AccentContent(pub Option); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct PlayerColorContent { + pub cursor: Option, + pub background: Option, + pub selection: Option, +} + +pub(crate) fn try_parse_color(color: &str) -> Result { + let rgba = gpui::Rgba::try_from(color)?; + let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); + let hsla = palette::Hsla::from_color(rgba); + + let hsla = gpui::hsla( + hsla.hue.into_positive_degrees() / 360., + hsla.saturation, + hsla.lightness, + hsla.alpha, + ); + + Ok(hsla) +} + +/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct ThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + todo!() + // replace_subschema::(generator, || json_schema!({ + // "type": "string", + // "enum": ThemeRegistry::global(cx).list_names(), + // })) + } + } +} + +/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct IconThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + todo!() + // replace_subschema::(generator, || json_schema!({ + // "type": "string", + // "enum": ThemeRegistry::global(cx) + // .list_icon_themes() + // .into_iter() + // .map(|icon_theme| icon_theme.name) + // .collect::>(), + // })) + } + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 5c2f3b8e73a46ba5033919c27a709ad6839136ab..551a3fb31bceb7e924e6685dd51016dae4c00593 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -10,9 +10,8 @@ use futures::{ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; -use schemars::JsonSchema; use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Value, json}; +use serde_json::Value; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, @@ -32,7 +31,8 @@ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, - VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, + VsCodeSettings, WorktreeId, default_settings, parse_json_with_comments, + replace_value_in_json_text, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, UserSettingsContent, @@ -201,7 +201,7 @@ pub struct SettingsLocation<'a> { /// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, - default_settings: Option, + default_settings: SettingsContent, user_settings: Option, global_settings: Option, @@ -276,11 +276,12 @@ trait AnySettingValue: 'static + Send + Sync { struct DeserializedSetting(Box); impl SettingsStore { - pub fn new(cx: &App) -> Self { + pub fn new(cx: &App, default_settings: &str) -> Self { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); + let default_settings = parse_json_with_comments(default_settings).unwrap(); Self { setting_values: Default::default(), - default_settings: Some(Default::default()), // todo!() + default_settings, global_settings: None, server_settings: None, user_settings: Some(Default::default()), // todo!() @@ -347,9 +348,8 @@ impl SettingsStore { 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(); + let mut value = T::from_file(&self.default_settings).unwrap(); for refinement in refinements { value.refine(refinement) } @@ -438,17 +438,13 @@ impl SettingsStore { } /// Access the raw JSON value of the default settings. - pub fn raw_default_settings(&self) -> Option<&SettingsContent> { - self.default_settings.as_ref() + pub fn raw_default_settings(&self) -> &SettingsContent { + &self.default_settings } #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> Self { - let mut this = Self::new(cx); - this.set_default_settings(&crate::test_settings(), cx) - .unwrap(); - this.set_user_settings("{}", cx).unwrap(); - this + Self::new(cx, &crate::test_settings()) } /// Updates the value of a setting in the user's global configuration. @@ -715,6 +711,12 @@ impl SettingsStore { parse_json_with_comments(server_settings_content)? }; + // Rewrite the server settings into a content type + self.server_settings = settings.map(|settings| SettingsContent { + project: settings.project, + ..Default::default() + }); + todo!(); // self.server_settings = Some(settings); self.recompute_values(None, cx)?; @@ -1110,13 +1112,12 @@ impl SettingsStore { 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 mut value = setting_value.from_file(&default).unwrap(); - setting_value.refine(&mut value, &refinements); + let mut value = setting_value.from_file(&self.default_settings).unwrap(); + setting_value.refine(value.as_mut(), &refinements); setting_value.set_global_value(value); } @@ -1151,9 +1152,9 @@ impl SettingsStore { continue; } - let mut value = setting_value.from_file(&default).unwrap(); - setting_value.refine(&mut value, &refinements); - setting_value.refine(&mut value, &project_settings_stack); + let mut value = setting_value.from_file(&self.default_settings).unwrap(); + setting_value.refine(value.as_mut(), &refinements); + setting_value.refine(value.as_mut(), &project_settings_stack); setting_value.set_local_value(*root_id, directory_path.clone(), value); } } @@ -1237,10 +1238,12 @@ impl Debug for SettingsStore { impl AnySettingValue for SettingValue { fn from_file(&self, s: &SettingsContent) -> Option> { + dbg!(type_name::(), TypeId::of::()); T::from_file(s).map(|result| Box::new(result) as _) } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent]) { + dbg!(type_name::(), TypeId::of::()); let value = value.downcast_mut::().unwrap(); for refinement in refinements { value.refine(refinement) @@ -1336,7 +1339,10 @@ impl AnySettingValue for SettingValue { #[cfg(test)] mod tests { - use crate::{VsCodeSettingsSource, settings_content::LanguageSettingsContent}; + use crate::{ + TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, + settings_content::LanguageSettingsContent, test_settings, + }; use super::*; // This is so the SettingsUi macro can still work properly @@ -1344,138 +1350,186 @@ mod tests { use serde::Deserialize; use settings_ui_macros::{SettingsKey, SettingsUi}; use unindent::Unindent; + use util::Refine; - // #[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(); + #[derive(Debug, PartialEq)] + struct AutoUpdateSetting { + auto_update: bool, + } - // 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(), - // } - // ); + impl Settings for AutoUpdateSetting { + fn from_file(content: &SettingsContent) -> Option { + content + .auto_update + .map(|auto_update| AutoUpdateSetting { auto_update }) + } - // store - // .set_user_settings( - // r#"{ - // "turbo": true, - // "user": { "age": 31 }, - // "key1": "a" - // }"#, - // cx, - // ) - // .unwrap(); + fn refine(&mut self, content: &SettingsContent) { + if let Some(auto_update) = content.auto_update { + self.auto_update = auto_update; + } + } - // assert_eq!(store.get::(None), &TurboSetting(true)); - // assert_eq!( - // store.get::(None), - // &UserSettings { - // name: "John Doe".to_string(), - // age: 31, - // staff: false - // } - // ); + fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + } - // 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(); + #[derive(Debug, PartialEq)] + struct TitleBarSettings { + show: TitleBarVisibilityContent, + } - // store - // .set_local_settings( - // WorktreeId::from_usize(1), - // Path::new("/root2").into(), - // LocalSettingsKind::Settings, - // Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), - // cx, - // ) - // .unwrap(); + impl Settings for TitleBarSettings { + fn from_file(content: &SettingsContent) -> Option { + let content = content.title_bar?; + Some(TitleBarSettings { + show: content.show?, + }) + } - // 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(), - // } - // ); - // } + fn refine(&mut self, content: &SettingsContent) { + let Some(content) = content.title_bar else { + return; + }; + self.show.refine(&content.show) + } + + fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + } + + #[gpui::test] + fn test_settings_store_basic(cx: &mut App) { + let mut store = SettingsStore::new( + cx, + r#"{ + "auto_update": false, + "user": { + "name": "John Doe", + "age": 30, + "staff": false + } + }"#, + ); + store.register_setting::(cx); + store.register_setting::(cx); + // store.register_setting::(cx); + + assert_eq!( + store.get::(None), + &AutoUpdateSetting { auto_update: 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#"{ + "auto_update": true, + "user": { "age": 31 }, + "key1": "a" + }"#, + cx, + ) + .unwrap(); + + assert_eq!( + store.get::(None), + &AutoUpdateSetting { auto_update: 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) { @@ -1538,7 +1592,7 @@ mod tests { #[gpui::test] fn test_setting_store_update(cx: &mut App) { - let mut store = SettingsStore::new(cx); + let mut store = SettingsStore::new(cx, &test_settings()); // store.register_setting::(cx); // store.register_setting::(cx); // store.register_setting::(cx); diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 8409c60b22b03b8d917b84ae20229dc2db63fe4a..500ab2c03cf7ee01b245ca2bc89bd2c218f66d1a 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -819,8 +819,6 @@ fn clamp_font_weight(weight: f32) -> FontWeight { } impl settings::Settings for ThemeSettings { - type FileContent = ThemeSettingsContent; - fn load(sources: SettingsSources, cx: &mut App) -> Result { let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 240c1fd74f0b6f49feef09ce20a81e5f54adfcb1..1af7c4547493e704c002bb08a2e4a862bd192931 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,7 +1,7 @@ use db::anyhow; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsContent, SettingsKey, SettingsSources, SettingsUi}; #[derive(Copy, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] #[serde(rename_all = "snake_case")] @@ -11,7 +11,7 @@ pub enum TitleBarVisibility { HideInFullScreen, } -#[derive(Copy, Clone, Deserialize, Debug)] +#[derive(Copy, Clone, Debug)] pub struct TitleBarSettings { pub show: TitleBarVisibility, pub show_branch_icon: bool, @@ -23,54 +23,34 @@ pub struct TitleBarSettings { pub show_menus: bool, } -#[derive( - Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey, -)] -#[settings_ui(group = "Title Bar")] -#[settings_key(key = "title_bar")] -pub struct TitleBarSettingsContent { - /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". - /// - /// Default: "always" - pub show: Option, - /// Whether to show the branch icon beside branch switcher in the title bar. - /// - /// Default: false - pub show_branch_icon: Option, - /// Whether to show onboarding banners in the title bar. - /// - /// Default: true - pub show_onboarding_banner: Option, - /// Whether to show user avatar in the title bar. - /// - /// Default: true - pub show_user_picture: Option, - /// Whether to show the branch name button in the titlebar. - /// - /// Default: true - pub show_branch_name: Option, - /// Whether to show the project host and name in the titlebar. - /// - /// Default: true - pub show_project_items: Option, - /// Whether to show the sign in button in the title bar. - /// - /// Default: true - pub show_sign_in: Option, - /// Whether to show the menus in the title bar. - /// - /// Default: false - pub show_menus: Option, -} - impl Settings for TitleBarSettings { - type FileContent = TitleBarSettingsContent; + fn from_file(s: &SettingsContent) -> Option { + let content = s.title_bar?; + TitleBarSettings { + show: content.show?, + show_branch_icon: content.show_branch_icon?, + show_onboarding_banner: content.show_onboarding_banner?, + show_user_picture: content.show_user_picture?, + show_branch_name: content.show_branch_name?, + show_project_items: content.show_project_items?, + show_sign_in: content.show_sign_in?, + show_menus: content.show_menus?, + } + } + + fn refine(&mut self, s: &SettingsContent) { + let Some(content) = s.title_bar else { + return + } - fn load(sources: SettingsSources, _: &mut gpui::App) -> anyhow::Result - where - Self: Sized, - { - sources.json_merge() + self.show.refine(&content.show); + self.show_branch_icon.refine(content.show_branch_icon); + self.show_onboarding_banner.refine(content.show_onboarding_banner); + self.show_user_picture.refine(content.show_user_picture); + self.show_branch_name.refine(content.show_branch_name); + self.show_project_items.refine(content.show_project_items); + self.show_sign_in.refine(content.show_sign_in); + self.show_menus.refine(content.show_menus); } fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut Self::FileContent) {} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 90f5be1c92875ac0b9b2d3e7352ae858371b3686..986b0c8ac822fe0ebb981a6bd0d168f915109f0a 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1381,3 +1381,21 @@ Line 3"# assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes } } + +pub fn refine(dest: &mut T, src: &Option) { + if let Some(src) = src { + *dest = src.clone() + } +} + +pub trait Refine: Sized + Clone { + fn refine(&mut self, src: &Option); +} + +impl Refine for T { + fn refine(&mut self, src: &Option) { + if let Some(src) = src { + *self = src.clone(); + } + } +} diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index a11fd707591d0403409b5bc9e243d35c70fd65d3..04d4e9279a5a6a82081078f338e215ca67dca3e8 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -31,10 +31,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { release_channel::init(app_version, cx); gpui_tokio::init(cx); - let mut settings_store = SettingsStore::new(cx); - settings_store - .set_default_settings(settings::default_settings().as_ref(), cx) - .unwrap(); + let mut settings_store = SettingsStore::new(cx, settings::default_settings()); cx.set_global(settings_store); client::init_settings(cx); From 4b4d711e07e314295da182e658afee1e60bc0fd7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 14:18:12 -0600 Subject: [PATCH 003/117] WIP Co-authored-by: Ben Kunkle --- Cargo.lock | 1 + crates/settings/Cargo.toml | 1 + crates/settings/src/settings_content.rs | 1184 +---------------- .../settings/src/settings_content/language.rs | 882 ++++++++++++ crates/settings/src/settings_content/theme.rs | 1022 ++++++++++++++ crates/settings/src/settings_store.rs | 20 +- crates/theme/src/schema.rs | 544 +------- 7 files changed, 1926 insertions(+), 1728 deletions(-) create mode 100644 crates/settings/src/settings_content/language.rs create mode 100644 crates/settings/src/settings_content/theme.rs diff --git a/Cargo.lock b/Cargo.lock index db61ba7b121773c6e6941dd940201ec0a9e82f73..933663474f4d8f1909c0747b6f4c94fde95f57fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14884,6 +14884,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "serde_path_to_error", + "serde_repr", "settings_ui_macros", "smallvec", "tree-sitter", diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 4748644604ace11febddd5dcf34b43954dc361c6..4ff12f017f4a379efce7e2054c9c0217db082831 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -32,6 +32,7 @@ serde.workspace = true serde_json.workspace = true settings_ui_macros.workspace = true serde_json_lenient.workspace = true +serde_repr.workspace = true serde_path_to_error.workspace = true smallvec.workspace = true tree-sitter-json.workspace = true diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 03510564d117d73db97b32d30b634c9f6c253b71..ef01d7692a00f3a140d6cde676d47b2b293ea945 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,3 +1,8 @@ +mod language; +mod theme; +pub use language::*; +pub use theme::*; + use std::borrow::Cow; use std::env; use std::num::NonZeroU32; @@ -111,871 +116,6 @@ pub struct ProjectSettingsContent { 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, -} - #[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct TitleBarSettingsContent { /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". @@ -1019,317 +159,3 @@ pub enum TitleBarVisibilityContent { Never, HideInFullScreen, } - -/// Settings for rendering text in UI and text buffers. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct ThemeSettingsContent { - /// The default font size for text in the UI. - #[serde(default)] - pub ui_font_size: Option, - /// The name of a font to use for rendering in the UI. - #[serde(default)] - pub ui_font_family: Option, - /// The font fallbacks to use for rendering in the UI. - #[serde(default)] - #[schemars(default = "default_font_fallbacks")] - #[schemars(extend("uniqueItems" = true))] - pub ui_font_fallbacks: Option>, - /// The OpenType features to enable for text in the UI. - #[serde(default)] - #[schemars(default = "default_font_features")] - pub ui_font_features: Option, - /// The weight of the UI font in CSS units from 100 to 900. - #[serde(default)] - pub ui_font_weight: Option, - /// The name of a font to use for rendering in text buffers. - #[serde(default)] - pub buffer_font_family: Option, - /// The font fallbacks to use for rendering in text buffers. - #[serde(default)] - #[schemars(extend("uniqueItems" = true))] - pub buffer_font_fallbacks: Option>, - /// The default font size for rendering in text buffers. - #[serde(default)] - pub buffer_font_size: Option, - /// The weight of the editor font in CSS units from 100 to 900. - #[serde(default)] - pub buffer_font_weight: Option, - /// The buffer's line height. - #[serde(default)] - pub buffer_line_height: Option, - /// The OpenType features to enable for rendering in text buffers. - #[serde(default)] - #[schemars(default = "default_font_features")] - pub buffer_font_features: Option, - /// The font size for the agent panel. Falls back to the UI font size if unset. - #[serde(default)] - pub agent_font_size: Option>, - /// The name of the Zed theme to use. - #[serde(default)] - pub theme: Option, - /// The name of the icon theme to use. - #[serde(default)] - pub icon_theme: Option, - - /// UNSTABLE: Expect many elements to be broken. - /// - // Controls the density of the UI. - #[serde(rename = "unstable.ui_density", default)] - pub ui_density: Option, - - /// How much to fade out unused code. - #[serde(default)] - pub unnecessary_code_fade: Option, - - /// EXPERIMENTAL: Overrides for the current theme. - /// - /// These values will override the ones on the current theme specified in `theme`. - #[serde(rename = "experimental.theme_overrides", default)] - pub experimental_theme_overrides: Option, - - /// Overrides per theme - /// - /// These values will override the ones on the specified theme - #[serde(default)] - pub theme_overrides: HashMap, -} - -fn default_font_features() -> Option { - Some(FontFeatures::default()) -} - -fn default_font_fallbacks() -> Option { - Some(FontFallbacks::default()) -} - -/// Represents the selection of a theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(untagged)] -pub enum ThemeSelection { - /// A static theme selection, represented by a single theme name. - Static(ThemeName), - /// A dynamic theme selection, which can change based the [ThemeMode]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeMode, - /// The theme to use for light mode. - light: ThemeName, - /// The theme to use for dark mode. - dark: ThemeName, - }, -} - -/// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(untagged)] -pub enum IconThemeSelection { - /// A static icon theme selection, represented by a single icon theme name. - Static(IconThemeName), - /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeMode, - /// The icon theme to use for light mode. - light: IconThemeName, - /// The icon theme to use for dark mode. - dark: IconThemeName, - }, -} - -// TODO: Rename ThemeMode -> ThemeAppearanceMode -/// The mode use to select a theme. -/// -/// `Light` and `Dark` will select their respective themes. -/// -/// `System` will select the theme based on the system's appearance. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ThemeMode { - /// Use the specified `light` theme. - Light, - - /// Use the specified `dark` theme. - Dark, - - /// Use the theme based on the system's appearance. - #[default] - System, -} - -/// Specifies the density of the UI. -/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) -#[derive( - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub enum UiDensity { - /// A denser UI with tighter spacing and smaller elements. - #[serde(alias = "compact")] - Compact, - #[default] - #[serde(alias = "default")] - /// The default UI density. - Default, - #[serde(alias = "comfortable")] - /// A looser UI with more spacing and larger elements. - Comfortable, -} - -impl UiDensity { - /// The spacing ratio of a given density. - /// TODO: Standardize usage throughout the app or remove - pub fn spacing_ratio(self) -> f32 { - match self { - UiDensity::Compact => 0.75, - UiDensity::Default => 1.0, - UiDensity::Comfortable => 1.25, - } - } -} - -/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at -/// runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct FontFamilyName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, params, _cx| { - replace_subschema::(generator, || { - json_schema!({ - "type": "string", - "enum": params.font_names, - }) - }) - } - } -} - -/// The buffer's line height. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), -} - -fn deserialize_line_height<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = f32::deserialize(deserializer)?; - if value < 1.0 { - return Err(serde::de::Error::custom( - "buffer_line_height.custom must be at least 1.0", - )); - } - - Ok(value) -} - -/// The content of a serialized theme. -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(default)] -pub struct ThemeStyleContent { - #[serde(default, rename = "background.appearance")] - pub window_background_appearance: Option, - - #[serde(default)] - pub accents: Vec, - - #[serde(flatten, default)] - pub colors: ThemeColorsContent, - - #[serde(flatten, default)] - pub status: StatusColorsContent, - - #[serde(default)] - pub players: Vec, - - /// The styles for syntax nodes. - #[serde(default)] - pub syntax: IndexMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct AccentContent(pub Option); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct PlayerColorContent { - pub cursor: Option, - pub background: Option, - pub selection: Option, -} - -pub(crate) fn try_parse_color(color: &str) -> Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} - -/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct ThemeName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { - todo!() - // replace_subschema::(generator, || json_schema!({ - // "type": "string", - // "enum": ThemeRegistry::global(cx).list_names(), - // })) - } - } -} - -/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at -/// runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct IconThemeName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { - todo!() - // replace_subschema::(generator, || json_schema!({ - // "type": "string", - // "enum": ThemeRegistry::global(cx) - // .list_icon_themes() - // .into_iter() - // .map(|icon_theme| icon_theme.name) - // .collect::>(), - // })) - } - } -} diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs new file mode 100644 index 0000000000000000000000000000000000000000..5d58618f2ac590cbc3f7ac62642f55b2f3d6d81c --- /dev/null +++ b/crates/settings/src/settings_content/language.rs @@ -0,0 +1,882 @@ +use std::{borrow::Cow, num::NonZeroU32}; + +use collections::{HashMap, HashSet}; +use gpui::{Modifiers, SharedString}; +use schemars::{JsonSchema, json_schema}; +use serde::{ + Deserialize, Deserializer, Serialize, + de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}, +}; +use std::sync::Arc; +use util::schemars::replace_subschema; + +use crate::ParameterizedJsonSchema; + +#[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, +} + +fn default_true() -> bool { + true +} + +#[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_content/theme.rs b/crates/settings/src/settings_content/theme.rs new file mode 100644 index 0000000000000000000000000000000000000000..e32f39469ef763448ba2da9ecbb88cab06d1ff95 --- /dev/null +++ b/crates/settings/src/settings_content/theme.rs @@ -0,0 +1,1022 @@ +use collections::{HashMap, IndexMap}; +use gpui::{FontFallbacks, FontFeatures}; +use schemars::{JsonSchema, JsonSchema_repr, json_schema}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::sync::Arc; +use util::schemars::replace_subschema; + +use crate::ParameterizedJsonSchema; + +/// Settings for rendering text in UI and text buffers. +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ThemeSettingsContent { + /// The default font size for text in the UI. + #[serde(default)] + pub ui_font_size: Option, + /// The name of a font to use for rendering in the UI. + #[serde(default)] + pub ui_font_family: Option, + /// The font fallbacks to use for rendering in the UI. + #[serde(default)] + #[schemars(default = "default_font_fallbacks")] + #[schemars(extend("uniqueItems" = true))] + pub ui_font_fallbacks: Option>, + /// The OpenType features to enable for text in the UI. + #[serde(default)] + #[schemars(default = "default_font_features")] + pub ui_font_features: Option, + /// The weight of the UI font in CSS units from 100 to 900. + #[serde(default)] + pub ui_font_weight: Option, + /// The name of a font to use for rendering in text buffers. + #[serde(default)] + pub buffer_font_family: Option, + /// The font fallbacks to use for rendering in text buffers. + #[serde(default)] + #[schemars(extend("uniqueItems" = true))] + pub buffer_font_fallbacks: Option>, + /// The default font size for rendering in text buffers. + #[serde(default)] + pub buffer_font_size: Option, + /// The weight of the editor font in CSS units from 100 to 900. + #[serde(default)] + pub buffer_font_weight: Option, + /// The buffer's line height. + #[serde(default)] + pub buffer_line_height: Option, + /// The OpenType features to enable for rendering in text buffers. + #[serde(default)] + #[schemars(default = "default_font_features")] + pub buffer_font_features: Option, + /// The font size for the agent panel. Falls back to the UI font size if unset. + #[serde(default)] + pub agent_font_size: Option>, + /// The name of the Zed theme to use. + #[serde(default)] + pub theme: Option, + /// The name of the icon theme to use. + #[serde(default)] + pub icon_theme: Option, + + /// UNSTABLE: Expect many elements to be broken. + /// + // Controls the density of the UI. + #[serde(rename = "unstable.ui_density", default)] + pub ui_density: Option, + + /// How much to fade out unused code. + #[serde(default)] + pub unnecessary_code_fade: Option, + + /// EXPERIMENTAL: Overrides for the current theme. + /// + /// These values will override the ones on the current theme specified in `theme`. + #[serde(rename = "experimental.theme_overrides", default)] + pub experimental_theme_overrides: Option, + + /// Overrides per theme + /// + /// These values will override the ones on the specified theme + #[serde(default)] + pub theme_overrides: HashMap, +} + +fn default_font_features() -> Option { + Some(FontFeatures::default()) +} + +fn default_font_fallbacks() -> Option { + Some(FontFallbacks::default()) +} + +/// Represents the selection of a theme, which can be either static or dynamic. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(untagged)] +pub enum ThemeSelection { + /// A static theme selection, represented by a single theme name. + Static(ThemeName), + /// A dynamic theme selection, which can change based the [ThemeMode]. + Dynamic { + /// The mode used to determine which theme to use. + #[serde(default)] + mode: ThemeMode, + /// The theme to use for light mode. + light: ThemeName, + /// The theme to use for dark mode. + dark: ThemeName, + }, +} + +/// Represents the selection of an icon theme, which can be either static or dynamic. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(untagged)] +pub enum IconThemeSelection { + /// A static icon theme selection, represented by a single icon theme name. + Static(IconThemeName), + /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. + Dynamic { + /// The mode used to determine which theme to use. + #[serde(default)] + mode: ThemeMode, + /// The icon theme to use for light mode. + light: IconThemeName, + /// The icon theme to use for dark mode. + dark: IconThemeName, + }, +} + +// TODO: Rename ThemeMode -> ThemeAppearanceMode +/// The mode use to select a theme. +/// +/// `Light` and `Dark` will select their respective themes. +/// +/// `System` will select the theme based on the system's appearance. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ThemeMode { + /// Use the specified `light` theme. + Light, + + /// Use the specified `dark` theme. + Dark, + + /// Use the theme based on the system's appearance. + #[default] + System, +} + +/// Specifies the density of the UI. +/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) +#[derive( + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum UiDensity { + /// A denser UI with tighter spacing and smaller elements. + #[serde(alias = "compact")] + Compact, + #[default] + #[serde(alias = "default")] + /// The default UI density. + Default, + #[serde(alias = "comfortable")] + /// A looser UI with more spacing and larger elements. + Comfortable, +} + +impl UiDensity { + /// The spacing ratio of a given density. + /// TODO: Standardize usage throughout the app or remove + pub fn spacing_ratio(self) -> f32 { + match self { + UiDensity::Compact => 0.75, + UiDensity::Default => 1.0, + UiDensity::Comfortable => 1.25, + } + } +} + +/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct FontFamilyName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + replace_subschema::(generator, || { + json_schema!({ + "type": "string", + "enum": params.font_names, + }) + }) + } + } +} + +/// The buffer's line height. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum BufferLineHeight { + /// A less dense line height. + #[default] + Comfortable, + /// The default line height. + Standard, + /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. + Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), +} + +fn deserialize_line_height<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = f32::deserialize(deserializer)?; + if value < 1.0 { + return Err(serde::de::Error::custom( + "buffer_line_height.custom must be at least 1.0", + )); + } + + Ok(value) +} + +/// The content of a serialized theme. +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(default)] +pub struct ThemeStyleContent { + #[serde(default, rename = "background.appearance")] + pub window_background_appearance: Option, + + #[serde(default)] + pub accents: Vec, + + #[serde(flatten, default)] + pub colors: ThemeColorsContent, + + #[serde(flatten, default)] + pub status: StatusColorsContent, + + #[serde(default)] + pub players: Vec, + + /// The styles for syntax nodes. + #[serde(default)] + pub syntax: IndexMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct AccentContent(pub Option); + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct PlayerColorContent { + pub cursor: Option, + pub background: Option, + pub selection: Option, +} + +/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct ThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + todo!() + // replace_subschema::(generator, || json_schema!({ + // "type": "string", + // "enum": ThemeRegistry::global(cx).list_names(), + // })) + } + } +} + +/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at +/// runtime. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(transparent)] +pub struct IconThemeName(pub Arc); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + todo!() + // replace_subschema::(generator, || json_schema!({ + // "type": "string", + // "enum": ThemeRegistry::global(cx) + // .list_icon_themes() + // .into_iter() + // .map(|icon_theme| icon_theme.name) + // .collect::>(), + // })) + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(default)] +pub struct ThemeColorsContent { + /// Border color. Used for most borders, is usually a high contrast color. + #[serde(rename = "border")] + pub border: Option, + + /// Border color. Used for deemphasized borders, like a visual divider between two sections + #[serde(rename = "border.variant")] + pub border_variant: Option, + + /// Border color. Used for focused elements, like keyboard focused list item. + #[serde(rename = "border.focused")] + pub border_focused: Option, + + /// Border color. Used for selected elements, like an active search filter or selected checkbox. + #[serde(rename = "border.selected")] + pub border_selected: Option, + + /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. + #[serde(rename = "border.transparent")] + pub border_transparent: Option, + + /// Border color. Used for disabled elements, like a disabled input or button. + #[serde(rename = "border.disabled")] + pub border_disabled: Option, + + /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog. + #[serde(rename = "elevated_surface.background")] + pub elevated_surface_background: Option, + + /// Background Color. Used for grounded surfaces like a panel or tab. + #[serde(rename = "surface.background")] + pub surface_background: Option, + + /// Background Color. Used for the app background and blank panels or windows. + #[serde(rename = "background")] + pub background: Option, + + /// Background Color. Used for the background of an element that should have a different background than the surface it's on. + /// + /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... + /// + /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. + #[serde(rename = "element.background")] + pub element_background: Option, + + /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. + /// + /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. + #[serde(rename = "element.hover")] + pub element_hover: Option, + + /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. + /// + /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. + #[serde(rename = "element.active")] + pub element_active: Option, + + /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. + /// + /// Selected states are triggered by the element being selected (or "activated") by the user. + /// + /// This could include a selected checkbox, a toggleable button that is toggled on, etc. + #[serde(rename = "element.selected")] + pub element_selected: Option, + + /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. + /// + /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. + #[serde(rename = "element.disabled")] + pub element_disabled: Option, + + /// Background Color. Used for the background of selections in a UI element. + #[serde(rename = "element.selection_background")] + pub element_selection_background: Option, + + /// Background Color. Used for the area that shows where a dragged element will be dropped. + #[serde(rename = "drop_target.background")] + pub drop_target_background: Option, + + /// Border Color. Used for the border that shows where a dragged element will be dropped. + #[serde(rename = "drop_target.border")] + pub drop_target_border: Option, + + /// Used for the background of a ghost element that should have the same background as the surface it's on. + /// + /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... + /// + /// For an element that should have a different background than the surface it's on, use `element_background`. + #[serde(rename = "ghost_element.background")] + pub ghost_element_background: Option, + + /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. + /// + /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. + #[serde(rename = "ghost_element.hover")] + pub ghost_element_hover: Option, + + /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. + /// + /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. + #[serde(rename = "ghost_element.active")] + pub ghost_element_active: Option, + + /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. + /// + /// Selected states are triggered by the element being selected (or "activated") by the user. + /// + /// This could include a selected checkbox, a toggleable button that is toggled on, etc. + #[serde(rename = "ghost_element.selected")] + pub ghost_element_selected: Option, + + /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. + /// + /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. + #[serde(rename = "ghost_element.disabled")] + pub ghost_element_disabled: Option, + + /// Text Color. Default text color used for most text. + #[serde(rename = "text")] + pub text: Option, + + /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. + #[serde(rename = "text.muted")] + pub text_muted: Option, + + /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. + #[serde(rename = "text.placeholder")] + pub text_placeholder: Option, + + /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. + #[serde(rename = "text.disabled")] + pub text_disabled: Option, + + /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. + #[serde(rename = "text.accent")] + pub text_accent: Option, + + /// Fill Color. Used for the default fill color of an icon. + #[serde(rename = "icon")] + pub icon: Option, + + /// Fill Color. Used for the muted or deemphasized fill color of an icon. + /// + /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. + #[serde(rename = "icon.muted")] + pub icon_muted: Option, + + /// Fill Color. Used for the disabled fill color of an icon. + /// + /// Disabled states are shown when a user cannot interact with an element, like a icon button. + #[serde(rename = "icon.disabled")] + pub icon_disabled: Option, + + /// Fill Color. Used for the placeholder fill color of an icon. + /// + /// This might be used to show an icon in an input that disappears when the user enters text. + #[serde(rename = "icon.placeholder")] + pub icon_placeholder: Option, + + /// Fill Color. Used for the accent fill color of an icon. + /// + /// This might be used to show when a toggleable icon button is selected. + #[serde(rename = "icon.accent")] + pub icon_accent: Option, + + /// Color used to accent some of the debuggers elements + /// Only accent breakpoint & breakpoint related symbols right now + #[serde(rename = "debugger.accent")] + pub debugger_accent: Option, + + #[serde(rename = "status_bar.background")] + pub status_bar_background: Option, + + #[serde(rename = "title_bar.background")] + pub title_bar_background: Option, + + #[serde(rename = "title_bar.inactive_background")] + pub title_bar_inactive_background: Option, + + #[serde(rename = "toolbar.background")] + pub toolbar_background: Option, + + #[serde(rename = "tab_bar.background")] + pub tab_bar_background: Option, + + #[serde(rename = "tab.inactive_background")] + pub tab_inactive_background: Option, + + #[serde(rename = "tab.active_background")] + pub tab_active_background: Option, + + #[serde(rename = "search.match_background")] + pub search_match_background: Option, + + #[serde(rename = "panel.background")] + pub panel_background: Option, + + #[serde(rename = "panel.focused_border")] + pub panel_focused_border: Option, + + #[serde(rename = "panel.indent_guide")] + pub panel_indent_guide: Option, + + #[serde(rename = "panel.indent_guide_hover")] + pub panel_indent_guide_hover: Option, + + #[serde(rename = "panel.indent_guide_active")] + pub panel_indent_guide_active: Option, + + #[serde(rename = "panel.overlay_background")] + pub panel_overlay_background: Option, + + #[serde(rename = "panel.overlay_hover")] + pub panel_overlay_hover: Option, + + #[serde(rename = "pane.focused_border")] + pub pane_focused_border: Option, + + #[serde(rename = "pane_group.border")] + pub pane_group_border: Option, + + /// The deprecated version of `scrollbar.thumb.background`. + /// + /// Don't use this field. + #[serde(rename = "scrollbar_thumb.background", skip_serializing)] + #[schemars(skip)] + pub deprecated_scrollbar_thumb_background: Option, + + /// The color of the scrollbar thumb. + #[serde(rename = "scrollbar.thumb.background")] + pub scrollbar_thumb_background: Option, + + /// The color of the scrollbar thumb when hovered over. + #[serde(rename = "scrollbar.thumb.hover_background")] + pub scrollbar_thumb_hover_background: Option, + + /// The color of the scrollbar thumb whilst being actively dragged. + #[serde(rename = "scrollbar.thumb.active_background")] + pub scrollbar_thumb_active_background: Option, + + /// The border color of the scrollbar thumb. + #[serde(rename = "scrollbar.thumb.border")] + pub scrollbar_thumb_border: Option, + + /// The background color of the scrollbar track. + #[serde(rename = "scrollbar.track.background")] + pub scrollbar_track_background: Option, + + /// The border color of the scrollbar track. + #[serde(rename = "scrollbar.track.border")] + pub scrollbar_track_border: Option, + + /// The color of the minimap thumb. + #[serde(rename = "minimap.thumb.background")] + pub minimap_thumb_background: Option, + + /// The color of the minimap thumb when hovered over. + #[serde(rename = "minimap.thumb.hover_background")] + pub minimap_thumb_hover_background: Option, + + /// The color of the minimap thumb whilst being actively dragged. + #[serde(rename = "minimap.thumb.active_background")] + pub minimap_thumb_active_background: Option, + + /// The border color of the minimap thumb. + #[serde(rename = "minimap.thumb.border")] + pub minimap_thumb_border: Option, + + #[serde(rename = "editor.foreground")] + pub editor_foreground: Option, + + #[serde(rename = "editor.background")] + pub editor_background: Option, + + #[serde(rename = "editor.gutter.background")] + pub editor_gutter_background: Option, + + #[serde(rename = "editor.subheader.background")] + pub editor_subheader_background: Option, + + #[serde(rename = "editor.active_line.background")] + pub editor_active_line_background: Option, + + #[serde(rename = "editor.highlighted_line.background")] + pub editor_highlighted_line_background: Option, + + /// Background of active line of debugger + #[serde(rename = "editor.debugger_active_line.background")] + pub editor_debugger_active_line_background: Option, + + /// Text Color. Used for the text of the line number in the editor gutter. + #[serde(rename = "editor.line_number")] + pub editor_line_number: Option, + + /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. + #[serde(rename = "editor.active_line_number")] + pub editor_active_line_number: Option, + + /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. + #[serde(rename = "editor.hover_line_number")] + pub editor_hover_line_number: Option, + + /// Text Color. Used to mark invisible characters in the editor. + /// + /// Example: spaces, tabs, carriage returns, etc. + #[serde(rename = "editor.invisible")] + pub editor_invisible: Option, + + #[serde(rename = "editor.wrap_guide")] + pub editor_wrap_guide: Option, + + #[serde(rename = "editor.active_wrap_guide")] + pub editor_active_wrap_guide: Option, + + #[serde(rename = "editor.indent_guide")] + pub editor_indent_guide: Option, + + #[serde(rename = "editor.indent_guide_active")] + pub editor_indent_guide_active: Option, + + /// Read-access of a symbol, like reading a variable. + /// + /// A document highlight is a range inside a text document which deserves + /// special attention. Usually a document highlight is visualized by changing + /// the background color of its range. + #[serde(rename = "editor.document_highlight.read_background")] + pub editor_document_highlight_read_background: Option, + + /// Read-access of a symbol, like reading a variable. + /// + /// A document highlight is a range inside a text document which deserves + /// special attention. Usually a document highlight is visualized by changing + /// the background color of its range. + #[serde(rename = "editor.document_highlight.write_background")] + pub editor_document_highlight_write_background: Option, + + /// Highlighted brackets background color. + /// + /// Matching brackets in the cursor scope are highlighted with this background color. + #[serde(rename = "editor.document_highlight.bracket_background")] + pub editor_document_highlight_bracket_background: Option, + + /// Terminal background color. + #[serde(rename = "terminal.background")] + pub terminal_background: Option, + + /// Terminal foreground color. + #[serde(rename = "terminal.foreground")] + pub terminal_foreground: Option, + + /// Terminal ANSI background color. + #[serde(rename = "terminal.ansi.background")] + pub terminal_ansi_background: Option, + + /// Bright terminal foreground color. + #[serde(rename = "terminal.bright_foreground")] + pub terminal_bright_foreground: Option, + + /// Dim terminal foreground color. + #[serde(rename = "terminal.dim_foreground")] + pub terminal_dim_foreground: Option, + + /// Black ANSI terminal color. + #[serde(rename = "terminal.ansi.black")] + pub terminal_ansi_black: Option, + + /// Bright black ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_black")] + pub terminal_ansi_bright_black: Option, + + /// Dim black ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_black")] + pub terminal_ansi_dim_black: Option, + + /// Red ANSI terminal color. + #[serde(rename = "terminal.ansi.red")] + pub terminal_ansi_red: Option, + + /// Bright red ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_red")] + pub terminal_ansi_bright_red: Option, + + /// Dim red ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_red")] + pub terminal_ansi_dim_red: Option, + + /// Green ANSI terminal color. + #[serde(rename = "terminal.ansi.green")] + pub terminal_ansi_green: Option, + + /// Bright green ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_green")] + pub terminal_ansi_bright_green: Option, + + /// Dim green ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_green")] + pub terminal_ansi_dim_green: Option, + + /// Yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.yellow")] + pub terminal_ansi_yellow: Option, + + /// Bright yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_yellow")] + pub terminal_ansi_bright_yellow: Option, + + /// Dim yellow ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_yellow")] + pub terminal_ansi_dim_yellow: Option, + + /// Blue ANSI terminal color. + #[serde(rename = "terminal.ansi.blue")] + pub terminal_ansi_blue: Option, + + /// Bright blue ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_blue")] + pub terminal_ansi_bright_blue: Option, + + /// Dim blue ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_blue")] + pub terminal_ansi_dim_blue: Option, + + /// Magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.magenta")] + pub terminal_ansi_magenta: Option, + + /// Bright magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_magenta")] + pub terminal_ansi_bright_magenta: Option, + + /// Dim magenta ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_magenta")] + pub terminal_ansi_dim_magenta: Option, + + /// Cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.cyan")] + pub terminal_ansi_cyan: Option, + + /// Bright cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_cyan")] + pub terminal_ansi_bright_cyan: Option, + + /// Dim cyan ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_cyan")] + pub terminal_ansi_dim_cyan: Option, + + /// White ANSI terminal color. + #[serde(rename = "terminal.ansi.white")] + pub terminal_ansi_white: Option, + + /// Bright white ANSI terminal color. + #[serde(rename = "terminal.ansi.bright_white")] + pub terminal_ansi_bright_white: Option, + + /// Dim white ANSI terminal color. + #[serde(rename = "terminal.ansi.dim_white")] + pub terminal_ansi_dim_white: Option, + + #[serde(rename = "link_text.hover")] + pub link_text_hover: Option, + + /// Added version control color. + #[serde(rename = "version_control.added")] + pub version_control_added: Option, + + /// Deleted version control color. + #[serde(rename = "version_control.deleted")] + pub version_control_deleted: Option, + + /// Modified version control color. + #[serde(rename = "version_control.modified")] + pub version_control_modified: Option, + + /// Renamed version control color. + #[serde(rename = "version_control.renamed")] + pub version_control_renamed: Option, + + /// Conflict version control color. + #[serde(rename = "version_control.conflict")] + pub version_control_conflict: Option, + + /// Ignored version control color. + #[serde(rename = "version_control.ignored")] + pub version_control_ignored: Option, + + /// Background color for row highlights of "ours" regions in merge conflicts. + #[serde(rename = "version_control.conflict_marker.ours")] + pub version_control_conflict_marker_ours: Option, + + /// Background color for row highlights of "theirs" regions in merge conflicts. + #[serde(rename = "version_control.conflict_marker.theirs")] + pub version_control_conflict_marker_theirs: Option, + + /// Deprecated in favor of `version_control_conflict_marker_ours`. + #[deprecated] + pub version_control_conflict_ours_background: Option, + + /// Deprecated in favor of `version_control_conflict_marker_theirs`. + #[deprecated] + pub version_control_conflict_theirs_background: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(default)] +pub struct HighlightStyleContent { + pub color: Option, + + #[serde(deserialize_with = "treat_error_as_none")] + pub background_color: Option, + + #[serde(deserialize_with = "treat_error_as_none")] + pub font_style: Option, + + #[serde(deserialize_with = "treat_error_as_none")] + pub font_weight: Option, +} + +impl HighlightStyleContent { + pub fn is_empty(&self) -> bool { + self.color.is_none() + && self.background_color.is_none() + && self.font_style.is_none() + && self.font_weight.is_none() + } +} + +fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(T::deserialize(value).ok()) +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(default)] +pub struct StatusColorsContent { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. + #[serde(rename = "conflict")] + pub conflict: Option, + + #[serde(rename = "conflict.background")] + pub conflict_background: Option, + + #[serde(rename = "conflict.border")] + pub conflict_border: Option, + + /// Indicates something new, like a new file added to a Git repository. + #[serde(rename = "created")] + pub created: Option, + + #[serde(rename = "created.background")] + pub created_background: Option, + + #[serde(rename = "created.border")] + pub created_border: Option, + + /// Indicates that something no longer exists, like a deleted file. + #[serde(rename = "deleted")] + pub deleted: Option, + + #[serde(rename = "deleted.background")] + pub deleted_background: Option, + + #[serde(rename = "deleted.border")] + pub deleted_border: Option, + + /// Indicates a system error, a failed operation or a diagnostic error. + #[serde(rename = "error")] + pub error: Option, + + #[serde(rename = "error.background")] + pub error_background: Option, + + #[serde(rename = "error.border")] + pub error_border: Option, + + /// Represents a hidden status, such as a file being hidden in a file tree. + #[serde(rename = "hidden")] + pub hidden: Option, + + #[serde(rename = "hidden.background")] + pub hidden_background: Option, + + #[serde(rename = "hidden.border")] + pub hidden_border: Option, + + /// Indicates a hint or some kind of additional information. + #[serde(rename = "hint")] + pub hint: Option, + + #[serde(rename = "hint.background")] + pub hint_background: Option, + + #[serde(rename = "hint.border")] + pub hint_border: Option, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. + #[serde(rename = "ignored")] + pub ignored: Option, + + #[serde(rename = "ignored.background")] + pub ignored_background: Option, + + #[serde(rename = "ignored.border")] + pub ignored_border: Option, + + /// Represents informational status updates or messages. + #[serde(rename = "info")] + pub info: Option, + + #[serde(rename = "info.background")] + pub info_background: Option, + + #[serde(rename = "info.border")] + pub info_border: Option, + + /// Indicates a changed or altered status, like a file that has been edited. + #[serde(rename = "modified")] + pub modified: Option, + + #[serde(rename = "modified.background")] + pub modified_background: Option, + + #[serde(rename = "modified.border")] + pub modified_border: Option, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + #[serde(rename = "predictive")] + pub predictive: Option, + + #[serde(rename = "predictive.background")] + pub predictive_background: Option, + + #[serde(rename = "predictive.border")] + pub predictive_border: Option, + + /// Represents a renamed status, such as a file that has been renamed. + #[serde(rename = "renamed")] + pub renamed: Option, + + #[serde(rename = "renamed.background")] + pub renamed_background: Option, + + #[serde(rename = "renamed.border")] + pub renamed_border: Option, + + /// Indicates a successful operation or task completion. + #[serde(rename = "success")] + pub success: Option, + + #[serde(rename = "success.background")] + pub success_background: Option, + + #[serde(rename = "success.border")] + pub success_border: Option, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + #[serde(rename = "unreachable")] + pub unreachable: Option, + + #[serde(rename = "unreachable.background")] + pub unreachable_background: Option, + + #[serde(rename = "unreachable.border")] + pub unreachable_border: Option, + + /// Represents a warning status, like an operation that is about to fail. + #[serde(rename = "warning")] + pub warning: Option, + + #[serde(rename = "warning.background")] + pub warning_background: Option, + + #[serde(rename = "warning.border")] + pub warning_border: Option, +} + +/// The background appearance of the window. +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WindowBackgroundContent { + Opaque, + Transparent, + Blurred, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum FontStyleContent { + Normal, + Italic, + Oblique, +} + +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] +#[repr(u16)] +pub enum FontWeightContent { + Thin = 100, + ExtraLight = 200, + Light = 300, + Normal = 400, + Medium = 500, + Semibold = 600, + Bold = 700, + ExtraBold = 800, + Black = 900, +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 551a3fb31bceb7e924e6685dd51016dae4c00593..e78c341bd7e3b337f1690cd637271e296028b025 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1398,24 +1398,14 @@ mod tests { #[gpui::test] fn test_settings_store_basic(cx: &mut App) { - let mut store = SettingsStore::new( - cx, - r#"{ - "auto_update": false, - "user": { - "name": "John Doe", - "age": 30, - "staff": false - } - }"#, - ); + let mut store = SettingsStore::new(cx, &default_settings()); store.register_setting::(cx); store.register_setting::(cx); // store.register_setting::(cx); assert_eq!( store.get::(None), - &AutoUpdateSetting { auto_update: false } + &AutoUpdateSetting { auto_update: true } ); // assert_eq!( // store.get::(None), @@ -1436,9 +1426,7 @@ mod tests { store .set_user_settings( r#"{ - "auto_update": true, - "user": { "age": 31 }, - "key1": "a" + "auto_update": false, }"#, cx, ) @@ -1446,7 +1434,7 @@ mod tests { assert_eq!( store.get::(None), - &AutoUpdateSetting { auto_update: true } + &AutoUpdateSetting { auto_update: false } ); // assert_eq!( // store.get::(None), diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index d2bdbb616a5b6f65f905375769e14dd2a2312ea9..2e8445a7917aa52f8c99d7356bcc5d7623d8faa9 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -148,508 +148,19 @@ impl ThemeStyleContent { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(default)] -pub struct ThemeColorsContent { - /// Border color. Used for most borders, is usually a high contrast color. - #[serde(rename = "border")] - pub border: Option, - - /// Border color. Used for deemphasized borders, like a visual divider between two sections - #[serde(rename = "border.variant")] - pub border_variant: Option, - - /// Border color. Used for focused elements, like keyboard focused list item. - #[serde(rename = "border.focused")] - pub border_focused: Option, - - /// Border color. Used for selected elements, like an active search filter or selected checkbox. - #[serde(rename = "border.selected")] - pub border_selected: Option, - - /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. - #[serde(rename = "border.transparent")] - pub border_transparent: Option, - - /// Border color. Used for disabled elements, like a disabled input or button. - #[serde(rename = "border.disabled")] - pub border_disabled: Option, - - /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog. - #[serde(rename = "elevated_surface.background")] - pub elevated_surface_background: Option, - - /// Background Color. Used for grounded surfaces like a panel or tab. - #[serde(rename = "surface.background")] - pub surface_background: Option, - - /// Background Color. Used for the app background and blank panels or windows. - #[serde(rename = "background")] - pub background: Option, - - /// Background Color. Used for the background of an element that should have a different background than the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. - #[serde(rename = "element.background")] - pub element_background: Option, - - /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "element.hover")] - pub element_hover: Option, - - /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "element.active")] - pub element_active: Option, - - /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "element.selected")] - pub element_selected: Option, - - /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "element.disabled")] - pub element_disabled: Option, - - /// Background Color. Used for the background of selections in a UI element. - #[serde(rename = "element.selection_background")] - pub element_selection_background: Option, - - /// Background Color. Used for the area that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.background")] - pub drop_target_background: Option, - - /// Border Color. Used for the border that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.border")] - pub drop_target_border: Option, - - /// Used for the background of a ghost element that should have the same background as the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have a different background than the surface it's on, use `element_background`. - #[serde(rename = "ghost_element.background")] - pub ghost_element_background: Option, - - /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "ghost_element.hover")] - pub ghost_element_hover: Option, - - /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "ghost_element.active")] - pub ghost_element_active: Option, - - /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "ghost_element.selected")] - pub ghost_element_selected: Option, - - /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "ghost_element.disabled")] - pub ghost_element_disabled: Option, - - /// Text Color. Default text color used for most text. - #[serde(rename = "text")] - pub text: Option, - - /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. - #[serde(rename = "text.muted")] - pub text_muted: Option, - - /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. - #[serde(rename = "text.placeholder")] - pub text_placeholder: Option, - - /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. - #[serde(rename = "text.disabled")] - pub text_disabled: Option, - - /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. - #[serde(rename = "text.accent")] - pub text_accent: Option, - - /// Fill Color. Used for the default fill color of an icon. - #[serde(rename = "icon")] - pub icon: Option, - - /// Fill Color. Used for the muted or deemphasized fill color of an icon. - /// - /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. - #[serde(rename = "icon.muted")] - pub icon_muted: Option, - - /// Fill Color. Used for the disabled fill color of an icon. - /// - /// Disabled states are shown when a user cannot interact with an element, like a icon button. - #[serde(rename = "icon.disabled")] - pub icon_disabled: Option, - - /// Fill Color. Used for the placeholder fill color of an icon. - /// - /// This might be used to show an icon in an input that disappears when the user enters text. - #[serde(rename = "icon.placeholder")] - pub icon_placeholder: Option, - - /// Fill Color. Used for the accent fill color of an icon. - /// - /// This might be used to show when a toggleable icon button is selected. - #[serde(rename = "icon.accent")] - pub icon_accent: Option, - - /// Color used to accent some of the debuggers elements - /// Only accent breakpoint & breakpoint related symbols right now - #[serde(rename = "debugger.accent")] - pub debugger_accent: Option, - - #[serde(rename = "status_bar.background")] - pub status_bar_background: Option, - - #[serde(rename = "title_bar.background")] - pub title_bar_background: Option, - - #[serde(rename = "title_bar.inactive_background")] - pub title_bar_inactive_background: Option, - - #[serde(rename = "toolbar.background")] - pub toolbar_background: Option, - - #[serde(rename = "tab_bar.background")] - pub tab_bar_background: Option, - - #[serde(rename = "tab.inactive_background")] - pub tab_inactive_background: Option, - - #[serde(rename = "tab.active_background")] - pub tab_active_background: Option, - - #[serde(rename = "search.match_background")] - pub search_match_background: Option, - - #[serde(rename = "panel.background")] - pub panel_background: Option, - - #[serde(rename = "panel.focused_border")] - pub panel_focused_border: Option, - - #[serde(rename = "panel.indent_guide")] - pub panel_indent_guide: Option, - - #[serde(rename = "panel.indent_guide_hover")] - pub panel_indent_guide_hover: Option, - - #[serde(rename = "panel.indent_guide_active")] - pub panel_indent_guide_active: Option, - - #[serde(rename = "panel.overlay_background")] - pub panel_overlay_background: Option, - - #[serde(rename = "panel.overlay_hover")] - pub panel_overlay_hover: Option, - - #[serde(rename = "pane.focused_border")] - pub pane_focused_border: Option, - - #[serde(rename = "pane_group.border")] - pub pane_group_border: Option, - - /// The deprecated version of `scrollbar.thumb.background`. - /// - /// Don't use this field. - #[serde(rename = "scrollbar_thumb.background", skip_serializing)] - #[schemars(skip)] - pub deprecated_scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.background")] - pub scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb when hovered over. - #[serde(rename = "scrollbar.thumb.hover_background")] - pub scrollbar_thumb_hover_background: Option, - - /// The color of the scrollbar thumb whilst being actively dragged. - #[serde(rename = "scrollbar.thumb.active_background")] - pub scrollbar_thumb_active_background: Option, - - /// The border color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.border")] - pub scrollbar_thumb_border: Option, - - /// The background color of the scrollbar track. - #[serde(rename = "scrollbar.track.background")] - pub scrollbar_track_background: Option, - - /// The border color of the scrollbar track. - #[serde(rename = "scrollbar.track.border")] - pub scrollbar_track_border: Option, - - /// The color of the minimap thumb. - #[serde(rename = "minimap.thumb.background")] - pub minimap_thumb_background: Option, - - /// The color of the minimap thumb when hovered over. - #[serde(rename = "minimap.thumb.hover_background")] - pub minimap_thumb_hover_background: Option, - - /// The color of the minimap thumb whilst being actively dragged. - #[serde(rename = "minimap.thumb.active_background")] - pub minimap_thumb_active_background: Option, - - /// The border color of the minimap thumb. - #[serde(rename = "minimap.thumb.border")] - pub minimap_thumb_border: Option, - - #[serde(rename = "editor.foreground")] - pub editor_foreground: Option, - - #[serde(rename = "editor.background")] - pub editor_background: Option, - - #[serde(rename = "editor.gutter.background")] - pub editor_gutter_background: Option, - - #[serde(rename = "editor.subheader.background")] - pub editor_subheader_background: Option, - - #[serde(rename = "editor.active_line.background")] - pub editor_active_line_background: Option, - - #[serde(rename = "editor.highlighted_line.background")] - pub editor_highlighted_line_background: Option, - - /// Background of active line of debugger - #[serde(rename = "editor.debugger_active_line.background")] - pub editor_debugger_active_line_background: Option, - - /// Text Color. Used for the text of the line number in the editor gutter. - #[serde(rename = "editor.line_number")] - pub editor_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. - #[serde(rename = "editor.active_line_number")] - pub editor_active_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. - #[serde(rename = "editor.hover_line_number")] - pub editor_hover_line_number: Option, - - /// Text Color. Used to mark invisible characters in the editor. - /// - /// Example: spaces, tabs, carriage returns, etc. - #[serde(rename = "editor.invisible")] - pub editor_invisible: Option, - - #[serde(rename = "editor.wrap_guide")] - pub editor_wrap_guide: Option, - - #[serde(rename = "editor.active_wrap_guide")] - pub editor_active_wrap_guide: Option, - - #[serde(rename = "editor.indent_guide")] - pub editor_indent_guide: Option, - - #[serde(rename = "editor.indent_guide_active")] - pub editor_indent_guide_active: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.read_background")] - pub editor_document_highlight_read_background: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.write_background")] - pub editor_document_highlight_write_background: Option, - - /// Highlighted brackets background color. - /// - /// Matching brackets in the cursor scope are highlighted with this background color. - #[serde(rename = "editor.document_highlight.bracket_background")] - pub editor_document_highlight_bracket_background: Option, - - /// Terminal background color. - #[serde(rename = "terminal.background")] - pub terminal_background: Option, - - /// Terminal foreground color. - #[serde(rename = "terminal.foreground")] - pub terminal_foreground: Option, - - /// Terminal ANSI background color. - #[serde(rename = "terminal.ansi.background")] - pub terminal_ansi_background: Option, - - /// Bright terminal foreground color. - #[serde(rename = "terminal.bright_foreground")] - pub terminal_bright_foreground: Option, - - /// Dim terminal foreground color. - #[serde(rename = "terminal.dim_foreground")] - pub terminal_dim_foreground: Option, - - /// Black ANSI terminal color. - #[serde(rename = "terminal.ansi.black")] - pub terminal_ansi_black: Option, - - /// Bright black ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_black")] - pub terminal_ansi_bright_black: Option, - - /// Dim black ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_black")] - pub terminal_ansi_dim_black: Option, - - /// Red ANSI terminal color. - #[serde(rename = "terminal.ansi.red")] - pub terminal_ansi_red: Option, - - /// Bright red ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_red")] - pub terminal_ansi_bright_red: Option, - - /// Dim red ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_red")] - pub terminal_ansi_dim_red: Option, - - /// Green ANSI terminal color. - #[serde(rename = "terminal.ansi.green")] - pub terminal_ansi_green: Option, - - /// Bright green ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_green")] - pub terminal_ansi_bright_green: Option, - - /// Dim green ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_green")] - pub terminal_ansi_dim_green: Option, - - /// Yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.yellow")] - pub terminal_ansi_yellow: Option, - - /// Bright yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_yellow")] - pub terminal_ansi_bright_yellow: Option, - - /// Dim yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_yellow")] - pub terminal_ansi_dim_yellow: Option, - - /// Blue ANSI terminal color. - #[serde(rename = "terminal.ansi.blue")] - pub terminal_ansi_blue: Option, - - /// Bright blue ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_blue")] - pub terminal_ansi_bright_blue: Option, - - /// Dim blue ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_blue")] - pub terminal_ansi_dim_blue: Option, - - /// Magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.magenta")] - pub terminal_ansi_magenta: Option, - - /// Bright magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_magenta")] - pub terminal_ansi_bright_magenta: Option, - - /// Dim magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_magenta")] - pub terminal_ansi_dim_magenta: Option, - - /// Cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.cyan")] - pub terminal_ansi_cyan: Option, - - /// Bright cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_cyan")] - pub terminal_ansi_bright_cyan: Option, - - /// Dim cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_cyan")] - pub terminal_ansi_dim_cyan: Option, - - /// White ANSI terminal color. - #[serde(rename = "terminal.ansi.white")] - pub terminal_ansi_white: Option, - - /// Bright white ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_white")] - pub terminal_ansi_bright_white: Option, - - /// Dim white ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_white")] - pub terminal_ansi_dim_white: Option, - - #[serde(rename = "link_text.hover")] - pub link_text_hover: Option, - - /// Added version control color. - #[serde(rename = "version_control.added")] - pub version_control_added: Option, - - /// Deleted version control color. - #[serde(rename = "version_control.deleted")] - pub version_control_deleted: Option, - - /// Modified version control color. - #[serde(rename = "version_control.modified")] - pub version_control_modified: Option, - - /// Renamed version control color. - #[serde(rename = "version_control.renamed")] - pub version_control_renamed: Option, - - /// Conflict version control color. - #[serde(rename = "version_control.conflict")] - pub version_control_conflict: Option, - - /// Ignored version control color. - #[serde(rename = "version_control.ignored")] - pub version_control_ignored: Option, - - /// Background color for row highlights of "ours" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.ours")] - pub version_control_conflict_marker_ours: Option, - - /// Background color for row highlights of "theirs" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.theirs")] - pub version_control_conflict_marker_theirs: Option, +pub(crate) fn try_parse_color(color: &str) -> Result { + let rgba = gpui::Rgba::try_from(color)?; + let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); + let hsla = palette::Hsla::from_color(rgba); - /// Deprecated in favor of `version_control_conflict_marker_ours`. - #[deprecated] - pub version_control_conflict_ours_background: Option, + let hsla = gpui::hsla( + hsla.hue.into_positive_degrees() / 360., + hsla.saturation, + hsla.lightness, + hsla.alpha, + ); - /// Deprecated in favor of `version_control_conflict_marker_theirs`. - #[deprecated] - pub version_control_conflict_theirs_background: Option, + Ok(hsla) } impl ThemeColorsContent { @@ -1547,36 +1058,3 @@ impl From for FontWeight { } } } - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(default)] -pub struct HighlightStyleContent { - pub color: Option, - - #[serde(deserialize_with = "treat_error_as_none")] - pub background_color: Option, - - #[serde(deserialize_with = "treat_error_as_none")] - pub font_style: Option, - - #[serde(deserialize_with = "treat_error_as_none")] - pub font_weight: Option, -} - -impl HighlightStyleContent { - pub fn is_empty(&self) -> bool { - self.color.is_none() - && self.background_color.is_none() - && self.font_style.is_none() - && self.font_weight.is_none() - } -} - -fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result, D::Error> -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - let value: Value = Deserialize::deserialize(deserializer)?; - Ok(T::deserialize(value).ok()) -} From d2ab0f9941374f65b81bc02faec44f405155b9bb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 14:19:43 -0600 Subject: [PATCH 004/117] clippy --- crates/settings/src/settings_content.rs | 17 +++++------------ crates/settings/src/settings_file.rs | 2 +- crates/settings/src/settings_store.rs | 10 ++++------ 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index ef01d7692a00f3a140d6cde676d47b2b293ea945..e8201a4fa143a2e31300ecf955e1f9abb6af3158 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -3,22 +3,15 @@ mod theme; pub use language::*; pub use theme::*; -use std::borrow::Cow; use std::env; -use std::num::NonZeroU32; -use std::sync::Arc; -use anyhow::Result; -use collections::{HashMap, HashSet}; -use gpui::{App, FontFallbacks, FontFeatures, HighlightStyle, Hsla, Modifiers, SharedString}; +use collections::HashMap; +use gpui::{App, 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 schemars::JsonSchema; +use serde::{Deserialize, Serialize}; -use crate::{ActiveSettingsProfileName, ParameterizedJsonSchema, Settings}; +use crate::{ActiveSettingsProfileName, Settings}; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct SettingsContent { diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index af8e3d6d4b8b8c06eeed2c1866959de6ee4b8f6d..b4038aa9168e6f49a49726ade197ec926385ac31 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,4 +1,4 @@ -use crate::{Settings, settings_content::SettingsContent, settings_store::SettingsStore}; +use crate::{settings_content::SettingsContent, settings_store::SettingsStore}; use collections::HashSet; use fs::{Fs, PathEventKind}; use futures::{StreamExt, channel::mpsc}; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index e78c341bd7e3b337f1690cd637271e296028b025..0bcd23ece66cd01dfe9f1fc4d5e5444f9ef7918b 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -15,7 +15,6 @@ use serde_json::Value; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, - env, fmt::Debug, ops::Range, path::{Path, PathBuf}, @@ -24,14 +23,13 @@ use std::{ }; use util::{ ResultExt as _, merge_non_null_json_value_into, - schemars::{DefaultDenyUnknownFields, add_new_subschema}, }; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, - VsCodeSettings, WorktreeId, default_settings, parse_json_with_comments, + ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry, + VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, @@ -1238,12 +1236,12 @@ impl Debug for SettingsStore { impl AnySettingValue for SettingValue { fn from_file(&self, s: &SettingsContent) -> Option> { - dbg!(type_name::(), TypeId::of::()); + (type_name::(), TypeId::of::()); T::from_file(s).map(|result| Box::new(result) as _) } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent]) { - dbg!(type_name::(), TypeId::of::()); + (type_name::(), TypeId::of::()); let value = value.downcast_mut::().unwrap(); for refinement in refinements { value.refine(refinement) From faf8947bb04f9b942d3027c2580fe424c60af86e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 15:23:39 -0600 Subject: [PATCH 005/117] Port theme settings :yolo: Co-authored-by: Ben Kunkle --- crates/settings/src/base_keymap_setting.rs | 5 +- crates/settings/src/settings_content.rs | 3 + crates/settings/src/settings_content/theme.rs | 89 +- crates/settings/src/settings_store.rs | 55 +- crates/theme/src/schema.rs | 1657 +++++++---------- crates/theme/src/settings.rs | 755 +++----- crates/theme/src/styles/accents.rs | 2 +- crates/theme/src/styles/players.rs | 2 +- crates/theme/src/theme.rs | 5 +- crates/title_bar/src/title_bar_settings.rs | 2 +- crates/util/src/util.rs | 8 +- 11 files changed, 1125 insertions(+), 1458 deletions(-) diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 599bdff9457c1b7eb947c28a1967fa2069ea11a5..f02ee49e7c778bfec604b5af9e7410de73eb9e01 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -4,6 +4,7 @@ use crate::{ self as settings, settings_content::{self, BaseKeymapContent, SettingsContent}, }; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, VsCodeSettings}; @@ -138,11 +139,11 @@ pub struct BaseKeymapSetting { } impl Settings for BaseKeymap { - fn from_file(s: &crate::settings_content::SettingsContent) -> Option { + fn from_file(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Option { s.base_keymap.map(Into::into) } - fn refine(&mut self, s: &settings_content::SettingsContent) { + fn refine(&mut self, s: &settings_content::SettingsContent, _cx: &mut App) { if let Some(base_keymap) = s.base_keymap { *self = base_keymap.into(); }; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index e8201a4fa143a2e31300ecf955e1f9abb6af3158..b09991854dd1190ed9588960253bc685604b8b75 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -18,6 +18,9 @@ pub struct SettingsContent { #[serde(flatten)] pub project: ProjectSettingsContent, + #[serde(flatten)] + pub theme: ThemeSettingsContent, + pub base_keymap: Option, pub auto_update: Option, diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index e32f39469ef763448ba2da9ecbb88cab06d1ff95..a7469ec2893e05482ce16b16e71e192c81181c74 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -1,5 +1,5 @@ use collections::{HashMap, IndexMap}; -use gpui::{FontFallbacks, FontFeatures}; +use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight}; use schemars::{JsonSchema, JsonSchema_repr, json_schema}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; @@ -999,6 +999,16 @@ pub enum WindowBackgroundContent { Blurred, } +impl Into for WindowBackgroundContent { + fn into(self) -> gpui::WindowBackgroundAppearance { + match self { + WindowBackgroundContent::Opaque => gpui::WindowBackgroundAppearance::Opaque, + WindowBackgroundContent::Transparent => gpui::WindowBackgroundAppearance::Transparent, + WindowBackgroundContent::Blurred => gpui::WindowBackgroundAppearance::Blurred, + } + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub enum FontStyleContent { @@ -1007,6 +1017,16 @@ pub enum FontStyleContent { Oblique, } +impl From for FontStyle { + fn from(value: FontStyleContent) -> Self { + match value { + FontStyleContent::Normal => FontStyle::Normal, + FontStyleContent::Italic => FontStyle::Italic, + FontStyleContent::Oblique => FontStyle::Oblique, + } + } +} + #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] #[repr(u16)] pub enum FontWeightContent { @@ -1020,3 +1040,70 @@ pub enum FontWeightContent { ExtraBold = 800, Black = 900, } + +impl From for FontWeight { + fn from(value: FontWeightContent) -> Self { + match value { + FontWeightContent::Thin => FontWeight::THIN, + FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT, + FontWeightContent::Light => FontWeight::LIGHT, + FontWeightContent::Normal => FontWeight::NORMAL, + FontWeightContent::Medium => FontWeight::MEDIUM, + FontWeightContent::Semibold => FontWeight::SEMIBOLD, + FontWeightContent::Bold => FontWeight::BOLD, + FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD, + FontWeightContent::Black => FontWeight::BLACK, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_buffer_line_height_deserialize_valid() { + assert_eq!( + serde_json::from_value::(json!("comfortable")).unwrap(), + BufferLineHeight::Comfortable + ); + assert_eq!( + serde_json::from_value::(json!("standard")).unwrap(), + BufferLineHeight::Standard + ); + assert_eq!( + serde_json::from_value::(json!({"custom": 1.0})).unwrap(), + BufferLineHeight::Custom(1.0) + ); + assert_eq!( + serde_json::from_value::(json!({"custom": 1.5})).unwrap(), + BufferLineHeight::Custom(1.5) + ); + } + + #[test] + fn test_buffer_line_height_deserialize_invalid() { + assert!( + serde_json::from_value::(json!({"custom": 0.99})) + .err() + .unwrap() + .to_string() + .contains("buffer_line_height.custom must be at least 1.0") + ); + assert!( + serde_json::from_value::(json!({"custom": 0.0})) + .err() + .unwrap() + .to_string() + .contains("buffer_line_height.custom must be at least 1.0") + ); + assert!( + serde_json::from_value::(json!({"custom": -1.0})) + .err() + .unwrap() + .to_string() + .contains("buffer_line_height.custom must be at least 1.0") + ); + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 0bcd23ece66cd01dfe9f1fc4d5e5444f9ef7918b..d1b17103edea989363a9c5549a4243a0f7e968da 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -21,16 +21,13 @@ use std::{ str::{self, FromStr}, sync::Arc, }; -use util::{ - ResultExt as _, merge_non_null_json_value_into, -}; +use util::{ResultExt as _, merge_non_null_json_value_into}; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry, - VsCodeSettings, WorktreeId, parse_json_with_comments, - replace_value_in_json_text, + ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings, + WorktreeId, parse_json_with_comments, replace_value_in_json_text, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, UserSettingsContent, @@ -60,9 +57,9 @@ pub trait Settings: 'static + Send + Sync + Sized { /// user settings match the current version of the settings. const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - fn from_file(content: &SettingsContent) -> Option; + fn from_file(content: &SettingsContent, cx: &mut App) -> Option; - fn refine(&mut self, content: &SettingsContent); + fn refine(&mut self, content: &SettingsContent, cx: &mut App); fn missing_default() -> anyhow::Error { anyhow::anyhow!("missing default for: {}", std::any::type_name::()) @@ -252,8 +249,8 @@ struct SettingValue { trait AnySettingValue: 'static + Send + Sync { fn setting_type_name(&self) -> &'static str; - fn from_file(&self, s: &SettingsContent) -> Option>; - fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent]); + fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option>; + fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App); fn value_for_path(&self, path: Option) -> &dyn Any; fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; @@ -347,9 +344,9 @@ impl SettingsStore { refinements.push(server_settings) } // todo!() unwrap... - let mut value = T::from_file(&self.default_settings).unwrap(); + let mut value = T::from_file(&self.default_settings, cx).unwrap(); for refinement in refinements { - value.refine(refinement) + value.refine(refinement, cx) } setting_value.set_global_value(Box::new(value)); @@ -1114,8 +1111,8 @@ impl SettingsStore { 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 mut value = setting_value.from_file(&self.default_settings).unwrap(); - setting_value.refine(value.as_mut(), &refinements); + let mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); + setting_value.refine(value.as_mut(), &refinements, cx); setting_value.set_global_value(value); } @@ -1137,7 +1134,7 @@ impl SettingsStore { // 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() { + if setting_value.from_file(local_settings, cx).is_some() { paths_stack.push(Some((*root_id, directory_path.as_ref()))); project_settings_stack.push(local_settings); @@ -1150,9 +1147,9 @@ impl SettingsStore { continue; } - let mut value = setting_value.from_file(&self.default_settings).unwrap(); - setting_value.refine(value.as_mut(), &refinements); - setting_value.refine(value.as_mut(), &project_settings_stack); + let mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); + setting_value.refine(value.as_mut(), &refinements, cx); + setting_value.refine(value.as_mut(), &project_settings_stack, cx); setting_value.set_local_value(*root_id, directory_path.clone(), value); } } @@ -1235,16 +1232,16 @@ impl Debug for SettingsStore { } impl AnySettingValue for SettingValue { - fn from_file(&self, s: &SettingsContent) -> Option> { + fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option> { (type_name::(), TypeId::of::()); - T::from_file(s).map(|result| Box::new(result) as _) + T::from_file(s, cx).map(|result| Box::new(result) as _) } - fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent]) { + fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) { (type_name::(), TypeId::of::()); let value = value.downcast_mut::().unwrap(); for refinement in refinements { - value.refine(refinement) + value.refine(refinement, cx) } } @@ -1338,7 +1335,7 @@ impl AnySettingValue for SettingValue { #[cfg(test)] mod tests { use crate::{ - TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, + TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, default_settings, settings_content::LanguageSettingsContent, test_settings, }; @@ -1348,7 +1345,7 @@ mod tests { use serde::Deserialize; use settings_ui_macros::{SettingsKey, SettingsUi}; use unindent::Unindent; - use util::Refine; + use util::MergeFrom; #[derive(Debug, PartialEq)] struct AutoUpdateSetting { @@ -1356,13 +1353,13 @@ mod tests { } impl Settings for AutoUpdateSetting { - fn from_file(content: &SettingsContent) -> Option { + fn from_file(content: &SettingsContent, _: &mut App) -> Option { content .auto_update .map(|auto_update| AutoUpdateSetting { auto_update }) } - fn refine(&mut self, content: &SettingsContent) { + fn refine(&mut self, content: &SettingsContent, _: &mut App) { if let Some(auto_update) = content.auto_update { self.auto_update = auto_update; } @@ -1377,18 +1374,18 @@ mod tests { } impl Settings for TitleBarSettings { - fn from_file(content: &SettingsContent) -> Option { + fn from_file(content: &SettingsContent, _: &mut App) -> Option { let content = content.title_bar?; Some(TitleBarSettings { show: content.show?, }) } - fn refine(&mut self, content: &SettingsContent) { + fn refine(&mut self, content: &SettingsContent, _: &mut App) { let Some(content) = content.title_bar else { return; }; - self.show.refine(&content.show) + self.show.merge_from(&content.show) } fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 2e8445a7917aa52f8c99d7356bcc5d7623d8faa9..ae904dd5d6b10ad896118ef304ddc5b6fc4109cd 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -11,21 +11,6 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{StatusColorsRefinement, ThemeColorsRefinement}; -pub(crate) fn try_parse_color(color: &str) -> Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} - fn ensure_non_opaque(color: Hsla) -> Hsla { const MAXIMUM_OPACITY: f32 = 0.7; if color.a <= MAXIMUM_OPACITY { @@ -81,7 +66,7 @@ pub struct ThemeFamilyContent { pub struct ThemeContent { pub name: String, pub appearance: AppearanceContent, - pub style: ThemeStyleContent, + pub style: settings::ThemeStyleContent, } /// The content of a serialized theme. @@ -95,909 +80,214 @@ pub struct ThemeStyleContent { pub accents: Vec, #[serde(flatten, default)] - pub colors: ThemeColorsContent, + pub colors: settings::ThemeColorsContent, #[serde(flatten, default)] - pub status: StatusColorsContent, + pub status: settings::StatusColorsContent, #[serde(default)] pub players: Vec, /// The styles for syntax nodes. #[serde(default)] - pub syntax: IndexMap, + pub syntax: IndexMap, } -impl ThemeStyleContent { - /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeContent`]. - #[inline(always)] - pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement { - self.colors - .theme_colors_refinement(&self.status_colors_refinement()) - } - - /// Returns a [`StatusColorsRefinement`] based on the colors in the [`ThemeContent`]. - #[inline(always)] - pub fn status_colors_refinement(&self) -> StatusColorsRefinement { - self.status.status_colors_refinement() - } - - /// Returns the syntax style overrides in the [`ThemeContent`]. - pub fn syntax_overrides(&self) -> Vec<(String, HighlightStyle)> { - self.syntax - .iter() - .map(|(key, style)| { - ( - key.clone(), - HighlightStyle { - color: style - .color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background_color: style - .background_color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - font_style: style.font_style.map(FontStyle::from), - font_weight: style.font_weight.map(FontWeight::from), - ..Default::default() - }, - ) - }) - .collect() - } +/// Returns the syntax style overrides in the [`ThemeContent`]. +pub fn syntax_overrides(this: &settings::ThemeStyleContent) -> Vec<(String, HighlightStyle)> { + this.syntax + .iter() + .map(|(key, style)| { + ( + key.clone(), + HighlightStyle { + color: style + .color + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + background_color: style + .background_color + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + font_style: style.font_style.map(FontStyle::from), + font_weight: style.font_weight.map(FontWeight::from), + ..Default::default() + }, + ) + }) + .collect() } -pub(crate) fn try_parse_color(color: &str) -> Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} - -impl ThemeColorsContent { - /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeColorsContent`]. - pub fn theme_colors_refinement( - &self, - status_colors: &StatusColorsRefinement, - ) -> ThemeColorsRefinement { - let border = self - .border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let editor_document_highlight_read_background = self - .editor_document_highlight_read_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_background = self - .scrollbar_thumb_background +pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> StatusColorsRefinement { + StatusColorsRefinement { + conflict: colors + .conflict .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or_else(|| { - self.deprecated_scrollbar_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - }); - let scrollbar_thumb_hover_background = self - .scrollbar_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_active_background = self - .scrollbar_thumb_active_background + .and_then(|color| try_parse_color(color).ok()), + conflict_background: colors + .conflict_background .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background); - let scrollbar_thumb_border = self - .scrollbar_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let element_hover = self - .element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let panel_background = self - .panel_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - ThemeColorsRefinement { - border, - border_variant: self - .border_variant - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_focused: self - .border_focused - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_selected: self - .border_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_transparent: self - .border_transparent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_disabled: self - .border_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - elevated_surface_background: self - .elevated_surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - surface_background: self - .surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background: self - .background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_background: self - .element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_hover, - element_active: self - .element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selected: self - .element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_disabled: self - .element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selection_background: self - .element_selection_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_background: self - .drop_target_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_border: self - .drop_target_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_background: self - .ghost_element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_hover: self - .ghost_element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_active: self - .ghost_element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_selected: self - .ghost_element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_disabled: self - .ghost_element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text: self - .text - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_muted: self - .text_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_placeholder: self - .text_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_disabled: self - .text_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_accent: self - .text_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon: self - .icon - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_muted: self - .icon_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_disabled: self - .icon_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_placeholder: self - .icon_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_accent: self - .icon_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - debugger_accent: self - .debugger_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - status_bar_background: self - .status_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_background: self - .title_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_inactive_background: self - .title_bar_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - toolbar_background: self - .toolbar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_bar_background: self - .tab_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_inactive_background: self - .tab_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_active_background: self - .tab_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - search_match_background: self - .search_match_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_background, - panel_focused_border: self - .panel_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide: self - .panel_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_hover: self - .panel_indent_guide_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_active: self - .panel_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_overlay_background: self - .panel_overlay_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background.map(ensure_opaque)), - panel_overlay_hover: self - .panel_overlay_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background - .zip(element_hover) - .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg)) - .map(ensure_opaque)), - pane_focused_border: self - .pane_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - pane_group_border: self - .pane_group_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(border), - scrollbar_thumb_background, - scrollbar_thumb_hover_background, - scrollbar_thumb_active_background, - scrollbar_thumb_border, - scrollbar_track_background: self - .scrollbar_track_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - scrollbar_track_border: self - .scrollbar_track_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - minimap_thumb_background: self - .minimap_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background.map(ensure_non_opaque)), - minimap_thumb_hover_background: self - .minimap_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)), - minimap_thumb_active_background: self - .minimap_thumb_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_active_background.map(ensure_non_opaque)), - minimap_thumb_border: self - .minimap_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_border), - editor_foreground: self - .editor_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_background: self - .editor_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_gutter_background: self - .editor_gutter_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_subheader_background: self - .editor_subheader_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_background: self - .editor_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_highlighted_line_background: self - .editor_highlighted_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_debugger_active_line_background: self - .editor_debugger_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_line_number: self - .editor_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_hover_line_number: self - .editor_hover_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_number: self - .editor_active_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_invisible: self - .editor_invisible - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_wrap_guide: self - .editor_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_wrap_guide: self - .editor_active_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide: self - .editor_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide_active: self - .editor_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_read_background, - editor_document_highlight_write_background: self - .editor_document_highlight_write_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_bracket_background: self - .editor_document_highlight_bracket_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `editor.document_highlight.read_background`, for backwards compatibility. - .or(editor_document_highlight_read_background), - terminal_background: self - .terminal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_background: self - .terminal_ansi_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_foreground: self - .terminal_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_bright_foreground: self - .terminal_bright_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_dim_foreground: self - .terminal_dim_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_black: self - .terminal_ansi_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_black: self - .terminal_ansi_bright_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_black: self - .terminal_ansi_dim_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_red: self - .terminal_ansi_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_red: self - .terminal_ansi_bright_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_red: self - .terminal_ansi_dim_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_green: self - .terminal_ansi_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_green: self - .terminal_ansi_bright_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_green: self - .terminal_ansi_dim_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_yellow: self - .terminal_ansi_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_yellow: self - .terminal_ansi_bright_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_yellow: self - .terminal_ansi_dim_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_blue: self - .terminal_ansi_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_blue: self - .terminal_ansi_bright_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_blue: self - .terminal_ansi_dim_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_magenta: self - .terminal_ansi_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_magenta: self - .terminal_ansi_bright_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_magenta: self - .terminal_ansi_dim_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_cyan: self - .terminal_ansi_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_cyan: self - .terminal_ansi_bright_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_cyan: self - .terminal_ansi_dim_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_white: self - .terminal_ansi_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_white: self - .terminal_ansi_bright_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_white: self - .terminal_ansi_dim_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - link_text_hover: self - .link_text_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - version_control_added: self - .version_control_added - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `created`, for backwards compatibility. - .or(status_colors.created), - version_control_deleted: self - .version_control_deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `deleted`, for backwards compatibility. - .or(status_colors.deleted), - version_control_modified: self - .version_control_modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_renamed: self - .version_control_renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_conflict: self - .version_control_conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `ignored`, for backwards compatibility. - .or(status_colors.ignored), - version_control_ignored: self - .version_control_ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `conflict`, for backwards compatibility. - .or(status_colors.ignored), - #[allow(deprecated)] - version_control_conflict_marker_ours: self - .version_control_conflict_marker_ours - .as_ref() - .or(self.version_control_conflict_ours_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - #[allow(deprecated)] - version_control_conflict_marker_theirs: self - .version_control_conflict_marker_theirs - .as_ref() - .or(self.version_control_conflict_theirs_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - } - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(default)] -pub struct StatusColorsContent { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - #[serde(rename = "conflict")] - pub conflict: Option, - - #[serde(rename = "conflict.background")] - pub conflict_background: Option, - - #[serde(rename = "conflict.border")] - pub conflict_border: Option, - - /// Indicates something new, like a new file added to a Git repository. - #[serde(rename = "created")] - pub created: Option, - - #[serde(rename = "created.background")] - pub created_background: Option, - - #[serde(rename = "created.border")] - pub created_border: Option, - - /// Indicates that something no longer exists, like a deleted file. - #[serde(rename = "deleted")] - pub deleted: Option, - - #[serde(rename = "deleted.background")] - pub deleted_background: Option, - - #[serde(rename = "deleted.border")] - pub deleted_border: Option, - - /// Indicates a system error, a failed operation or a diagnostic error. - #[serde(rename = "error")] - pub error: Option, - - #[serde(rename = "error.background")] - pub error_background: Option, - - #[serde(rename = "error.border")] - pub error_border: Option, - - /// Represents a hidden status, such as a file being hidden in a file tree. - #[serde(rename = "hidden")] - pub hidden: Option, - - #[serde(rename = "hidden.background")] - pub hidden_background: Option, - - #[serde(rename = "hidden.border")] - pub hidden_border: Option, - - /// Indicates a hint or some kind of additional information. - #[serde(rename = "hint")] - pub hint: Option, - - #[serde(rename = "hint.background")] - pub hint_background: Option, - - #[serde(rename = "hint.border")] - pub hint_border: Option, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - #[serde(rename = "ignored")] - pub ignored: Option, - - #[serde(rename = "ignored.background")] - pub ignored_background: Option, - - #[serde(rename = "ignored.border")] - pub ignored_border: Option, - - /// Represents informational status updates or messages. - #[serde(rename = "info")] - pub info: Option, - - #[serde(rename = "info.background")] - pub info_background: Option, - - #[serde(rename = "info.border")] - pub info_border: Option, - - /// Indicates a changed or altered status, like a file that has been edited. - #[serde(rename = "modified")] - pub modified: Option, - - #[serde(rename = "modified.background")] - pub modified_background: Option, - - #[serde(rename = "modified.border")] - pub modified_border: Option, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - #[serde(rename = "predictive")] - pub predictive: Option, - - #[serde(rename = "predictive.background")] - pub predictive_background: Option, - - #[serde(rename = "predictive.border")] - pub predictive_border: Option, - - /// Represents a renamed status, such as a file that has been renamed. - #[serde(rename = "renamed")] - pub renamed: Option, - - #[serde(rename = "renamed.background")] - pub renamed_background: Option, - - #[serde(rename = "renamed.border")] - pub renamed_border: Option, - - /// Indicates a successful operation or task completion. - #[serde(rename = "success")] - pub success: Option, - - #[serde(rename = "success.background")] - pub success_background: Option, - - #[serde(rename = "success.border")] - pub success_border: Option, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - #[serde(rename = "unreachable")] - pub unreachable: Option, - - #[serde(rename = "unreachable.background")] - pub unreachable_background: Option, - - #[serde(rename = "unreachable.border")] - pub unreachable_border: Option, - - /// Represents a warning status, like an operation that is about to fail. - #[serde(rename = "warning")] - pub warning: Option, - - #[serde(rename = "warning.background")] - pub warning_background: Option, - - #[serde(rename = "warning.border")] - pub warning_border: Option, -} - -impl StatusColorsContent { - /// Returns a [`StatusColorsRefinement`] based on the colors in the [`StatusColorsContent`]. - pub fn status_colors_refinement(&self) -> StatusColorsRefinement { - StatusColorsRefinement { - conflict: self - .conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_background: self - .conflict_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_border: self - .conflict_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created: self - .created - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_background: self - .created_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_border: self - .created_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted: self - .deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_background: self - .deleted_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_border: self - .deleted_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error: self - .error - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_background: self - .error_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_border: self - .error_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden: self - .hidden - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_background: self - .hidden_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_border: self - .hidden_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint: self - .hint - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_background: self - .hint_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_border: self - .hint_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored: self - .ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_background: self - .ignored_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_border: self - .ignored_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info: self - .info - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_background: self - .info_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_border: self - .info_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified: self - .modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_background: self - .modified_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_border: self - .modified_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive: self - .predictive - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_background: self - .predictive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_border: self - .predictive_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed: self - .renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_background: self - .renamed_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_border: self - .renamed_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success: self - .success - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_background: self - .success_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_border: self - .success_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable: self - .unreachable - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_background: self - .unreachable_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_border: self - .unreachable_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning: self - .warning - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_background: self - .warning_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_border: self - .warning_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - } + .and_then(|color| try_parse_color(color).ok()), + conflict_border: colors + .conflict_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + created: colors + .created + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + created_background: colors + .created_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + created_border: colors + .created_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + deleted: colors + .deleted + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + deleted_background: colors + .deleted_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + deleted_border: colors + .deleted_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + error: colors + .error + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + error_background: colors + .error_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + error_border: colors + .error_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hidden: colors + .hidden + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hidden_background: colors + .hidden_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hidden_border: colors + .hidden_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hint: colors + .hint + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hint_background: colors + .hint_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + hint_border: colors + .hint_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ignored: colors + .ignored + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ignored_background: colors + .ignored_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ignored_border: colors + .ignored_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + info: colors + .info + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + info_background: colors + .info_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + info_border: colors + .info_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + modified: colors + .modified + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + modified_background: colors + .modified_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + modified_border: colors + .modified_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + predictive: colors + .predictive + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + predictive_background: colors + .predictive_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + predictive_border: colors + .predictive_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + renamed: colors + .renamed + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + renamed_background: colors + .renamed_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + renamed_border: colors + .renamed_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + success: colors + .success + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + success_background: colors + .success_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + success_border: colors + .success_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + unreachable: colors + .unreachable + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + unreachable_background: colors + .unreachable_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + unreachable_border: colors + .unreachable_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + warning: colors + .warning + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + warning_background: colors + .warning_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + warning_border: colors + .warning_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), } } @@ -1011,24 +301,6 @@ pub struct PlayerColorContent { pub selection: Option, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum FontStyleContent { - Normal, - Italic, - Oblique, -} - -impl From for FontStyle { - fn from(value: FontStyleContent) -> Self { - match value { - FontStyleContent::Normal => FontStyle::Normal, - FontStyleContent::Italic => FontStyle::Italic, - FontStyleContent::Oblique => FontStyle::Oblique, - } - } -} - #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] #[repr(u16)] pub enum FontWeightContent { @@ -1043,18 +315,531 @@ pub enum FontWeightContent { Black = 900, } -impl From for FontWeight { - fn from(value: FontWeightContent) -> Self { - match value { - FontWeightContent::Thin => FontWeight::THIN, - FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT, - FontWeightContent::Light => FontWeight::LIGHT, - FontWeightContent::Normal => FontWeight::NORMAL, - FontWeightContent::Medium => FontWeight::MEDIUM, - FontWeightContent::Semibold => FontWeight::SEMIBOLD, - FontWeightContent::Bold => FontWeight::BOLD, - FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD, - FontWeightContent::Black => FontWeight::BLACK, - } +pub fn theme_colors_refinement( + this: &settings::ThemeColorsContent, + status_colors: &StatusColorsRefinement, +) -> ThemeColorsRefinement { + let border = this + .border + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let editor_document_highlight_read_background = this + .editor_document_highlight_read_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let scrollbar_thumb_background = this + .scrollbar_thumb_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or_else(|| { + this.deprecated_scrollbar_thumb_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + }); + let scrollbar_thumb_hover_background = this + .scrollbar_thumb_hover_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let scrollbar_thumb_active_background = this + .scrollbar_thumb_active_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(scrollbar_thumb_background); + let scrollbar_thumb_border = this + .scrollbar_thumb_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let element_hover = this + .element_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let panel_background = this + .panel_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + ThemeColorsRefinement { + border, + border_variant: this + .border_variant + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + border_focused: this + .border_focused + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + border_selected: this + .border_selected + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + border_transparent: this + .border_transparent + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + border_disabled: this + .border_disabled + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + elevated_surface_background: this + .elevated_surface_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + surface_background: this + .surface_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + background: this + .background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + element_background: this + .element_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + element_hover, + element_active: this + .element_active + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + element_selected: this + .element_selected + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + element_disabled: this + .element_disabled + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + element_selection_background: this + .element_selection_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + drop_target_background: this + .drop_target_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + drop_target_border: this + .drop_target_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ghost_element_background: this + .ghost_element_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ghost_element_hover: this + .ghost_element_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ghost_element_active: this + .ghost_element_active + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ghost_element_selected: this + .ghost_element_selected + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + ghost_element_disabled: this + .ghost_element_disabled + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + text: this + .text + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + text_muted: this + .text_muted + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + text_placeholder: this + .text_placeholder + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + text_disabled: this + .text_disabled + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + text_accent: this + .text_accent + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + icon: this + .icon + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + icon_muted: this + .icon_muted + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + icon_disabled: this + .icon_disabled + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + icon_placeholder: this + .icon_placeholder + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + icon_accent: this + .icon_accent + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + debugger_accent: this + .debugger_accent + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + status_bar_background: this + .status_bar_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + title_bar_background: this + .title_bar_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + title_bar_inactive_background: this + .title_bar_inactive_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + toolbar_background: this + .toolbar_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + tab_bar_background: this + .tab_bar_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + tab_inactive_background: this + .tab_inactive_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + tab_active_background: this + .tab_active_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + search_match_background: this + .search_match_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + panel_background, + panel_focused_border: this + .panel_focused_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + panel_indent_guide: this + .panel_indent_guide + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + panel_indent_guide_hover: this + .panel_indent_guide_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + panel_indent_guide_active: this + .panel_indent_guide_active + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + panel_overlay_background: this + .panel_overlay_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(panel_background.map(ensure_opaque)), + panel_overlay_hover: this + .panel_overlay_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(panel_background + .zip(element_hover) + .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg)) + .map(ensure_opaque)), + pane_focused_border: this + .pane_focused_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + pane_group_border: this + .pane_group_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(border), + scrollbar_thumb_background, + scrollbar_thumb_hover_background, + scrollbar_thumb_active_background, + scrollbar_thumb_border, + scrollbar_track_background: this + .scrollbar_track_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + scrollbar_track_border: this + .scrollbar_track_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + minimap_thumb_background: this + .minimap_thumb_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(scrollbar_thumb_background.map(ensure_non_opaque)), + minimap_thumb_hover_background: this + .minimap_thumb_hover_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)), + minimap_thumb_active_background: this + .minimap_thumb_active_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(scrollbar_thumb_active_background.map(ensure_non_opaque)), + minimap_thumb_border: this + .minimap_thumb_border + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(scrollbar_thumb_border), + editor_foreground: this + .editor_foreground + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_background: this + .editor_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_gutter_background: this + .editor_gutter_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_subheader_background: this + .editor_subheader_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_active_line_background: this + .editor_active_line_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_highlighted_line_background: this + .editor_highlighted_line_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_debugger_active_line_background: this + .editor_debugger_active_line_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_line_number: this + .editor_line_number + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_hover_line_number: this + .editor_hover_line_number + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_active_line_number: this + .editor_active_line_number + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_invisible: this + .editor_invisible + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_wrap_guide: this + .editor_wrap_guide + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_active_wrap_guide: this + .editor_active_wrap_guide + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_indent_guide: this + .editor_indent_guide + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_indent_guide_active: this + .editor_indent_guide_active + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_document_highlight_read_background, + editor_document_highlight_write_background: this + .editor_document_highlight_write_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + editor_document_highlight_bracket_background: this + .editor_document_highlight_bracket_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `editor.document_highlight.read_background`, for backwards compatibility. + .or(editor_document_highlight_read_background), + terminal_background: this + .terminal_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_background: this + .terminal_ansi_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_foreground: this + .terminal_foreground + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_bright_foreground: this + .terminal_bright_foreground + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_dim_foreground: this + .terminal_dim_foreground + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_black: this + .terminal_ansi_black + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_black: this + .terminal_ansi_bright_black + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_black: this + .terminal_ansi_dim_black + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_red: this + .terminal_ansi_red + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_red: this + .terminal_ansi_bright_red + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_red: this + .terminal_ansi_dim_red + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_green: this + .terminal_ansi_green + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_green: this + .terminal_ansi_bright_green + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_green: this + .terminal_ansi_dim_green + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_yellow: this + .terminal_ansi_yellow + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_yellow: this + .terminal_ansi_bright_yellow + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_yellow: this + .terminal_ansi_dim_yellow + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_blue: this + .terminal_ansi_blue + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_blue: this + .terminal_ansi_bright_blue + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_blue: this + .terminal_ansi_dim_blue + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_magenta: this + .terminal_ansi_magenta + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_magenta: this + .terminal_ansi_bright_magenta + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_magenta: this + .terminal_ansi_dim_magenta + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_cyan: this + .terminal_ansi_cyan + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_cyan: this + .terminal_ansi_bright_cyan + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_cyan: this + .terminal_ansi_dim_cyan + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_white: this + .terminal_ansi_white + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_bright_white: this + .terminal_ansi_bright_white + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + terminal_ansi_dim_white: this + .terminal_ansi_dim_white + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + link_text_hover: this + .link_text_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + version_control_added: this + .version_control_added + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `created`, for backwards compatibility. + .or(status_colors.created), + version_control_deleted: this + .version_control_deleted + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `deleted`, for backwards compatibility. + .or(status_colors.deleted), + version_control_modified: this + .version_control_modified + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `modified`, for backwards compatibility. + .or(status_colors.modified), + version_control_renamed: this + .version_control_renamed + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `modified`, for backwards compatibility. + .or(status_colors.modified), + version_control_conflict: this + .version_control_conflict + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `ignored`, for backwards compatibility. + .or(status_colors.ignored), + version_control_ignored: this + .version_control_ignored + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + // Fall back to `conflict`, for backwards compatibility. + .or(status_colors.ignored), + #[allow(deprecated)] + version_control_conflict_marker_ours: this + .version_control_conflict_marker_ours + .as_ref() + .or(this.version_control_conflict_ours_background.as_ref()) + .and_then(|color| try_parse_color(color).ok()), + #[allow(deprecated)] + version_control_conflict_marker_theirs: this + .version_control_conflict_marker_theirs + .as_ref() + .or(this.version_control_conflict_theirs_background.as_ref()) + .and_then(|color| try_parse_color(color).ok()), } } + +pub(crate) fn try_parse_color(color: &str) -> anyhow::Result { + let rgba = gpui::Rgba::try_from(color)?; + let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); + let hsla = palette::Hsla::from_color(rgba); + + let hsla = gpui::hsla( + hsla.hue.into_positive_degrees() / 360., + hsla.saturation, + hsla.lightness, + hsla.alpha, + ); + + Ok(hsla) +} diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 500ab2c03cf7ee01b245ca2bc89bd2c218f66d1a..a6c99c8219fd4ff993e48d725e4142d16dcc2670 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,22 +1,25 @@ use crate::fallback_themes::zed_default_dark; use crate::{ Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme, - ThemeNotFoundError, ThemeRegistry, ThemeStyleContent, + ThemeColorsRefinement, ThemeNotFoundError, ThemeRegistry, ThemeStyleContent, + status_colors_refinement, syntax_overrides, theme_colors_refinement, }; -use anyhow::Result; use collections::HashMap; use derive_more::{Deref, DerefMut}; use gpui::{ - App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels, - SharedString, Subscription, Window, px, + App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, Subscription, Window, + px, }; use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ParameterizedJsonSchema, Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::{ + FontFamilyName, IconThemeName, ParameterizedJsonSchema, Settings, SettingsContent, ThemeMode, + ThemeName, +}; use std::sync::Arc; -use util::ResultExt as _; use util::schemars::replace_subschema; +use util::{MergeFrom, ResultExt as _}; const MIN_FONT_SIZE: Pixels = px(6.0); const MAX_FONT_SIZE: Pixels = px(100.0); @@ -119,16 +122,16 @@ pub struct ThemeSettings { /// Manual overrides for the active theme. /// /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) - pub experimental_theme_overrides: Option, + pub experimental_theme_overrides: Option, /// Manual overrides per theme - pub theme_overrides: HashMap, + pub theme_overrides: HashMap, /// The current icon theme selection. pub icon_theme_selection: Option, /// The active icon theme. pub active_icon_theme: Arc, /// The density of the UI. /// Note: This setting is still experimental. See [this tracking issue]( - pub ui_density: UiDensity, + pub ui_density: settings::UiDensity, /// The amount of fading applied to unnecessary code. pub unnecessary_code_fade: f32, } @@ -277,24 +280,15 @@ pub enum ThemeSelection { }, } -// TODO: Rename ThemeMode -> ThemeAppearanceMode -/// The mode use to select a theme. -/// -/// `Light` and `Dark` will select their respective themes. -/// -/// `System` will select the theme based on the system's appearance. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ThemeMode { - /// Use the specified `light` theme. - Light, - - /// Use the specified `dark` theme. - Dark, - - /// Use the theme based on the system's appearance. - #[default] - System, +impl From for ThemeSelection { + fn from(selection: settings::ThemeSelection) -> Self { + match selection { + settings::ThemeSelection::Static(theme) => ThemeSelection::Static(theme), + settings::ThemeSelection::Dynamic { mode, light, dark } => { + ThemeSelection::Dynamic { mode, light, dark } + } + } + } } impl ThemeSelection { @@ -323,15 +317,13 @@ impl ThemeSelection { } /// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum IconThemeSelection { /// A static icon theme selection, represented by a single icon theme name. Static(IconThemeName), /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. Dynamic { /// The mode used to determine which theme to use. - #[serde(default)] mode: ThemeMode, /// The icon theme to use for light mode. light: IconThemeName, @@ -340,6 +332,17 @@ pub enum IconThemeSelection { }, } +impl From for IconThemeSelection { + fn from(selection: settings::IconThemeSelection) -> Self { + match selection { + settings::IconThemeSelection::Static(theme) => IconThemeSelection::Static(theme), + settings::IconThemeSelection::Dynamic { mode, light, dark } => { + IconThemeSelection::Dynamic { mode, light, dark } + } + } + } +} + impl IconThemeSelection { /// Returns the icon theme name based on the given [`Appearance`]. pub fn icon_theme(&self, system_appearance: Appearance) -> &str { @@ -365,189 +368,105 @@ impl IconThemeSelection { } } -/// Settings for rendering text in UI and text buffers. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] -pub struct ThemeSettingsContent { - /// The default font size for text in the UI. - #[serde(default)] - pub ui_font_size: Option, - /// The name of a font to use for rendering in the UI. - #[serde(default)] - pub ui_font_family: Option, - /// The font fallbacks to use for rendering in the UI. - #[serde(default)] - #[schemars(default = "default_font_fallbacks")] - #[schemars(extend("uniqueItems" = true))] - pub ui_font_fallbacks: Option>, - /// The OpenType features to enable for text in the UI. - #[serde(default)] - #[schemars(default = "default_font_features")] - pub ui_font_features: Option, - /// The weight of the UI font in CSS units from 100 to 900. - #[serde(default)] - pub ui_font_weight: Option, - /// The name of a font to use for rendering in text buffers. - #[serde(default)] - pub buffer_font_family: Option, - /// The font fallbacks to use for rendering in text buffers. - #[serde(default)] - #[schemars(extend("uniqueItems" = true))] - pub buffer_font_fallbacks: Option>, - /// The default font size for rendering in text buffers. - #[serde(default)] - pub buffer_font_size: Option, - /// The weight of the editor font in CSS units from 100 to 900. - #[serde(default)] - pub buffer_font_weight: Option, - /// The buffer's line height. - #[serde(default)] - pub buffer_line_height: Option, - /// The OpenType features to enable for rendering in text buffers. - #[serde(default)] - #[schemars(default = "default_font_features")] - pub buffer_font_features: Option, - /// The font size for the agent panel. Falls back to the UI font size if unset. - #[serde(default)] - pub agent_font_size: Option>, - /// The name of the Zed theme to use. - #[serde(default)] - pub theme: Option, - /// The name of the icon theme to use. - #[serde(default)] - pub icon_theme: Option, - - /// UNSTABLE: Expect many elements to be broken. - /// - // Controls the density of the UI. - #[serde(rename = "unstable.ui_density", default)] - pub ui_density: Option, - - /// How much to fade out unused code. - #[serde(default)] - pub unnecessary_code_fade: Option, - - /// EXPERIMENTAL: Overrides for the current theme. - /// - /// These values will override the ones on the current theme specified in `theme`. - #[serde(rename = "experimental.theme_overrides", default)] - pub experimental_theme_overrides: Option, - - /// Overrides per theme - /// - /// These values will override the ones on the specified theme - #[serde(default)] - pub theme_overrides: HashMap, -} - -fn default_font_features() -> Option { - Some(FontFeatures::default()) -} - -fn default_font_fallbacks() -> Option { - Some(FontFallbacks::default()) -} - -impl ThemeSettingsContent { - /// Sets the theme for the given appearance to the theme with the specified name. - pub fn set_theme(&mut self, theme_name: impl Into>, appearance: Appearance) { - if let Some(selection) = self.theme.as_mut() { - let theme_to_update = match selection { - ThemeSelection::Static(theme) => theme, - ThemeSelection::Dynamic { mode, light, dark } => match mode { - ThemeMode::Light => light, - ThemeMode::Dark => dark, - ThemeMode::System => match appearance { - Appearance::Light => light, - Appearance::Dark => dark, - }, - }, - }; - - *theme_to_update = ThemeName(theme_name.into()); - } else { - self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into()))); - } - } - - /// Sets the icon theme for the given appearance to the icon theme with the specified name. - pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) { - if let Some(selection) = self.icon_theme.as_mut() { - let icon_theme_to_update = match selection { - IconThemeSelection::Static(theme) => theme, - IconThemeSelection::Dynamic { mode, light, dark } => match mode { - ThemeMode::Light => light, - ThemeMode::Dark => dark, - ThemeMode::System => match appearance { - Appearance::Light => light, - Appearance::Dark => dark, - }, - }, - }; - - *icon_theme_to_update = IconThemeName(icon_theme_name.into()); - } else { - self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( - icon_theme_name.into(), - ))); - } - } - - /// Sets the mode for the theme. - pub fn set_mode(&mut self, mode: ThemeMode) { - if let Some(selection) = self.theme.as_mut() { - match selection { - ThemeSelection::Static(theme) => { - // If the theme was previously set to a single static theme, - // we don't know whether it was a light or dark theme, so we - // just use it for both. - self.theme = Some(ThemeSelection::Dynamic { - mode, - light: theme.clone(), - dark: theme.clone(), - }); - } - ThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - self.theme = Some(ThemeSelection::Dynamic { - mode, - light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()), - dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()), - }); - } - - if let Some(selection) = self.icon_theme.as_mut() { - match selection { - IconThemeSelection::Static(icon_theme) => { - // If the icon theme was previously set to a single static - // theme, we don't know whether it was a light or dark - // theme, so we just use it for both. - self.icon_theme = Some(IconThemeSelection::Dynamic { - mode, - light: icon_theme.clone(), - dark: icon_theme.clone(), - }); - } - IconThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( - DEFAULT_ICON_THEME_NAME.into(), - ))); - } - } -} +// impl ThemeSettingsContent { +// /// Sets the theme for the given appearance to the theme with the specified name. +// pub fn set_theme(&mut self, theme_name: impl Into>, appearance: Appearance) { +// if let Some(selection) = self.theme.as_mut() { +// let theme_to_update = match selection { +// ThemeSelection::Static(theme) => theme, +// ThemeSelection::Dynamic { mode, light, dark } => match mode { +// ThemeMode::Light => light, +// ThemeMode::Dark => dark, +// ThemeMode::System => match appearance { +// Appearance::Light => light, +// Appearance::Dark => dark, +// }, +// }, +// }; + +// *theme_to_update = ThemeName(theme_name.into()); +// } else { +// self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into()))); +// } +// } + +// /// Sets the icon theme for the given appearance to the icon theme with the specified name. +// pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) { +// if let Some(selection) = self.icon_theme.as_mut() { +// let icon_theme_to_update = match selection { +// IconThemeSelection::Static(theme) => theme, +// IconThemeSelection::Dynamic { mode, light, dark } => match mode { +// ThemeMode::Light => light, +// ThemeMode::Dark => dark, +// ThemeMode::System => match appearance { +// Appearance::Light => light, +// Appearance::Dark => dark, +// }, +// }, +// }; + +// *icon_theme_to_update = IconThemeName(icon_theme_name.into()); +// } else { +// self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( +// icon_theme_name.into(), +// ))); +// } +// } + +// /// Sets the mode for the theme. +// pub fn set_mode(&mut self, mode: ThemeMode) { +// if let Some(selection) = self.theme.as_mut() { +// match selection { +// ThemeSelection::Static(theme) => { +// // If the theme was previously set to a single static theme, +// // we don't know whether it was a light or dark theme, so we +// // just use it for both. +// self.theme = Some(ThemeSelection::Dynamic { +// mode, +// light: theme.clone(), +// dark: theme.clone(), +// }); +// } +// ThemeSelection::Dynamic { +// mode: mode_to_update, +// .. +// } => *mode_to_update = mode, +// } +// } else { +// self.theme = Some(ThemeSelection::Dynamic { +// mode, +// light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()), +// dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()), +// }); +// } + +// if let Some(selection) = self.icon_theme.as_mut() { +// match selection { +// IconThemeSelection::Static(icon_theme) => { +// // If the icon theme was previously set to a single static +// // theme, we don't know whether it was a light or dark +// // theme, so we just use it for both. +// self.icon_theme = Some(IconThemeSelection::Dynamic { +// mode, +// light: icon_theme.clone(), +// dark: icon_theme.clone(), +// }); +// } +// IconThemeSelection::Dynamic { +// mode: mode_to_update, +// .. +// } => *mode_to_update = mode, +// } +// } else { +// self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( +// DEFAULT_ICON_THEME_NAME.into(), +// ))); +// } +// } +// } /// The buffer's line height. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] +#[derive(Clone, Copy, Debug, PartialEq, Default)] pub enum BufferLineHeight { /// A less dense line height. #[default] @@ -555,21 +474,19 @@ pub enum BufferLineHeight { /// The default line height. Standard, /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), + Custom(f32), } -fn deserialize_line_height<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = f32::deserialize(deserializer)?; - if value < 1.0 { - return Err(serde::de::Error::custom( - "buffer_line_height.custom must be at least 1.0", - )); +impl From for BufferLineHeight { + fn from(value: settings::BufferLineHeight) -> Self { + match value { + settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable, + settings::BufferLineHeight::Standard => BufferLineHeight::Standard, + settings::BufferLineHeight::Custom(line_height) => { + BufferLineHeight::Custom(line_height) + } + } } - - Ok(value) } impl BufferLineHeight { @@ -681,24 +598,22 @@ impl ThemeSettings { } } - fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) { + fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) { if let Some(window_background_appearance) = theme_overrides.window_background_appearance { base_theme.styles.window_background_appearance = window_background_appearance.into(); } + let status_color_refinement = status_colors_refinement(&theme_overrides.status); - base_theme - .styles - .colors - .refine(&theme_overrides.theme_colors_refinement()); - base_theme - .styles - .status - .refine(&theme_overrides.status_colors_refinement()); + base_theme.styles.colors.refine(&theme_colors_refinement( + &theme_overrides.colors, + &status_color_refinement, + )); + base_theme.styles.status.refine(&status_color_refinement); base_theme.styles.player.merge(&theme_overrides.players); base_theme.styles.accents.merge(&theme_overrides.accents); base_theme.styles.syntax = SyntaxTheme::merge( base_theme.styles.syntax.clone(), - theme_overrides.syntax_overrides(), + syntax_overrides(&theme_overrides), ); } @@ -818,292 +733,170 @@ fn clamp_font_weight(weight: f32) -> FontWeight { FontWeight(weight.clamp(100., 950.)) } +fn font_fallbacks_from_settings( + fallbacks: Option>, +) -> Option { + fallbacks.map(|fallbacks| { + FontFallbacks::from_fonts( + fallbacks + .into_iter() + .map(|font_family| font_family.0.to_string()) + .collect(), + ) + }) +} + impl settings::Settings for ThemeSettings { - fn load(sources: SettingsSources, cx: &mut App) -> Result { + fn from_file(content: &settings::SettingsContent, cx: &mut App) -> Option { + let content = &content.theme; let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); - - fn font_fallbacks_from_settings( - fallbacks: Option>, - ) -> Option { - fallbacks.map(|fallbacks| { - FontFallbacks::from_fonts( - fallbacks - .into_iter() - .map(|font_family| font_family.0.to_string()) - .collect(), - ) - }) - } - - let defaults = sources.default; - let mut this = Self { - ui_font_size: defaults.ui_font_size.unwrap().into(), + let theme_selection: ThemeSelection = content.theme.clone()?.into(); + let icon_theme_selection: IconThemeSelection = content.icon_theme.clone()?.into(); + let this = Self { + ui_font_size: content.ui_font_size?.into(), ui_font: Font { - family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(), - features: defaults.ui_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()), - weight: defaults.ui_font_weight.map(FontWeight).unwrap(), + family: content.ui_font_family.as_ref()?.0.clone().into(), + features: content.ui_font_features.clone()?, + fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()), + weight: content.ui_font_weight.map(FontWeight)?, style: Default::default(), }, buffer_font: Font { - family: defaults - .buffer_font_family - .as_ref() - .unwrap() - .0 - .clone() - .into(), - features: defaults.buffer_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()), - weight: defaults.buffer_font_weight.map(FontWeight).unwrap(), + family: content.buffer_font_family.as_ref()?.0.clone().into(), + features: content.buffer_font_features.clone()?, + fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()), + weight: content.buffer_font_weight.map(FontWeight)?, style: FontStyle::default(), }, - buffer_font_size: defaults.buffer_font_size.unwrap().into(), - buffer_line_height: defaults.buffer_line_height.unwrap(), - agent_font_size: defaults.agent_font_size.flatten().map(Into::into), - theme_selection: defaults.theme.clone(), + buffer_font_size: content.buffer_font_size?.into(), + buffer_line_height: content.buffer_line_height?.into(), + agent_font_size: content.agent_font_size.flatten().map(Into::into), active_theme: themes - .get(defaults.theme.as_ref().unwrap().theme(*system_appearance)) + .get(theme_selection.theme(*system_appearance)) .or(themes.get(&zed_default_dark().name)) .unwrap(), + theme_selection: Some(theme_selection), experimental_theme_overrides: None, theme_overrides: HashMap::default(), - icon_theme_selection: defaults.icon_theme.clone(), - active_icon_theme: defaults - .icon_theme - .as_ref() - .and_then(|selection| { - themes - .get_icon_theme(selection.icon_theme(*system_appearance)) - .ok() - }) - .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()), - ui_density: defaults.ui_density.unwrap_or(UiDensity::Default), - unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0), + active_icon_theme: themes + .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance)) + .ok()?, + icon_theme_selection: Some(icon_theme_selection), + ui_density: content.ui_density?, + unnecessary_code_fade: content.unnecessary_code_fade?, }; - for value in sources - .user - .into_iter() - .chain(sources.release_channel) - .chain(sources.operating_system) - .chain(sources.profile) - .chain(sources.server) - { - if let Some(value) = value.ui_density { - this.ui_density = value; - } - - if let Some(value) = value.buffer_font_family.clone() { - this.buffer_font.family = value.0.into(); - } - if let Some(value) = value.buffer_font_features.clone() { - this.buffer_font.features = value; - } - if let Some(value) = value.buffer_font_fallbacks.clone() { - this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value)); - } - if let Some(value) = value.buffer_font_weight { - this.buffer_font.weight = clamp_font_weight(value); - } + Some(this) + } - if let Some(value) = value.ui_font_family.clone() { - this.ui_font.family = value.0.into(); - } - if let Some(value) = value.ui_font_features.clone() { - this.ui_font.features = value; - } - if let Some(value) = value.ui_font_fallbacks.clone() { - this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value)); - } - if let Some(value) = value.ui_font_weight { - this.ui_font.weight = clamp_font_weight(value); - } + fn refine(&mut self, content: &SettingsContent, cx: &mut App) { + let value = &content.theme; - if let Some(value) = &value.theme { - this.theme_selection = Some(value.clone()); + let themes = ThemeRegistry::default_global(cx); + let system_appearance = SystemAppearance::default_global(cx); - let theme_name = value.theme(*system_appearance); + self.ui_density.merge_from(&value.ui_density); - match themes.get(theme_name) { - Ok(theme) => { - this.active_theme = theme; - } - Err(err @ ThemeNotFoundError(_)) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } - } - } - } + if let Some(value) = value.buffer_font_family.clone() { + self.buffer_font.family = value.0.into(); + } + if let Some(value) = value.buffer_font_features.clone() { + self.buffer_font.features = value; + } + if let Some(value) = value.buffer_font_fallbacks.clone() { + self.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value)); + } + if let Some(value) = value.buffer_font_weight { + self.buffer_font.weight = clamp_font_weight(value); + } - this.experimental_theme_overrides - .clone_from(&value.experimental_theme_overrides); - this.theme_overrides.clone_from(&value.theme_overrides); - this.apply_theme_overrides(); + if let Some(value) = value.ui_font_family.clone() { + self.ui_font.family = value.0.into(); + } + if let Some(value) = value.ui_font_features.clone() { + self.ui_font.features = value; + } + if let Some(value) = value.ui_font_fallbacks.clone() { + self.ui_font.fallbacks = font_fallbacks_from_settings(Some(value)); + } + if let Some(value) = value.ui_font_weight { + self.ui_font.weight = clamp_font_weight(value); + } - if let Some(value) = &value.icon_theme { - this.icon_theme_selection = Some(value.clone()); + if let Some(value) = &value.theme { + self.theme_selection = Some(value.clone().into()); - let icon_theme_name = value.icon_theme(*system_appearance); + let theme_name = self + .theme_selection + .as_ref() + .unwrap() + .theme(*system_appearance); - match themes.get_icon_theme(icon_theme_name) { - Ok(icon_theme) => { - this.active_icon_theme = icon_theme; - } - Err(err @ IconThemeNotFoundError(_)) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } + match themes.get(theme_name) { + Ok(theme) => { + self.active_theme = theme; + } + Err(err @ ThemeNotFoundError(_)) => { + if themes.extensions_loaded() { + log::error!("{err}"); } } } - - merge( - &mut this.ui_font_size, - value.ui_font_size.map(Into::into).map(clamp_font_size), - ); - merge( - &mut this.buffer_font_size, - value.buffer_font_size.map(Into::into).map(clamp_font_size), - ); - merge( - &mut this.agent_font_size, - value - .agent_font_size - .map(|value| value.map(Into::into).map(clamp_font_size)), - ); - - merge(&mut this.buffer_line_height, value.buffer_line_height); - - // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely. - merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade); - this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9); } - Ok(this) - } + self.experimental_theme_overrides + .clone_from(&value.experimental_theme_overrides); + self.theme_overrides.clone_from(&value.theme_overrides); - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight); - vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size); - if let Some(font) = vscode.read_string("editor.font") { - current.buffer_font_family = Some(FontFamilyName(font.into())); - } - // TODO: possibly map editor.fontLigatures to buffer_font_features? - } -} + self.apply_theme_overrides(); -/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct ThemeName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { - replace_subschema::(generator, || json_schema!({ - "type": "string", - "enum": ThemeRegistry::global(cx).list_names(), - })) - } - } -} + if let Some(value) = &value.icon_theme { + self.icon_theme_selection = Some(value.clone().into()); -/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at -/// runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct IconThemeName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { - replace_subschema::(generator, || json_schema!({ - "type": "string", - "enum": ThemeRegistry::global(cx) - .list_icon_themes() - .into_iter() - .map(|icon_theme| icon_theme.name) - .collect::>(), - })) - } - } -} + let icon_theme_name = self + .icon_theme_selection + .as_ref() + .unwrap() + .icon_theme(*system_appearance); -/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at -/// runtime. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(transparent)] -pub struct FontFamilyName(pub Arc); - -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, params, _cx| { - replace_subschema::(generator, || { - json_schema!({ - "type": "string", - "enum": params.font_names, - }) - }) + match themes.get_icon_theme(icon_theme_name) { + Ok(icon_theme) => { + self.active_icon_theme = icon_theme; + } + Err(err @ IconThemeNotFoundError(_)) => { + if themes.extensions_loaded() { + log::error!("{err}"); + } + } + } } - } -} -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } -} + self.ui_font_size + .merge_from(&value.ui_font_size.map(Into::into).map(clamp_font_size)); + self.buffer_font_size + .merge_from(&value.buffer_font_size.map(Into::into).map(clamp_font_size)); + self.agent_font_size.merge_from( + &value + .agent_font_size + .map(|value| value.map(Into::into).map(clamp_font_size)), + ); -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; + self.buffer_line_height + .merge_from(&value.buffer_line_height.map(Into::into)); - #[test] - fn test_buffer_line_height_deserialize_valid() { - assert_eq!( - serde_json::from_value::(json!("comfortable")).unwrap(), - BufferLineHeight::Comfortable - ); - assert_eq!( - serde_json::from_value::(json!("standard")).unwrap(), - BufferLineHeight::Standard - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.0})).unwrap(), - BufferLineHeight::Custom(1.0) - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.5})).unwrap(), - BufferLineHeight::Custom(1.5) - ); + // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely. + self.unnecessary_code_fade + .merge_from(&value.unnecessary_code_fade); + self.unnecessary_code_fade = self.unnecessary_code_fade.clamp(0.0, 0.9); } - #[test] - fn test_buffer_line_height_deserialize_invalid() { - assert!( - serde_json::from_value::(json!({"custom": 0.99})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": 0.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": -1.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { + vscode.f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight); + vscode.f32_setting("editor.fontSize", &mut current.theme.buffer_font_size); + if let Some(font) = vscode.read_string("editor.font") { + current.theme.buffer_font_family = Some(FontFamilyName(font.into())); + } + // TODO: possibly map editor.fontLigatures to buffer_font_features? } } diff --git a/crates/theme/src/styles/accents.rs b/crates/theme/src/styles/accents.rs index 52244fee46ce12edbbe361c3bec9bb80139f1167..5eda83637d2b6ebb736ba6bb2f3f80cf557bf847 100644 --- a/crates/theme/src/styles/accents.rs +++ b/crates/theme/src/styles/accents.rs @@ -66,7 +66,7 @@ impl AccentColors { } /// Merges the given accent colors into this [`AccentColors`] instance. - pub fn merge(&mut self, accent_colors: &[AccentContent]) { + pub fn merge(&mut self, accent_colors: &[settings::AccentContent]) { if accent_colors.is_empty() { return; } diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index d10ac61ba6bfeabbf0642d832e68841c39176546..db66fbe212cc407679f69acff4f27710835babd6 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -152,7 +152,7 @@ impl PlayerColors { } /// Merges the given player colors into this [`PlayerColors`] instance. - pub fn merge(&mut self, user_player_colors: &[PlayerColorContent]) { + pub fn merge(&mut self, user_player_colors: &[settings::PlayerColorContent]) { if user_player_colors.is_empty() { return; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c54010b4b0c6fe56168c3e15271d3423921b15da..c17761b7dfd828337132fb2c21b716fa0a24d8b6 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -178,7 +178,7 @@ impl ThemeFamily { AppearanceContent::Light => StatusColors::light(), AppearanceContent::Dark => StatusColors::dark(), }; - let mut status_colors_refinement = theme.style.status_colors_refinement(); + let mut status_colors_refinement = status_colors_refinement(&theme.style.status); apply_status_color_defaults(&mut status_colors_refinement); refined_status_colors.refine(&status_colors_refinement); @@ -192,7 +192,8 @@ impl ThemeFamily { AppearanceContent::Light => ThemeColors::light(), AppearanceContent::Dark => ThemeColors::dark(), }; - let mut theme_colors_refinement = theme.style.theme_colors_refinement(); + let mut theme_colors_refinement = + theme_colors_refinement(&theme.style.colors, &status_colors_refinement); apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors); refined_theme_colors.refine(&theme_colors_refinement); diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 1af7c4547493e704c002bb08a2e4a862bd192931..01939211c4f3c40dbeb17bbd0b9dd62a2247e723 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -38,7 +38,7 @@ impl Settings for TitleBarSettings { } } - fn refine(&mut self, s: &SettingsContent) { + fn refine(&mut self, s: &SettingsContent, _: &mut App) { let Some(content) = s.title_bar else { return } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 986b0c8ac822fe0ebb981a6bd0d168f915109f0a..298d7cc846dad8c3a0727af76d5d93d3be95079e 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1388,12 +1388,12 @@ pub fn refine(dest: &mut T, src: &Option) { } } -pub trait Refine: Sized + Clone { - fn refine(&mut self, src: &Option); +pub trait MergeFrom: Sized + Clone { + fn merge_from(&mut self, src: &Option); } -impl Refine for T { - fn refine(&mut self, src: &Option) { +impl MergeFrom for T { + fn merge_from(&mut self, src: &Option) { if let Some(src) = src { *self = src.clone(); } From 6d883e31cd7bdfbe4814f92bef7fdbc5873d59dd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 15:28:29 -0600 Subject: [PATCH 006/117] Fix schema generator Co-authored-by: Ben Kunkle --- crates/settings/src/settings_store.rs | 203 ++------------------------ 1 file changed, 14 insertions(+), 189 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index d1b17103edea989363a9c5549a4243a0f7e968da..4bc90c0ce24e15ea3725def1f249f917e669e560 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -10,8 +10,9 @@ use futures::{ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; +use schemars::JsonSchema; use serde::{Serialize, de::DeserializeOwned}; -use serde_json::Value; +use serde_json::{Value, json}; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, @@ -21,13 +22,13 @@ use std::{ str::{self, FromStr}, sync::Arc, }; -use util::{ResultExt as _, merge_non_null_json_value_into}; +use util::{ResultExt as _, merge_non_null_json_value_into, schemars::DefaultDenyUnknownFields}; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - ActiveSettingsProfileName, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings, - WorktreeId, parse_json_with_comments, replace_value_in_json_text, + ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, + VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, UserSettingsContent, @@ -886,194 +887,18 @@ impl SettingsStore { } pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value { - 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 - // ); - // } - // } - // } - // } + let mut generator = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) + .into_generator(); - // // 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); - // } + let schema = UserSettingsContent::json_schema(&mut generator); - // // 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(), - // ); + // 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); + } - // // 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, - // }) + schema.to_value() } fn recompute_values( From eeb5e24bb168e9b2285c9221f791c8bf9ddc1bdf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 15:57:57 -0600 Subject: [PATCH 007/117] Editing working again. Co-authored-by: Ben Kunkle --- crates/settings/src/settings_content.rs | 32 +- .../settings/src/settings_content/language.rs | 6 +- crates/settings/src/settings_store.rs | 376 ++++++++---------- 3 files changed, 174 insertions(+), 240 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index b09991854dd1190ed9588960253bc685604b8b75..d3d818d2834b8c9977dc66a081d394851f57534f 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -11,7 +11,7 @@ use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{ActiveSettingsProfileName, Settings}; +use crate::ActiveSettingsProfileName; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct SettingsContent { @@ -42,29 +42,29 @@ pub struct ServerSettingsContent { } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -pub(crate) struct UserSettingsContent { +pub struct UserSettingsContent { #[serde(flatten)] - pub(crate) content: SettingsContent, + pub content: SettingsContent, - pub(crate) dev: Option, - pub(crate) nightly: Option, - pub(crate) preview: Option, - pub(crate) stable: Option, + pub dev: Option, + pub nightly: Option, + pub preview: Option, + pub stable: Option, - pub(crate) macos: Option, - pub(crate) windows: Option, - pub(crate) linux: Option, + pub macos: Option, + pub windows: Option, + pub linux: Option, #[serde(default)] - pub(crate) profiles: HashMap, + pub profiles: HashMap, } pub struct ExtensionsSettingsContent { - pub(crate) all_languages: AllLanguageSettingsContent, + pub all_languages: AllLanguageSettingsContent, } impl UserSettingsContent { - pub(crate) fn for_release_channel(&self) -> Option<&SettingsContent> { + pub fn for_release_channel(&self) -> Option<&SettingsContent> { match *release_channel::RELEASE_CHANNEL { ReleaseChannel::Dev => self.dev.as_ref(), ReleaseChannel::Nightly => self.nightly.as_ref(), @@ -73,7 +73,7 @@ impl UserSettingsContent { } } - pub(crate) fn for_os(&self) -> Option<&SettingsContent> { + pub fn for_os(&self) -> Option<&SettingsContent> { match env::consts::OS { "macos" => self.macos.as_ref(), "linux" => self.linux.as_ref(), @@ -82,7 +82,7 @@ impl UserSettingsContent { } } - pub(crate) fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { + pub fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { let Some(active_profile) = cx.try_global::() else { return None; }; @@ -112,7 +112,7 @@ pub struct ProjectSettingsContent { pub(crate) all_languages: AllLanguageSettingsContent, } -#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct TitleBarSettingsContent { /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". /// diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 5d58618f2ac590cbc3f7ac62642f55b2f3d6d81c..c771d6fd705b3e2445eb30a8a4059482cd9bb445 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -379,7 +379,7 @@ pub struct JsxTagAutoCloseSettings { } /// The settings for inlay hints. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettings { /// Global switch to toggle hints on and off. /// @@ -446,7 +446,7 @@ fn scroll_debounce_ms() -> u64 { } /// Controls how completions are processed for this language. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct CompletionSettings { /// Controls how words are completed. @@ -773,7 +773,7 @@ pub enum Formatter { } /// The settings for indent guides. -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct IndentGuideSettingsContent { /// Whether to display indent guides in the editor. /// diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 4bc90c0ce24e15ea3725def1f249f917e669e560..53821ecc847253e1bc6cc2e13caf6e92772fb927 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -12,7 +12,7 @@ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGl use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Value, json}; +use serde_json::Value; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, @@ -257,20 +257,14 @@ trait AnySettingValue: 'static + Send + Sync { fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); - fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema; - fn edits_for_update( + fn import_from_vscode( &self, - raw_settings: &serde_json::Value, - tab_size: usize, vscode_settings: &VsCodeSettings, - text: &mut String, - edits: &mut Vec<(Range, String)>, + settings_content: &mut SettingsContent, ); fn settings_ui_item(&self) -> SettingsUiEntry; } -struct DeserializedSetting(Box); - impl SettingsStore { pub fn new(cx: &App, default_settings: &str) -> Self { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); @@ -605,18 +599,12 @@ impl SettingsStore { new_text } - pub fn get_vscode_edits(&self, mut old_text: String, vscode: &VsCodeSettings) -> String { - let mut new_text = old_text.clone(); - let mut edits: Vec<(Range, String)> = Vec::new(); - let raw_settings = parse_json_with_comments::(&old_text).unwrap_or_default(); - let tab_size = self.json_tab_size(); - for v in self.setting_values.values() { - v.edits_for_update(&raw_settings, tab_size, vscode, &mut old_text, &mut edits); - } - for (range, replacement) in edits.into_iter() { - new_text.replace_range(range, &replacement); - } - new_text + pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String { + self.new_text_for_update(old_text, |settings_content| { + for v in self.setting_values.values() { + v.import_from_vscode(vscode, settings_content) + } + }) } /// Updates the value of a setting in a JSON file, returning a list @@ -628,10 +616,13 @@ impl SettingsStore { ) -> Vec<(Range, String)> { let old_content: UserSettingsContent = serde_json::from_str(text).unwrap_or_default(); let mut new_content = old_content.clone(); + dbg!(&new_content.content.title_bar); update(&mut new_content.content); + dbg!(&new_content.content.title_bar); let old_value = serde_json::to_value(&old_content).unwrap(); let new_value = serde_json::to_value(new_content).unwrap(); + // dbg!(&old_value, &new_value); let mut key_path = Vec::new(); let mut edits = Vec::new(); @@ -713,8 +704,6 @@ impl SettingsStore { ..Default::default() }); - todo!(); - // self.server_settings = Some(settings); self.recompute_values(None, cx)?; Ok(()) } @@ -1110,45 +1099,12 @@ impl AnySettingValue for SettingValue { } } - fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - todo!() - // T::FileContent::json_schema(generator) - } - - fn edits_for_update( + fn import_from_vscode( &self, - raw_settings: &serde_json::Value, - tab_size: usize, vscode_settings: &VsCodeSettings, - text: &mut String, - edits: &mut Vec<(Range, String)>, + settings_content: &mut SettingsContent, ) { - 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, - // ); + T::import_from_vscode(vscode_settings, settings_content); } fn settings_ui_item(&self) -> SettingsUiEntry { @@ -1196,18 +1152,20 @@ mod tests { #[derive(Debug, PartialEq)] struct TitleBarSettings { show: TitleBarVisibilityContent, + show_branch_name: bool, } impl Settings for TitleBarSettings { fn from_file(content: &SettingsContent, _: &mut App) -> Option { - let content = content.title_bar?; + let content = content.title_bar.clone()?; Some(TitleBarSettings { show: content.show?, + show_branch_name: content.show_branch_name?, }) } fn refine(&mut self, content: &SettingsContent, _: &mut App) { - let Some(content) = content.title_bar else { + let Some(content) = content.title_bar.as_ref() else { return; }; self.show.merge_from(&content.show) @@ -1227,26 +1185,18 @@ mod tests { store.get::(None), &AutoUpdateSetting { auto_update: true } ); - // 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(), - // } - // ); + assert_eq!( + store.get::(None).show, + TitleBarVisibilityContent::Always + ); store .set_user_settings( r#"{ "auto_update": false, + "title_bar": { + "show": "never" + } }"#, cx, ) @@ -1256,43 +1206,40 @@ mod tests { store.get::(None), &AutoUpdateSetting { auto_update: false } ); - // 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(); + assert_eq!( + store.get::(None).show, + TitleBarVisibilityContent::Never + ); - store - .set_local_settings( - WorktreeId::from_usize(1), - Path::new("/root2").into(), - LocalSettingsKind::Settings, - Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), - cx, - ) - .unwrap(); + // todo!() + // 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 { @@ -1339,49 +1286,26 @@ mod tests { // ); } - // #[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, - // } - // ); + #[gpui::test] + fn test_setting_store_assign_json_before_register(cx: &mut App) { + let mut store = SettingsStore::new(cx, &test_settings()); + store + .set_user_settings(r#"{ "auto_update": false }"#, cx) + .unwrap(); + store.register_setting::(cx); + store.register_setting::(cx); - // store.register_setting::(cx); - // assert_eq!( - // store.get::(None), - // &MultiKeySettings { - // key1: "x".into(), - // key2: String::new(), - // } - // ); - // } + assert_eq!( + store.get::(None), + &AutoUpdateSetting { auto_update: false } + ); + assert_eq!( + store.get::(None).show, + TitleBarVisibilityContent::Always, + ); + } + #[track_caller] fn check_settings_update( store: &mut SettingsStore, old_json: String, @@ -1391,6 +1315,7 @@ mod tests { ) { store.set_user_settings(&old_json, cx).ok(); let edits = store.edits_for_update(&old_json, update); + dbg!(&edits); let mut new_json = old_json; for (range, replacement) in edits.into_iter() { new_json.replace_range(range, &replacement); @@ -1500,78 +1425,87 @@ 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, - // ); + // weird formatting + check_settings_update( + &mut store, + r#"{ + "title_bar": { "show": "always", "name": "Max" } + }"# + .unindent(), + |settings| { + dbg!(&settings.title_bar); + settings.title_bar.as_mut().unwrap().show = Some(TitleBarVisibilityContent::Never); + dbg!(&settings.title_bar); + }, + r#"{ + "title_bar": { "show": "never", "name": "Max" } + }"# + .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, - // ); + // single-line formatting, other keys + check_settings_update( + &mut store, + r#"{ "one": 1, "two": 2 }"#.to_owned(), + |settings| settings.auto_update = Some(true), + r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(), + cx, + ); - // // empty object - // check_settings_update::( - // &mut store, - // r#"{ - // "user": {} - // }"# - // .unindent(), - // |settings| settings.age = Some(37), - // r#"{ - // "user": { - // "age": 37 - // } - // }"# - // .unindent(), - // cx, - // ); + // empty object + check_settings_update( + &mut store, + r#"{ + "title_bar": {} + }"# + .unindent(), + |settings| settings.title_bar.as_mut().unwrap().show_menus = Some(true), + r#"{ + "title_bar": { + "show_menus": true + } + }"# + .unindent(), + cx, + ); - // // no content - // check_settings_update::( - // &mut store, - // r#""#.unindent(), - // |settings| settings.age = Some(37), - // r#"{ - // "user": { - // "age": 37 - // } - // } - // "# - // .unindent(), - // cx, - // ); + // no content + check_settings_update( + &mut store, + r#""#.unindent(), + |settings| { + settings.title_bar = Some(TitleBarSettingsContent { + show_branch_name: Some(true), + ..Default::default() + }) + }, + r#"{ + "title_bar": { + "show_branch_name": true + } + } + "# + .unindent(), + cx, + ); - // 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.title_bar.get_or_insert_default().show_branch_name = Some(true), + r#"{ + "title_bar": { + "show_branch_name": true + } + } + "# + .unindent(), + cx, + ); } // #[gpui::test] From 2932ee576eea47fb12daa123b7f728036ebad968 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:00:09 -0600 Subject: [PATCH 008/117] Some warnings --- crates/settings/src/settings_content/theme.rs | 4 ++-- crates/settings/src/settings_store.rs | 10 +--------- crates/theme_importer/src/vscode/converter.rs | 8 ++++---- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index a7469ec2893e05482ce16b16e71e192c81181c74..d46cf76d1103ef7f0524e5abb1a734f07547d578 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -276,7 +276,7 @@ pub struct ThemeName(pub Arc); inventory::submit! { ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { + add_and_get_ref: |_generator, _params, _cx| { todo!() // replace_subschema::(generator, || json_schema!({ // "type": "string", @@ -294,7 +294,7 @@ pub struct IconThemeName(pub Arc); inventory::submit! { ParameterizedJsonSchema { - add_and_get_ref: |generator, _params, cx| { + add_and_get_ref: |_generator, _params, _cx| { todo!() // replace_subschema::(generator, || json_schema!({ // "type": "string", diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 53821ecc847253e1bc6cc2e13caf6e92772fb927..b687df1b062f99080c7fc95faddcf1be5189a280 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -616,9 +616,7 @@ impl SettingsStore { ) -> Vec<(Range, String)> { let old_content: UserSettingsContent = serde_json::from_str(text).unwrap_or_default(); let mut new_content = old_content.clone(); - dbg!(&new_content.content.title_bar); update(&mut new_content.content); - dbg!(&new_content.content.title_bar); let old_value = serde_json::to_value(&old_content).unwrap(); let new_value = serde_json::to_value(new_content).unwrap(); @@ -1047,12 +1045,10 @@ impl Debug for SettingsStore { impl AnySettingValue for SettingValue { fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option> { - (type_name::(), TypeId::of::()); T::from_file(s, cx).map(|result| Box::new(result) as _) } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) { - (type_name::(), TypeId::of::()); let value = value.downcast_mut::().unwrap(); for refinement in refinements { value.refine(refinement, cx) @@ -1116,15 +1112,11 @@ impl AnySettingValue for SettingValue { #[cfg(test)] mod tests { use crate::{ - TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, default_settings, + TitleBarSettingsContent, TitleBarVisibilityContent, default_settings, settings_content::LanguageSettingsContent, test_settings, }; use super::*; - // This is so the SettingsUi macro can still work properly - use crate as settings; - use serde::Deserialize; - use settings_ui_macros::{SettingsKey, SettingsUi}; use unindent::Unindent; use util::MergeFrom; diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index b3b846d91d5ac3a7e3d88983787d23fe9f0adece..15f0e91e75ba866e64dc082f7a7bc10b10dfb15c 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -1,10 +1,10 @@ -use anyhow::Result; -use indexmap::IndexMap; -use strum::IntoEnumIterator; -use theme::{ +use ::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, ThemeContent, ThemeStyleContent, }; +use anyhow::Result; +use indexmap::IndexMap; +use strum::IntoEnumIterator; use crate::ThemeMetadata; use crate::vscode::{VsCodeTheme, VsCodeTokenScope}; From 72bf0910f6fd4c87edf91b3ceb0b7ed1497564de Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:09:29 -0600 Subject: [PATCH 009/117] Remove warnings from settings/theme Co-authored-by: Ben Kunkle --- crates/settings/src/settings_content/theme.rs | 46 +------------------ crates/theme/src/schema.rs | 15 +----- crates/theme/src/settings.rs | 45 ++++++++++++++++-- crates/theme/src/styles/accents.rs | 4 +- crates/theme/src/styles/players.rs | 4 +- crates/theme_importer/src/vscode/converter.rs | 6 +-- crates/ui_macros/src/dynamic_spacing.rs | 12 ++--- 7 files changed, 57 insertions(+), 75 deletions(-) diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index d46cf76d1103ef7f0524e5abb1a734f07547d578..640726869b6592a890937c63e1a256d7f09b7594 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -1,13 +1,10 @@ use collections::{HashMap, IndexMap}; use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight}; -use schemars::{JsonSchema, JsonSchema_repr, json_schema}; +use schemars::{JsonSchema, JsonSchema_repr}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::sync::Arc; -use util::schemars::replace_subschema; - -use crate::ParameterizedJsonSchema; /// Settings for rendering text in UI and text buffers. #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -195,19 +192,6 @@ impl UiDensity { #[serde(transparent)] pub struct FontFamilyName(pub Arc); -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |generator, params, _cx| { - replace_subschema::(generator, || { - json_schema!({ - "type": "string", - "enum": params.font_names, - }) - }) - } - } -} - /// The buffer's line height. #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] @@ -274,40 +258,12 @@ pub struct PlayerColorContent { #[serde(transparent)] pub struct ThemeName(pub Arc); -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |_generator, _params, _cx| { - todo!() - // replace_subschema::(generator, || json_schema!({ - // "type": "string", - // "enum": ThemeRegistry::global(cx).list_names(), - // })) - } - } -} - /// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at /// runtime. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(transparent)] pub struct IconThemeName(pub Arc); -inventory::submit! { - ParameterizedJsonSchema { - add_and_get_ref: |_generator, _params, _cx| { - todo!() - // replace_subschema::(generator, || json_schema!({ - // "type": "string", - // "enum": ThemeRegistry::global(cx) - // .list_icon_themes() - // .into_iter() - // .map(|icon_theme| icon_theme.name) - // .collect::>(), - // })) - } - } -} - #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(default)] pub struct ThemeColorsContent { diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index ae904dd5d6b10ad896118ef304ddc5b6fc4109cd..54bc12e5909833bdbef87eb8f8241ae272417f19 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -1,13 +1,12 @@ #![allow(missing_docs)] -use anyhow::Result; use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance}; use indexmap::IndexMap; use palette::FromColor; use schemars::{JsonSchema, JsonSchema_repr}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use settings::{AccentContent, PlayerColorContent}; use crate::{StatusColorsRefinement, ThemeColorsRefinement}; @@ -291,16 +290,6 @@ pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> Statu } } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct AccentContent(pub Option); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct PlayerColorContent { - pub cursor: Option, - pub background: Option, - pub selection: Option, -} - #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] #[repr(u16)] pub enum FontWeightContent { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index a6c99c8219fd4ff993e48d725e4142d16dcc2670..cb11546a4b055bbf0b7449ce5e1d11973a365d72 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,14 +1,14 @@ use crate::fallback_themes::zed_default_dark; use crate::{ Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme, - ThemeColorsRefinement, ThemeNotFoundError, ThemeRegistry, ThemeStyleContent, + ThemeNotFoundError, ThemeRegistry, status_colors_refinement, syntax_overrides, theme_colors_refinement, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; use gpui::{ - App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, Subscription, Window, - px, + App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, SharedString, + Subscription, Window, px, }; use refineable::Refineable; use schemars::{JsonSchema, json_schema}; @@ -262,6 +262,45 @@ pub struct AgentFontSize(Pixels); impl Global for AgentFontSize {} +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + replace_subschema::(generator, || json_schema!({ + "type": "string", + "enum": ThemeRegistry::global(cx).list_names(), + })) + } + } +} + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + replace_subschema::(generator, || json_schema!({ + "type": "string", + "enum": ThemeRegistry::global(cx) + .list_icon_themes() + .into_iter() + .map(|icon_theme| icon_theme.name) + .collect::>(), + })) + } + } +} + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + replace_subschema::(generator, || { + json_schema!({ + "type": "string", + "enum": params.font_names, + }) + }) + } + } +} + /// Represents the selection of a theme, which can be either static or dynamic. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(untagged)] diff --git a/crates/theme/src/styles/accents.rs b/crates/theme/src/styles/accents.rs index 5eda83637d2b6ebb736ba6bb2f3f80cf557bf847..49ae755bf811da4f5cbd53eb521ae3b300278c99 100644 --- a/crates/theme/src/styles/accents.rs +++ b/crates/theme/src/styles/accents.rs @@ -2,8 +2,8 @@ use gpui::Hsla; use serde::Deserialize; use crate::{ - AccentContent, amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, - tomato, try_parse_color, + amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato, + try_parse_color, }; /// A collection of colors that are used to color indent aware lines in the editor. diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index db66fbe212cc407679f69acff4f27710835babd6..439dbdd437aa64e034004a4495e64a96e76ce87e 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -3,9 +3,7 @@ use gpui::Hsla; use serde::Deserialize; -use crate::{ - PlayerColorContent, amber, blue, jade, lime, orange, pink, purple, red, try_parse_color, -}; +use crate::{amber, blue, jade, lime, orange, pink, purple, red, try_parse_color}; #[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)] pub struct PlayerColor { diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 15f0e91e75ba866e64dc082f7a7bc10b10dfb15c..33bbb8b18abcfb078f81ea19beac76c702ede37e 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -1,9 +1,9 @@ -use ::{ +use anyhow::Result; +use indexmap::IndexMap; +use settings::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, ThemeContent, ThemeStyleContent, }; -use anyhow::Result; -use indexmap::IndexMap; use strum::IntoEnumIterator; use crate::ThemeMetadata; diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs index bd7c72e90eda94508e627781043bb23b63ba576a..fa1a90a15f6ec26e94b02d3d267ebbd3d04085ec 100644 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ b/crates/ui_macros/src/dynamic_spacing.rs @@ -66,9 +66,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let n = n.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, - UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, - UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, } } } @@ -78,9 +78,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let c = c.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, - UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, - UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, + settings::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, } } } From 6254673c91c40927baeda0136c8d609548f0fcc0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:26:04 -0600 Subject: [PATCH 010/117] Local project settings --- crates/settings/src/settings_store.rs | 203 ++++++++++-------- crates/theme/src/settings.rs | 21 +- crates/theme/src/theme.rs | 4 + crates/theme_importer/src/vscode/converter.rs | 4 +- crates/ui_macros/src/dynamic_spacing.rs | 12 +- 5 files changed, 139 insertions(+), 105 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index b687df1b062f99080c7fc95faddcf1be5189a280..92213dc10fe35805d035e76d7952603cd5b1aa1a 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -944,26 +944,21 @@ impl SettingsStore { break; } - // 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, cx).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(&self.default_settings, cx).unwrap(); - setting_value.refine(value.as_mut(), &refinements, cx); - setting_value.refine(value.as_mut(), &project_settings_stack, cx); - setting_value.set_local_value(*root_id, directory_path.clone(), value); + 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(&self.default_settings, cx).unwrap(); + setting_value.refine(value.as_mut(), &refinements, cx); + setting_value.refine(value.as_mut(), &project_settings_stack, cx); + setting_value.set_local_value(*root_id, directory_path.clone(), value); } } Ok(()) @@ -1111,6 +1106,8 @@ impl AnySettingValue for SettingValue { #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use crate::{ TitleBarSettingsContent, TitleBarVisibilityContent, default_settings, settings_content::LanguageSettingsContent, test_settings, @@ -1166,12 +1163,37 @@ mod tests { fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} } + #[derive(Debug, PartialEq)] + struct DefaultLanguageSettings { + tab_size: NonZeroU32, + preferred_line_length: u32, + } + + impl Settings for DefaultLanguageSettings { + fn from_file(content: &SettingsContent, _: &mut App) -> Option { + let content = &content.project.all_languages.defaults; + Some(DefaultLanguageSettings { + tab_size: content.tab_size?, + preferred_line_length: content.preferred_line_length?, + }) + } + + fn refine(&mut self, content: &SettingsContent, _: &mut App) { + let content = &content.project.all_languages.defaults; + self.tab_size.merge_from(&content.tab_size); + self.preferred_line_length + .merge_from(&content.preferred_line_length); + } + + fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + } + #[gpui::test] fn test_settings_store_basic(cx: &mut App) { let mut store = SettingsStore::new(cx, &default_settings()); store.register_setting::(cx); store.register_setting::(cx); - // store.register_setting::(cx); + store.register_setting::(cx); assert_eq!( store.get::(None), @@ -1204,78 +1226,75 @@ mod tests { ); // todo!() - // 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(), - // } - // ); + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root1").into(), + LocalSettingsKind::Settings, + Some(r#"{ "tab_size": 5 }"#), + cx, + ) + .unwrap(); + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root1/subdir").into(), + LocalSettingsKind::Settings, + Some(r#"{ "preferred_line_length": 50 }"#), + cx, + ) + .unwrap(); + + store + .set_local_settings( + WorktreeId::from_usize(1), + Path::new("/root2").into(), + LocalSettingsKind::Settings, + Some(r#"{ "tab_size": 9, "title_bar": { "show_branch_name": false } }"#), + cx, + ) + .unwrap(); + + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root1/something"), + })), + &DefaultLanguageSettings { + preferred_line_length: 80, + tab_size: 5.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root1/subdir/something") + })), + &DefaultLanguageSettings { + preferred_line_length: 50, + tab_size: 5.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root2/something") + })), + &DefaultLanguageSettings { + preferred_line_length: 80, + tab_size: 9.try_into().unwrap(), + } + ); + assert_eq!( + store.get::(Some(SettingsLocation { + worktree_id: WorktreeId::from_usize(1), + path: Path::new("/root2/something") + })), + &TitleBarSettings { + show: TitleBarVisibilityContent::Never, + show_branch_name: true, + } + ); } #[gpui::test] diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index cb11546a4b055bbf0b7449ce5e1d11973a365d72..568e9e1044ea281f1305864a18329466735f6102 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,8 +1,8 @@ use crate::fallback_themes::zed_default_dark; use crate::{ Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme, - ThemeNotFoundError, ThemeRegistry, - status_colors_refinement, syntax_overrides, theme_colors_refinement, + ThemeNotFoundError, ThemeRegistry, status_colors_refinement, syntax_overrides, + theme_colors_refinement, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -89,6 +89,16 @@ impl From for String { } } +impl From for UiDensity { + fn from(val: settings::UiDensity) -> Self { + match val { + settings::UiDensity::Compact => Self::Compact, + settings::UiDensity::Default => Self::Default, + settings::UiDensity::Comfortable => Self::Comfortable, + } + } +} + /// Customizable settings for the UI and theme system. #[derive(Clone, PartialEq)] pub struct ThemeSettings { @@ -131,7 +141,7 @@ pub struct ThemeSettings { pub active_icon_theme: Arc, /// The density of the UI. /// Note: This setting is still experimental. See [this tracking issue]( - pub ui_density: settings::UiDensity, + pub ui_density: UiDensity, /// The amount of fading applied to unnecessary code. pub unnecessary_code_fade: f32, } @@ -822,7 +832,7 @@ impl settings::Settings for ThemeSettings { .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance)) .ok()?, icon_theme_selection: Some(icon_theme_selection), - ui_density: content.ui_density?, + ui_density: content.ui_density?.into(), unnecessary_code_fade: content.unnecessary_code_fade?, }; @@ -835,7 +845,8 @@ impl settings::Settings for ThemeSettings { let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); - self.ui_density.merge_from(&value.ui_density); + self.ui_density + .merge_from(&value.ui_density.map(Into::into)); if let Some(value) = value.buffer_font_family.clone() { self.buffer_font.family = value.0.into(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c17761b7dfd828337132fb2c21b716fa0a24d8b6..29269c15074a158acd742c7f6e258b0c94b7bebe 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -44,6 +44,10 @@ pub use crate::scale::*; pub use crate::schema::*; pub use crate::settings::*; pub use crate::styles::*; +pub use ::settings::{ + FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, + ThemeStyleContent, +}; /// Defines window border radius for platforms that use client side decorations. pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 33bbb8b18abcfb078f81ea19beac76c702ede37e..b3b846d91d5ac3a7e3d88983787d23fe9f0adece 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -1,10 +1,10 @@ use anyhow::Result; use indexmap::IndexMap; -use settings::{ +use strum::IntoEnumIterator; +use theme::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, ThemeContent, ThemeStyleContent, }; -use strum::IntoEnumIterator; use crate::ThemeMetadata; use crate::vscode::{VsCodeTheme, VsCodeTokenScope}; diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs index fa1a90a15f6ec26e94b02d3d267ebbd3d04085ec..15ba3e241ec43d02b83e4143eb620505a0a2f02e 100644 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ b/crates/ui_macros/src/dynamic_spacing.rs @@ -66,9 +66,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let n = n.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - settings::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, } } } @@ -78,9 +78,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { let c = c.base10_parse::().unwrap(); quote! { DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - settings::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, - settings::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, + ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, } } } From 947fa3cfc845a6be66376943245b4ae49c6799c2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:49:01 -0600 Subject: [PATCH 011/117] VSCode import tests Co-authored-by: Ben Kunkle --- crates/settings/src/base_keymap_setting.rs | 2 +- crates/settings/src/settings_store.rs | 556 +++++++-------------- crates/settings/src/vscode_import.rs | 7 +- crates/theme/src/settings.rs | 2 +- crates/title_bar/src/title_bar_settings.rs | 2 +- 5 files changed, 182 insertions(+), 387 deletions(-) diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index f02ee49e7c778bfec604b5af9e7410de73eb9e01..b7ac08f620d1aba93178c8cd74e3a271a8689a82 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -139,7 +139,7 @@ pub struct BaseKeymapSetting { } impl Settings for BaseKeymap { - fn from_file(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Option { + fn from_default(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Option { s.base_keymap.map(Into::into) } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 92213dc10fe35805d035e76d7952603cd5b1aa1a..6e4f0aff1fa723b086154b69f18688514ff2f168 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -58,7 +58,7 @@ pub trait Settings: 'static + Send + Sync + Sized { /// user settings match the current version of the settings. const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - fn from_file(content: &SettingsContent, cx: &mut App) -> Option; + fn from_default(content: &SettingsContent, cx: &mut App) -> Option; fn refine(&mut self, content: &SettingsContent, cx: &mut App); @@ -322,6 +322,10 @@ impl SettingsStore { refinements.push(extension_settings) } + if let Some(global_settings) = self.global_settings.as_ref() { + refinements.push(global_settings) + } + 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() { @@ -338,8 +342,12 @@ impl SettingsStore { if let Some(server_settings) = self.server_settings.as_ref() { refinements.push(server_settings) } - // todo!() unwrap... - let mut value = T::from_file(&self.default_settings, cx).unwrap(); + let Some(mut value) = T::from_default(&self.default_settings, cx) else { + panic!( + "{}::from_file return None for default.json", + type_name::() + ) + }; for refinement in refinements { value.refine(refinement, cx) } @@ -903,6 +911,10 @@ impl SettingsStore { refinements.push(extension_settings) } + if let Some(global_settings) = self.global_settings.as_ref() { + refinements.push(global_settings) + } + 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() { @@ -1040,7 +1052,7 @@ impl Debug for SettingsStore { impl AnySettingValue for SettingValue { fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option> { - T::from_file(s, cx).map(|result| Box::new(result) as _) + T::from_default(s, cx).map(|result| Box::new(result) as _) } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) { @@ -1109,7 +1121,7 @@ mod tests { use std::num::NonZeroU32; use crate::{ - TitleBarSettingsContent, TitleBarVisibilityContent, default_settings, + TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, default_settings, settings_content::LanguageSettingsContent, test_settings, }; @@ -1123,7 +1135,7 @@ mod tests { } impl Settings for AutoUpdateSetting { - fn from_file(content: &SettingsContent, _: &mut App) -> Option { + fn from_default(content: &SettingsContent, _: &mut App) -> Option { content .auto_update .map(|auto_update| AutoUpdateSetting { auto_update }) @@ -1145,7 +1157,7 @@ mod tests { } impl Settings for TitleBarSettings { - fn from_file(content: &SettingsContent, _: &mut App) -> Option { + fn from_default(content: &SettingsContent, _: &mut App) -> Option { let content = content.title_bar.clone()?; Some(TitleBarSettings { show: content.show?, @@ -1160,7 +1172,18 @@ mod tests { self.show.merge_from(&content.show) } - fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { + let mut show = None; + + vscode.enum_setting("window.titleBarStyle", &mut show, |value| match value { + "never" => Some(TitleBarVisibilityContent::Never), + "always" => Some(TitleBarVisibilityContent::Always), + _ => None, + }); + if let Some(show) = show { + content.title_bar.get_or_insert_default().show.replace(show); + } + } } #[derive(Debug, PartialEq)] @@ -1170,7 +1193,7 @@ mod tests { } impl Settings for DefaultLanguageSettings { - fn from_file(content: &SettingsContent, _: &mut App) -> Option { + fn from_default(content: &SettingsContent, _: &mut App) -> Option { let content = &content.project.all_languages.defaults; Some(DefaultLanguageSettings { tab_size: content.tab_size?, @@ -1185,7 +1208,17 @@ mod tests { .merge_from(&content.preferred_line_length); } - fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} + fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { + let content = &mut content.project.all_languages.defaults; + + if let Some(size) = vscode + .read_value("editor.tabSize") + .and_then(|v| v.as_u64()) + .and_then(|n| NonZeroU32::new(n as u32)) + { + content.tab_size = Some(size); + } + } } #[gpui::test] @@ -1337,9 +1370,6 @@ mod tests { #[gpui::test] fn test_setting_store_update(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); - // store.register_setting::(cx); - // store.register_setting::(cx); - // store.register_setting::(cx); // entries added and updated check_settings_update( @@ -1519,381 +1549,151 @@ mod tests { ); } - // #[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; + #[gpui::test] + fn test_vscode_import(cx: &mut App) { + let mut store = SettingsStore::new(cx, &test_settings()); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); - // fn load(sources: SettingsSources, _: &mut App) -> Result { - // sources.json_merge() - // } + // create settings that werent present + check_vscode_import( + &mut store, + r#"{ + } + "# + .unindent(), + r#" { "editor.tabSize": 37 } "#.to_owned(), + r#"{ + "tab_size": 37 + } + "# + .unindent(), + cx, + ); - // 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"); + // persist settings that were present + check_vscode_import( + &mut store, + r#"{ + "preferred_line_length": 99, + } + "# + .unindent(), + r#"{ "editor.tabSize": 42 }"#.to_owned(), + r#"{ + "tab_size": 42, + "preferred_line_length": 99, + } + "# + .unindent(), + cx, + ); - // if let Some((first, second)) = first_value.zip(second_value) { - // current.key1 = Some(format!("{} {}", first, second)); - // } - // } - // } + // don't clobber settings that aren't present in vscode + check_vscode_import( + &mut store, + r#"{ + "preferred_line_length": 99, + "tab_size": 42 + } + "# + .unindent(), + r#"{}"#.to_owned(), + r#"{ + "preferred_line_length": 99, + "tab_size": 42 + } + "# + .unindent(), + cx, + ); - // #[derive(Debug, Deserialize)] - // struct JournalSettings { - // #[expect(unused)] - // pub path: String, - // #[expect(unused)] - // pub hour_format: HourFormat, - // } + // custom enum + check_vscode_import( + &mut store, + r#"{ + "title_bar": { + "show": "always" + } + } + "# + .unindent(), + r#"{ "window.titleBarStyle": "never" }"#.to_owned(), + r#"{ + "title_bar": { + "show": "never" + } + } + "# + .unindent(), + cx, + ); + } - // #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] - // #[serde(rename_all = "snake_case")] - // enum HourFormat { - // Hour12, - // Hour24, - // } + #[track_caller] + 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( - // Clone, Default, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, - // )] - // #[settings_key(key = "journal")] - // struct JournalSettingsJson { - // pub path: Option, - // pub hour_format: Option, - // } + #[gpui::test] + fn test_global_settings(cx: &mut App) { + let mut store = SettingsStore::new(cx, &test_settings()); + store.register_setting::(cx); - // impl Settings for JournalSettings { - // type FileContent = JournalSettingsJson; + // Set global settings - these should override defaults but not user settings + store + .set_global_settings( + r#"{ + "title_bar": { + "show": "never", + } + }"#, + cx, + ) + .unwrap(); - // fn load(sources: SettingsSources, _: &mut App) -> Result { - // sources.json_merge() - // } + // Before user settings, global settings should apply + assert_eq!( + store.get::(None), + &TitleBarSettings { + show: TitleBarVisibilityContent::Never, + show_branch_name: true, + } + ); - // 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, - // }); - // } - // } + // Set user settings - these should override both defaults and global + store + .set_user_settings( + r#"{ + "title_bar": { + "show": "always" + } + }"#, + cx, + ) + .unwrap(); - // #[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 - // } - // ); - // } + // User settings should override global settings + assert_eq!( + store.get::(None), + &TitleBarSettings { + show: TitleBarVisibilityContent::Always, + show_branch_name: true, // Staff from global settings + } + ); + } // #[derive( // Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 53fbf797c3d9e56e49b1d96e7dabcac19ddde8e2..5792dcc12bd687faf50c91530f027b1f90d7ff92 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -78,12 +78,7 @@ impl VsCodeSettings { } pub fn read_value(&self, setting: &str) -> Option<&Value> { - if let Some(value) = self.content.get(setting) { - return Some(value); - } - // TODO: maybe check if it's in [platform] settings for current platform as a fallback - // TODO: deal with language specific settings - None + self.content.get(setting) } pub fn read_string(&self, setting: &str) -> Option<&str> { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 568e9e1044ea281f1305864a18329466735f6102..71841a130410265ac45d03a284e2334746b5c0ca 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -796,7 +796,7 @@ fn font_fallbacks_from_settings( } impl settings::Settings for ThemeSettings { - fn from_file(content: &settings::SettingsContent, cx: &mut App) -> Option { + fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { let content = &content.theme; let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 01939211c4f3c40dbeb17bbd0b9dd62a2247e723..9d0d8ff511a9315b2fc68bfc1fe3e346e836e6d4 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -24,7 +24,7 @@ pub struct TitleBarSettings { } impl Settings for TitleBarSettings { - fn from_file(s: &SettingsContent) -> Option { + fn from_default(s: &SettingsContent) -> Option { let content = s.title_bar?; TitleBarSettings { show: content.show?, From 23d594cea7402638ed2bba4eae905eaf08ad695d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 16:58:56 -0600 Subject: [PATCH 012/117] vim mode setting Co-authored-by: Ben Kunkle --- Cargo.lock | 2 - crates/settings/src/settings.rs | 3 +- crates/settings/src/settings_content.rs | 10 ++ crates/settings/src/settings_store.rs | 130 +----------------- crates/vim_mode_setting/Cargo.toml | 2 - .../vim_mode_setting/src/vim_mode_setting.rs | 99 +++---------- 6 files changed, 31 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 933663474f4d8f1909c0747b6f4c94fde95f57fc..f37ab3db52b125dd31dd742b64a2529a094ab4d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18089,8 +18089,6 @@ version = "0.1.0" dependencies = [ "anyhow", "gpui", - "schemars", - "serde", "settings", "workspace-hack", ] diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 41302a430441107dd7c14f9c9c121be84a0012ba..bb242aaac28e2910d72982eeb7e92c0ea8f6c896 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -24,8 +24,7 @@ pub use keymap_file::{ pub use settings_file::*; pub use settings_json::*; pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, - SettingsSources, SettingsStore, + InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, SettingsStore, }; pub use settings_ui_core::*; // Re-export the derive macro diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index d3d818d2834b8c9977dc66a081d394851f57534f..b5022c5c4172b3fa5bc52a92a044eccd9a242cae 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -21,11 +21,21 @@ pub struct SettingsContent { #[serde(flatten)] pub theme: ThemeSettingsContent, + // todo!() comments?! pub base_keymap: Option, pub auto_update: Option, pub title_bar: Option, + + /// Whether or not to enable Vim mode. + /// + /// Default: false + pub vim_mode: Option, + /// Whether or not to enable Helix mode. + /// + /// Default: false + pub helix_mode: Option, } impl SettingsContent { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 6e4f0aff1fa723b086154b69f18688514ff2f168..fd1ab64001aa14366c1284a5f0b523df4d1ceb76 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -11,7 +11,6 @@ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGl use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; -use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use smallvec::SmallVec; use std::{ @@ -22,7 +21,7 @@ use std::{ str::{self, FromStr}, sync::Arc, }; -use util::{ResultExt as _, merge_non_null_json_value_into, schemars::DefaultDenyUnknownFields}; +use util::{ResultExt as _, schemars::DefaultDenyUnknownFields}; pub type EditorconfigProperties = ec4rs::Properties; @@ -125,69 +124,6 @@ pub trait Settings: 'static + Send + Sync + Sized { } } -#[derive(Clone, Copy, Debug)] -pub struct SettingsSources<'a, T> { - /// The default Zed settings. - pub default: &'a T, - /// Global settings (loaded before user settings). - pub global: Option<&'a T>, - /// Settings provided by extensions. - pub extensions: Option<&'a T>, - /// The user settings. - pub user: Option<&'a T>, - /// The user settings for the current release channel. - pub release_channel: Option<&'a T>, - /// The user settings for the current operating system. - pub operating_system: Option<&'a T>, - /// The settings associated with an enabled settings profile - pub profile: Option<&'a T>, - /// The server's settings. - pub server: Option<&'a T>, - /// The project settings, ordered from least specific to most specific. - pub project: &'a [&'a T], -} - -impl<'a, T: Serialize> SettingsSources<'a, T> { - /// Returns an iterator over the default settings as well as all settings customizations. - pub fn defaults_and_customizations(&self) -> impl Iterator { - [self.default].into_iter().chain(self.customizations()) - } - - /// Returns an iterator over all of the settings customizations. - pub fn customizations(&self) -> impl Iterator { - self.global - .into_iter() - .chain(self.extensions) - .chain(self.user) - .chain(self.release_channel) - .chain(self.operating_system) - .chain(self.profile) - .chain(self.server) - .chain(self.project.iter().copied()) - } - - /// Returns the settings after performing a JSON merge of the provided customizations. - /// - /// Customizations later in the iterator win out over the earlier ones. - pub fn json_merge_with( - customizations: impl Iterator, - ) -> Result { - let mut merged = Value::Null; - for value in customizations { - merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); - } - Ok(serde_json::from_value(merged)?) - } - - /// Returns the settings after performing a JSON merge of the customizations into the - /// default settings. - /// - /// More-specific customizations win out over the less-specific ones. - pub fn json_merge(&'a self) -> Result { - Self::json_merge_with(self.defaults_and_customizations()) - } -} - #[derive(Clone, Copy, Debug)] pub struct SettingsLocation<'a> { pub worktree_id: WorktreeId, @@ -262,7 +198,6 @@ trait AnySettingValue: 'static + Send + Sync { vscode_settings: &VsCodeSettings, settings_content: &mut SettingsContent, ); - fn settings_ui_item(&self) -> SettingsUiEntry; } impl SettingsStore { @@ -274,7 +209,7 @@ impl SettingsStore { default_settings, global_settings: None, server_settings: None, - user_settings: Some(Default::default()), // todo!() + user_settings: None, extension_settings: None, local_settings: BTreeMap::default(), raw_editorconfig_settings: BTreeMap::default(), @@ -353,9 +288,6 @@ impl SettingsStore { } setting_value.set_global_value(Box::new(value)); - - // todo!() local settings - // (they weren't handled before...) } /// Get the value of a setting. @@ -585,9 +517,7 @@ impl SettingsStore { } pub fn settings_ui_items(&self) -> impl IntoIterator { - self.setting_values - .values() - .map(|item| item.settings_ui_item()) + [].into_iter() } } @@ -628,7 +558,6 @@ impl SettingsStore { let old_value = serde_json::to_value(&old_content).unwrap(); let new_value = serde_json::to_value(new_content).unwrap(); - // dbg!(&old_value, &new_value); let mut key_path = Vec::new(); let mut edits = Vec::new(); @@ -1109,11 +1038,6 @@ impl AnySettingValue for SettingValue { ) { T::import_from_vscode(vscode_settings, settings_content); } - - fn settings_ui_item(&self) -> SettingsUiEntry { - todo!() - // <::FileContent as SettingsUi>::settings_ui_entry() - } } #[cfg(test)] @@ -1694,52 +1618,4 @@ mod tests { } ); } - - // #[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(), - // ); - // } - // } } diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml index 61d265b958b10fac700bd78577ac5fefb19b7d09..fbb7f30b4c2a03aca48ad5db26283c33aedb885b 100644 --- a/crates/vim_mode_setting/Cargo.toml +++ b/crates/vim_mode_setting/Cargo.toml @@ -14,7 +14,5 @@ path = "src/vim_mode_setting.rs" [dependencies] anyhow.workspace = true gpui.workspace = true -schemars.workspace = true -serde.workspace = true settings.workspace = true workspace-hack.workspace = true diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index 7d0f8aa7fe3766f18ef163ce882e55ebb73aa03a..8fa12bbd644f48929e6be8e2ba6fa9ad087b8356 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -4,10 +4,8 @@ //! disable Vim/Helix modes without having to depend on the `vim` crate in its //! entirety. -use anyhow::Result; use gpui::App; -use schemars::JsonSchema; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsContent}; /// Initializes the `vim_mode_setting` crate. pub fn init(cx: &mut App) { @@ -17,97 +15,34 @@ pub fn init(cx: &mut App) { pub struct VimModeSetting(pub bool); -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Debug, - Default, - serde::Serialize, - serde::Deserialize, - SettingsUi, - SettingsKey, - JsonSchema, -)] -#[settings_key(None)] -pub struct VimModeSettingContent { - /// Whether or not to enable Vim mode. - /// - /// Default: false - pub vim_mode: Option, -} - impl Settings for VimModeSetting { - type FileContent = VimModeSettingContent; + fn from_default(content: &SettingsContent, _cx: &mut App) -> Option { + Some(Self(content.vim_mode?)) + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self( - [ - sources.profile, - sources.release_channel, - sources.user, - sources.server, - Some(sources.default), - ] - .into_iter() - .flatten() - .filter_map(|mode| mode.vim_mode) - .next() - .ok_or_else(Self::missing_default)?, - )) + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { + if let Some(vim_mode) = content.vim_mode { + self.0 = vim_mode; + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _content: &mut SettingsContent) { // TODO: could possibly check if any of the `vim.` keys are set? } } -#[derive(Debug)] pub struct HelixModeSetting(pub bool); -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Debug, - Default, - serde::Serialize, - serde::Deserialize, - SettingsUi, - SettingsKey, - JsonSchema, -)] -#[settings_key(None)] -pub struct HelixModeSettingContent { - /// Whether or not to enable Helix mode. - /// - /// Default: false - pub helix_mode: Option, -} - impl Settings for HelixModeSetting { - type FileContent = HelixModeSettingContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self( - [ - sources.profile, - sources.release_channel, - sources.user, - sources.server, - Some(sources.default), - ] - .into_iter() - .flatten() - .filter_map(|mode| mode.helix_mode) - .next() - .ok_or_else(Self::missing_default)?, - )) + fn from_default(content: &SettingsContent, _cx: &mut App) -> Option { + Some(Self(content.helix_mode?)) } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { - // TODO: could possibly check if any of the `helix.` keys are set? + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { + if let Some(helix_mode) = content.helix_mode { + self.0 = helix_mode; + } } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {} } From f339c75ed376c2684460f93fad802cde69d52eef Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 17:09:53 -0600 Subject: [PATCH 013/117] audio settings Co-authored-by: Ben Kunkle --- assets/settings/default.json | 15 ++++++ crates/audio/src/audio_settings.rs | 61 +++++++++++-------------- crates/settings/src/settings_content.rs | 24 ++++++++++ 3 files changed, 65 insertions(+), 35 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5dcb418184f592e941d23763f7fe39898413cb79..fc53e2c3e8cd07db8eb133c90809fd23f35be307 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -408,6 +408,21 @@ // Whether to show the menus in the titlebar. "show_menus": false }, + "audio": { + /// Opt into the new audio system. + "experimental.rodio_audio": false, + /// Requires 'rodio_audio: true' + /// + /// Use the new audio systems automatic gain control for your microphone. + /// This affects how loud you sound to others. + "experimental.control_input_volume": false, + /// Requires 'rodio_audio: true' + /// + /// Use the new audio systems automatic gain control on everyone in the + /// call. This makes call members who are too quite louder and those who are + /// too loud quieter. This only affects how things sound for you. + "experimental.control_output_volume": false + }, // Scrollbar related settings "scrollbar": { // When to show the scrollbar in the editor. diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index ea0ea5f3558e015f5579cca43eeb8c529273cb52..0c66cf08c4c0959c65df6b3554de8012137d897a 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -1,62 +1,53 @@ use std::sync::atomic::{AtomicBool, Ordering}; -use anyhow::Result; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsStore}; +use util::MergeFrom as _; -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] +#[derive(Clone, Default, Debug)] pub struct AudioSettings { /// Opt into the new audio system. - #[serde(rename = "experimental.rodio_audio", default)] pub rodio_audio: bool, // default is false /// Requires 'rodio_audio: true' /// /// Use the new audio systems automatic gain control for your microphone. /// This affects how loud you sound to others. - #[serde(rename = "experimental.control_input_volume", default)] pub control_input_volume: bool, /// Requires 'rodio_audio: true' /// /// Use the new audio systems automatic gain control on everyone in the /// call. This makes call members who are too quite louder and those who are /// too loud quieter. This only affects how things sound for you. - #[serde(rename = "experimental.control_output_volume", default)] - pub control_output_volume: bool, -} - -/// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[serde(default)] -#[settings_key(key = "audio")] -pub struct AudioSettingsContent { - /// Opt into the new audio system. - #[serde(rename = "experimental.rodio_audio", default)] - pub rodio_audio: bool, // default is false - /// Requires 'rodio_audio: true' - /// - /// Use the new audio systems automatic gain control for your microphone. - /// This affects how loud you sound to others. - #[serde(rename = "experimental.control_input_volume", default)] - pub control_input_volume: bool, - /// Requires 'rodio_audio: true' - /// - /// Use the new audio systems automatic gain control on everyone in the - /// call. This makes call members who are too quite louder and those who are - /// too loud quieter. This only affects how things sound for you. - #[serde(rename = "experimental.control_output_volume", default)] pub control_output_volume: bool, } +/// Configuration of audio in Zed impl Settings for AudioSettings { - type FileContent = AudioSettingsContent; + fn from_default(content: &settings::SettingsContent, _cx: &mut App) -> Option { + let audio = &content.audio.as_ref()?; + Some(AudioSettings { + control_input_volume: audio.control_input_volume?, + control_output_volume: audio.control_output_volume?, + rodio_audio: audio.rodio_audio?, + }) + } - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(audio) = content.audio.as_ref() else { + return; + }; + self.control_input_volume + .merge_from(&audio.control_input_volume); + self.control_output_volume + .merge_from(&audio.control_output_volume); + self.rodio_audio.merge_from(&audio.rodio_audio); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { + } } /// See docs on [LIVE_SETTINGS] diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index b5022c5c4172b3fa5bc52a92a044eccd9a242cae..e93cd2fb05ea2c6331bda90e363d90b5265674ef 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -36,6 +36,9 @@ pub struct SettingsContent { /// /// Default: false pub helix_mode: Option, + + /// Configuration of audio in Zed. + pub audio: Option, } impl SettingsContent { @@ -165,3 +168,24 @@ pub enum TitleBarVisibilityContent { Never, HideInFullScreen, } + +/// Configuration of audio in Zed. +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AudioSettingsContent { + /// Opt into the new audio system. + #[serde(rename = "experimental.rodio_audio", default)] + pub rodio_audio: Option, + /// Requires 'rodio_audio: true' + /// + /// Use the new audio systems automatic gain control for your microphone. + /// This affects how loud you sound to others. + #[serde(rename = "experimental.control_input_volume", default)] + pub control_input_volume: Option, + /// Requires 'rodio_audio: true' + /// + /// Use the new audio systems automatic gain control on everyone in the + /// call. This makes call members who are too quite louder and those who are + /// too loud quieter. This only affects how things sound for you. + #[serde(rename = "experimental.control_output_volume", default)] + pub control_output_volume: Option, +} From fdaaa558c9315bf5b3bdc203d45c655a1de4fe8d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 17:16:16 -0600 Subject: [PATCH 014/117] zlog settings Co-authored-by: Ben Kunkle --- Cargo.lock | 4 +- crates/settings/src/settings_content.rs | 6 +++ crates/zlog/Cargo.toml | 1 + crates/zlog/src/filter.rs | 11 +++--- crates/zlog_settings/Cargo.toml | 3 +- crates/zlog_settings/src/zlog_settings.rs | 45 ++++++++++------------- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f37ab3db52b125dd31dd742b64a2529a094ab4d5..6e2845aabe589d688343a6216c2b13f7a92d3308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21001,6 +21001,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "collections", "log", "tempfile", "workspace-hack", @@ -21011,9 +21012,8 @@ name = "zlog_settings" version = "0.1.0" dependencies = [ "anyhow", + "collections", "gpui", - "schemars", - "serde", "settings", "workspace-hack", "zlog", diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index e93cd2fb05ea2c6331bda90e363d90b5265674ef..a08b6b77b79a70190cb45a9d89fe3f5c75679d76 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -39,6 +39,12 @@ pub struct SettingsContent { /// Configuration of audio in Zed. pub audio: Option, + + /// A map of log scopes to the desired log level. + /// Useful for filtering out noisy logs or enabling more verbose logging. + /// + /// Example: {"log": {"client": "warn"}} + pub log: Option>, } impl SettingsContent { diff --git a/crates/zlog/Cargo.toml b/crates/zlog/Cargo.toml index d0632d14f2d5b9ca6f26d7f9cbcbb8a341104860..4b758437d5e1608aaa68e86f90215f57b928e883 100644 --- a/crates/zlog/Cargo.toml +++ b/crates/zlog/Cargo.toml @@ -15,6 +15,7 @@ path = "src/zlog.rs" default = [] [dependencies] +collections.workspace = true chrono.workspace = true log.workspace = true workspace-hack.workspace = true diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs index 31a58894774e6c0d08ea22b585350eb26ff09907..9a2de13cb3d33a1a6f4d17f7eddd4754cae40ea3 100644 --- a/crates/zlog/src/filter.rs +++ b/crates/zlog/src/filter.rs @@ -1,9 +1,8 @@ -use std::{ - collections::{HashMap, VecDeque}, - sync::{ - OnceLock, RwLock, - atomic::{AtomicU8, Ordering}, - }, +use collections::HashMap; +use std::collections::VecDeque; +use std::sync::{ + OnceLock, RwLock, + atomic::{AtomicU8, Ordering}, }; use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, Scope, ScopeAlloc, env_config, private}; diff --git a/crates/zlog_settings/Cargo.toml b/crates/zlog_settings/Cargo.toml index 2555d18aadc162161eb3aedf5c948385f347c6cc..00c81e0ef45863d36014d63ac5e07d690c507207 100644 --- a/crates/zlog_settings/Cargo.toml +++ b/crates/zlog_settings/Cargo.toml @@ -17,8 +17,7 @@ default = [] [dependencies] anyhow.workspace = true gpui.workspace = true -schemars.workspace = true -serde.workspace = true +collections.workspace = true settings.workspace = true zlog.workspace = true workspace-hack.workspace = true diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index dd74fc574ff23dc78beca1feafeb34d874a68c22..fc350e035b8f42ad6ab6515960e6506637de2b31 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -1,9 +1,8 @@ //! # zlog_settings -use anyhow::Result; +use collections::HashMap; + use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsStore}; pub fn init(cx: &mut App) { ZlogSettings::register(cx); @@ -15,33 +14,27 @@ pub fn init(cx: &mut App) { .detach(); } -#[derive( - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - SettingsUi, - SettingsKey, -)] -#[settings_key(key = "log")] +#[derive(Clone, Debug)] pub struct ZlogSettings { - #[serde(default, flatten)] - pub scopes: std::collections::HashMap, + /// A map of log scopes to the desired log level. + /// Useful for filtering out noisy logs or enabling more verbose logging. + /// + /// Example: {"log": {"client": "warn"}} + pub scopes: HashMap, } impl Settings for ZlogSettings { - type FileContent = Self; + fn from_default(content: &settings::SettingsContent, _: &mut App) -> Option { + Some(ZlogSettings { + scopes: content.log.clone()?, + }) + } - fn load(sources: settings::SettingsSources, _: &mut App) -> Result - where - Self: Sized, - { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + if let Some(log) = &content.log { + self.scopes.extend(log.clone()); + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {} } From e650020f2676b296baac37161de7e3dbd079eb8a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 17:21:05 -0600 Subject: [PATCH 015/117] Git Hosting Provider Co-authored-by: Ben Kunkle --- crates/git_hosting_providers/src/settings.rs | 43 ++++++------------ crates/settings/src/settings_content.rs | 47 +++++++++++++++----- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 3249981db91015479bab728484341519db357683..e2c33c8f2802dd3ad28ad6d34c9c214c63f02633 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -1,11 +1,13 @@ use std::sync::Arc; -use anyhow::Result; use git::GitHostingProviderRegistry; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsStore, SettingsUi}; +use settings::{ + GitHostingProviderConfig, GitHostingProviderKind, Settings, SettingsKey, SettingsStore, + SettingsUi, +}; use url::Url; use util::ResultExt as _; @@ -55,29 +57,6 @@ fn update_git_hosting_providers_from_settings(cx: &mut App) { provider_registry.set_setting_providers(iter); } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitHostingProviderKind { - Github, - Gitlab, - Bitbucket, -} - -/// A custom Git hosting provider. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GitHostingProviderConfig { - /// The type of the provider. - /// - /// Must be one of `github`, `gitlab`, or `bitbucket`. - pub provider: GitHostingProviderKind, - - /// The base URL for the provider (e.g., "https://code.corp.big.com"). - pub base_url: String, - - /// The display name for the provider (e.g., "BigCorp GitHub"). - pub name: String, -} - #[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] #[settings_key(None)] pub struct GitHostingProviderSettings { @@ -87,11 +66,17 @@ pub struct GitHostingProviderSettings { } impl Settings for GitHostingProviderSettings { - type FileContent = Self; + fn from_default(content: &settings::SettingsContent, _cx: &mut App) -> Option { + Some(Self { + git_hosting_providers: content.git_hosting_providers.clone()?, + }) + } - fn load(sources: settings::SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + if let Some(more) = &content.git_hosting_providers { + self.git_hosting_providers.extend_from_slice(&more.clone()); + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {} } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index a08b6b77b79a70190cb45a9d89fe3f5c75679d76..de9c5a0d4247aed6ff80cc0429258edb98f68afe 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -21,30 +21,32 @@ pub struct SettingsContent { #[serde(flatten)] pub theme: ThemeSettingsContent, + /// Configuration of audio in Zed. + pub audio: Option, + pub auto_update: Option, + // todo!() comments?! pub base_keymap: Option, - pub auto_update: Option, - - pub title_bar: Option, + /// The list of custom Git hosting providers. + pub git_hosting_providers: Option>, - /// Whether or not to enable Vim mode. - /// - /// Default: false - pub vim_mode: Option, /// Whether or not to enable Helix mode. /// /// Default: false pub helix_mode: Option, - - /// Configuration of audio in Zed. - pub audio: Option, - /// A map of log scopes to the desired log level. /// Useful for filtering out noisy logs or enabling more verbose logging. /// /// Example: {"log": {"client": "warn"}} pub log: Option>, + + pub title_bar: Option, + + /// Whether or not to enable Vim mode. + /// + /// Default: false + pub vim_mode: Option, } impl SettingsContent { @@ -195,3 +197,26 @@ pub struct AudioSettingsContent { #[serde(rename = "experimental.control_output_volume", default)] pub control_output_volume: Option, } + +/// A custom Git hosting provider. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GitHostingProviderConfig { + /// The type of the provider. + /// + /// Must be one of `github`, `gitlab`, or `bitbucket`. + pub provider: GitHostingProviderKind, + + /// The base URL for the provider (e.g., "https://code.corp.big.com"). + pub base_url: String, + + /// The display name for the provider (e.g., "BigCorp GitHub"). + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitHostingProviderKind { + Github, + Gitlab, + Bitbucket, +} From 231870e1cc4764724710a4c51d7988c383a0d466 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 17:32:10 -0600 Subject: [PATCH 016/117] =?UTF-8?q?Start=20on=20language=20settings=20?= =?UTF-8?q?=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/language/src/buffer_tests.rs | 6 +- crates/language/src/language_settings.rs | 645 ++---------------- crates/settings/src/settings_content.rs | 2 +- .../settings/src/settings_content/language.rs | 5 +- 4 files changed, 57 insertions(+), 601 deletions(-) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index fcd93390c891f1d65b2f424a5bc70cd7f23c7912..d756a2f60cd8652b44a4e9bac6df1d3eb8218686 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1,8 +1,6 @@ use super::*; use crate::Buffer; -use crate::language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, -}; +use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; @@ -3849,7 +3847,7 @@ fn init_settings(cx: &mut App, f: fn(&mut AllLanguageSettingsContent)) { cx.set_global(settings_store); crate::init(cx); cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, f); + settings.update_user_settings(cx, |content| f(&mut content.project.all_languages)); }); } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index af9e6edbfa4ed2ef44d7a5789069a83b7db829c7..8dffeff13d0a313891f2c619f3ba7b0375c32760 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -17,8 +17,9 @@ use serde::{ }; use settings::{ - ParameterizedJsonSchema, Settings, SettingsKey, SettingsLocation, SettingsSources, - SettingsStore, SettingsUi, + FormatOnSave, IndentGuideSettingsContent, LanguageSettingsContent, ParameterizedJsonSchema, + RewrapBehavior, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore, + SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; @@ -86,7 +87,8 @@ pub struct LanguageSettings { /// Character counts at which to show wrap guides (vertical rulers) in the editor. pub wrap_guides: Vec, /// Indent guide related settings. - pub indent_guides: IndentGuideSettings, + /// todo!() shouldthis be not the content type? + pub indent_guides: IndentGuideSettingsContent, /// Whether or not to perform a buffer format before saving. pub format_on_save: FormatOnSave, /// Whether or not to remove any trailing whitespace from lines of a buffer @@ -96,9 +98,9 @@ pub struct LanguageSettings { /// when saving it. pub ensure_final_newline_on_save: bool, /// How to perform a buffer format. - pub formatter: SelectedFormatter, + pub formatter: settings::SelectedFormatter, /// Zed's Prettier integration settings. - pub prettier: PrettierSettings, + pub prettier: settings::PrettierSettingsContent, /// Whether to automatically close JSX tags. pub jsx_tag_auto_close: JsxTagAutoCloseSettings, /// Whether to use language servers to provide code intelligence. @@ -128,7 +130,7 @@ pub struct LanguageSettings { /// Whether to start a new line with a comment when a previous line is a comment as well. pub extend_comment_on_newline: bool, /// Inlay hint related settings. - pub inlay_hints: InlayHintSettings, + pub inlay_hints: settings::InlayHintSettings, /// Whether to automatically close brackets. pub use_autoclose: bool, /// Whether to automatically surround text with brackets. @@ -301,75 +303,13 @@ pub struct CopilotSettings { pub enterprise_uri: Option, } -/// The settings for all languages. -#[derive( - Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey, -)] -#[settings_key(None)] -#[settings_ui(group = "Default Language Settings")] -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)] - #[settings_ui(skip)] - pub file_types: HashMap, Vec>, -} - -/// 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); - -impl SettingsUi for LanguageToSettingsMap { - fn settings_ui_item() -> settings::SettingsUiItem { - settings::SettingsUiItem::DynamicMap(settings::SettingsUiItemDynamicMap { - item: LanguageSettingsContent::settings_ui_item, - defaults_path: &[], - determine_items: |settings_value, cx| { - use settings::SettingsUiEntryMetaData; - - // todo(settings_ui): We should be using a global LanguageRegistry, but it's not implemented yet - _ = cx; - - let Some(settings_language_map) = settings_value.as_object() else { - return Vec::new(); - }; - let mut languages = Vec::with_capacity(settings_language_map.len()); - - for language_name in settings_language_map.keys().map(gpui::SharedString::from) { - languages.push(SettingsUiEntryMetaData { - title: language_name.clone(), - path: language_name, - // todo(settings_ui): Implement documentation for each language - // ideally based on the language's official docs from extension or builtin info - documentation: None, - }); - } - return languages; - }, - }) - } -} - 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!({ + replace_subschema::(generator, || json_schema!({ "type": "object", "properties": params .language_names @@ -461,386 +401,6 @@ fn default_3() -> usize { 3 } -/// The settings for a particular language. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)] -#[settings_ui(group = "Default")] -pub struct LanguageSettingsContent { - /// How many columns a tab should occupy. - /// - /// Default: 4 - #[serde(default)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - 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). - #[settings_ui(skip)] - 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: [] - #[settings_ui(skip)] - pub debuggers: Option>, -} - -/// The behavior of `editor::Rewrap`. -#[derive( - Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, SettingsUi, -)] -#[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, -} - -/// The contents of the edit prediction settings. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)] -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)] - #[settings_ui(skip)] - pub disabled_globs: Option>, - /// The mode used to display edit predictions in the buffer. - /// Provider support required. - #[serde(default)] - pub mode: EditPredictionsMode, - /// 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, SettingsUi)] -pub struct CopilotSettingsContent { - /// HTTP/HTTPS proxy to use for Copilot. - /// - /// Default: none - #[serde(default)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - pub enterprise_uri: Option, -} - -/// The settings for enabling/disabling features. -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] -#[serde(rename_all = "snake_case")] -#[settings_ui(group = "Features")] -pub struct FeaturesContent { - /// Determines which edit prediction provider to use. - pub edit_prediction_provider: Option, -} - -/// Controls the soft-wrapping behavior in the editor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - /// 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, -} - -/// Controls the behavior of formatting files when they are saved. -#[derive(Debug, Clone, PartialEq, Eq, SettingsUi)] -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 how whitespace should be displayedin the editor. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] @@ -884,150 +444,6 @@ impl WhitespaceMap { } } -/// Controls which formatter should be used when formatting code. -#[derive(Clone, Debug, Default, PartialEq, Eq, SettingsUi)] -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, SettingsUi)] -#[serde(untagged)] -pub enum FormatterList { - Single(Formatter), - Vec(#[settings_ui(skip)] Vec), -} - -impl Default for FormatterList { - fn default() -> Self { - Self::Single(Formatter::default()) - } -} - -impl AsRef<[Formatter]> for FormatterList { - fn as_ref(&self) -> &[Formatter] { - match &self { - Self::Single(single) => slice::from_ref(single), - Self::Vec(v) => v, - } - } -} - -/// 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, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum Formatter { - /// Format code using the current language server. - LanguageServer { - #[settings_ui(skip)] - name: Option, - }, - /// Format code using Zed's Prettier integration. - #[default] - Prettier, - /// Format code using an external command. - External { - /// The external program to run. - #[settings_ui(skip)] - command: Arc, - /// The arguments to pass to the program. - #[settings_ui(skip)] - arguments: Option>, - }, - /// Files should be formatted using code actions executed by language servers. - CodeActions(#[settings_ui(skip)] HashMap), -} - /// The settings for indent guides. #[derive( Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi, @@ -1314,7 +730,48 @@ impl InlayHintKind { } impl settings::Settings for AllLanguageSettings { - type FileContent = AllLanguageSettingsContent; + fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { + let defaults = content.project.all_languages.defaults; + let default_language_settings = LanguageSettings { + tab_size: defaults.tab_size?, + hard_tabs: defaults.hard_tabs?, + soft_wrap: defaults.soft_wrap?, + preferred_line_length: defaults.preferred_line_length?, + show_wrap_guides: defaults.show_wrap_guides?, + wrap_guides: defaults.wrap_guides?, + indent_guides: defaults.indent_guides?, + format_on_save: defaults.format_on_save?, + remove_trailing_whitespace_on_save: defaults.remove_trailing_whitespace_on_save?, + ensure_final_newline_on_save: defaults.ensure_final_newline_on_save?, + formatter: defaults.formatter?, + prettier: defaults.prettier?, + jsx_tag_auto_close: defaults.jsx_tag_auto_close?, + enable_language_server: defaults.enable_language_server?, + language_servers: defaults.language_servers?, + allow_rewrap: defaults.allow_rewrap?, + show_edit_predictions: defaults.show_edit_predictions?, + edit_predictions_disabled_in: defaults.edit_predictions_disabled_in?, + show_whitespaces: defaults.show_whitespaces?, + whitespace_map: defaults.whitespace_map?, + extend_comment_on_newline: defaults.extend_comment_on_newline?, + inlay_hints: defaults.inlay_hints?, + use_autoclose: defaults.use_autoclose?, + use_auto_surround: defaults.use_auto_surround?, + use_on_type_format: defaults.use_on_type_format?, + auto_indent: defaults.auto_indent?, + auto_indent_on_paste: defaults.auto_indent_on_paste?, + always_treat_brackets_as_autoclosed: defaults.always_treat_brackets_as_autoclosed?, + code_actions_on_format: defaults.code_actions_on_format?, + linked_edits: defaults.linked_edits?, + tasks: defaults.tasks?, + show_completions_on_input: defaults.show_completions_on_input?, + show_completion_documentation: defaults.show_completion_documentation?, + completions: defaults.completions?, + debuggers: defaults.debuggers?, + }; + + todo!(); + } fn load(sources: SettingsSources, _: &mut App) -> Result { let default_value = sources.default; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index de9c5a0d4247aed6ff80cc0429258edb98f68afe..ba0649396b21e0f2c719142bc033bc86006bdce7 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -130,7 +130,7 @@ pub enum BaseKeymapContent { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettingsContent { #[serde(flatten)] - pub(crate) all_languages: AllLanguageSettingsContent, + pub all_languages: AllLanguageSettingsContent, } #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index c771d6fd705b3e2445eb30a8a4059482cd9bb445..242228689876ea77d0c1f2fd0b309dd4aadb1048 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -195,7 +195,7 @@ pub struct LanguageSettingsContent { /// /// Default: off #[serde(default)] - pub prettier: Option, + pub prettier: Option, /// Whether to automatically close JSX tags. #[serde(default)] pub jsx_tag_auto_close: Option, @@ -524,7 +524,7 @@ fn default_3() -> usize { /// 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 { +pub struct PrettierSettingsContent { /// Enables or disables formatting with Prettier for a given language. #[serde(default)] pub allowed: bool, @@ -543,6 +543,7 @@ pub struct PrettierSettings { #[serde(flatten)] pub options: HashMap, } + /// Controls the behavior of formatting files when they are saved. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FormatOnSave { From 93c129beb8bcdaa5e94bf00206d36b83b0bbbc2f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 22:41:34 -0600 Subject: [PATCH 017/117] Fix language too --- crates/language/src/buffer_tests.rs | 2 +- crates/language/src/language_registry.rs | 9 +- crates/language/src/language_settings.rs | 859 +++++------------- .../settings/src/settings_content/language.rs | 112 ++- crates/settings/src/settings_store.rs | 16 +- crates/theme/src/settings.rs | 3 +- 6 files changed, 357 insertions(+), 644 deletions(-) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index d756a2f60cd8652b44a4e9bac6df1d3eb8218686..e079029b478af1c70ff626acea0791cdb204421f 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1,6 +1,5 @@ use super::*; use crate::Buffer; -use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; @@ -11,6 +10,7 @@ use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; use settings::SettingsStore; +use settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use std::collections::BTreeSet; use std::{ env, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 5d9d5529c145a8769d142a1f943b6ae00aeaaeb8..9ec57b90b39f40b9530bef7ff02753ea8f37d2e5 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,14 +1,11 @@ use crate::{ CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister, - language_settings::{ - AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings, - }, - task_context::ContextProvider, - with_parser, + language_settings::all_language_settings, task_context::ContextProvider, with_parser, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, HashMap, HashSet, hash_map}; +use settings::{AllLanguageSettingsContent, LanguageSettingsContent}; use futures::{ Future, @@ -1175,7 +1172,7 @@ impl LanguageRegistryState { language.set_theme(theme.syntax()); } self.language_settings.languages.0.insert( - language.name(), + language.name().0, LanguageSettingsContent { tab_size: language.config.tab_size, hard_tabs: language.config.hard_tabs, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 8dffeff13d0a313891f2c619f3ba7b0375c32760..5fb4291bdc045e4fee0cbd68103329cde51932bc 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,30 +1,29 @@ //! Provides `language`-related settings. use crate::{File, Language, LanguageName, LanguageServerName}; -use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; -use gpui::{App, Modifiers, SharedString}; +use gpui::App; use itertools::{Either, Itertools}; use schemars::{JsonSchema, json_schema}; -use serde::{ - Deserialize, Deserializer, Serialize, - de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}, -}; +use serde::{Deserialize, Serialize}; +pub use settings::{ + CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, + IndentGuideSettings, LanguageSettingsContent, LspInsertMode, RewrapBehavior, + ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, +}; use settings::{ - FormatOnSave, IndentGuideSettingsContent, LanguageSettingsContent, ParameterizedJsonSchema, - RewrapBehavior, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore, - SettingsUi, + ParameterizedJsonSchema, Settings, SettingsContent, SettingsLocation, SettingsStore, SettingsUi, }; use shellexpand; -use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; -use util::schemars::replace_subschema; -use util::serde::default_true; +use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; +use util::MergeFrom; +use util::{ResultExt, schemars::replace_subschema}; /// Initializes the language settings. pub fn init(cx: &mut App) { @@ -64,10 +63,11 @@ pub struct AllLanguageSettings { pub defaults: LanguageSettings, languages: HashMap, pub(crate) file_types: FxHashMap, GlobSet>, + pub(crate) file_globs: FxHashMap, Vec>, } /// The settings for a particular language. -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct LanguageSettings { /// How many columns a tab should occupy. pub tab_size: NonZeroU32, @@ -75,7 +75,7 @@ pub struct LanguageSettings { /// spaces. pub hard_tabs: bool, /// How to soft-wrap long lines of text. - pub soft_wrap: SoftWrap, + pub soft_wrap: settings::SoftWrap, /// The column at which to soft-wrap lines, for buffers where soft-wrap /// is enabled. pub preferred_line_length: u32, @@ -88,7 +88,7 @@ pub struct LanguageSettings { pub wrap_guides: Vec, /// Indent guide related settings. /// todo!() shouldthis be not the content type? - pub indent_guides: IndentGuideSettingsContent, + pub indent_guides: IndentGuideSettings, /// Whether or not to perform a buffer format before saving. pub format_on_save: FormatOnSave, /// Whether or not to remove any trailing whitespace from lines of a buffer @@ -102,7 +102,7 @@ pub struct LanguageSettings { /// Zed's Prettier integration settings. pub prettier: settings::PrettierSettingsContent, /// Whether to automatically close JSX tags. - pub jsx_tag_auto_close: JsxTagAutoCloseSettings, + pub jsx_tag_auto_close: settings::JsxTagAutoCloseSettings, /// Whether to use language servers to provide code intelligence. pub enable_language_server: bool, /// The list of language servers to use (or disable) for this language. @@ -124,9 +124,9 @@ pub struct LanguageSettings { /// scopes. pub edit_predictions_disabled_in: Vec, /// Whether to show tabs and spaces in the editor. - pub show_whitespaces: ShowWhitespaceSetting, + pub show_whitespaces: settings::ShowWhitespaceSetting, /// Visible characters used to render whitespace when show_whitespaces is enabled. - pub whitespace_map: WhitespaceMap, + pub whitespace_map: settings::WhitespaceMap, /// Whether to start a new line with a comment when a previous line is a comment as well. pub extend_comment_on_newline: bool, /// Inlay hint related settings. @@ -149,7 +149,7 @@ pub struct LanguageSettings { /// Whether to perform linked edits pub linked_edits: bool, /// Task configuration for this language. - pub tasks: LanguageTaskConfig, + pub tasks: settings::LanguageTaskConfig, /// Whether to pop the completions menu while typing in an editor without /// explicitly requesting it. pub show_completions_on_input: bool, @@ -157,7 +157,7 @@ pub struct LanguageSettings { /// completions menu. pub show_completion_documentation: bool, /// Completion settings for this language. - pub completions: CompletionSettings, + pub completions: settings::CompletionSettings, /// Preferred debuggers for this language. pub debuggers: Vec, } @@ -211,43 +211,19 @@ impl LanguageSettings { } } -/// The provider that supplies edit predictions. -#[derive( - Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionProvider { - None, - #[default] - Copilot, - Supermaven, - Zed, -} - -impl EditPredictionProvider { - pub fn is_zed(&self) -> bool { - match self { - EditPredictionProvider::Zed => true, - EditPredictionProvider::None - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven => false, - } - } -} - /// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot) /// or [Supermaven](https://supermaven.com). #[derive(Clone, Debug, Default, SettingsUi)] pub struct EditPredictionSettings { /// The provider that supplies edit predictions. - pub provider: EditPredictionProvider, + pub provider: settings::EditPredictionProvider, /// 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. #[settings_ui(skip)] pub disabled_globs: Vec, /// Configures how edit predictions are displayed in the buffer. - pub mode: EditPredictionsMode, + pub mode: settings::EditPredictionsMode, /// Settings specific to GitHub Copilot. pub copilot: CopilotSettings, /// Whether edit predictions are enabled in the assistant panel. @@ -275,22 +251,6 @@ pub struct DisabledGlob { is_absolute: bool, } -/// The mode in which edit predictions should be displayed. -#[derive( - Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionsMode { - /// 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, -} - #[derive(Clone, Debug, Default, SettingsUi)] pub struct CopilotSettings { /// HTTP/HTTPS proxy to use for Copilot. @@ -326,291 +286,6 @@ inventory::submit! { } } -/// Controls how completions are processed for this language. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] -#[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, -} - -/// 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, -} - -#[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, -} - -fn default_words_completion_mode() -> WordsCompletionMode { - WordsCompletionMode::Fallback -} - -fn default_lsp_insert_mode() -> LspInsertMode { - LspInsertMode::ReplaceSuffix -} - -fn default_3() -> usize { - 3 -} - -/// Controls how whitespace should be displayedin the editor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] -#[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, SettingsUi)] -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 settings for indent guides. -#[derive( - Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi, -)] -pub struct IndentGuideSettings { - /// 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() -} - -/// 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, -} - -/// The settings for inlay hints. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -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 -} - -/// The task settings for a particular language. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, SettingsUi)] -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, -} - -impl InlayHintSettings { - /// Returns the kinds of inlay hints that are enabled based on the settings. - pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { - let mut kinds = HashSet::default(); - if self.show_type_hints { - kinds.insert(Some(InlayHintKind::Type)); - } - if self.show_parameter_hints { - kinds.insert(Some(InlayHintKind::Parameter)); - } - if self.show_other_hints { - kinds.insert(None); - } - kinds - } -} - impl AllLanguageSettings { /// Returns the [`LanguageSettings`] for the language with the specified name. pub fn language<'a>( @@ -698,113 +373,71 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr ); } -/// The kind of an inlay hint. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum InlayHintKind { - /// An inlay hint for a type. - Type, - /// An inlay hint for a parameter. - Parameter, -} - -impl InlayHintKind { - /// Returns the [`InlayHintKind`] from the given name. - /// - /// Returns `None` if `name` does not match any of the expected - /// string representations. - pub fn from_name(name: &str) -> Option { - match name { - "type" => Some(InlayHintKind::Type), - "parameter" => Some(InlayHintKind::Parameter), - _ => None, - } - } - - /// Returns the name of this [`InlayHintKind`]. - pub fn name(&self) -> &'static str { - match self { - InlayHintKind::Type => "type", - InlayHintKind::Parameter => "parameter", - } - } -} - impl settings::Settings for AllLanguageSettings { fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { - let defaults = content.project.all_languages.defaults; + let all_languages = &content.project.all_languages; + let defaults = &all_languages.defaults; let default_language_settings = LanguageSettings { tab_size: defaults.tab_size?, hard_tabs: defaults.hard_tabs?, soft_wrap: defaults.soft_wrap?, preferred_line_length: defaults.preferred_line_length?, show_wrap_guides: defaults.show_wrap_guides?, - wrap_guides: defaults.wrap_guides?, - indent_guides: defaults.indent_guides?, - format_on_save: defaults.format_on_save?, + wrap_guides: defaults.wrap_guides.clone()?, + indent_guides: defaults.indent_guides.clone()?, + format_on_save: defaults.format_on_save.clone()?, remove_trailing_whitespace_on_save: defaults.remove_trailing_whitespace_on_save?, ensure_final_newline_on_save: defaults.ensure_final_newline_on_save?, - formatter: defaults.formatter?, - prettier: defaults.prettier?, - jsx_tag_auto_close: defaults.jsx_tag_auto_close?, + formatter: defaults.formatter.clone()?, + prettier: defaults.prettier.clone()?, + jsx_tag_auto_close: defaults.jsx_tag_auto_close.clone()?, enable_language_server: defaults.enable_language_server?, - language_servers: defaults.language_servers?, + language_servers: defaults.language_servers.clone()?, allow_rewrap: defaults.allow_rewrap?, show_edit_predictions: defaults.show_edit_predictions?, - edit_predictions_disabled_in: defaults.edit_predictions_disabled_in?, + edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.clone()?, show_whitespaces: defaults.show_whitespaces?, - whitespace_map: defaults.whitespace_map?, + whitespace_map: defaults.whitespace_map.clone()?, extend_comment_on_newline: defaults.extend_comment_on_newline?, - inlay_hints: defaults.inlay_hints?, + inlay_hints: defaults.inlay_hints.clone()?, use_autoclose: defaults.use_autoclose?, use_auto_surround: defaults.use_auto_surround?, use_on_type_format: defaults.use_on_type_format?, auto_indent: defaults.auto_indent?, auto_indent_on_paste: defaults.auto_indent_on_paste?, always_treat_brackets_as_autoclosed: defaults.always_treat_brackets_as_autoclosed?, - code_actions_on_format: defaults.code_actions_on_format?, + code_actions_on_format: defaults.code_actions_on_format.clone()?, linked_edits: defaults.linked_edits?, - tasks: defaults.tasks?, + tasks: defaults.tasks.clone()?, show_completions_on_input: defaults.show_completions_on_input?, show_completion_documentation: defaults.show_completion_documentation?, - completions: defaults.completions?, - debuggers: defaults.debuggers?, + completions: defaults.completions.clone()?, + debuggers: defaults.debuggers.clone()?, }; - todo!(); - } - - fn load(sources: SettingsSources, _: &mut App) -> Result { - let default_value = sources.default; - - // A default is provided for all settings. - let mut defaults: LanguageSettings = - serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?; - let mut languages = HashMap::default(); - for (language_name, settings) in &default_value.languages.0 { - let mut language_settings = defaults.clone(); + for (language_name, settings) in &all_languages.languages.0 { + let mut language_settings = default_language_settings.clone(); merge_settings(&mut language_settings, settings); - languages.insert(language_name.clone(), language_settings); + languages.insert(LanguageName(language_name.clone()), language_settings); } - let mut edit_prediction_provider = default_value + let edit_prediction_provider = all_languages .features .as_ref() .and_then(|f| f.edit_prediction_provider); - let mut edit_predictions_mode = default_value + let edit_predictions_mode = all_languages .edit_predictions .as_ref() - .map(|edit_predictions| edit_predictions.mode) - .ok_or_else(Self::missing_default)?; + .map(|edit_predictions| edit_predictions.mode)?; - let mut completion_globs: HashSet<&String> = default_value + let disabled_globs: HashSet<&String> = all_languages .edit_predictions .as_ref() .and_then(|c| c.disabled_globs.as_ref()) - .map(|globs| globs.iter().collect()) - .ok_or_else(Self::missing_default)?; + .map(|globs| globs.iter().collect())?; - let mut copilot_settings = default_value + let copilot_settings = all_languages .edit_predictions .as_ref() .map(|settings| CopilotSettings { @@ -814,111 +447,34 @@ impl settings::Settings for AllLanguageSettings { }) .unwrap_or_default(); - let mut enabled_in_text_threads = default_value + let enabled_in_text_threads = all_languages .edit_predictions .as_ref() .map(|settings| settings.enabled_in_text_threads) .unwrap_or(true); let mut file_types: FxHashMap, GlobSet> = FxHashMap::default(); + let mut file_globs: FxHashMap, Vec> = FxHashMap::default(); - for (language, patterns) in &default_value.file_types { + for (language, patterns) in &all_languages.file_types { let mut builder = GlobSetBuilder::new(); for pattern in patterns { - builder.add(Glob::new(pattern)?); - } - - file_types.insert(language.clone(), builder.build()?); - } - - for user_settings in sources.customizations() { - if let Some(provider) = user_settings - .features - .as_ref() - .and_then(|f| f.edit_prediction_provider) - { - edit_prediction_provider = Some(provider); - } - - if let Some(edit_predictions) = user_settings.edit_predictions.as_ref() { - edit_predictions_mode = edit_predictions.mode; - enabled_in_text_threads = edit_predictions.enabled_in_text_threads; - - if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() { - completion_globs.extend(disabled_globs.iter()); - } - } - - if let Some(proxy) = user_settings - .edit_predictions - .as_ref() - .and_then(|settings| settings.copilot.proxy.clone()) - { - copilot_settings.proxy = Some(proxy); - } - - if let Some(proxy_no_verify) = user_settings - .edit_predictions - .as_ref() - .and_then(|settings| settings.copilot.proxy_no_verify) - { - copilot_settings.proxy_no_verify = Some(proxy_no_verify); + builder.add(Glob::new(pattern).log_err()?); } - if let Some(enterprise_uri) = user_settings - .edit_predictions - .as_ref() - .and_then(|settings| settings.copilot.enterprise_uri.clone()) - { - copilot_settings.enterprise_uri = Some(enterprise_uri); - } - - // A user's global settings override the default global settings and - // all default language-specific settings. - merge_settings(&mut defaults, &user_settings.defaults); - for language_settings in languages.values_mut() { - merge_settings(language_settings, &user_settings.defaults); - } - - // A user's language-specific settings override default language-specific settings. - for (language_name, user_language_settings) in &user_settings.languages.0 { - merge_settings( - languages - .entry(language_name.clone()) - .or_insert_with(|| defaults.clone()), - user_language_settings, - ); - } - - for (language, patterns) in &user_settings.file_types { - let mut builder = GlobSetBuilder::new(); - - let default_value = default_value.file_types.get(&language.clone()); - - // Merge the default value with the user's value. - if let Some(patterns) = default_value { - for pattern in patterns { - builder.add(Glob::new(pattern)?); - } - } - - for pattern in patterns { - builder.add(Glob::new(pattern)?); - } - - file_types.insert(language.clone(), builder.build()?); - } + file_types.insert(language.clone(), builder.build().log_err()?); + file_globs.insert(language.clone(), patterns.clone()); } - Ok(Self { + Some(Self { edit_predictions: EditPredictionSettings { provider: if let Some(provider) = edit_prediction_provider { provider } else { EditPredictionProvider::None }, - disabled_globs: completion_globs + disabled_globs: disabled_globs .iter() .filter_map(|g| { let expanded_g = shellexpand::tilde(g).into_owned(); @@ -932,14 +488,115 @@ impl settings::Settings for AllLanguageSettings { copilot: copilot_settings, enabled_in_text_threads, }, - defaults, + defaults: default_language_settings, languages, file_types, + file_globs, }) } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - let d = &mut current.defaults; + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { + let all_languages = &content.project.all_languages; + if let Some(provider) = all_languages + .features + .as_ref() + .and_then(|f| f.edit_prediction_provider) + { + self.edit_predictions.provider = provider; + } + + if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() { + self.edit_predictions.mode = edit_predictions.mode; + self.edit_predictions.enabled_in_text_threads = + edit_predictions.enabled_in_text_threads; + + if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() { + self.edit_predictions + .disabled_globs + .extend(disabled_globs.iter().filter_map(|g| { + let expanded_g = shellexpand::tilde(g).into_owned(); + Some(DisabledGlob { + matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(), + is_absolute: Path::new(&expanded_g).is_absolute(), + }) + })); + } + } + + if let Some(proxy) = all_languages + .edit_predictions + .as_ref() + .and_then(|settings| settings.copilot.proxy.clone()) + { + self.edit_predictions.copilot.proxy = Some(proxy); + } + + if let Some(proxy_no_verify) = all_languages + .edit_predictions + .as_ref() + .and_then(|settings| settings.copilot.proxy_no_verify) + { + self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify); + } + + if let Some(enterprise_uri) = all_languages + .edit_predictions + .as_ref() + .and_then(|settings| settings.copilot.enterprise_uri.clone()) + { + self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri); + } + + // A user's global settings override the default global settings and + // all default language-specific settings. + merge_settings(&mut self.defaults, &all_languages.defaults); + for language_settings in self.languages.values_mut() { + merge_settings(language_settings, &all_languages.defaults); + } + + // A user's language-specific settings override default language-specific settings. + for (language_name, user_language_settings) in &all_languages.languages.0 { + merge_settings( + self.languages + .entry(LanguageName(language_name.clone())) + .or_insert_with(|| self.defaults.clone()), + user_language_settings, + ); + } + + for (language, patterns) in &all_languages.file_types { + let mut builder = GlobSetBuilder::new(); + + let default_value = self.file_globs.get(&language.clone()); + + // Merge the default value with the user's value. + if let Some(patterns) = default_value { + for pattern in patterns { + if let Some(glob) = Glob::new(pattern).log_err() { + builder.add(glob); + } + } + } + + for pattern in patterns { + if let Some(glob) = Glob::new(pattern).log_err() { + builder.add(glob); + } + } + + self.file_globs + .entry(language.clone()) + .or_default() + .extend(patterns.clone()); + + if let Some(matcher) = builder.build().log_err() { + self.file_types.insert(language.clone(), matcher); + } + } + } + + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { + let d = &mut current.project.all_languages.defaults; if let Some(size) = vscode .read_value("editor.tabSize") .and_then(|v| v.as_u64()) @@ -1052,7 +709,11 @@ impl settings::Settings for AllLanguageSettings { } // TODO: do we want to merge imported globs per filetype? for now we'll just replace - current.file_types.extend(associations); + current + .project + .all_languages + .file_types + .extend(associations); // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs if let Some(disabled_globs) = vscode @@ -1060,6 +721,8 @@ impl settings::Settings for AllLanguageSettings { .and_then(|v| v.as_array()) { current + .project + .all_languages .edit_predictions .get_or_insert_default() .disabled_globs @@ -1075,87 +738,81 @@ impl settings::Settings for AllLanguageSettings { } fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } - } - - merge(&mut settings.tab_size, src.tab_size); + settings.tab_size.merge_from(&src.tab_size); settings.tab_size = settings .tab_size .clamp(NonZeroU32::new(1).unwrap(), NonZeroU32::new(16).unwrap()); - merge(&mut settings.hard_tabs, src.hard_tabs); - merge(&mut settings.soft_wrap, src.soft_wrap); - merge(&mut settings.use_autoclose, src.use_autoclose); - merge(&mut settings.use_auto_surround, src.use_auto_surround); - merge(&mut settings.use_on_type_format, src.use_on_type_format); - merge(&mut settings.auto_indent, src.auto_indent); - merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste); - merge( - &mut settings.always_treat_brackets_as_autoclosed, - src.always_treat_brackets_as_autoclosed, - ); - merge(&mut settings.show_wrap_guides, src.show_wrap_guides); - merge(&mut settings.wrap_guides, src.wrap_guides.clone()); - merge(&mut settings.indent_guides, src.indent_guides); - merge( - &mut settings.code_actions_on_format, - src.code_actions_on_format.clone(), - ); - merge(&mut settings.linked_edits, src.linked_edits); - merge(&mut settings.tasks, src.tasks.clone()); - - merge( - &mut settings.preferred_line_length, - src.preferred_line_length, - ); - merge(&mut settings.formatter, src.formatter.clone()); - merge(&mut settings.prettier, src.prettier.clone()); - merge( - &mut settings.jsx_tag_auto_close, - src.jsx_tag_auto_close.clone(), - ); - merge(&mut settings.format_on_save, src.format_on_save.clone()); - merge( - &mut settings.remove_trailing_whitespace_on_save, - src.remove_trailing_whitespace_on_save, - ); - merge( - &mut settings.ensure_final_newline_on_save, - src.ensure_final_newline_on_save, - ); - merge( - &mut settings.enable_language_server, - src.enable_language_server, - ); - merge(&mut settings.language_servers, src.language_servers.clone()); - merge(&mut settings.allow_rewrap, src.allow_rewrap); - merge( - &mut settings.show_edit_predictions, - src.show_edit_predictions, - ); - merge( - &mut settings.edit_predictions_disabled_in, - src.edit_predictions_disabled_in.clone(), - ); - merge(&mut settings.show_whitespaces, src.show_whitespaces); - merge(&mut settings.whitespace_map, src.whitespace_map.clone()); - merge( - &mut settings.extend_comment_on_newline, - src.extend_comment_on_newline, - ); - merge(&mut settings.inlay_hints, src.inlay_hints); - merge( - &mut settings.show_completions_on_input, - src.show_completions_on_input, - ); - merge( - &mut settings.show_completion_documentation, - src.show_completion_documentation, - ); - merge(&mut settings.completions, src.completions); + settings.hard_tabs.merge_from(&src.hard_tabs); + settings.soft_wrap.merge_from(&src.soft_wrap); + settings.use_autoclose.merge_from(&src.use_autoclose); + settings + .use_auto_surround + .merge_from(&src.use_auto_surround); + settings + .use_on_type_format + .merge_from(&src.use_on_type_format); + settings.auto_indent.merge_from(&src.auto_indent); + settings + .auto_indent_on_paste + .merge_from(&src.auto_indent_on_paste); + settings + .always_treat_brackets_as_autoclosed + .merge_from(&src.always_treat_brackets_as_autoclosed); + settings.show_wrap_guides.merge_from(&src.show_wrap_guides); + settings.wrap_guides.merge_from(&src.wrap_guides); + settings.indent_guides.merge_from(&src.indent_guides); + settings + .code_actions_on_format + .merge_from(&src.code_actions_on_format.clone()); + settings.linked_edits.merge_from(&src.linked_edits); + settings.tasks.merge_from(&src.tasks.clone()); + + settings + .preferred_line_length + .merge_from(&src.preferred_line_length); + settings.formatter.merge_from(&src.formatter.clone()); + settings.prettier.merge_from(&src.prettier.clone()); + settings + .jsx_tag_auto_close + .merge_from(&src.jsx_tag_auto_close.clone()); + settings + .format_on_save + .merge_from(&src.format_on_save.clone()); + settings + .remove_trailing_whitespace_on_save + .merge_from(&src.remove_trailing_whitespace_on_save); + settings + .ensure_final_newline_on_save + .merge_from(&src.ensure_final_newline_on_save); + settings + .enable_language_server + .merge_from(&src.enable_language_server); + settings + .language_servers + .merge_from(&src.language_servers.clone()); + settings.allow_rewrap.merge_from(&src.allow_rewrap); + settings + .show_edit_predictions + .merge_from(&src.show_edit_predictions); + settings + .edit_predictions_disabled_in + .merge_from(&src.edit_predictions_disabled_in.clone()); + settings.show_whitespaces.merge_from(&src.show_whitespaces); + settings + .whitespace_map + .merge_from(&src.whitespace_map.clone()); + settings + .extend_comment_on_newline + .merge_from(&src.extend_comment_on_newline); + settings.inlay_hints.merge_from(&src.inlay_hints.clone()); + settings + .show_completions_on_input + .merge_from(&src.show_completions_on_input); + settings + .show_completion_documentation + .merge_from(&src.show_completion_documentation); + settings.completions.merge_from(&src.completions.clone()); } /// Allows to enable/disable formatting with Prettier @@ -1193,48 +850,8 @@ pub struct JsxTagAutoCloseSettings { #[cfg(test)] mod tests { - use gpui::TestAppContext; - use super::*; - - #[test] - fn test_formatter_deserialization() { - let raw_auto = "{\"formatter\": \"auto\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap(); - assert_eq!(settings.formatter, Some(SelectedFormatter::Auto)); - let raw = "{\"formatter\": \"language_server\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(SelectedFormatter::List(FormatterList::Single( - Formatter::LanguageServer { name: None } - ))) - ); - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(SelectedFormatter::List(FormatterList::Vec(vec![ - Formatter::LanguageServer { name: None } - ]))) - ); - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(SelectedFormatter::List(FormatterList::Vec(vec![ - Formatter::LanguageServer { name: None }, - Formatter::Prettier - ]))) - ); - } - - #[test] - fn test_formatter_deserialization_invalid() { - let raw_auto = "{\"formatter\": {}}"; - let result: Result = serde_json::from_str(raw_auto); - assert!(result.is_err()); - } + use gpui::TestAppContext; #[gpui::test] fn test_edit_predictions_enabled_for_file(cx: &mut TestAppContext) { diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 242228689876ea77d0c1f2fd0b309dd4aadb1048..ced6140c85dffca07028f3059fdeca9b94631d4c 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -29,7 +29,7 @@ pub struct AllLanguageSettingsContent { /// Settings for associating file extensions and filenames /// with languages. #[serde(default)] - pub file_types: HashMap>, + pub file_types: HashMap, Vec>, } /// The settings for enabling/disabling features. @@ -37,13 +37,13 @@ pub struct AllLanguageSettingsContent { #[serde(rename_all = "snake_case")] pub struct FeaturesContent { /// Determines which edit prediction provider to use. - pub edit_prediction_provider: Option, + 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 { +pub enum EditPredictionProvider { None, #[default] Copilot, @@ -62,7 +62,7 @@ pub struct EditPredictionSettingsContent { /// The mode used to display edit predictions in the buffer. /// Provider support required. #[serde(default)] - pub mode: EditPredictionsModeContent, + pub mode: EditPredictionsMode, /// Settings specific to GitHub Copilot. #[serde(default)] pub copilot: CopilotSettingsContent, @@ -98,7 +98,7 @@ pub struct CopilotSettingsContent { /// 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 { +pub enum EditPredictionsMode { /// If provider supports it, display inline when holding modifier key (e.g., alt). /// Otherwise, eager preview is used. #[serde(alias = "auto")] @@ -112,7 +112,7 @@ pub enum EditPredictionsModeContent { /// 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 { +pub enum SoftWrap { /// Prefer a single line generally, unless an overly long line is encountered. None, /// Deprecated: use None instead. Left to avoid breaking existing users' configs. @@ -144,7 +144,7 @@ pub struct LanguageSettingsContent { /// /// Default: none #[serde(default)] - pub soft_wrap: Option, + pub soft_wrap: Option, /// The column at which to soft-wrap lines, for buffers where soft-wrap /// is enabled. /// @@ -166,7 +166,7 @@ pub struct LanguageSettingsContent { pub wrap_guides: Option>, /// Indent guide related settings. #[serde(default)] - pub indent_guides: Option, + pub indent_guides: Option, /// Whether or not to perform a buffer format before saving. /// /// Default: on @@ -379,6 +379,8 @@ pub struct JsxTagAutoCloseSettings { } /// The settings for inlay hints. +/// todo(settings_refactor) the fields of this struct should likely be optional, +/// and a similar struct exposed from the language crate. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettings { /// Global switch to toggle hints on and off. @@ -437,6 +439,54 @@ pub struct InlayHintSettings { pub toggle_on_modifiers_press: Option, } +impl InlayHintSettings { + /// Returns the kinds of inlay hints that are enabled based on the settings. + pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { + let mut kinds = HashSet::default(); + if self.show_type_hints { + kinds.insert(Some(InlayHintKind::Type)); + } + if self.show_parameter_hints { + kinds.insert(Some(InlayHintKind::Parameter)); + } + if self.show_other_hints { + kinds.insert(None); + } + kinds + } +} + +/// The kind of an inlay hint. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InlayHintKind { + /// An inlay hint for a type. + Type, + /// An inlay hint for a parameter. + Parameter, +} + +impl InlayHintKind { + /// Returns the [`InlayHintKind`] from the given name. + /// + /// Returns `None` if `name` does not match any of the expected + /// string representations. + pub fn from_name(name: &str) -> Option { + match name { + "type" => Some(InlayHintKind::Type), + "parameter" => Some(InlayHintKind::Parameter), + _ => None, + } + } + + /// Returns the name of this [`InlayHintKind`]. + pub fn name(&self) -> &'static str { + match self { + InlayHintKind::Type => "type", + InlayHintKind::Parameter => "parameter", + } + } +} + fn edit_debounce_ms() -> u64 { 700 } @@ -775,7 +825,7 @@ pub enum Formatter { /// The settings for indent guides. #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct IndentGuideSettingsContent { +pub struct IndentGuideSettings { /// Whether to display indent guides in the editor. /// /// Default: true @@ -881,3 +931,47 @@ pub enum IndentGuideBackgroundColoring { /// Use a different color for each indentation level. IndentAware, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_formatter_deserialization() { + let raw_auto = "{\"formatter\": \"auto\"}"; + let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap(); + assert_eq!(settings.formatter, Some(SelectedFormatter::Auto)); + let raw = "{\"formatter\": \"language_server\"}"; + let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); + assert_eq!( + settings.formatter, + Some(SelectedFormatter::List(FormatterList::Single( + Formatter::LanguageServer { name: None } + ))) + ); + let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}"; + let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); + assert_eq!( + settings.formatter, + Some(SelectedFormatter::List(FormatterList::Vec(vec![ + Formatter::LanguageServer { name: None } + ]))) + ); + let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}"; + let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); + assert_eq!( + settings.formatter, + Some(SelectedFormatter::List(FormatterList::Vec(vec![ + Formatter::LanguageServer { name: None }, + Formatter::Prettier + ]))) + ); + } + + #[test] + fn test_formatter_deserialization_invalid() { + let raw_auto = "{\"formatter\": {}}"; + let result: Result = serde_json::from_str(raw_auto); + assert!(result.is_err()); + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fd1ab64001aa14366c1284a5f0b523df4d1ceb76..e99e9eb452f6c148a18d4cadb04ac7dcf2eafc05 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -186,7 +186,7 @@ struct SettingValue { trait AnySettingValue: 'static + Send + Sync { fn setting_type_name(&self) -> &'static str; - fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option>; + fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Option>; fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App); fn value_for_path(&self, path: Option) -> &dyn Any; @@ -279,7 +279,7 @@ impl SettingsStore { } let Some(mut value) = T::from_default(&self.default_settings, cx) else { panic!( - "{}::from_file return None for default.json", + "{}::from_default return None for default.json", type_name::() ) }; @@ -387,7 +387,7 @@ impl SettingsStore { cx: &mut App, update: impl FnOnce(&mut SettingsContent), ) { - let mut content = self.user_settings.as_ref().unwrap().content.clone(); + let mut content = self.user_settings.clone().unwrap_or_default().content; update(&mut content); let new_text = serde_json::to_string(&UserSettingsContent { content, @@ -864,7 +864,9 @@ impl SettingsStore { 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 mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); + let mut value = setting_value + .from_default(&self.default_settings, cx) + .unwrap(); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.set_global_value(value); } @@ -896,7 +898,9 @@ impl SettingsStore { continue; } - let mut value = setting_value.from_file(&self.default_settings, cx).unwrap(); + let mut value = setting_value + .from_default(&self.default_settings, cx) + .unwrap(); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.refine(value.as_mut(), &project_settings_stack, cx); setting_value.set_local_value(*root_id, directory_path.clone(), value); @@ -980,7 +984,7 @@ impl Debug for SettingsStore { } impl AnySettingValue for SettingValue { - fn from_file(&self, s: &SettingsContent, cx: &mut App) -> Option> { + fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Option> { T::from_default(s, cx).map(|result| Box::new(result) as _) } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 71841a130410265ac45d03a284e2334746b5c0ca..fd0e2d633e83f0efedd1537c5be1fb9af1f416d0 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -798,6 +798,7 @@ fn font_fallbacks_from_settings( impl settings::Settings for ThemeSettings { fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { let content = &content.theme; + dbg!(&content); let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); let theme_selection: ThemeSelection = content.theme.clone()?.into(); @@ -832,7 +833,7 @@ impl settings::Settings for ThemeSettings { .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance)) .ok()?, icon_theme_selection: Some(icon_theme_selection), - ui_density: content.ui_density?.into(), + ui_density: content.ui_density.unwrap_or_default().into(), unnecessary_code_fade: content.unnecessary_code_fade?, }; From c2a1829d4aedec6fb48c4d5924c0091632d24c8c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 22:52:03 -0600 Subject: [PATCH 018/117] No more options --- crates/audio/src/audio_settings.rs | 16 +-- crates/git_hosting_providers/src/settings.rs | 20 ++-- crates/language/src/language_settings.rs | 99 ++++++++++--------- crates/multi_buffer/src/multi_buffer.rs | 2 +- crates/settings/src/base_keymap_setting.rs | 4 +- crates/settings/src/settings_store.rs | 60 ++++++----- crates/theme/src/settings.rs | 43 ++++---- crates/title_bar/src/title_bar_settings.rs | 2 +- .../vim_mode_setting/src/vim_mode_setting.rs | 8 +- crates/zlog_settings/src/zlog_settings.rs | 8 +- 10 files changed, 131 insertions(+), 131 deletions(-) diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 0c66cf08c4c0959c65df6b3554de8012137d897a..28f97c4b4df6f8741c3fd0552ed883cc7a8838a3 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -4,7 +4,7 @@ use gpui::App; use settings::{Settings, SettingsStore}; use util::MergeFrom as _; -#[derive(Clone, Default, Debug)] +#[derive(Clone, Debug)] pub struct AudioSettings { /// Opt into the new audio system. pub rodio_audio: bool, // default is false @@ -23,13 +23,13 @@ pub struct AudioSettings { /// Configuration of audio in Zed impl Settings for AudioSettings { - fn from_default(content: &settings::SettingsContent, _cx: &mut App) -> Option { - let audio = &content.audio.as_ref()?; - Some(AudioSettings { - control_input_volume: audio.control_input_volume?, - control_output_volume: audio.control_output_volume?, - rodio_audio: audio.rodio_audio?, - }) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let audio = &content.audio.as_ref().unwrap(); + AudioSettings { + control_input_volume: audio.control_input_volume.unwrap(), + control_output_volume: audio.control_output_volume.unwrap(), + rodio_audio: audio.rodio_audio.unwrap(), + } } fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index e2c33c8f2802dd3ad28ad6d34c9c214c63f02633..fd706adf73cfe5890dac4133fd5be1d4ac967a05 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -2,12 +2,7 @@ use std::sync::Arc; use git::GitHostingProviderRegistry; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{ - GitHostingProviderConfig, GitHostingProviderKind, Settings, SettingsKey, SettingsStore, - SettingsUi, -}; +use settings::{GitHostingProviderConfig, GitHostingProviderKind, Settings, SettingsStore}; use url::Url; use util::ResultExt as _; @@ -57,19 +52,16 @@ fn update_git_hosting_providers_from_settings(cx: &mut App) { provider_registry.set_setting_providers(iter); } -#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] +#[derive(Debug, Clone)] pub struct GitHostingProviderSettings { - /// The list of custom Git hosting providers. - #[serde(default)] pub git_hosting_providers: Vec, } impl Settings for GitHostingProviderSettings { - fn from_default(content: &settings::SettingsContent, _cx: &mut App) -> Option { - Some(Self { - git_hosting_providers: content.git_hosting_providers.clone()?, - }) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + git_hosting_providers: content.git_hosting_providers.clone().unwrap(), + } } fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 5fb4291bdc045e4fee0cbd68103329cde51932bc..f2c43cb34303f50c5cd6c8e76c74ef25811bc262 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -374,45 +374,49 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr } impl settings::Settings for AllLanguageSettings { - fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { let all_languages = &content.project.all_languages; - let defaults = &all_languages.defaults; + let defaults = all_languages.defaults.clone(); let default_language_settings = LanguageSettings { - tab_size: defaults.tab_size?, - hard_tabs: defaults.hard_tabs?, - soft_wrap: defaults.soft_wrap?, - preferred_line_length: defaults.preferred_line_length?, - show_wrap_guides: defaults.show_wrap_guides?, - wrap_guides: defaults.wrap_guides.clone()?, - indent_guides: defaults.indent_guides.clone()?, - format_on_save: defaults.format_on_save.clone()?, - remove_trailing_whitespace_on_save: defaults.remove_trailing_whitespace_on_save?, - ensure_final_newline_on_save: defaults.ensure_final_newline_on_save?, - formatter: defaults.formatter.clone()?, - prettier: defaults.prettier.clone()?, - jsx_tag_auto_close: defaults.jsx_tag_auto_close.clone()?, - enable_language_server: defaults.enable_language_server?, - language_servers: defaults.language_servers.clone()?, - allow_rewrap: defaults.allow_rewrap?, - show_edit_predictions: defaults.show_edit_predictions?, - edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.clone()?, - show_whitespaces: defaults.show_whitespaces?, - whitespace_map: defaults.whitespace_map.clone()?, - extend_comment_on_newline: defaults.extend_comment_on_newline?, - inlay_hints: defaults.inlay_hints.clone()?, - use_autoclose: defaults.use_autoclose?, - use_auto_surround: defaults.use_auto_surround?, - use_on_type_format: defaults.use_on_type_format?, - auto_indent: defaults.auto_indent?, - auto_indent_on_paste: defaults.auto_indent_on_paste?, - always_treat_brackets_as_autoclosed: defaults.always_treat_brackets_as_autoclosed?, - code_actions_on_format: defaults.code_actions_on_format.clone()?, - linked_edits: defaults.linked_edits?, - tasks: defaults.tasks.clone()?, - show_completions_on_input: defaults.show_completions_on_input?, - show_completion_documentation: defaults.show_completion_documentation?, - completions: defaults.completions.clone()?, - debuggers: defaults.debuggers.clone()?, + tab_size: defaults.tab_size.unwrap(), + hard_tabs: defaults.hard_tabs.unwrap(), + soft_wrap: defaults.soft_wrap.unwrap(), + preferred_line_length: defaults.preferred_line_length.unwrap(), + show_wrap_guides: defaults.show_wrap_guides.unwrap(), + wrap_guides: defaults.wrap_guides.unwrap(), + indent_guides: defaults.indent_guides.unwrap(), + format_on_save: defaults.format_on_save.unwrap(), + remove_trailing_whitespace_on_save: defaults + .remove_trailing_whitespace_on_save + .unwrap(), + ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(), + formatter: defaults.formatter.unwrap(), + prettier: defaults.prettier.unwrap(), + jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap(), + enable_language_server: defaults.enable_language_server.unwrap(), + language_servers: defaults.language_servers.unwrap(), + allow_rewrap: defaults.allow_rewrap.unwrap(), + show_edit_predictions: defaults.show_edit_predictions.unwrap(), + edit_predictions_disabled_in: defaults.edit_predictions_disabled_in.unwrap(), + show_whitespaces: defaults.show_whitespaces.unwrap(), + whitespace_map: defaults.whitespace_map.unwrap(), + extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(), + inlay_hints: defaults.inlay_hints.unwrap(), + use_autoclose: defaults.use_autoclose.unwrap(), + use_auto_surround: defaults.use_auto_surround.unwrap(), + use_on_type_format: defaults.use_on_type_format.unwrap(), + auto_indent: defaults.auto_indent.unwrap(), + auto_indent_on_paste: defaults.auto_indent_on_paste.unwrap(), + always_treat_brackets_as_autoclosed: defaults + .always_treat_brackets_as_autoclosed + .unwrap(), + code_actions_on_format: defaults.code_actions_on_format.unwrap(), + linked_edits: defaults.linked_edits.unwrap(), + tasks: defaults.tasks.unwrap(), + show_completions_on_input: defaults.show_completions_on_input.unwrap(), + show_completion_documentation: defaults.show_completion_documentation.unwrap(), + completions: defaults.completions.unwrap(), + debuggers: defaults.debuggers.unwrap(), }; let mut languages = HashMap::default(); @@ -426,16 +430,17 @@ impl settings::Settings for AllLanguageSettings { .features .as_ref() .and_then(|f| f.edit_prediction_provider); - let edit_predictions_mode = all_languages - .edit_predictions - .as_ref() - .map(|edit_predictions| edit_predictions.mode)?; + let edit_predictions_mode = all_languages.edit_predictions.as_ref().unwrap().mode; let disabled_globs: HashSet<&String> = all_languages .edit_predictions .as_ref() - .and_then(|c| c.disabled_globs.as_ref()) - .map(|globs| globs.iter().collect())?; + .unwrap() + .disabled_globs + .as_ref() + .unwrap() + .iter() + .collect(); let copilot_settings = all_languages .edit_predictions @@ -460,14 +465,14 @@ impl settings::Settings for AllLanguageSettings { let mut builder = GlobSetBuilder::new(); for pattern in patterns { - builder.add(Glob::new(pattern).log_err()?); + builder.add(Glob::new(pattern).unwrap()); } - file_types.insert(language.clone(), builder.build().log_err()?); + file_types.insert(language.clone(), builder.build().unwrap()); file_globs.insert(language.clone(), patterns.clone()); } - Some(Self { + Self { edit_predictions: EditPredictionSettings { provider: if let Some(provider) = edit_prediction_provider { provider @@ -492,7 +497,7 @@ impl settings::Settings for AllLanguageSettings { languages, file_types, file_globs, - }) + } } fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f9d99473fbd196152418376d1bfbf63e21ad8b00..5273007a9fc44700eea5078c35636fe9282d486f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5913,7 +5913,7 @@ impl MultiBufferSnapshot { end_row: last_row, depth: next_depth, tab_size, - settings: settings.indent_guides, + settings: settings.indent_guides.clone(), }); } } diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index b7ac08f620d1aba93178c8cd74e3a271a8689a82..4c212e60d3c7ac427cd71608640470da8f3cbe4b 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -139,8 +139,8 @@ pub struct BaseKeymapSetting { } impl Settings for BaseKeymap { - fn from_default(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Option { - s.base_keymap.map(Into::into) + fn from_defaults(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Self { + s.base_keymap.unwrap().into() } fn refine(&mut self, s: &settings_content::SettingsContent, _cx: &mut App) { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index e99e9eb452f6c148a18d4cadb04ac7dcf2eafc05..93ec6cc995a7e14f8eaf3aafad65dbdd7b35e7ef 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -57,8 +57,15 @@ pub trait Settings: 'static + Send + Sync + Sized { /// user settings match the current version of the settings. const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - fn from_default(content: &SettingsContent, cx: &mut App) -> Option; + /// Read the value from default.json. + /// This function *should* panic if default values are missing, + /// and you should add a default to default.json for documentation. + fn from_defaults(content: &SettingsContent, cx: &mut App) -> Self; + /// Update the value based on the content from the current file. + /// + /// This function *should not* panic if there are problems, as the + /// content of user-provided settings files may be incomplete or invalid. fn refine(&mut self, content: &SettingsContent, cx: &mut App); fn missing_default() -> anyhow::Error { @@ -186,7 +193,7 @@ struct SettingValue { trait AnySettingValue: 'static + Send + Sync { fn setting_type_name(&self) -> &'static str; - fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Option>; + fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box; fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App); fn value_for_path(&self, path: Option) -> &dyn Any; @@ -277,12 +284,7 @@ impl SettingsStore { if let Some(server_settings) = self.server_settings.as_ref() { refinements.push(server_settings) } - let Some(mut value) = T::from_default(&self.default_settings, cx) else { - panic!( - "{}::from_default return None for default.json", - type_name::() - ) - }; + let mut value = T::from_defaults(&self.default_settings, cx); for refinement in refinements { value.refine(refinement, cx) } @@ -864,9 +866,7 @@ impl SettingsStore { 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 mut value = setting_value - .from_default(&self.default_settings, cx) - .unwrap(); + let mut value = setting_value.from_default(&self.default_settings, cx); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.set_global_value(value); } @@ -898,9 +898,7 @@ impl SettingsStore { continue; } - let mut value = setting_value - .from_default(&self.default_settings, cx) - .unwrap(); + let mut value = setting_value.from_default(&self.default_settings, cx); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.refine(value.as_mut(), &project_settings_stack, cx); setting_value.set_local_value(*root_id, directory_path.clone(), value); @@ -984,8 +982,8 @@ impl Debug for SettingsStore { } impl AnySettingValue for SettingValue { - fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Option> { - T::from_default(s, cx).map(|result| Box::new(result) as _) + fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box { + Box::new(T::from_defaults(s, cx)) as _ } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) { @@ -1063,10 +1061,10 @@ mod tests { } impl Settings for AutoUpdateSetting { - fn from_default(content: &SettingsContent, _: &mut App) -> Option { - content - .auto_update - .map(|auto_update| AutoUpdateSetting { auto_update }) + fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { + AutoUpdateSetting { + auto_update: content.auto_update.unwrap(), + } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { @@ -1085,12 +1083,12 @@ mod tests { } impl Settings for TitleBarSettings { - fn from_default(content: &SettingsContent, _: &mut App) -> Option { - let content = content.title_bar.clone()?; - Some(TitleBarSettings { - show: content.show?, - show_branch_name: content.show_branch_name?, - }) + fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { + let content = content.title_bar.clone().unwrap(); + TitleBarSettings { + show: content.show.unwrap(), + show_branch_name: content.show_branch_name.unwrap(), + } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { @@ -1121,12 +1119,12 @@ mod tests { } impl Settings for DefaultLanguageSettings { - fn from_default(content: &SettingsContent, _: &mut App) -> Option { + fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { let content = &content.project.all_languages.defaults; - Some(DefaultLanguageSettings { - tab_size: content.tab_size?, - preferred_line_length: content.preferred_line_length?, - }) + DefaultLanguageSettings { + tab_size: content.tab_size.unwrap(), + preferred_line_length: content.preferred_line_length.unwrap(), + } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index fd0e2d633e83f0efedd1537c5be1fb9af1f416d0..1c2d834027103c6b38593657d2ffb3a27fe2b44b 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -796,31 +796,37 @@ fn font_fallbacks_from_settings( } impl settings::Settings for ThemeSettings { - fn from_default(content: &settings::SettingsContent, cx: &mut App) -> Option { + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { let content = &content.theme; - dbg!(&content); + // todo(settings_refactor). This should *not* require cx... let themes = ThemeRegistry::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); - let theme_selection: ThemeSelection = content.theme.clone()?.into(); - let icon_theme_selection: IconThemeSelection = content.icon_theme.clone()?.into(); - let this = Self { - ui_font_size: content.ui_font_size?.into(), + let theme_selection: ThemeSelection = content.theme.clone().unwrap().into(); + let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into(); + Self { + ui_font_size: content.ui_font_size.unwrap().into(), ui_font: Font { - family: content.ui_font_family.as_ref()?.0.clone().into(), - features: content.ui_font_features.clone()?, + family: content.ui_font_family.as_ref().unwrap().0.clone().into(), + features: content.ui_font_features.clone().unwrap(), fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()), - weight: content.ui_font_weight.map(FontWeight)?, + weight: content.ui_font_weight.map(FontWeight).unwrap(), style: Default::default(), }, buffer_font: Font { - family: content.buffer_font_family.as_ref()?.0.clone().into(), - features: content.buffer_font_features.clone()?, + family: content + .buffer_font_family + .as_ref() + .unwrap() + .0 + .clone() + .into(), + features: content.buffer_font_features.clone().unwrap(), fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()), - weight: content.buffer_font_weight.map(FontWeight)?, + weight: content.buffer_font_weight.map(FontWeight).unwrap(), style: FontStyle::default(), }, - buffer_font_size: content.buffer_font_size?.into(), - buffer_line_height: content.buffer_line_height?.into(), + buffer_font_size: content.buffer_font_size.unwrap().into(), + buffer_line_height: content.buffer_line_height.unwrap().into(), agent_font_size: content.agent_font_size.flatten().map(Into::into), active_theme: themes .get(theme_selection.theme(*system_appearance)) @@ -831,13 +837,12 @@ impl settings::Settings for ThemeSettings { theme_overrides: HashMap::default(), active_icon_theme: themes .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance)) - .ok()?, + .ok() + .unwrap(), icon_theme_selection: Some(icon_theme_selection), ui_density: content.ui_density.unwrap_or_default().into(), - unnecessary_code_fade: content.unnecessary_code_fade?, - }; - - Some(this) + unnecessary_code_fade: content.unnecessary_code_fade.unwrap(), + } } fn refine(&mut self, content: &SettingsContent, cx: &mut App) { diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 9d0d8ff511a9315b2fc68bfc1fe3e346e836e6d4..0b386d496788547a91255ec5f175a99c1391bda4 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -24,7 +24,7 @@ pub struct TitleBarSettings { } impl Settings for TitleBarSettings { - fn from_default(s: &SettingsContent) -> Option { + fn from_defaults(s: &SettingsContent) -> Option { let content = s.title_bar?; TitleBarSettings { show: content.show?, diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index 8fa12bbd644f48929e6be8e2ba6fa9ad087b8356..7c0a21e443fe908191bf3cff9d557a1bf38971b9 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -16,8 +16,8 @@ pub fn init(cx: &mut App) { pub struct VimModeSetting(pub bool); impl Settings for VimModeSetting { - fn from_default(content: &SettingsContent, _cx: &mut App) -> Option { - Some(Self(content.vim_mode?)) + fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self { + Self(content.vim_mode.unwrap()) } fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { @@ -34,8 +34,8 @@ impl Settings for VimModeSetting { pub struct HelixModeSetting(pub bool); impl Settings for HelixModeSetting { - fn from_default(content: &SettingsContent, _cx: &mut App) -> Option { - Some(Self(content.helix_mode?)) + fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self { + Self(content.helix_mode.unwrap()) } fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index fc350e035b8f42ad6ab6515960e6506637de2b31..0d464b872067683e7fad8db6f4049eff92d5ed8b 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -24,10 +24,10 @@ pub struct ZlogSettings { } impl Settings for ZlogSettings { - fn from_default(content: &settings::SettingsContent, _: &mut App) -> Option { - Some(ZlogSettings { - scopes: content.log.clone()?, - }) + fn from_defaults(content: &settings::SettingsContent, _: &mut App) -> Self { + ZlogSettings { + scopes: content.log.clone().unwrap(), + } } fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { From d50548ab1ab608f53f89f323579210c0741599ba Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 23:37:36 -0600 Subject: [PATCH 019/117] Terminal settings too --- crates/settings/src/settings_content.rs | 5 + .../settings/src/settings_content/terminal.rs | 314 +++++++++++++ crates/terminal/src/terminal_settings.rs | 438 ++++++------------ crates/theme/src/settings.rs | 9 +- 4 files changed, 470 insertions(+), 296 deletions(-) create mode 100644 crates/settings/src/settings_content/terminal.rs diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index ba0649396b21e0f2c719142bc033bc86006bdce7..f1547f769d432fa400ea068622f969f9f7c0d183 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,6 +1,8 @@ mod language; +mod terminal; mod theme; pub use language::*; +pub use terminal::*; pub use theme::*; use std::env; @@ -41,6 +43,9 @@ pub struct SettingsContent { /// Example: {"log": {"client": "warn"}} pub log: Option>, + /// Configuration of the terminal in Zed. + pub terminal: Option, + pub title_bar: Option, /// Whether or not to enable Vim mode. diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs new file mode 100644 index 0000000000000000000000000000000000000000..351ee41402ee9aea77282d23ea6fd0a35f8b9e8e --- /dev/null +++ b/crates/settings/src/settings_content/terminal.rs @@ -0,0 +1,314 @@ +use std::path::PathBuf; + +use collections::HashMap; +use gpui::{AbsoluteLength, FontFeatures, SharedString, px}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::FontFamilyName; + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +pub struct TerminalSettingsContent { + /// What shell to use when opening a terminal. + /// + /// Default: system + pub shell: Option, + /// What working directory to use when launching the terminal + /// + /// Default: current_project_directory + pub working_directory: Option, + /// Sets the terminal's font size. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font size. + pub font_size: Option, + /// Sets the terminal's font family. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font family. + pub font_family: Option, + + /// Sets the terminal's font fallbacks. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font fallbacks. + #[schemars(extend("uniqueItems" = true))] + pub font_fallbacks: Option>, + + /// Sets the terminal's line height. + /// + /// Default: comfortable + pub line_height: Option, + pub font_features: Option, + /// Sets the terminal's font weight in CSS weight units 0-900. + pub font_weight: Option, + /// Any key-value pairs added to this list will be added to the terminal's + /// environment. Use `:` to separate multiple values. + /// + /// Default: {} + pub env: Option>, + /// Default cursor shape for the terminal. + /// Can be "bar", "block", "underline", or "hollow". + /// + /// Default: None + pub cursor_shape: Option, + /// Sets the cursor blinking behavior in the terminal. + /// + /// Default: terminal_controlled + pub blinking: Option, + /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. + /// Alternate Scroll mode converts mouse scroll events into up / down key + /// presses when in the alternate screen (e.g. when running applications + /// like vim or less). The terminal can still set and unset this mode. + /// + /// Default: on + pub alternate_scroll: Option, + /// Sets whether the option key behaves as the meta key. + /// + /// Default: false + pub option_as_meta: Option, + /// Whether or not selecting text in the terminal will automatically + /// copy to the system clipboard. + /// + /// Default: false + pub copy_on_select: Option, + /// Whether to keep the text selection after copying it to the clipboard. + /// + /// Default: false + pub keep_selection_on_copy: Option, + /// Whether to show the terminal button in the status bar. + /// + /// Default: true + pub button: Option, + pub dock: Option, + /// Default width when the terminal is docked to the left or right. + /// + /// Default: 640 + pub default_width: Option, + /// Default height when the terminal is docked to the bottom. + /// + /// Default: 320 + pub default_height: Option, + /// Activates the python virtual environment, if one is found, in the + /// terminal's working directory (as resolved by the working_directory + /// setting). Set this to "off" to disable this behavior. + /// + /// Default: on + pub detect_venv: Option, + /// The maximum number of lines to keep in the scrollback history. + /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. + /// 0 disables the scrolling. + /// Existing terminals will not pick up this change until they are recreated. + /// See Alacritty documentation for more information. + /// + /// Default: 10_000 + pub max_scroll_history_lines: Option, + /// Toolbar related settings + pub toolbar: Option, + /// Scrollbar-related settings + pub scrollbar: Option, + /// The minimum APCA perceptual contrast between foreground and background colors. + /// + /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, + /// especially for dark mode. Values range from 0 to 106. + /// + /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: + /// https://readtech.org/ARC/tests/bronze-simple-mode/ + /// - 0: No contrast adjustment + /// - 45: Minimum for large fluent text (36px+) + /// - 60: Minimum for other content text + /// - 75: Minimum for body text + /// - 90: Preferred for body text + /// + /// Default: 45 + pub minimum_contrast: Option, +} + +/// Shell configuration to open the terminal with. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + /// Use the system's default terminal configuration in /etc/passwd + #[default] + System, + /// Use a specific program with no arguments. + Program(String), + /// Use a specific program with arguments. + WithArguments { + /// The program to run. + program: String, + /// The arguments to pass to the program. + args: Vec, + /// An optional string to override the title of the terminal tab + title_override: Option, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectory { + /// Use the current file's project directory. Will Fallback to the + /// first project directory strategy if unsuccessful. + CurrentProjectDirectory, + /// Use the first project in this workspace's directory. + FirstProjectDirectory, + /// Always use this platform's home directory (if it can be found). + AlwaysHome, + /// Always use a specific directory. This value will be shell expanded. + /// If this path is not a valid directory the terminal will default to + /// this platform's home directory (if it can be found). + Always { directory: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ScrollbarSettingsContent { + /// When to show the scrollbar in the terminal. + /// + /// Default: inherits editor scrollbar settings + pub show: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum TerminalLineHeight { + /// Use a line height that's comfortable for reading, 1.618 + #[default] + Comfortable, + /// Use a standard line height, 1.3. This option is useful for TUIs, + /// particularly if they use box characters + Standard, + /// Use a custom line height. + Custom(f32), +} + +impl TerminalLineHeight { + pub fn value(&self) -> AbsoluteLength { + let value = match self { + TerminalLineHeight::Comfortable => 1.618, + TerminalLineHeight::Standard => 1.3, + TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), + }; + px(value).into() + } +} + +/// When to show the scrollbar in the terminal. +/// +/// Default: auto +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowScrollbar { + /// Show the scrollbar if there's important information or + /// follow the system's configured behavior. + Auto, + /// Match the system's configured behavior. + System, + /// Always show the scrollbar. + Always, + /// Never show the scrollbar. + Never, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CursorShapeContent { + /// Cursor is a block like `█`. + #[default] + Block, + /// Cursor is an underscore like `_`. + Underline, + /// Cursor is a vertical bar like `⎸`. + Bar, + /// Cursor is a hollow box like `▯`. + Hollow, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TerminalBlink { + /// Never blink the cursor, ignoring the terminal mode. + Off, + /// Default the cursor blink to off, but allow the terminal to + /// set blinking. + TerminalControlled, + /// Always blink the cursor, ignoring the terminal mode. + On, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AlternateScroll { + On, + Off, +} + +// Toolbar related settings +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct TerminalToolbarContent { + /// Whether to display the terminal title in breadcrumbs inside the terminal pane. + /// Only shown if the terminal title is not empty. + /// + /// The shell running in the terminal needs to be configured to emit the title. + /// Example: `echo -e "\e]2;New Title\007";` + /// + /// Default: true + pub breadcrumbs: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VenvSettings { + #[default] + Off, + On { + /// Default directories to search for virtual environments, relative + /// to the current working directory. We recommend overriding this + /// in your project's settings, rather than globally. + activate_script: Option, + venv_name: Option, + directories: Option>, + }, +} + +pub struct VenvSettingsContent<'a> { + pub activate_script: ActivateScript, + pub venv_name: &'a str, + pub directories: &'a [PathBuf], +} + +impl VenvSettings { + pub fn as_option(&self) -> Option> { + match self { + VenvSettings::Off => None, + VenvSettings::On { + activate_script, + venv_name, + directories, + } => Some(VenvSettingsContent { + activate_script: activate_script.unwrap_or(ActivateScript::Default), + venv_name: venv_name.as_deref().unwrap_or(""), + directories: directories.as_deref().unwrap_or(&[]), + }), + } + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateScript { + #[default] + Default, + Csh, + Fish, + Nushell, + PowerShell, + Pyenv, +} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index d0a25257b91eb60b60390cde751d1af8af5866e1..9d4037154a92307ac3088e936d3903f0db8c8703 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -2,22 +2,18 @@ use alacritty_terminal::vte::ansi::{ CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, }; use collections::HashMap; -use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px}; +use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{SettingsKey, SettingsSources, SettingsUi}; -use std::path::PathBuf; +pub use settings::AlternateScroll; +use settings::{ + CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition, + TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory, +}; use task::Shell; use theme::FontFamilyName; - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} +use util::MergeFrom; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { @@ -28,7 +24,7 @@ pub struct Toolbar { pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, - pub font_size: Option, + pub font_size: Option, // todo(settings_refactor) can be non-optional... pub font_family: Option, pub font_fallbacks: Option, pub font_features: Option, @@ -60,218 +56,135 @@ pub struct ScrollbarSettings { pub show: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettingsContent { - /// When to show the scrollbar in the terminal. - /// - /// Default: inherits editor scrollbar settings - pub show: Option>, -} - -/// When to show the scrollbar in the terminal. -/// -/// Default: auto -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum VenvSettings { - #[default] - Off, - On { - /// Default directories to search for virtual environments, relative - /// to the current working directory. We recommend overriding this - /// in your project's settings, rather than globally. - activate_script: Option, - venv_name: Option, - directories: Option>, - }, -} - -pub struct VenvSettingsContent<'a> { - pub activate_script: ActivateScript, - pub venv_name: &'a str, - pub directories: &'a [PathBuf], +fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell { + match shell { + settings::Shell::System => Shell::System, + settings::Shell::Program(program) => Shell::Program(program), + settings::Shell::WithArguments { + program, + args, + title_override, + } => Shell::WithArguments { + program, + args, + title_override, + }, + } } -impl VenvSettings { - pub fn as_option(&self) -> Option> { - match self { - VenvSettings::Off => None, - VenvSettings::On { - activate_script, - venv_name, - directories, - } => Some(VenvSettingsContent { - activate_script: activate_script.unwrap_or(ActivateScript::Default), - venv_name: venv_name.as_deref().unwrap_or(""), - directories: directories.as_deref().unwrap_or(&[]), +impl settings::Settings for TerminalSettings { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let content = content.terminal.clone().unwrap(); + TerminalSettings { + shell: settings_shell_to_task_shell(content.shell.unwrap()), + working_directory: content.working_directory.unwrap(), + font_size: content.font_size.map(px), + font_family: content.font_family, + font_fallbacks: content.font_fallbacks.map(|fallbacks| { + FontFallbacks::from_fonts( + fallbacks + .into_iter() + .map(|family| family.0.to_string()) + .collect(), + ) }), + font_features: content.font_features, + font_weight: content.font_weight.map(FontWeight), + line_height: content.line_height.unwrap(), + env: content.env.unwrap(), + cursor_shape: content.cursor_shape.map(Into::into), + blinking: content.blinking.unwrap(), + alternate_scroll: content.alternate_scroll.unwrap(), + option_as_meta: content.option_as_meta.unwrap(), + copy_on_select: content.copy_on_select.unwrap(), + keep_selection_on_copy: content.keep_selection_on_copy.unwrap(), + button: content.button.unwrap(), + dock: content.dock.unwrap(), + default_width: px(content.default_width.unwrap()), + default_height: px(content.default_height.unwrap()), + detect_venv: content.detect_venv.unwrap(), + max_scroll_history_lines: content.max_scroll_history_lines, + toolbar: Toolbar { + breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(), + }, + scrollbar: ScrollbarSettings { + show: content.scrollbar.unwrap().show.unwrap(), + }, + minimum_contrast: content.minimum_contrast.unwrap(), } } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ActivateScript { - #[default] - Default, - Csh, - Fish, - Nushell, - PowerShell, - Pyenv, -} -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "terminal")] -pub struct TerminalSettingsContent { - /// What shell to use when opening a terminal. - /// - /// Default: system - pub shell: Option, - /// What working directory to use when launching the terminal - /// - /// Default: current_project_directory - pub working_directory: Option, - /// Sets the terminal's font size. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font size. - pub font_size: Option, - /// Sets the terminal's font family. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font family. - pub font_family: Option, - - /// Sets the terminal's font fallbacks. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font fallbacks. - #[schemars(extend("uniqueItems" = true))] - pub font_fallbacks: Option>, - - /// Sets the terminal's line height. - /// - /// Default: comfortable - pub line_height: Option, - pub font_features: Option, - /// Sets the terminal's font weight in CSS weight units 0-900. - pub font_weight: Option, - /// Any key-value pairs added to this list will be added to the terminal's - /// environment. Use `:` to separate multiple values. - /// - /// Default: {} - pub env: Option>, - /// Default cursor shape for the terminal. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: None - pub cursor_shape: Option, - /// Sets the cursor blinking behavior in the terminal. - /// - /// Default: terminal_controlled - pub blinking: Option, - /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. - /// Alternate Scroll mode converts mouse scroll events into up / down key - /// presses when in the alternate screen (e.g. when running applications - /// like vim or less). The terminal can still set and unset this mode. - /// - /// Default: on - pub alternate_scroll: Option, - /// Sets whether the option key behaves as the meta key. - /// - /// Default: false - pub option_as_meta: Option, - /// Whether or not selecting text in the terminal will automatically - /// copy to the system clipboard. - /// - /// Default: false - pub copy_on_select: Option, - /// Whether to keep the text selection after copying it to the clipboard. - /// - /// Default: false - pub keep_selection_on_copy: Option, - /// Whether to show the terminal button in the status bar. - /// - /// Default: true - pub button: Option, - pub dock: Option, - /// Default width when the terminal is docked to the left or right. - /// - /// Default: 640 - pub default_width: Option, - /// Default height when the terminal is docked to the bottom. - /// - /// Default: 320 - pub default_height: Option, - /// Activates the python virtual environment, if one is found, in the - /// terminal's working directory (as resolved by the working_directory - /// setting). Set this to "off" to disable this behavior. - /// - /// Default: on - pub detect_venv: Option, - /// The maximum number of lines to keep in the scrollback history. - /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. - /// 0 disables the scrolling. - /// Existing terminals will not pick up this change until they are recreated. - /// See Alacritty documentation for more information. - /// - /// Default: 10_000 - pub max_scroll_history_lines: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// The minimum APCA perceptual contrast between foreground and background colors. - /// - /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, - /// especially for dark mode. Values range from 0 to 106. - /// - /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: - /// https://readtech.org/ARC/tests/bronze-simple-mode/ - /// - 0: No contrast adjustment - /// - 45: Minimum for large fluent text (36px+) - /// - 60: Minimum for other content text - /// - 75: Minimum for body text - /// - 90: Preferred for body text - /// - /// Default: 45 - pub minimum_contrast: Option, -} - -impl settings::Settings for TerminalSettings { - type FileContent = TerminalSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - let settings: Self = sources.json_merge()?; - - // Validate minimum_contrast for APCA - if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 { - anyhow::bail!( - "terminal.minimum_contrast must be between 0 and 106, but got {}. \ - APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.", - settings.minimum_contrast - ); + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(content) = &content.terminal else { + return; + }; + self.shell + .merge_from(&content.shell.clone().map(settings_shell_to_task_shell)); + self.working_directory + .merge_from(&content.working_directory); + if let Some(font_size) = content.font_size.map(px) { + self.font_size = Some(font_size) } - - Ok(settings) + if let Some(font_family) = content.font_family.clone() { + self.font_family = Some(font_family); + } + if let Some(fallbacks) = content.font_fallbacks.clone() { + self.font_fallbacks = Some(FontFallbacks::from_fonts( + fallbacks + .into_iter() + .map(|family| family.0.to_string()) + .collect(), + )) + } + if let Some(font_features) = content.font_features.clone() { + self.font_features = Some(font_features) + } + if let Some(font_weight) = content.font_weight { + self.font_weight = Some(FontWeight(font_weight)); + } + self.line_height.merge_from(&content.line_height); + if let Some(env) = &content.env { + for (key, value) in env { + self.env.insert(key.clone(), value.clone()); + } + } + if let Some(cursor_shape) = content.cursor_shape { + self.cursor_shape = Some(cursor_shape.into()) + } + self.blinking.merge_from(&content.blinking); + self.alternate_scroll.merge_from(&content.alternate_scroll); + self.option_as_meta.merge_from(&content.option_as_meta); + self.copy_on_select.merge_from(&content.copy_on_select); + self.keep_selection_on_copy + .merge_from(&content.keep_selection_on_copy); + self.button.merge_from(&content.button); + self.dock.merge_from(&content.dock); + self.default_width + .merge_from(&content.default_width.map(px)); + self.default_height + .merge_from(&content.default_height.map(px)); + self.detect_venv.merge_from(&content.detect_venv); + if let Some(max_scroll_history_lines) = content.max_scroll_history_lines { + self.max_scroll_history_lines = Some(max_scroll_history_lines) + } + self.toolbar.breadcrumbs.merge_from( + &content + .toolbar + .as_ref() + .and_then(|toolbar| toolbar.breadcrumbs), + ); + self.scrollbar.show.merge_from( + &content + .scrollbar + .as_ref() + .and_then(|scrollbar| scrollbar.show), + ); + self.minimum_contrast.merge_from(&content.minimum_contrast); } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) { + let mut default = TerminalSettingsContent::default(); + let current = content.terminal.as_mut().unwrap_or(&mut default); let name = |s| format!("terminal.integrated.{s}"); vscode.f32_setting(&name("fontSize"), &mut current.font_size); @@ -290,9 +203,9 @@ impl settings::Settings for TerminalSettings { &name("cursorStyle"), &mut current.cursor_shape, |s| match s { - "block" => Some(CursorShape::Block), - "line" => Some(CursorShape::Bar), - "underline" => Some(CursorShape::Underline), + "block" => Some(CursorShapeContent::Block), + "line" => Some(CursorShapeContent::Bar), + "underline" => Some(CursorShapeContent::Underline), _ => None, }, ); @@ -316,7 +229,7 @@ impl settings::Settings for TerminalSettings { // TODO: handle arguments let shell_name = format!("{platform}Exec"); if let Some(s) = vscode.read_string(&name(&shell_name)) { - current.shell = Some(Shell::Program(s.to_owned())) + current.shell = Some(settings::Shell::Program(s.to_owned())) } if let Some(env) = vscode @@ -337,81 +250,13 @@ impl settings::Settings for TerminalSettings { } } } + // todo!() test that this works. + if content.terminal.is_none() && default != TerminalSettingsContent::default() { + content.terminal = Some(default) + } } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - /// Use a line height that's comfortable for reading, 1.618 - #[default] - Comfortable, - /// Use a standard line height, 1.3. This option is useful for TUIs, - /// particularly if they use box characters - Standard, - /// Use a custom line height. - Custom(f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> AbsoluteLength { - let value = match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - }; - px(value).into() - } -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - /// Never blink the cursor, ignoring the terminal mode. - Off, - /// Default the cursor blink to off, but allow the terminal to - /// set blinking. - TerminalControlled, - /// Always blink the cursor, ignoring the terminal mode. - On, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - /// Use the current file's project directory. Will Fallback to the - /// first project directory strategy if unsuccessful. - CurrentProjectDirectory, - /// Use the first project in this workspace's directory. - FirstProjectDirectory, - /// Always use this platform's home directory (if it can be found). - AlwaysHome, - /// Always use a specific directory. This value will be shell expanded. - /// If this path is not a valid directory the terminal will default to - /// this platform's home directory (if it can be found). - Always { directory: String }, -} - -// Toolbar related settings -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ToolbarContent { - /// Whether to display the terminal title in breadcrumbs inside the terminal pane. - /// Only shown if the terminal title is not empty. - /// - /// The shell running in the terminal needs to be configured to emit the title. - /// Example: `echo -e "\e]2;New Title\007";` - /// - /// Default: true - pub breadcrumbs: Option, -} - #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CursorShape { @@ -426,6 +271,17 @@ pub enum CursorShape { Hollow, } +impl From for CursorShape { + fn from(value: settings::CursorShapeContent) -> Self { + match value { + settings::CursorShapeContent::Block => CursorShape::Block, + settings::CursorShapeContent::Underline => CursorShape::Underline, + settings::CursorShapeContent::Bar => CursorShape::Bar, + settings::CursorShapeContent::Hollow => CursorShape::Hollow, + } + } +} + impl From for AlacCursorShape { fn from(value: CursorShape) -> Self { match value { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 1c2d834027103c6b38593657d2ffb3a27fe2b44b..7328c4fcccda5130777c38cefad7ded10476d371 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -13,10 +13,8 @@ use gpui::{ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ - FontFamilyName, IconThemeName, ParameterizedJsonSchema, Settings, SettingsContent, ThemeMode, - ThemeName, -}; +pub use settings::{FontFamilyName, IconThemeName, ThemeMode, ThemeName}; +use settings::{ParameterizedJsonSchema, Settings, SettingsContent}; use std::sync::Arc; use util::schemars::replace_subschema; use util::{MergeFrom, ResultExt as _}; @@ -782,7 +780,8 @@ fn clamp_font_weight(weight: f32) -> FontWeight { FontWeight(weight.clamp(100., 950.)) } -fn font_fallbacks_from_settings( +/// font fallback from settings +pub fn font_fallbacks_from_settings( fallbacks: Option>, ) -> Option { fallbacks.map(|fallbacks| { From 809d2810c16fec4b3fc022d10ff82f0a5ea8f224 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 23:42:32 -0600 Subject: [PATCH 020/117] theme importer --- crates/theme/src/schema.rs | 41 ++----------------- crates/theme_importer/src/vscode/converter.rs | 4 +- 2 files changed, 6 insertions(+), 39 deletions(-) diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 54bc12e5909833bdbef87eb8f8241ae272417f19..945517daea904e3bb5bdfe4954813a81af2a9e2b 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -1,12 +1,12 @@ #![allow(missing_docs)] -use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance}; +use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla}; use indexmap::IndexMap; use palette::FromColor; -use schemars::{JsonSchema, JsonSchema_repr}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; use settings::{AccentContent, PlayerColorContent}; +pub use settings::{FontWeightContent, WindowBackgroundContent}; use crate::{StatusColorsRefinement, ThemeColorsRefinement}; @@ -33,25 +33,6 @@ pub enum AppearanceContent { Dark, } -/// The background appearance of the window. -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WindowBackgroundContent { - Opaque, - Transparent, - Blurred, -} - -impl From for WindowBackgroundAppearance { - fn from(value: WindowBackgroundContent) -> Self { - match value { - WindowBackgroundContent::Opaque => WindowBackgroundAppearance::Opaque, - WindowBackgroundContent::Transparent => WindowBackgroundAppearance::Transparent, - WindowBackgroundContent::Blurred => WindowBackgroundAppearance::Blurred, - } - } -} - /// The content of a serialized theme family. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ThemeFamilyContent { @@ -73,7 +54,7 @@ pub struct ThemeContent { #[serde(default)] pub struct ThemeStyleContent { #[serde(default, rename = "background.appearance")] - pub window_background_appearance: Option, + pub window_background_appearance: Option, #[serde(default)] pub accents: Vec, @@ -290,20 +271,6 @@ pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> Statu } } -#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] -#[repr(u16)] -pub enum FontWeightContent { - Thin = 100, - ExtraLight = 200, - Light = 300, - Normal = 400, - Medium = 500, - Semibold = 600, - Bold = 700, - ExtraBold = 800, - Black = 900, -} - pub fn theme_colors_refinement( this: &settings::ThemeColorsContent, status_colors: &StatusColorsRefinement, diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index b3b846d91d5ac3a7e3d88983787d23fe9f0adece..b0fd312fff7cb45ed80743187ca313e3957b3fba 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -3,7 +3,7 @@ use indexmap::IndexMap; use strum::IntoEnumIterator; use theme::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, - ThemeColorsContent, ThemeContent, ThemeStyleContent, + ThemeColorsContent, ThemeContent, ThemeStyleContent, WindowBackgroundContent, }; use crate::ThemeMetadata; @@ -56,7 +56,7 @@ impl VsCodeThemeConverter { name: self.theme_metadata.name, appearance, style: ThemeStyleContent { - window_background_appearance: Some(theme::WindowBackgroundContent::Opaque), + window_background_appearance: Some(WindowBackgroundContent::Opaque), accents: Vec::new(), //TODO can we read this from the theme? colors: theme_colors, status: status_colors, From 286fb0437a934cb6c43c2933c58ec047f19408cb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 15 Sep 2025 23:48:19 -0600 Subject: [PATCH 021/117] TEMP --- crates/settings/src/settings_content.rs | 42 ++++++++++++++++++- crates/worktree/src/worktree_settings.rs | 52 ++++-------------------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index f1547f769d432fa400ea068622f969f9f7c0d183..30b762af2e06be684b3153855a930b3c3eab7596 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -132,10 +132,13 @@ pub enum BaseKeymapContent { None, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettingsContent { #[serde(flatten)] pub all_languages: AllLanguageSettingsContent, + + #[serde(flatten)] + pub worktree: WorktreeSettingsContent, } #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -225,3 +228,40 @@ pub enum GitHostingProviderKind { Gitlab, Bitbucket, } + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct WorktreeSettingsContent { + /// The displayed name of this project. If not set, the root directory name + /// will be displayed. + /// + /// Default: none + pub project_name: Option, + + /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides + /// `file_scan_inclusions`. + /// + /// Default: [ + /// "**/.git", + /// "**/.svn", + /// "**/.hg", + /// "**/.jj", + /// "**/CVS", + /// "**/.DS_Store", + /// "**/Thumbs.db", + /// "**/.classpath", + /// "**/.settings" + /// ] + pub file_scan_exclusions: Option>, + + /// Always include files that match these globs when scanning for files, even if they're + /// ignored by git. This setting is overridden by `file_scan_exclusions`. + /// Default: [ + /// ".env*", + /// "docker-compose.*.yml", + /// ] + pub file_scan_inclusions: Option>, + + /// Treat the files matching these globs as `.env` files. + /// Default: [ "**/.env*" ] + pub private_files: Option>, +} diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index ca7714fa7315117a5c2e31b06d2f059ad43cb1a9..18aa9c0ae41eca95a77eeff2a8b57ab943f1f2b0 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -4,7 +4,7 @@ use anyhow::Context as _; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsKey, SettingsUi}; use util::paths::PathMatcher; #[derive(Clone, PartialEq, Eq)] @@ -32,50 +32,14 @@ impl WorktreeSettings { } } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] -pub struct WorktreeSettingsContent { - /// The displayed name of this project. If not set, the root directory name - /// will be displayed. - /// - /// Default: none - #[serde(default)] - pub project_name: Option, - - /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides - /// `file_scan_inclusions`. - /// - /// Default: [ - /// "**/.git", - /// "**/.svn", - /// "**/.hg", - /// "**/.jj", - /// "**/CVS", - /// "**/.DS_Store", - /// "**/Thumbs.db", - /// "**/.classpath", - /// "**/.settings" - /// ] - #[serde(default)] - pub file_scan_exclusions: Option>, - - /// Always include files that match these globs when scanning for files, even if they're - /// ignored by git. This setting is overridden by `file_scan_exclusions`. - /// Default: [ - /// ".env*", - /// "docker-compose.*.yml", - /// ] - #[serde(default)] - pub file_scan_inclusions: Option>, - - /// Treat the files matching these globs as `.env` files. - /// Default: [ "**/.env*" ] - pub private_files: Option>, -} - impl Settings for WorktreeSettings { - type FileContent = WorktreeSettingsContent; - + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + project_name: None, + file_scan_exclusions: content.project.worktree.file_scan_exclusions.unwrap(), + file_scan_inclusions: PathMatcher::default(), + } + } fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { let result: WorktreeSettingsContent = sources.json_merge()?; let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default(); From f5c93f2f1a37b08541e6c2cf58dace4a5c887d7f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 00:05:07 -0600 Subject: [PATCH 022/117] worktree --- crates/settings/src/settings_content.rs | 4 +- crates/settings/src/settings_store.rs | 1 + crates/worktree/src/worktree_settings.rs | 98 ++++++++++++++++-------- crates/worktree/src/worktree_tests.rs | 39 +++++----- 4 files changed, 89 insertions(+), 53 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 30b762af2e06be684b3153855a930b3c3eab7596..54c25198c4f0d6f56e963cd1670caf4ee40815e4 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -132,7 +132,7 @@ pub enum BaseKeymapContent { None, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettingsContent { #[serde(flatten)] pub all_languages: AllLanguageSettingsContent, @@ -229,7 +229,7 @@ pub enum GitHostingProviderKind { Bitbucket, } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct WorktreeSettingsContent { /// The displayed name of this project. If not set, the root directory name /// will be displayed. diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 93ec6cc995a7e14f8eaf3aafad65dbdd7b35e7ef..7f6d332c4f6e5084c759dd475886fe454f2375a6 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -765,6 +765,7 @@ impl SettingsStore { self.extension_settings = Some(SettingsContent { project: ProjectSettingsContent { all_languages: content.all_languages, + ..Default::default() }, ..Default::default() }); diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 18aa9c0ae41eca95a77eeff2a8b57ab943f1f2b0..92f1d7220c3984bea98f439dfca51890c2d2e473 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -2,10 +2,8 @@ use std::path::Path; use anyhow::Context as _; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsUi}; -use util::paths::PathMatcher; +use settings::{Settings, SettingsContent}; +use util::{ResultExt, paths::PathMatcher}; #[derive(Clone, PartialEq, Eq)] pub struct WorktreeSettings { @@ -34,19 +32,11 @@ impl WorktreeSettings { impl Settings for WorktreeSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { - Self { - project_name: None, - file_scan_exclusions: content.project.worktree.file_scan_exclusions.unwrap(), - file_scan_inclusions: PathMatcher::default(), - } - } - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - let result: WorktreeSettingsContent = sources.json_merge()?; - let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default(); - let mut private_files = result.private_files.unwrap_or_default(); - let mut parsed_file_scan_inclusions: Vec = result - .file_scan_inclusions - .unwrap_or_default() + let worktree = content.project.worktree.clone(); + let file_scan_exclusions = worktree.file_scan_exclusions.unwrap(); + let file_scan_inclusions = worktree.file_scan_inclusions.unwrap(); + let private_files = worktree.private_files.unwrap(); + let parsed_file_scan_inclusions: Vec = file_scan_inclusions .iter() .flat_map(|glob| { Path::new(glob) @@ -55,30 +45,71 @@ impl Settings for WorktreeSettings { }) .filter(|p: &String| !p.is_empty()) .collect(); - file_scan_exclusions.sort(); - private_files.sort(); - parsed_file_scan_inclusions.sort(); - Ok(Self { - file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?, - private_files: path_matchers(&private_files, "private_files")?, + + Self { + project_name: None, + file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions") + .unwrap(), file_scan_inclusions: path_matchers( - &parsed_file_scan_inclusions, + parsed_file_scan_inclusions, "file_scan_inclusions", - )?, - project_name: result.project_name, - }) + ) + .unwrap(), + private_files: path_matchers(private_files, "private_files").unwrap(), + } + } + + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { + let worktree = &content.project.worktree; + + if let Some(project_name) = worktree.project_name.clone() { + self.project_name = Some(project_name); + } + + // todo!() test this. Did it used to extend the arrays, or overwrite them? + + if let Some(private_files) = worktree.private_files.clone() { + if let Some(matchers) = path_matchers(private_files, "private_files").log_err() { + self.private_files = matchers + } + } + + if let Some(file_scan_exclusions) = worktree.file_scan_exclusions.clone() { + if let Some(matchers) = + path_matchers(file_scan_exclusions, "file_scan_exclusions").log_err() + { + self.file_scan_exclusions = matchers + } + } + + if let Some(file_scan_inclusions) = worktree.file_scan_inclusions.clone() { + let parsed_file_scan_inclusions: Vec = file_scan_inclusions + .iter() + .flat_map(|glob| { + Path::new(glob) + .ancestors() + .map(|a| a.to_string_lossy().into()) + }) + .filter(|p: &String| !p.is_empty()) + .collect(); + if let Some(matchers) = + path_matchers(parsed_file_scan_inclusions, "file_scan_inclusions").log_err() + { + self.file_scan_inclusions = matchers + } + } } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { if let Some(inclusions) = vscode .read_value("files.watcherInclude") .and_then(|v| v.as_array()) .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) { - if let Some(old) = current.file_scan_inclusions.as_mut() { + if let Some(old) = current.project.worktree.file_scan_inclusions.as_mut() { old.extend(inclusions) } else { - current.file_scan_inclusions = Some(inclusions) + current.project.worktree.file_scan_inclusions = Some(inclusions) } } if let Some(exclusions) = vscode @@ -86,15 +117,16 @@ impl Settings for WorktreeSettings { .and_then(|v| v.as_array()) .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) { - if let Some(old) = current.file_scan_exclusions.as_mut() { + if let Some(old) = current.project.worktree.file_scan_exclusions.as_mut() { old.extend(exclusions) } else { - current.file_scan_exclusions = Some(exclusions) + current.project.worktree.file_scan_exclusions = Some(exclusions) } } } } -fn path_matchers(values: &[String], context: &'static str) -> anyhow::Result { +fn path_matchers(mut values: Vec, context: &'static str) -> anyhow::Result { + values.sort(); PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context)) } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 796840367a0dca717fda7f3973eb8bcc17dc5f57..87db17347e59a07f9c4a9456e9e7c7a74af4853f 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -770,9 +770,9 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) { })); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(vec![]); - project_settings.file_scan_inclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_inclusions = Some(vec![ "node_modules/**/package.json".to_string(), "**/.DS_Store".to_string(), ]); @@ -836,9 +836,11 @@ async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]); - project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]); + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = + Some(vec!["**/.DS_Store".to_string()]); + settings.project.worktree.file_scan_inclusions = + Some(vec!["**/.DS_Store".to_string()]); }); }); }); @@ -894,9 +896,10 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(vec![]); - project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]); + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_inclusions = + Some(vec!["node_modules/**".to_string()]); }); }); }); @@ -926,9 +929,9 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(vec![]); - project_settings.file_scan_inclusions = Some(vec![]); + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_inclusions = Some(vec![]); }); }); }); @@ -978,8 +981,8 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { })); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]); }); }); @@ -1015,8 +1018,8 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/node_modules/**".to_string()]); }); }); @@ -1080,8 +1083,8 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { })); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.git".to_string(), "node_modules/".to_string(), "build_output".to_string(), From 81af11cc58595afb96540a9b4dd5477c33aa46df Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 00:17:21 -0600 Subject: [PATCH 023/117] client --- crates/client/src/client.rs | 96 ++++++++++++------------- crates/settings/src/settings_content.rs | 20 ++++++ 2 files changed, 68 insertions(+), 48 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index cb8185c7ed326ed7d45726a99077c53903118316..e3f87645953b2d8efff862bc50c51f25f1f04bf7 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel}; use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::{Settings, SettingsContent, SettingsKey, SettingsUi}; use std::{ any::TypeId, convert::TryFrom, @@ -50,7 +50,7 @@ use telemetry::Telemetry; use thiserror::Error; use tokio::net::TcpStream; use url::Url; -use util::{ConnectionResult, ResultExt}; +use util::{ConnectionResult, MergeFrom, ResultExt}; pub use rpc::*; pub use telemetry_events::Event; @@ -96,29 +96,25 @@ actions!( ] ); -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] -pub struct ClientSettingsContent { - server_url: Option, -} - #[derive(Deserialize)] pub struct ClientSettings { pub server_url: String, } impl Settings for ClientSettings { - type FileContent = ClientSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + server_url: content.server_url.clone().unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - let mut result = sources.json_merge::()?; - if let Some(server_url) = &*ZED_SERVER_URL { - result.server_url.clone_from(server_url) + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + if let Some(server_url) = content.server_url.clone() { + self.server_url = server_url } - Ok(result) } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {} } #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] @@ -133,19 +129,19 @@ pub struct ProxySettings { } impl Settings for ProxySettings { - type FileContent = ProxySettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - Ok(Self { - proxy: sources - .user - .or(sources.server) - .and_then(|value| value.proxy.clone()) - .or(sources.default.proxy.clone()), - }) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + proxy: content.proxy.clone(), + } + } + + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + if let Some(proxy) = content.proxy.clone() { + self.proxy = Some(proxy) + } } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { vscode.string_setting("http.proxy", &mut current.proxy); } } @@ -524,37 +520,41 @@ pub struct TelemetrySettings { pub metrics: bool, } -/// Control what info is collected by Zed. -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "telemetry")] -pub struct TelemetrySettingsContent { - /// Send debug info like crash reports. - /// - /// Default: true - pub diagnostics: Option, - /// Send anonymized usage data like what languages you're using Zed with. - /// - /// Default: true - pub metrics: Option, -} - impl settings::Settings for TelemetrySettings { - type FileContent = TelemetrySettingsContent; + fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self { + Self { + diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(), + metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &SettingsContent, cx: &mut App) { + let Some(telemetry) = &content.telemetry else { + return; + }; + self.diagnostics.merge_from(&telemetry.diagnostics); + self.metrics.merge_from(&telemetry.metrics); } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.enum_setting("telemetry.telemetryLevel", &mut current.metrics, |s| { + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { + let mut telemetry = settings::TelemetrySettingsContent::default(); + vscode.enum_setting("telemetry.telemetryLevel", &mut telemetry.metrics, |s| { Some(s == "all") }); - vscode.enum_setting("telemetry.telemetryLevel", &mut current.diagnostics, |s| { - Some(matches!(s, "all" | "error" | "crash")) - }); + vscode.enum_setting( + "telemetry.telemetryLevel", + &mut telemetry.diagnostics, + |s| Some(matches!(s, "all" | "error" | "crash")), + ); // we could translate telemetry.telemetryLevel, but just because users didn't want // to send microsoft telemetry doesn't mean they don't want to send it to zed. their // all/error/crash/off correspond to combinations of our "diagnostics" and "metrics". + if let Some(diagnostics) = telemetry.diagnostics { + current.telemetry.get_or_insert_default().diagnostics = Some(diagnostics) + } + if let Some(metrics) = telemetry.metrics { + current.telemetry.get_or_insert_default().metrics = Some(metrics) + } } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 54c25198c4f0d6f56e963cd1670caf4ee40815e4..6fee5fad886ac47ec97cb4a87a975ff2406e4861 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -43,6 +43,14 @@ pub struct SettingsContent { /// Example: {"log": {"client": "warn"}} pub log: Option>, + pub proxy: Option, + + /// The URL of the Zed server to connect to. + pub server_url: Option, + + /// Control what info is collected by Zed. + pub telemetry: Option, + /// Configuration of the terminal in Zed. pub terminal: Option, @@ -265,3 +273,15 @@ pub struct WorktreeSettingsContent { /// Default: [ "**/.env*" ] pub private_files: Option>, } +/// Control what info is collected by Zed. +#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)] +pub struct TelemetrySettingsContent { + /// Send debug info like crash reports. + /// + /// Default: true + pub diagnostics: Option, + /// Send anonymized usage data like what languages you're using Zed with. + /// + /// Default: true + pub metrics: Option, +} From 425ecbbef7d45a470983ba8f0622ad48b0c33fee Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 09:00:23 -0600 Subject: [PATCH 024/117] Fix SERVER_URL env var --- crates/client/src/client.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e3f87645953b2d8efff862bc50c51f25f1f04bf7..a1ac476bbba40d97d611c3016c0f06a6cb08f2ae 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -103,14 +103,22 @@ pub struct ClientSettings { impl Settings for ClientSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + if let Some(server_url) = &*ZED_SERVER_URL { + return Self { + server_url: server_url.clone(), + }; + } Self { server_url: content.server_url.clone().unwrap(), } } fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + if ZED_SERVER_URL.is_some() { + return; + } if let Some(server_url) = content.server_url.clone() { - self.server_url = server_url + self.server_url = server_url; } } @@ -528,7 +536,7 @@ impl settings::Settings for TelemetrySettings { } } - fn refine(&mut self, content: &SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { let Some(telemetry) = &content.telemetry else { return; }; From 37000a261ed878574ba377f970a035696eb89d4d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 09:34:34 -0600 Subject: [PATCH 025/117] Agent settings Co-authored-by: Ben Kunkle --- crates/settings/src/settings_content/agent.rs | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 crates/settings/src/settings_content/agent.rs diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs new file mode 100644 index 0000000000000000000000000000000000000000..370b69b070db50136db4a59d832a7a1bb2ca55e5 --- /dev/null +++ b/crates/settings/src/settings_content/agent.rs @@ -0,0 +1,264 @@ +use collections::IndexMap; +use gpui::SharedString; +use schemars::{JsonSchema, json_schema}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, sync::Arc}; + +#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)] +pub struct AgentSettingsContent { + /// Whether the Agent is enabled. + /// + /// Default: true + pub enabled: Option, + /// Whether to show the agent panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Where to dock the agent panel. + /// + /// Default: right + pub dock: Option, + /// Default width in pixels when the agent panel is docked to the left or right. + /// + /// Default: 640 + pub default_width: Option, + /// Default height in pixels when the agent panel is docked to the bottom. + /// + /// Default: 320 + pub default_height: Option, + /// The default model to use when creating new chats and for other features when a specific model is not specified. + pub default_model: Option, + /// Model to use for the inline assistant. Defaults to default_model when not specified. + pub inline_assistant_model: Option, + /// Model to use for generating git commit messages. Defaults to default_model when not specified. + pub commit_message_model: Option, + /// Model to use for generating thread summaries. Defaults to default_model when not specified. + pub thread_summary_model: Option, + /// Additional models with which to generate alternatives when performing inline assists. + pub inline_alternatives: Option>, + /// The default profile to use in the Agent. + /// + /// Default: write + pub default_profile: Option>, + /// Which view type to show by default in the agent panel. + /// + /// Default: "thread" + pub default_view: Option, + /// The available agent profiles. + pub profiles: Option, AgentProfileContent>>, + /// Whenever a tool action would normally wait for your confirmation + /// that you allow it, always choose to allow it. + /// + /// This setting has no effect on external agents that support permission modes, such as Claude Code. + /// + /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code. + /// + /// Default: false + pub always_allow_tool_actions: Option, + /// Where to show a popup notification when the agent is waiting for user input. + /// + /// Default: "primary_screen" + pub notify_when_agent_waiting: Option, + /// Whether to play a sound when the agent has either completed its response, or needs user input. + /// + /// Default: false + pub play_sound_when_agent_done: Option, + /// Whether to stream edits from the agent as they are received. + /// + /// Default: false + pub stream_edits: Option, + /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. + /// + /// Default: true + pub single_file_review: Option, + /// Additional parameters for language model requests. When making a request + /// to a model, parameters will be taken from the last entry in this list + /// that matches the model's provider and name. In each entry, both provider + /// and model are optional, so that you can specify parameters for either + /// one. + /// + /// Default: [] + #[serde(default)] + pub model_parameters: Vec, + /// What completion mode to enable for new threads + /// + /// Default: normal + pub preferred_completion_mode: Option, + /// Whether to show thumb buttons for feedback in the agent panel. + /// + /// Default: true + pub enable_feedback: Option, + /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. + /// + /// Default: true + pub expand_edit_card: Option, + /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. + /// + /// Default: true + pub expand_terminal_card: Option, + /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. + /// + /// Default: false + pub use_modifier_to_send: Option, +} + +impl AgentSettingsContent { + pub fn set_dock(&mut self, dock: AgentDockPosition) { + self.dock = Some(dock); + } + + pub fn set_model(&mut self, language_model: LanguageModelSelection) { + // let model = language_model.id().0.to_string(); + // let provider = language_model.provider_id().0.to_string(); + // self.default_model = Some(LanguageModelSelection { + // provider: provider.into(), + // model, + // }); + self.default_model = Some(language_model) + } + + pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { + self.inline_assistant_model = Some(LanguageModelSelection { + provider: provider.into(), + model, + }); + } + + pub fn set_commit_message_model(&mut self, provider: String, model: String) { + self.commit_message_model = Some(LanguageModelSelection { + provider: provider.into(), + model, + }); + } + + pub fn set_thread_summary_model(&mut self, provider: String, model: String) { + self.thread_summary_model = Some(LanguageModelSelection { + provider: provider.into(), + model, + }); + } + + pub fn set_always_allow_tool_actions(&mut self, allow: bool) { + self.always_allow_tool_actions = Some(allow); + } + + pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { + self.play_sound_when_agent_done = Some(allow); + } + + pub fn set_single_file_review(&mut self, allow: bool) { + self.single_file_review = Some(allow); + } + + pub fn set_use_modifier_to_send(&mut self, always_use: bool) { + self.use_modifier_to_send = Some(always_use); + } + + pub fn set_profile(&mut self, profile_id: Arc) { + self.default_profile = Some(profile_id); + } +} +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AgentProfileContent { + pub name: Arc, + #[serde(default)] + pub tools: IndexMap, bool>, + /// Whether all context servers are enabled by default. + pub enable_all_context_servers: Option, + #[serde(default)] + pub context_servers: IndexMap, ContextServerPresetContent>, +} + +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ContextServerPresetContent { + pub tools: IndexMap, bool>, +} + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AgentDockPosition { + Left, + #[default] + Right, + Bottom, +} + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DefaultAgentView { + #[default] + Thread, + TextThread, +} + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum NotifyWhenAgentWaiting { + #[default] + PrimaryScreen, + AllScreens, + Never, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct LanguageModelSelection { + pub provider: LanguageModelProviderSetting, + pub model: String, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] +#[serde(rename_all = "snake_case")] +pub enum CompletionMode { + #[default] + Normal, + #[serde(alias = "max")] + Burn, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct LanguageModelParameters { + pub provider: Option, + pub model: Option, + pub temperature: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct LanguageModelProviderSetting(pub String); + +impl JsonSchema for LanguageModelProviderSetting { + fn schema_name() -> Cow<'static, str> { + "LanguageModelProviderSetting".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "enum": [ + "amazon-bedrock", + "anthropic", + "copilot_chat", + "deepseek", + "google", + "lmstudio", + "mistral", + "ollama", + "openai", + "openrouter", + "vercel", + "x_ai", + "zed.dev" + ] + }) + } +} + +impl From for LanguageModelProviderSetting { + fn from(provider: String) -> Self { + Self(provider) + } +} + +impl From<&str> for LanguageModelProviderSetting { + fn from(provider: &str) -> Self { + Self(provider.to_string()) + } +} From 53e740947d243e13b90ba62e86805e9232ff5cc2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 09:35:02 -0600 Subject: [PATCH 026/117] Agent settings Co-authored-by: Ben Kunkle --- crates/agent/src/agent_profile.rs | 2 +- crates/agent_settings/src/agent_profile.rs | 97 +++- crates/agent_settings/src/agent_settings.rs | 553 +++++--------------- crates/agent_ui/src/agent_panel.rs | 2 +- crates/settings/src/settings_content.rs | 4 + 5 files changed, 205 insertions(+), 453 deletions(-) diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index c9e73372f60686cf330531926f4129e9c9b25db8..76f0199afb170e9843f32d2c3cd93d0a0c7b1b5e 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -52,7 +52,7 @@ impl AgentProfile { update_settings_file::(fs, cx, { let id = id.clone(); move |settings, _cx| { - settings.create_profile(id, profile_settings).log_err(); + profile_settings.save_to_settings(id, settings).log_err(); } }); diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 42a273e2dcfccfb839e6e7d97efb42dcd7b0bba9..2d39fe85adb92ce67006f4e0c40e8be817222f6a 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -1,15 +1,17 @@ use std::sync::Arc; +use anyhow::{Result, bail}; use collections::IndexMap; use convert_case::{Case, Casing as _}; use fs::Fs; use gpui::{App, SharedString}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; +use settings::{ + AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent, + update_settings_file, +}; use util::ResultExt as _; -use crate::AgentSettings; +use crate::{AgentProfileId, AgentSettings}; pub mod builtin_profiles { use super::AgentProfileId; @@ -23,27 +25,6 @@ pub mod builtin_profiles { } } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileId(pub Arc); - -impl AgentProfileId { - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for AgentProfileId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for AgentProfileId { - fn default() -> Self { - Self("write".into()) - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct AgentProfile { id: AgentProfileId, @@ -87,10 +68,10 @@ impl AgentProfile { .unwrap_or_default(), }; - update_settings_file::(fs, cx, { + update_settings_file(fs, cx, { let id = id.clone(); move |settings, _cx| { - settings.create_profile(id, profile_settings).log_err(); + profile_settings.save_to_settings(id, settings).log_err(); } }); @@ -129,9 +110,71 @@ impl AgentProfileSettings { .get(server_id) .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) } + + pub fn save_to_settings( + &self, + profile_id: AgentProfileId, + content: &mut SettingsContent, + ) -> Result<()> { + let profiles = content + .agent + .get_or_insert_default() + .profiles + .get_or_insert_default(); + if profiles.contains_key(&profile_id.0) { + bail!("profile with ID '{profile_id}' already exists"); + } + + profiles.insert( + profile_id.0.clone(), + AgentProfileContent { + name: self.name.clone().into(), + tools: self.tools.clone(), + enable_all_context_servers: Some(self.enable_all_context_servers), + context_servers: self + .context_servers + .clone() + .into_iter() + .map(|(server_id, preset)| { + ( + server_id, + ContextServerPresetContent { + tools: preset.tools, + }, + ) + }) + .collect(), + }, + ); + + Ok(()) + } +} + +impl From for AgentProfileSettings { + fn from(content: AgentProfileContent) -> Self { + Self { + name: content.name.into(), + tools: content.tools, + enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(), + context_servers: content + .context_servers + .into_iter() + .map(|(server_id, preset)| (server_id, preset.into())) + .collect(), + } + } } #[derive(Debug, Clone, Default)] pub struct ContextServerPreset { pub tools: IndexMap, bool>, } + +impl From for ContextServerPreset { + fn from(content: settings::ContextServerPresetContent) -> Self { + Self { + tools: content.tools, + } + } +} diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index e850945a40f46f31543fad2631216139706b405a..546594e663f642559bc9fbdba794ec574bcfd495 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -2,14 +2,16 @@ mod agent_profile; use std::sync::Arc; -use anyhow::{Result, bail}; use collections::IndexMap; -use gpui::{App, Pixels, SharedString}; +use gpui::{App, Pixels, px}; use language_model::LanguageModel; -use schemars::{JsonSchema, json_schema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; -use std::borrow::Cow; +use settings::{ + AgentDockPosition, DefaultAgentView, LanguageModelParameters, LanguageModelSelection, + NotifyWhenAgentWaiting, Settings, SettingsContent, +}; +use util::MergeFrom; pub use crate::agent_profile::*; @@ -22,32 +24,6 @@ pub fn init(cx: &mut App) { AgentSettings::register(cx); } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AgentDockPosition { - Left, - #[default] - Right, - Bottom, -} - -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum DefaultView { - #[default] - Thread, - TextThread, -} - -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum NotifyWhenAgentWaiting { - #[default] - PrimaryScreen, - AllScreens, - Never, -} - #[derive(Default, Clone, Debug)] pub struct AgentSettings { pub enabled: bool, @@ -60,9 +36,8 @@ pub struct AgentSettings { pub commit_message_model: Option, pub thread_summary_model: Option, pub inline_alternatives: Vec, - pub using_outdated_settings_version: bool, pub default_profile: AgentProfileId, - pub default_view: DefaultView, + pub default_view: DefaultAgentView, pub profiles: IndexMap, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, @@ -80,71 +55,20 @@ pub struct AgentSettings { impl AgentSettings { pub fn temperature_for_model(model: &Arc, cx: &App) -> Option { let settings = Self::get_global(cx); - settings - .model_parameters - .iter() - .rfind(|setting| setting.matches(model)) - .and_then(|m| m.temperature) - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct LanguageModelParameters { - pub provider: Option, - pub model: Option, - pub temperature: Option, -} - -impl LanguageModelParameters { - pub fn matches(&self, model: &Arc) -> bool { - if let Some(provider) = &self.provider - && provider.0 != model.provider_id().0 - { - return false; - } - if let Some(setting_model) = &self.model - && *setting_model != model.id().0 - { - return false; + for setting in settings.model_parameters.iter().rev() { + if let Some(provider) = &setting.provider + && provider.0 != model.provider_id().0 + { + continue; + } + if let Some(setting_model) = &setting.model + && *setting_model != model.id().0 + { + continue; + } + return setting.temperature; } - true - } -} - -impl AgentSettingsContent { - pub fn set_dock(&mut self, dock: AgentDockPosition) { - self.dock = Some(dock); - } - - pub fn set_model(&mut self, language_model: Arc) { - let model = language_model.id().0.to_string(); - let provider = language_model.provider_id().0.to_string(); - - self.default_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); + return None; } pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { @@ -167,159 +91,6 @@ impl AgentSettingsContent { model, }); } - - pub fn set_always_allow_tool_actions(&mut self, allow: bool) { - self.always_allow_tool_actions = Some(allow); - } - - pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { - self.play_sound_when_agent_done = Some(allow); - } - - pub fn set_single_file_review(&mut self, allow: bool) { - self.single_file_review = Some(allow); - } - - pub fn set_use_modifier_to_send(&mut self, always_use: bool) { - self.use_modifier_to_send = Some(always_use); - } - - pub fn set_profile(&mut self, profile_id: AgentProfileId) { - self.default_profile = Some(profile_id); - } - - pub fn create_profile( - &mut self, - profile_id: AgentProfileId, - profile_settings: AgentProfileSettings, - ) -> Result<()> { - let profiles = self.profiles.get_or_insert_default(); - if profiles.contains_key(&profile_id) { - bail!("profile with ID '{profile_id}' already exists"); - } - - profiles.insert( - profile_id, - AgentProfileContent { - name: profile_settings.name.into(), - tools: profile_settings.tools, - enable_all_context_servers: Some(profile_settings.enable_all_context_servers), - context_servers: profile_settings - .context_servers - .into_iter() - .map(|(server_id, preset)| { - ( - server_id, - ContextServerPresetContent { - tools: preset.tools, - }, - ) - }) - .collect(), - }, - ); - - Ok(()) - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)] -#[settings_key(key = "agent", fallback_key = "assistant")] -pub struct AgentSettingsContent { - /// Whether the Agent is enabled. - /// - /// Default: true - enabled: Option, - /// Whether to show the agent panel button in the status bar. - /// - /// Default: true - button: Option, - /// Where to dock the agent panel. - /// - /// Default: right - dock: Option, - /// Default width in pixels when the agent panel is docked to the left or right. - /// - /// Default: 640 - default_width: Option, - /// Default height in pixels when the agent panel is docked to the bottom. - /// - /// Default: 320 - default_height: Option, - /// The default model to use when creating new chats and for other features when a specific model is not specified. - default_model: Option, - /// Model to use for the inline assistant. Defaults to default_model when not specified. - inline_assistant_model: Option, - /// Model to use for generating git commit messages. Defaults to default_model when not specified. - commit_message_model: Option, - /// Model to use for generating thread summaries. Defaults to default_model when not specified. - thread_summary_model: Option, - /// Additional models with which to generate alternatives when performing inline assists. - inline_alternatives: Option>, - /// The default profile to use in the Agent. - /// - /// Default: write - default_profile: Option, - /// Which view type to show by default in the agent panel. - /// - /// Default: "thread" - default_view: Option, - /// The available agent profiles. - pub profiles: Option>, - /// Whenever a tool action would normally wait for your confirmation - /// that you allow it, always choose to allow it. - /// - /// This setting has no effect on external agents that support permission modes, such as Claude Code. - /// - /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code. - /// - /// Default: false - always_allow_tool_actions: Option, - /// Where to show a popup notification when the agent is waiting for user input. - /// - /// Default: "primary_screen" - notify_when_agent_waiting: Option, - /// Whether to play a sound when the agent has either completed its response, or needs user input. - /// - /// Default: false - play_sound_when_agent_done: Option, - /// Whether to stream edits from the agent as they are received. - /// - /// Default: false - stream_edits: Option, - /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. - /// - /// Default: true - single_file_review: Option, - /// Additional parameters for language model requests. When making a request - /// to a model, parameters will be taken from the last entry in this list - /// that matches the model's provider and name. In each entry, both provider - /// and model are optional, so that you can specify parameters for either - /// one. - /// - /// Default: [] - #[serde(default)] - model_parameters: Vec, - /// What completion mode to enable for new threads - /// - /// Default: normal - preferred_completion_mode: Option, - /// Whether to show thumb buttons for feedback in the agent panel. - /// - /// Default: true - enable_feedback: Option, - /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. - /// - /// Default: true - expand_edit_card: Option, - /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. - /// - /// Default: true - expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. - /// - /// Default: false - use_modifier_to_send: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] @@ -340,202 +111,136 @@ impl From for cloud_llm_client::CompletionMode { } } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct LanguageModelSelection { - pub provider: LanguageModelProviderSetting, - pub model: String, +impl From for CompletionMode { + fn from(value: settings::CompletionMode) -> Self { + match value { + settings::CompletionMode::Normal => CompletionMode::Normal, + settings::CompletionMode::Burn => CompletionMode::Burn, + } + } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct LanguageModelProviderSetting(pub String); - -impl JsonSchema for LanguageModelProviderSetting { - fn schema_name() -> Cow<'static, str> { - "LanguageModelProviderSetting".into() - } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] +pub struct AgentProfileId(pub Arc); - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "enum": [ - "amazon-bedrock", - "anthropic", - "copilot_chat", - "deepseek", - "google", - "lmstudio", - "mistral", - "ollama", - "openai", - "openrouter", - "vercel", - "x_ai", - "zed.dev" - ] - }) +impl AgentProfileId { + pub fn as_str(&self) -> &str { + &self.0 } } -impl From for LanguageModelProviderSetting { - fn from(provider: String) -> Self { - Self(provider) +impl std::fmt::Display for AgentProfileId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } -impl From<&str> for LanguageModelProviderSetting { - fn from(provider: &str) -> Self { - Self(provider.to_string()) +impl Default for AgentProfileId { + fn default() -> Self { + Self("write".into()) } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileContent { - pub name: Arc, - #[serde(default)] - pub tools: IndexMap, bool>, - /// Whether all context servers are enabled by default. - pub enable_all_context_servers: Option, - #[serde(default)] - pub context_servers: IndexMap, ContextServerPresetContent>, -} - -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct ContextServerPresetContent { - pub tools: IndexMap, bool>, -} - impl Settings for AgentSettings { - const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]); - - type FileContent = AgentSettingsContent; - - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - let mut settings = AgentSettings::default(); + // todo!() test preserved keys logic + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let agent = content.agent.clone().unwrap(); + Self { + enabled: agent.enabled.unwrap(), + button: agent.button.unwrap(), + dock: agent.dock.unwrap(), + default_width: px(agent.default_width.unwrap()), + default_height: px(agent.default_height.unwrap()), + default_model: Some(agent.default_model.unwrap()), + inline_assistant_model: agent.inline_assistant_model, + commit_message_model: agent.commit_message_model, + thread_summary_model: agent.thread_summary_model, + inline_alternatives: agent.inline_alternatives.unwrap_or_default(), + default_profile: AgentProfileId(agent.default_profile.unwrap()), + default_view: agent.default_view.unwrap(), + profiles: agent + .profiles + .unwrap() + .into_iter() + .map(|(key, val)| (AgentProfileId(key), val.into())) + .collect(), + always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), + notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), + play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), + stream_edits: agent.stream_edits.unwrap(), + single_file_review: agent.single_file_review.unwrap(), + model_parameters: agent.model_parameters, + preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(), + enable_feedback: agent.enable_feedback.unwrap(), + expand_edit_card: agent.expand_edit_card.unwrap(), + expand_terminal_card: agent.expand_terminal_card.unwrap(), + use_modifier_to_send: agent.use_modifier_to_send.unwrap(), + } + } - for value in sources.defaults_and_customizations() { - merge(&mut settings.enabled, value.enabled); - merge(&mut settings.button, value.button); - merge(&mut settings.dock, value.dock); - merge( - &mut settings.default_width, - value.default_width.map(Into::into), - ); - merge( - &mut settings.default_height, - value.default_height.map(Into::into), - ); - settings.default_model = value - .default_model - .clone() - .or(settings.default_model.take()); - settings.inline_assistant_model = value - .inline_assistant_model - .clone() - .or(settings.inline_assistant_model.take()); - settings.commit_message_model = value - .clone() - .commit_message_model - .or(settings.commit_message_model.take()); - settings.thread_summary_model = value - .clone() - .thread_summary_model - .or(settings.thread_summary_model.take()); - merge( - &mut settings.inline_alternatives, - value.inline_alternatives.clone(), - ); - merge( - &mut settings.notify_when_agent_waiting, - value.notify_when_agent_waiting, - ); - merge( - &mut settings.play_sound_when_agent_done, - value.play_sound_when_agent_done, - ); - merge(&mut settings.stream_edits, value.stream_edits); - merge(&mut settings.single_file_review, value.single_file_review); - merge(&mut settings.default_profile, value.default_profile.clone()); - merge(&mut settings.default_view, value.default_view); - merge( - &mut settings.preferred_completion_mode, - value.preferred_completion_mode, - ); - merge(&mut settings.enable_feedback, value.enable_feedback); - merge(&mut settings.expand_edit_card, value.expand_edit_card); - merge( - &mut settings.expand_terminal_card, - value.expand_terminal_card, - ); - merge( - &mut settings.use_modifier_to_send, - value.use_modifier_to_send, + fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { + let Some(value) = &content.agent else { return }; + self.enabled.merge_from(&value.enabled); + self.button.merge_from(&value.button); + self.dock.merge_from(&value.dock); + self.default_width + .merge_from(&value.default_width.map(Into::into)); + self.default_height + .merge_from(&value.default_height.map(Into::into)); + self.default_model = value.default_model.clone().or(self.default_model.take()); + + self.inline_assistant_model = value + .inline_assistant_model + .clone() + .or(self.inline_assistant_model.take()); + self.commit_message_model = value + .clone() + .commit_message_model + .or(self.commit_message_model.take()); + self.thread_summary_model = value + .clone() + .thread_summary_model + .or(self.thread_summary_model.take()); + self.inline_alternatives + .merge_from(&value.inline_alternatives.clone()); + self.notify_when_agent_waiting + .merge_from(&value.notify_when_agent_waiting); + self.play_sound_when_agent_done + .merge_from(&value.play_sound_when_agent_done); + self.stream_edits.merge_from(&value.stream_edits); + self.single_file_review + .merge_from(&value.single_file_review); + self.default_profile + .merge_from(&value.default_profile.clone().map(AgentProfileId)); + self.default_view.merge_from(&value.default_view); + self.preferred_completion_mode + .merge_from(&value.preferred_completion_mode.map(Into::into)); + self.enable_feedback.merge_from(&value.enable_feedback); + self.expand_edit_card.merge_from(&value.expand_edit_card); + self.expand_terminal_card + .merge_from(&value.expand_terminal_card); + self.use_modifier_to_send + .merge_from(&value.use_modifier_to_send); + + self.model_parameters + .extend_from_slice(&value.model_parameters); + + if let Some(profiles) = value.profiles.as_ref() { + self.profiles.extend( + profiles + .into_iter() + .map(|(id, profile)| (AgentProfileId(id.clone()), profile.clone().into())), ); - - settings - .model_parameters - .extend_from_slice(&value.model_parameters); - - if let Some(profiles) = value.profiles.as_ref() { - settings - .profiles - .extend(profiles.into_iter().map(|(id, profile)| { - ( - id.clone(), - AgentProfileSettings { - name: profile.name.clone().into(), - tools: profile.tools.clone(), - enable_all_context_servers: profile - .enable_all_context_servers - .unwrap_or_default(), - context_servers: profile - .context_servers - .iter() - .map(|(context_server_id, preset)| { - ( - context_server_id.clone(), - ContextServerPreset { - tools: preset.tools.clone(), - }, - ) - }) - .collect(), - }, - ) - })); - } } - - debug_assert!( - !sources.default.always_allow_tool_actions.unwrap_or(false), - "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!" - ); - - // For security reasons, only trust the user's global settings for whether to always allow tool actions. - // If this could be overridden locally, an attacker could (e.g. by committing to source control and - // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks. - settings.always_allow_tool_actions = sources - .user - .and_then(|setting| setting.always_allow_tool_actions) - .unwrap_or(false); - - Ok(settings) } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { if let Some(b) = vscode .read_value("chat.agent.enabled") .and_then(|b| b.as_bool()) { - current.enabled = Some(b); - current.button = Some(b); + current.agent.get_or_insert_default().enabled = Some(b); + current.agent.get_or_insert_default().button = Some(b); } } } - -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } -} diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7334f1cf1283ad45e0e98d296b3641c9b8812ef5..7dd72c53631fa3c818fa1f4573e65c41bb5f0944 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -33,7 +33,7 @@ use agent::{ history_store::{HistoryEntryId, HistoryStore}, thread_store::{TextThreadStore, ThreadStore}, }; -use agent_settings::{AgentDockPosition, AgentSettings, DefaultView}; +use agent_settings::{AgentDockPosition, AgentSettings, DefaultAgentView}; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 6fee5fad886ac47ec97cb4a87a975ff2406e4861..9b4750dc793a2bc072ac49a51d74ac1efb4d95db 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,6 +1,8 @@ +mod agent; mod language; mod terminal; mod theme; +pub use agent::*; pub use language::*; pub use terminal::*; pub use theme::*; @@ -23,6 +25,8 @@ pub struct SettingsContent { #[serde(flatten)] pub theme: ThemeSettingsContent, + pub agent: Option, + /// Configuration of audio in Zed. pub audio: Option, pub auto_update: Option, From 63a0cc19fb06999b3de5d595a69604eef3fb4568 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 09:47:05 -0600 Subject: [PATCH 027/117] dap settings Co-authored-by: Ben Kunkle --- crates/dap/src/debugger_settings.rs | 80 +++++++++++++----------- crates/debugger_ui/src/debugger_panel.rs | 8 +-- crates/settings/src/settings_content.rs | 56 +++++++++++++++++ 3 files changed, 103 insertions(+), 41 deletions(-) diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index 4b841450462f1f59787df584cc4ba48eddf792c1..5fa3d60dfc08cd847dd961c322555b5ef54c78f7 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -1,28 +1,12 @@ use dap_types::SteppingGranularity; -use gpui::{App, Global}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use gpui::App; +use settings::{Settings, SettingsContent}; +use util::MergeFrom; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum DebugPanelDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Serialize, Deserialize, JsonSchema, Clone, SettingsUi, SettingsKey)] -#[serde(default)] -// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug, -// it means the defaults will override previously set values if a single key is missing -#[settings_ui(group = "Debugger")] -#[settings_key(key = "debugger")] pub struct DebuggerSettings { /// Determines the stepping granularity. /// /// Default: line - #[settings_ui(skip)] pub stepping_granularity: SteppingGranularity, /// Whether the breakpoints should be reused across Zed sessions. /// @@ -47,31 +31,53 @@ pub struct DebuggerSettings { /// The dock position of the debug panel /// /// Default: Bottom - pub dock: DebugPanelDockPosition, + pub dock: settings::DockPosition, } -impl Default for DebuggerSettings { - fn default() -> Self { +impl Settings for DebuggerSettings { + fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self { + let content = content.debugger.clone().unwrap(); Self { - button: true, - save_breakpoints: true, - stepping_granularity: SteppingGranularity::Line, - timeout: 2000, - log_dap_communications: true, - format_dap_log_messages: true, - dock: DebugPanelDockPosition::Bottom, + stepping_granularity: dap_granularity_from_settings( + content.stepping_granularity.unwrap(), + ), + save_breakpoints: content.save_breakpoints.unwrap(), + button: content.button.unwrap(), + timeout: content.timeout.unwrap(), + log_dap_communications: content.log_dap_communications.unwrap(), + format_dap_log_messages: content.format_dap_log_messages.unwrap(), + dock: content.dock.unwrap(), } } -} -impl Settings for DebuggerSettings { - type FileContent = Self; - - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &SettingsContent, _cx: &mut App) { + let Some(content) = &content.debugger else { + return; + }; + self.stepping_granularity.merge_from( + &content + .stepping_granularity + .map(dap_granularity_from_settings), + ); + self.save_breakpoints.merge_from(&content.save_breakpoints); + self.button.merge_from(&content.button); + self.timeout.merge_from(&content.timeout); + self.log_dap_communications + .merge_from(&content.log_dap_communications); + self.format_dap_log_messages + .merge_from(&content.format_dap_log_messages); + self.dock.merge_from(&content.dock); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {} } -impl Global for DebuggerSettings {} +fn dap_granularity_from_settings( + granularity: settings::SteppingGranularity, +) -> dap_types::SteppingGranularity { + match granularity { + settings::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction, + settings::SteppingGranularity::Line => dap_types::SteppingGranularity::Line, + settings::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement, + } +} diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ef714a1f6710f54c5673eac097e7530b3c605b58..3001e21136ff2d1948ff4a7396798a03fd87c997 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -29,7 +29,7 @@ use project::debugger::session::{Session, SessionQuirks, SessionState, SessionSt use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; -use settings::Settings; +use settings::{DockPosition, Settings}; use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; @@ -1401,9 +1401,9 @@ impl Panel for DebugPanel { fn position(&self, _window: &Window, cx: &App) -> DockPosition { match DebuggerSettings::get_global(cx).dock { - DebugPanelDockPosition::Left => DockPosition::Left, - DebugPanelDockPosition::Bottom => DockPosition::Bottom, - DebugPanelDockPosition::Right => DockPosition::Right, + DockPosition::Left => DockPosition::Left, + DockPosition::Bottom => DockPosition::Bottom, + DockPosition::Right => DockPosition::Right, } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 9b4750dc793a2bc072ac49a51d74ac1efb4d95db..9fab8bcbf1c0a05f01eb2429bfa358f9fb324c36 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -34,6 +34,8 @@ pub struct SettingsContent { // todo!() comments?! pub base_keymap: Option, + pub debugger: Option, + /// The list of custom Git hosting providers. pub git_hosting_providers: Option>, @@ -289,3 +291,57 @@ pub struct TelemetrySettingsContent { /// Default: true pub metrics: Option, } + +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +pub struct DebuggerSettingsContent { + /// Determines the stepping granularity. + /// + /// Default: line + pub stepping_granularity: Option, + /// Whether the breakpoints should be reused across Zed sessions. + /// + /// Default: true + pub save_breakpoints: Option, + /// Whether to show the debug button in the status bar. + /// + /// Default: true + pub button: Option, + /// Time in milliseconds until timeout error when connecting to a TCP debug adapter + /// + /// Default: 2000ms + pub timeout: Option, + /// Whether to log messages between active debug adapters and Zed + /// + /// Default: true + pub log_dap_communications: Option, + /// Whether to format dap messages in when adding them to debug adapter logger + /// + /// Default: true + pub format_dap_log_messages: Option, + /// The dock position of the debug panel + /// + /// Default: Bottom + pub dock: Option, +} + +/// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`. +#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SteppingGranularity { + /// The step should allow the program to run until the current statement has finished executing. + /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. + /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. + Statement, + /// The step should allow the program to run until the current source line has executed. + Line, + /// The step should allow one instruction to execute (e.g. one x86 instruction). + Instruction, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DockPosition { + Left, + Bottom, + Right, +} From 7eaaf63791d92f7a1f37d368897771b4ca83d9ce Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 10:01:50 -0600 Subject: [PATCH 028/117] AllAgentServerSettings Co-authored-by: Ben Kunkle --- crates/project/src/agent_server_store.rs | 116 +++++++++--------- crates/settings/src/settings_content.rs | 1 + crates/settings/src/settings_content/agent.rs | 60 ++++++++- 3 files changed, 114 insertions(+), 63 deletions(-) diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index bdb2297624e4a404cb3c918f07eab15004944f97..9c50f9060665ac459ec79c53028932961eb5a7fb 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -22,8 +22,8 @@ use rpc::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{SettingsKey, SettingsSources, SettingsStore, SettingsUi}; -use util::{ResultExt as _, debug_panic}; +use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi}; +use util::{MergeFrom, ResultExt as _, debug_panic}; use crate::ProjectEnvironment; @@ -994,47 +994,19 @@ impl ExternalAgentServer for LocalCustomAgent { pub const GEMINI_NAME: &'static str = "gemini"; pub const CLAUDE_CODE_NAME: &'static str = "claude"; -#[derive( - Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq, -)] +#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)] #[settings_key(key = "agent_servers")] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, - - /// Custom agent servers configured by the user - #[serde(flatten)] pub custom: HashMap, } - -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +#[derive(Default, Clone, JsonSchema, Debug, PartialEq)] pub struct BuiltinAgentServerSettings { - /// Absolute path to a binary to be used when launching this agent. - /// - /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. - #[serde(rename = "command")] pub path: Option, - /// If a binary is specified in `command`, it will be passed these arguments. pub args: Option>, - /// If a binary is specified in `command`, it will be passed these environment variables. pub env: Option>, - /// Whether to skip searching `$PATH` for an agent server binary when - /// launching this agent. - /// - /// This has no effect if a `command` is specified. Otherwise, when this is - /// `false`, Zed will search `$PATH` for an agent server binary and, if one - /// is found, use it for threads with this agent. If no agent binary is - /// found on `$PATH`, Zed will automatically install and use its own binary. - /// When this is `true`, Zed will not search `$PATH`, and will always use - /// its own binary. - /// - /// Default: true pub ignore_system_version: Option, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None pub default_mode: Option, } @@ -1048,6 +1020,18 @@ impl BuiltinAgentServerSettings { } } +impl From for BuiltinAgentServerSettings { + fn from(value: settings::BuiltinAgentServerSettings) -> Self { + BuiltinAgentServerSettings { + path: value.path, + args: value.args, + env: value.env, + ignore_system_version: value.ignore_system_version, + default_mode: value.default_mode, + } + } +} + impl From for BuiltinAgentServerSettings { fn from(value: AgentServerCommand) -> Self { BuiltinAgentServerSettings { @@ -1059,9 +1043,8 @@ impl From for BuiltinAgentServerSettings { } } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +#[derive(Clone, JsonSchema, Debug, PartialEq)] pub struct CustomAgentServerSettings { - #[serde(flatten)] pub command: AgentServerCommand, /// The default mode to use for this agent. /// @@ -1071,36 +1054,47 @@ pub struct CustomAgentServerSettings { pub default_mode: Option, } -impl settings::Settings for AllAgentServersSettings { - type FileContent = Self; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - let mut settings = AllAgentServersSettings::default(); - - for AllAgentServersSettings { - gemini, - claude, - custom, - } in sources.defaults_and_customizations() - { - if gemini.is_some() { - settings.gemini = gemini.clone(); - } - if claude.is_some() { - settings.claude = claude.clone(); - } +impl From for CustomAgentServerSettings { + fn from(value: settings::CustomAgentServerSettings) -> Self { + CustomAgentServerSettings { + command: AgentServerCommand { + path: value.path, + args: value.args, + env: value.env, + }, + default_mode: value.default_mode, + } + } +} - // Merge custom agents - for (name, config) in custom { - // Skip built-in agent names to avoid conflicts - if name != GEMINI_NAME && name != CLAUDE_CODE_NAME { - settings.custom.insert(name.clone(), config.clone()); - } - } +impl settings::Settings for AllAgentServersSettings { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let agent_settings = content.agent_servers.clone().unwrap(); + Self { + gemini: agent_settings.gemini.map(Into::into), + claude: agent_settings.claude.map(Into::into), + custom: agent_settings + .custom + .into_iter() + .map(|(k, v)| (k.clone(), v.into())) + .collect(), } + } - Ok(settings) + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(content) = &content.agent_servers else { + return; + }; + if let Some(gemini) = content.gemini.clone() { + self.gemini = Some(gemini.into()) + }; + if let Some(claude) = content.claude.clone() { + self.claude = Some(claude.into()); + } + for (name, config) in content.custom.clone() { + self.custom.insert(name, config.into()); + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {} } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 9fab8bcbf1c0a05f01eb2429bfa358f9fb324c36..8acbf49361bd7669732e35faad84f16ec99a7431 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -26,6 +26,7 @@ pub struct SettingsContent { pub theme: ThemeSettingsContent, pub agent: Option, + pub agent_servers: Option, /// Configuration of audio in Zed. pub audio: Option, diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 370b69b070db50136db4a59d832a7a1bb2ca55e5..c051491a6537808241cc2eea30c25fa0dfd40a91 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -1,8 +1,8 @@ -use collections::IndexMap; +use collections::{HashMap, IndexMap}; use gpui::SharedString; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, sync::Arc}; #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct AgentSettingsContent { @@ -262,3 +262,59 @@ impl From<&str> for LanguageModelProviderSetting { Self(provider.to_string()) } } + +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +pub struct AllAgentServersSettings { + pub gemini: Option, + pub claude: Option, + + /// Custom agent servers configured by the user + #[serde(flatten)] + pub custom: HashMap, +} + +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +pub struct BuiltinAgentServerSettings { + /// Absolute path to a binary to be used when launching this agent. + /// + /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. + #[serde(rename = "command")] + pub path: Option, + /// If a binary is specified in `command`, it will be passed these arguments. + pub args: Option>, + /// If a binary is specified in `command`, it will be passed these environment variables. + pub env: Option>, + /// Whether to skip searching `$PATH` for an agent server binary when + /// launching this agent. + /// + /// This has no effect if a `command` is specified. Otherwise, when this is + /// `false`, Zed will search `$PATH` for an agent server binary and, if one + /// is found, use it for threads with this agent. If no agent binary is + /// found on `$PATH`, Zed will automatically install and use its own binary. + /// When this is `true`, Zed will not search `$PATH`, and will always use + /// its own binary. + /// + /// Default: true + pub ignore_system_version: Option, + /// The default mode to use for this agent. + /// + /// Note: Not only all agents support modes. + /// + /// Default: None + pub default_mode: Option, +} + +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +pub struct CustomAgentServerSettings { + #[serde(rename = "command")] + pub path: PathBuf, + #[serde(default)] + pub args: Vec, + pub env: Option>, + /// The default mode to use for this agent. + /// + /// Note: Not only all agents support modes. + /// + /// Default: None + pub default_mode: Option, +} From 48664bf514b977631f861811326d162860763224 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 16 Sep 2025 11:42:57 -0500 Subject: [PATCH 029/117] wip - disable AI --- crates/project/src/debugger/dap_store.rs | 3 +- crates/project/src/project.rs | 235 ++++++++--------------- crates/settings/src/settings_content.rs | 2 + 3 files changed, 85 insertions(+), 155 deletions(-) diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 6c1449b728d3ee5b8c8b019d5e527e9adfb3bf25..ab0d69b6d9f7da254e659a0d5e5d0cfee0322711 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -28,8 +28,9 @@ use futures::{ }; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use http_client::HttpClient; -use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; +use language::{Buffer, LanguageToolchainStore}; use node_runtime::NodeRuntime; +use settings::InlayHintKind; use remote::RemoteClient; use rpc::{ diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index db19aed6135f9de13e83fe07cbbf3c7186c0c603..fde9087e3480256fd0afe7f698fa2317e086b920 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -980,46 +980,22 @@ pub struct DisableAiSettings { pub disable_ai: bool, } -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Debug, - Default, - serde::Serialize, - serde::Deserialize, - SettingsUi, - SettingsKey, - JsonSchema, -)] -#[settings_key(None)] -pub struct DisableAiSettingContent { - pub disable_ai: Option, -} - impl settings::Settings for DisableAiSettings { - type FileContent = DisableAiSettingContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - // For security reasons, settings can only make AI restrictions MORE strict, not less. - // (For example, if someone is working on a project that contractually - // requires no AI use, that should override the user's setting which - // permits AI use.) - // This also prevents an attacker from using project or server settings to enable AI when it should be disabled. - let disable_ai = sources - .project - .iter() - .chain(sources.user.iter()) - .chain(sources.server.iter()) - .chain(sources.release_channel.iter()) - .chain(sources.profile.iter()) - .any(|disabled| disabled.disable_ai == Some(true)); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + disable_ai: content.project.disable_ai.unwrap(), + } + } - Ok(Self { disable_ai }) + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + self.disable_ai = self.disable_ai || content.project.disable_ai.unwrap_or(false); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { + } } impl Project { @@ -5668,145 +5644,96 @@ fn provide_inline_values( mod disable_ai_settings_tests { use super::*; use gpui::TestAppContext; - use settings::{Settings, SettingsSources}; + use rpc::proto::cancel_language_server_work::Work; + use settings::Settings; #[gpui::test] async fn test_disable_ai_settings_security(cx: &mut TestAppContext) { - fn disable_setting(value: Option) -> DisableAiSettingContent { - DisableAiSettingContent { disable_ai: value } - } cx.update(|cx| { + let mut store = SettingsStore::new(cx, &settings::test_settings()); + store.register_setting::(cx); + let ai_disabled = |cx| DisableAiSettings::get_global(cx).disable_ai; // Test 1: Default is false (AI enabled) - let sources = SettingsSources { - default: &DisableAiSettingContent { - disable_ai: Some(false), - }, - global: None, - extensions: None, - user: None, - release_channel: None, - operating_system: None, - profile: None, - server: None, - project: &[], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert!(!settings.disable_ai, "Default should allow AI"); + assert!(!ai_disabled(cx), "Default should allow AI"); + + let disable_true = serde_json::json!({ + "disable_ai": true + }) + .to_string(); + let disable_false = serde_json::json!({ + "disable_ai": false + }) + .to_string(); // Test 2: Global true, local false -> still disabled (local cannot re-enable) - let global_true = disable_setting(Some(true)); - let local_false = disable_setting(Some(false)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&global_true), - release_channel: None, - operating_system: None, - profile: None, - server: None, - project: &[&local_false], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert!( - settings.disable_ai, - "Local false cannot override global true" - ); + store.set_user_settings(&disable_false, cx); + assert!(ai_disabled(cx), "Local false cannot override global true"); // Test 3: Global false, local true -> disabled (local can make more restrictive) - let global_false = disable_setting(Some(false)); - let local_true = disable_setting(Some(true)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&global_false), - release_channel: None, - operating_system: None, - profile: None, - server: None, - project: &[&local_true], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert!(settings.disable_ai, "Local true can override global false"); + store.set_user_settings(&disable_false, cx); + store.set_local_settings( + WorktreeId::from_usize(0), + Path::new("project.json").into(), + settings::LocalSettingsKind::Settings, + Some(&disable_true), + cx, + ); + assert!(ai_disabled(cx), "Local true can override global false"); + store.clear_local_settings(WorktreeId::from_usize(0), cx); // Test 4: Server can only make more restrictive (set to true) - let user_false = disable_setting(Some(false)); - let server_true = disable_setting(Some(true)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&user_false), - release_channel: None, - operating_system: None, - profile: None, - server: Some(&server_true), - project: &[], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); + store.set_user_settings(&disable_false, cx); + store.set_server_settings(&disable_true, cx); assert!( - settings.disable_ai, + ai_disabled(cx), "Server can set to true even if user is false" ); // Test 5: Server false cannot override user true - let user_true = disable_setting(Some(true)); - let server_false = disable_setting(Some(false)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&user_true), - release_channel: None, - operating_system: None, - profile: None, - server: Some(&server_false), - project: &[], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert!( - settings.disable_ai, - "Server false cannot override user true" - ); + store.set_server_settings(&disable_false, cx); + store.set_user_settings(&disable_true, cx); + assert!(ai_disabled(cx), "Server false cannot override user true"); // Test 6: Multiple local settings, any true disables AI - let global_false = disable_setting(Some(false)); - let local_false3 = disable_setting(Some(false)); - let local_true2 = disable_setting(Some(true)); - let local_false4 = disable_setting(Some(false)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&global_false), - release_channel: None, - operating_system: None, - profile: None, - server: None, - project: &[&local_false3, &local_true2, &local_false4], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); - assert!(settings.disable_ai, "Any local true should disable AI"); + store.set_local_settings( + WorktreeId::from_usize(0), + Path::new("a").into(), + settings::LocalSettingsKind::Settings, + Some(&disable_false), + cx, + ); + store.set_local_settings( + WorktreeId::from_usize(1), + Path::new("b").into(), + settings::LocalSettingsKind::Settings, + Some(&disable_true), + cx, + ); + store.set_local_settings( + WorktreeId::from_usize(2), + Path::new("c").into(), + settings::LocalSettingsKind::Settings, + Some(&disable_false), + cx, + ); + store.set_user_settings(&disable_false, cx); + assert!(ai_disabled(cx), "Any local true should disable AI"); + store.clear_local_settings(WorktreeId::from_usize(0), cx); + store.clear_local_settings(WorktreeId::from_usize(1), cx); + store.clear_local_settings(WorktreeId::from_usize(2), cx); // Test 7: All three sources can independently disable AI - let user_false2 = disable_setting(Some(false)); - let server_false2 = disable_setting(Some(false)); - let local_true3 = disable_setting(Some(true)); - let sources = SettingsSources { - default: &disable_setting(Some(false)), - global: None, - extensions: None, - user: Some(&user_false2), - release_channel: None, - operating_system: None, - profile: None, - server: Some(&server_false2), - project: &[&local_true3], - }; - let settings = DisableAiSettings::load(sources, cx).unwrap(); + store.set_server_settings(&disable_false, cx); + store.set_user_settings(&disable_false, cx); + store.set_local_settings( + WorktreeId::from_usize(0), + Path::new("a").into(), + settings::LocalSettingsKind::Settings, + Some(&disable_true), + cx, + ); assert!( - settings.disable_ai, + ai_disabled(cx), "Local can disable even if user and server are false" ); }); diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 8acbf49361bd7669732e35faad84f16ec99a7431..ca1e20a30aa8373e84cf618ad03d1966e8a7865c 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -154,6 +154,8 @@ pub struct ProjectSettingsContent { #[serde(flatten)] pub worktree: WorktreeSettingsContent, + + pub disable_ai: Option, } #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] From 31aeb56e3af772aecaf129264eae170d9dbeef61 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 11:00:28 -0600 Subject: [PATCH 030/117] Project --- crates/language/src/language_settings.rs | 6 +- crates/project/src/agent_server_store.rs | 2 +- crates/project/src/lsp_store.rs | 3 +- crates/project/src/project.rs | 86 +++---------------- crates/project/src/project_settings.rs | 2 +- crates/project/src/project_tests.rs | 16 ++-- .../settings/src/settings_content/language.rs | 9 ++ 7 files changed, 34 insertions(+), 90 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index f2c43cb34303f50c5cd6c8e76c74ef25811bc262..72271dbd58c5def6a4017a94e811289f2f5d8b8a 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -13,9 +13,9 @@ use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; pub use settings::{ - CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, - IndentGuideSettings, LanguageSettingsContent, LspInsertMode, RewrapBehavior, - ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, + CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter, + FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, LspInsertMode, + RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{ ParameterizedJsonSchema, Settings, SettingsContent, SettingsLocation, SettingsStore, SettingsUi, diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 9c50f9060665ac459ec79c53028932961eb5a7fb..79ff8badaf87734cfc4786fcbf54bfb2d6b39b5a 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -23,7 +23,7 @@ use rpc::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi}; -use util::{MergeFrom, ResultExt as _, debug_panic}; +use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 2040b10bb39ceff6d9d6d06a5dbfb0f68f41a459..09b0c7b5351dd0b9bd4b03ff89a46f61e30a72ad 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5816,7 +5816,8 @@ impl LspStore { buffer.read(cx).file(), cx, ) - .completions; + .completions + .clone(); if !completion_settings.lsp { return Task::ready(Ok(Vec::new())); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fde9087e3480256fd0afe7f698fa2317e086b920..b3b0daa4460df1e3a5a813252a9a159e4daf5003 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -29,7 +29,6 @@ use context_server_store::ContextServerStore; pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent}; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; -use schemars::JsonSchema; pub mod search_history; mod yarn; @@ -101,10 +100,7 @@ use rpc::{ }; use search::{SearchInputKind, SearchQuery, SearchResult}; use search_history::SearchHistory; -use settings::{ - InvalidSettingsError, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore, - SettingsUi, -}; +use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore}; use smol::channel::Receiver; use snippet::Snippet; use snippet_provider::SnippetProvider; @@ -988,6 +984,7 @@ impl settings::Settings for DisableAiSettings { } fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + // If disable_ai is true *in any file*, it is disabled. self.disable_ai = self.disable_ai || content.project.disable_ai.unwrap_or(false); } @@ -5644,7 +5641,6 @@ fn provide_inline_values( mod disable_ai_settings_tests { use super::*; use gpui::TestAppContext; - use rpc::proto::cancel_language_server_work::Work; use settings::Settings; #[gpui::test] @@ -5652,7 +5648,9 @@ mod disable_ai_settings_tests { cx.update(|cx| { let mut store = SettingsStore::new(cx, &settings::test_settings()); store.register_setting::(cx); - let ai_disabled = |cx| DisableAiSettings::get_global(cx).disable_ai; + fn ai_disabled(cx: &App) -> bool { + DisableAiSettings::get_global(cx).disable_ai + } // Test 1: Default is false (AI enabled) assert!(!ai_disabled(cx), "Default should allow AI"); @@ -5665,77 +5663,13 @@ mod disable_ai_settings_tests { }) .to_string(); - // Test 2: Global true, local false -> still disabled (local cannot re-enable) - store.set_user_settings(&disable_false, cx); + store.set_user_settings(&disable_false, cx).unwrap(); + store.set_global_settings(&disable_true, cx).unwrap(); assert!(ai_disabled(cx), "Local false cannot override global true"); - // Test 3: Global false, local true -> disabled (local can make more restrictive) - store.set_user_settings(&disable_false, cx); - store.set_local_settings( - WorktreeId::from_usize(0), - Path::new("project.json").into(), - settings::LocalSettingsKind::Settings, - Some(&disable_true), - cx, - ); - assert!(ai_disabled(cx), "Local true can override global false"); - store.clear_local_settings(WorktreeId::from_usize(0), cx); - - // Test 4: Server can only make more restrictive (set to true) - store.set_user_settings(&disable_false, cx); - store.set_server_settings(&disable_true, cx); - assert!( - ai_disabled(cx), - "Server can set to true even if user is false" - ); - - // Test 5: Server false cannot override user true - store.set_server_settings(&disable_false, cx); - store.set_user_settings(&disable_true, cx); - assert!(ai_disabled(cx), "Server false cannot override user true"); - - // Test 6: Multiple local settings, any true disables AI - store.set_local_settings( - WorktreeId::from_usize(0), - Path::new("a").into(), - settings::LocalSettingsKind::Settings, - Some(&disable_false), - cx, - ); - store.set_local_settings( - WorktreeId::from_usize(1), - Path::new("b").into(), - settings::LocalSettingsKind::Settings, - Some(&disable_true), - cx, - ); - store.set_local_settings( - WorktreeId::from_usize(2), - Path::new("c").into(), - settings::LocalSettingsKind::Settings, - Some(&disable_false), - cx, - ); - store.set_user_settings(&disable_false, cx); - assert!(ai_disabled(cx), "Any local true should disable AI"); - store.clear_local_settings(WorktreeId::from_usize(0), cx); - store.clear_local_settings(WorktreeId::from_usize(1), cx); - store.clear_local_settings(WorktreeId::from_usize(2), cx); - - // Test 7: All three sources can independently disable AI - store.set_server_settings(&disable_false, cx); - store.set_user_settings(&disable_false, cx); - store.set_local_settings( - WorktreeId::from_usize(0), - Path::new("a").into(), - settings::LocalSettingsKind::Settings, - Some(&disable_true), - cx, - ); - assert!( - ai_disabled(cx), - "Local can disable even if user and server are false" - ); + store.set_global_settings(&disable_false, cx).unwrap(); + store.set_user_settings(&disable_true, cx).unwrap(); + assert!(ai_disabled(cx), "Local false cannot override global true"); }); } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 0d3cda3b8fd5beb89ac8aa8ffc119c6e5a5400da..f6fc66dcc6fd60c49b77e0dcb8c0097c2a27588f 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -19,7 +19,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, - SettingsSources, SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, + SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, }; use std::{ collections::BTreeMap, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 3745b1fdd0a784bc90fcd787c1f5454672b4b429..5daaa09b9bafc7aa6419e017f2c378a020bdba91 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -23,7 +23,7 @@ use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister, - language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, + language_settings::{LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; use lsp::{ @@ -2246,8 +2246,8 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { // Disable Rust language server, ensuring only that server gets stopped. cx.update(|cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.languages.0.insert( + settings.update_user_settings(cx, |settings| { + settings.languages_mut().insert( "Rust".into(), LanguageSettingsContent { enable_language_server: Some(false), @@ -2265,16 +2265,16 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { // former gets started again and that the latter stops. cx.update(|cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.languages.0.insert( - LanguageName::new("Rust"), + settings.update_user_settings(cx, |settings| { + settings.languages_mut().insert( + "Rust".into(), LanguageSettingsContent { enable_language_server: Some(true), ..Default::default() }, ); - settings.languages.0.insert( - LanguageName::new("JavaScript"), + settings.languages_mut().insert( + "JavaScript".into(), LanguageSettingsContent { enable_language_server: Some(false), ..Default::default() diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index ced6140c85dffca07028f3059fdeca9b94631d4c..c86c36954a022c20aac184c993be2b59117c9958 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -803,6 +803,15 @@ impl Default for FormatterList { } } +impl AsRef<[Formatter]> for FormatterList { + fn as_ref(&self) -> &[Formatter] { + match &self { + Self::Single(single) => std::slice::from_ref(single), + Self::Vec(v) => v, + } + } +} + /// 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")] From 742d196f359573cdfeae010539c41c1e6e7f49c6 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 16 Sep 2025 12:24:07 -0500 Subject: [PATCH 031/117] wip - project --- Cargo.lock | 1 + assets/settings/default.json | 13 + crates/agent_ui/src/slash_command_settings.rs | 24 +- crates/call/src/call_settings.rs | 37 +- crates/context_server/Cargo.toml | 3 +- crates/context_server/src/context_server.rs | 27 +- crates/project/src/project.rs | 36 +- crates/project/src/project_settings.rs | 648 +++++++++--------- crates/settings/src/settings_content.rs | 98 +-- .../settings/src/settings_content/project.rs | 384 +++++++++++ crates/settings/src/vscode_import.rs | 5 + 11 files changed, 829 insertions(+), 447 deletions(-) create mode 100644 crates/settings/src/settings_content/project.rs diff --git a/Cargo.lock b/Cargo.lock index 6e2845aabe589d688343a6216c2b13f7a92d3308..cdc3adb1f170eb23bc02abd511b9cc6c093fcc53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3596,6 +3596,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "smol", "tempfile", "url", diff --git a/assets/settings/default.json b/assets/settings/default.json index fc53e2c3e8cd07db8eb133c90809fd23f35be307..b544e83a0af27b42b2848d47f289c610b6f4d4f9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1807,6 +1807,15 @@ "api_url": "https://api.mistral.ai/v1" } }, + "session": { + /// Whether or not to restore unsaved buffers on restart. + /// + /// If this is true, user won't be prompted whether to save/discard + /// dirty files when closing the application. + /// + /// Default: true + "restore_unsaved_buffers": true + }, // 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. @@ -1845,6 +1854,10 @@ // } // } }, + // DAP Specific settings. + "dap": { + // Specify the DAP name as a key here. + }, // Common language server settings. "global_lsp_settings": { // Whether to show the LSP servers button in the status bar. diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 9580ffef0f317fbe726c57041fad4f0fa438e143..939459a6129a85dc6b1d8297ad3659f3a0982067 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -5,32 +5,30 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; /// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "slash_commands")] +#[derive(Debug, Default, Clone)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. - #[serde(default)] pub cargo_workspace: CargoWorkspaceCommandSettings, } /// Settings for the `/cargo-workspace` slash command. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Debug, Default, Clone)] pub struct CargoWorkspaceCommandSettings { /// Whether `/cargo-workspace` is enabled. - #[serde(default)] pub enabled: bool, } impl Settings for SlashCommandSettings { - type FileContent = Self; + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + Self { + cargo_workspace: CargoWorkspaceCommandSettings { + enabled: content.project.slash_commands.unwrap(), + }, + } + } - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - SettingsSources::::json_merge_with( - [sources.default] - .into_iter() - .chain(sources.user) - .chain(sources.server), - ) + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + todo!() } fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index c458c17e9fcd8788aae2661fbaaa0d45b117e10f..6fa10a77da327a2e02f15c2793e9d35b9a1f583e 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -3,6 +3,7 @@ use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use util::MergeFrom; #[derive(Deserialize, Debug)] pub struct CallSettings { @@ -10,27 +11,25 @@ pub struct CallSettings { pub share_on_join: bool, } -/// Configuration of voice calls in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "calls")] -pub struct CallSettingsContent { - /// Whether the microphone should be muted when joining a channel or a call. - /// - /// Default: false - pub mute_on_join: Option, - - /// Whether your current project should be shared when joining an empty channel. - /// - /// Default: false - pub share_on_join: Option, -} - impl Settings for CallSettings { - type FileContent = CallSettingsContent; + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let call = content.call.unwrap(); + CallSettings { + mute_on_join: call.mute_on_join.unwrap(), + share_on_join: call.share_on_join.unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + if let Some(call) = content.call.clone() { + self.mute_on_join.merge_from(call.mute_on_join); + self.share_on_join.merge_from(call.share_on_join); + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &settings::SettingsContent, + ) { + } } diff --git a/crates/context_server/Cargo.toml b/crates/context_server/Cargo.toml index 5e4f8369c45f0edb58efda1618bf8fe0aad55749..1c5745408041ae2f67e91ba0d9365188ab957d4e 100644 --- a/crates/context_server/Cargo.toml +++ b/crates/context_server/Cargo.toml @@ -25,8 +25,9 @@ net.workspace = true parking_lot.workspace = true postage.workspace = true schemars.workspace = true -serde.workspace = true serde_json.workspace = true +serde.workspace = true +settings.workspace = true smol.workspace = true tempfile.workspace = true url = { workspace = true, features = ["serde"] } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index b126bb393784664692b5de39fee5ed7f66e9948a..92b1b2277239a876f35d9d430beb2276361d8f90 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -17,6 +17,7 @@ use gpui::AsyncApp; use parking_lot::RwLock; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub use settings::ContextServerCommand; use util::redact::should_redact; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -28,32 +29,6 @@ impl Display for ContextServerId { } } -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] -pub struct ContextServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - pub args: Vec, - pub env: Option>, - /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified. - pub timeout: Option, -} - -impl std::fmt::Debug for ContextServerCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self.env.as_ref().map(|env| { - env.iter() - .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v })) - .collect::>() - }); - - f.debug_struct("ContextServerCommand") - .field("path", &self.path) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - enum ContextServerTransport { Stdio(ContextServerCommand, Option), Custom(Arc), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3b0daa4460df1e3a5a813252a9a159e4daf5003..e880144368160ae445db7c79141a2c016b84dd1a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -979,7 +979,7 @@ pub struct DisableAiSettings { impl settings::Settings for DisableAiSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { Self { - disable_ai: content.project.disable_ai.unwrap(), + disable_ai: content.disable_ai.unwrap(), } } @@ -3246,21 +3246,7 @@ impl Project { let first_insertion = self.buffers_needing_diff.len() == 1; let settings = ProjectSettings::get_global(cx); - let delay = if let Some(delay) = settings.git.gutter_debounce { - delay - } else { - if first_insertion { - let this = cx.weak_entity(); - cx.defer(move |cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.recalculate_buffer_diffs(cx).detach(); - }); - } - }); - } - return; - }; + let delay = settings.git.gutter_debounce; const MIN_DELAY: u64 = 50; let delay = delay.max(MIN_DELAY); @@ -5648,11 +5634,11 @@ mod disable_ai_settings_tests { cx.update(|cx| { let mut store = SettingsStore::new(cx, &settings::test_settings()); store.register_setting::(cx); - fn ai_disabled(cx: &App) -> bool { - DisableAiSettings::get_global(cx).disable_ai - } // Test 1: Default is false (AI enabled) - assert!(!ai_disabled(cx), "Default should allow AI"); + assert!( + !DisableAiSettings::get_global(cx).disable_ai, + "Default should allow AI" + ); let disable_true = serde_json::json!({ "disable_ai": true @@ -5665,11 +5651,17 @@ mod disable_ai_settings_tests { store.set_user_settings(&disable_false, cx).unwrap(); store.set_global_settings(&disable_true, cx).unwrap(); - assert!(ai_disabled(cx), "Local false cannot override global true"); + assert!( + DisableAiSettings::get_global(cx).disable_ai, + "Local false cannot override global true" + ); store.set_global_settings(&disable_false, cx).unwrap(); store.set_user_settings(&disable_true, cx).unwrap(); - assert!(ai_disabled(cx), "Local false cannot override global true"); + assert!( + DisableAiSettings::get_global(cx).disable_ai, + "Local false cannot override global true" + ); }); } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index f6fc66dcc6fd60c49b77e0dcb8c0097c2a27588f..212d3262e6fff1907cf2bd6ac91af8300cc1c739 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1,10 +1,14 @@ use anyhow::Context as _; +use clock::Global; use collections::HashMap; use context_server::ContextServerCommand; use dap::adapters::DebugAdapterName; use fs::Fs; use futures::StreamExt as _; -use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task}; +use gpui::{ + App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, SharedString, Subscription, + Task, +}; use lsp::LanguageServerName; use paths::{ EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path, @@ -13,7 +17,7 @@ use paths::{ }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto}, + proto::{self, FromProto, Message, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -28,7 +32,7 @@ use std::{ time::Duration, }; use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile}; -use util::{ResultExt, serde::default_true}; +use util::{MergeFrom as _, ResultExt, serde::default_true}; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; use crate::{ @@ -36,8 +40,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] +#[derive(Debug, Clone)] pub struct ProjectSettings { /// Configuration for language servers. /// @@ -47,50 +50,49 @@ pub struct ProjectSettings { /// To override settings for a language, add an entry for that language server's /// name to the lsp value. /// Default: null - #[serde(default)] - pub lsp: HashMap, + // todo! should these hash map types be Map or Map + pub lsp: HashMap, /// Common language server settings. - #[serde(default)] pub global_lsp_settings: GlobalLspSettings, /// Configuration for Debugger-related features - #[serde(default)] pub dap: HashMap, /// Settings for context servers used for AI-related features. - #[serde(default)] pub context_servers: HashMap, ContextServerSettings>, /// Configuration for Diagnostics-related features. - #[serde(default)] pub diagnostics: DiagnosticsSettings, /// Configuration for Git-related features - #[serde(default)] pub git: GitSettings, /// Configuration for Node-related features - #[serde(default)] pub node: NodeBinarySettings, /// Configuration for how direnv configuration should be loaded - #[serde(default)] pub load_direnv: DirenvSettings, /// Configuration for session-related features - #[serde(default)] pub session: SessionSettings, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[derive(Debug, Clone, Default, PartialEq)] pub struct DapSettings { - pub binary: Option, - #[serde(default)] + pub binary: String, pub args: Vec, } +/// Common language server settings. +#[derive(Debug, Clone, PartialEq)] +pub struct GlobalLspSettings { + /// Whether to show the LSP servers button in the status bar. + /// + /// Default: `true` + pub button: bool, +} + #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] #[serde(tag = "source", rename_all = "snake_case")] pub enum ContextServerSettings { @@ -114,14 +116,17 @@ pub enum ContextServerSettings { }, } -/// Common language server settings. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct GlobalLspSettings { - /// Whether to show the LSP servers button in the status bar. - /// - /// Default: `true` - #[serde(default = "default_true")] - pub button: bool, +impl From for ContextServerSettings { + fn from(value: settings::ContextServerSettingsContent) -> Self { + match value { + settings::ContextServerSettingsContent::Custom { enabled, command } => { + ContextServerSettings::Custom { enabled, command } + } + settings::ContextServerSettingsContent::Extension { enabled, settings } => { + ContextServerSettings::Extension { enabled, settings } + } + } + } } impl ContextServerSettings { @@ -147,140 +152,6 @@ impl ContextServerSettings { } } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct NodeBinarySettings { - /// The path to the Node binary. - pub path: Option, - /// The path to the npm binary Zed should use (defaults to `.path/../npm`). - pub npm_path: Option, - /// If enabled, Zed will download its own copy of Node. - #[serde(default)] - pub ignore_system_version: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum DirenvSettings { - /// Load direnv configuration through a shell hook - ShellHook, - /// Load direnv configuration directly using `direnv export json` - #[default] - Direct, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(default)] -pub struct DiagnosticsSettings { - /// Whether to show the project diagnostics button in the status bar. - pub button: bool, - - /// Whether or not to include warning diagnostics. - pub include_warnings: bool, - - /// Settings for using LSP pull diagnostics mechanism in Zed. - pub lsp_pull_diagnostics: LspPullDiagnosticsSettings, - - /// Settings for showing inline diagnostics. - pub inline: InlineDiagnosticsSettings, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(default)] -pub struct LspPullDiagnosticsSettings { - /// Whether to pull for diagnostics or not. - /// - /// Default: true - #[serde(default = "default_true")] - pub enabled: bool, - /// Minimum time to wait before pulling diagnostics from the language server(s). - /// 0 turns the debounce off. - /// - /// Default: 50 - #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")] - pub debounce_ms: u64, -} - -fn default_lsp_diagnostics_pull_debounce_ms() -> u64 { - 50 -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(default)] -pub struct InlineDiagnosticsSettings { - /// Whether or not to show inline diagnostics - /// - /// Default: false - pub enabled: bool, - /// Whether to only show the inline diagnostics after a delay after the - /// last editor event. - /// - /// Default: 150 - #[serde(default = "default_inline_diagnostics_update_debounce_ms")] - pub update_debounce_ms: u64, - /// The amount of padding between the end of the source line and the start - /// of the inline diagnostic in units of columns. - /// - /// Default: 4 - #[serde(default = "default_inline_diagnostics_padding")] - pub padding: u32, - /// The minimum column to display inline diagnostics. This setting can be - /// used to horizontally align inline diagnostics at some position. Lines - /// longer than this value will still push diagnostics further to the right. - /// - /// Default: 0 - pub min_column: u32, - - pub max_severity: Option, -} - -fn default_inline_diagnostics_update_debounce_ms() -> u64 { - 150 -} - -fn default_inline_diagnostics_padding() -> u32 { - 4 -} - -impl Default for DiagnosticsSettings { - fn default() -> Self { - Self { - button: true, - include_warnings: true, - lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), - inline: InlineDiagnosticsSettings::default(), - } - } -} - -impl Default for LspPullDiagnosticsSettings { - fn default() -> Self { - Self { - enabled: true, - debounce_ms: default_lsp_diagnostics_pull_debounce_ms(), - } - } -} - -impl Default for InlineDiagnosticsSettings { - fn default() -> Self { - Self { - enabled: false, - update_debounce_ms: default_inline_diagnostics_update_debounce_ms(), - padding: default_inline_diagnostics_padding(), - min_column: 0, - max_severity: None, - } - } -} - -impl Default for GlobalLspSettings { - fn default() -> Self { - Self { - button: default_true(), - } - } -} - #[derive( Clone, Copy, @@ -301,7 +172,6 @@ pub enum DiagnosticSeverity { Error, Warning, Info, - #[serde(alias = "all")] Hint, } @@ -317,6 +187,18 @@ impl DiagnosticSeverity { } } +impl From for DiagnosticSeverity { + fn from(severity: settings::DiagnosticSeverityContent) -> Self { + match severity { + settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off, + settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error, + settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning, + settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info, + settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint, + } + } +} + /// Determines the severity of the diagnostic that should be moved to. #[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -390,126 +272,90 @@ impl GoToDiagnosticSeverityFilter { } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Debug)] pub struct GitSettings { /// Whether or not to show the git gutter. /// /// Default: tracked_files - pub git_gutter: Option, + pub git_gutter: settings::GitGutterSetting, /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. /// /// Default: null - pub gutter_debounce: Option, + pub gutter_debounce: u64, /// Whether or not to show git blame data inline in /// the currently focused line. /// /// Default: on - pub inline_blame: Option, + pub inline_blame: InlineBlameSettings, /// Which information to show in the branch picker. /// /// Default: on - pub branch_picker: Option, + pub branch_picker: BranchPickerSettings, /// How hunks are displayed visually in the editor. /// /// Default: staged_hollow - pub hunk_style: Option, -} - -impl GitSettings { - pub fn inline_blame_enabled(&self) -> bool { - #[allow(unknown_lints, clippy::manual_unwrap_or_default)] - match self.inline_blame { - Some(InlineBlameSettings { enabled, .. }) => enabled, - _ => false, - } - } - - pub fn inline_blame_delay(&self) -> Option { - match self.inline_blame { - Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => { - Some(Duration::from_millis(delay_ms)) - } - _ => None, - } - } - - pub fn show_inline_commit_summary(&self) -> bool { - match self.inline_blame { - Some(InlineBlameSettings { - show_commit_summary, - .. - }) => show_commit_summary, - _ => false, - } - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitHunkStyleSetting { - /// Show unstaged hunks with a filled background and staged hunks hollow. - #[default] - StagedHollow, - /// Show unstaged hunks hollow and staged hunks with a filled background. - UnstagedHollow, -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitGutterSetting { - /// Show git gutter in tracked files. - #[default] - TrackedFiles, - /// Hide git gutter - Hide, + pub hunk_style: settings::GitHunkStyleSetting, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[derive(Clone, Copy, Debug)] pub struct InlineBlameSettings { /// Whether or not to show git blame data inline in /// the currently focused line. /// /// Default: true - #[serde(default = "default_true")] pub enabled: bool, /// Whether to only show the inline blame information /// after a delay once the cursor stops moving. /// /// Default: 0 - #[serde(default)] - pub delay_ms: u64, + pub delay_ms: std::time::Duration, /// The amount of padding between the end of the source line and the start /// of the inline blame in units of columns. /// /// Default: 7 - #[serde(default = "default_inline_blame_padding")] pub padding: u32, /// The minimum column number to show the inline blame information at /// /// Default: 0 - #[serde(default)] pub min_column: u32, /// Whether to show commit summary as part of the inline blame. /// /// Default: false - #[serde(default)] pub show_commit_summary: bool, } -fn default_inline_blame_padding() -> u32 { - 7 -} +impl GitSettings { + // todo! remove + pub fn inline_blame_enabled(&self) -> bool { + self.inline_blame.enabled + // #[allow(unknown_lints, clippy::manual_unwrap_or_default)] + // match self.inline_blame { + // Some(InlineBlameSettings { enabled, .. }) => enabled, + // _ => false, + // } + } -impl Default for InlineBlameSettings { - fn default() -> Self { - Self { - enabled: true, - delay_ms: 0, - padding: default_inline_blame_padding(), - min_column: 0, - show_commit_summary: false, - } + // todo! remove + pub fn inline_blame_delay(&self) -> Option { + Some(self.inline_blame.delay_ms) + // match self.inline_blame { + // Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => { + // Some(Duration::from_millis(delay_ms)) + // } + // _ => None, + // } + } + + // todo! remove + pub fn show_inline_commit_summary(&self) -> bool { + self.inline_blame.show_commit_summary + // match self.inline_blame { + // Some(InlineBlameSettings { + // show_commit_summary, + // .. + // }) => show_commit_summary, + // _ => false, + // } } } @@ -531,93 +377,263 @@ impl Default for BranchPickerSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] -pub struct BinarySettings { - pub path: Option, - pub arguments: Option>, - pub env: Option>, - pub ignore_system_version: Option, -} +#[derive(Clone, Debug)] +pub struct DiagnosticsSettings { + /// Whether to show the project diagnostics button in the status bar. + pub button: bool, -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] -pub struct FetchSettings { - // Whether to consider pre-releases for fetching - pub pre_release: Option, -} + /// Whether or not to include warning diagnostics. + pub include_warnings: bool, -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] -#[serde(rename_all = "snake_case")] -pub struct LspSettings { - pub binary: Option, - pub initialization_options: Option, - pub settings: Option, - /// If the server supports sending tasks over LSP extensions, - /// this setting can be used to enable or disable them in Zed. - /// Default: true - #[serde(default = "default_true")] - pub enable_lsp_tasks: bool, - pub fetch: Option, -} + /// Settings for using LSP pull diagnostics mechanism in Zed. + pub lsp_pull_diagnostics: LspPullDiagnosticsSettings, -impl Default for LspSettings { - fn default() -> Self { - Self { - binary: None, - initialization_options: None, - settings: None, - enable_lsp_tasks: true, - fetch: None, - } - } + /// Settings for showing inline diagnostics. + pub inline: InlineDiagnosticsSettings, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct SessionSettings { - /// Whether or not to restore unsaved buffers on restart. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InlineDiagnosticsSettings { + /// Whether or not to show inline diagnostics /// - /// If this is true, user won't be prompted whether to save/discard - /// dirty files when closing the application. + /// Default: false + pub enabled: bool, + /// Whether to only show the inline diagnostics after a delay after the + /// last editor event. + /// + /// Default: 150 + pub update_debounce_ms: u64, + /// The amount of padding between the end of the source line and the start + /// of the inline diagnostic in units of columns. + /// + /// Default: 4 + pub padding: u32, + /// The minimum column to display inline diagnostics. This setting can be + /// used to horizontally align inline diagnostics at some position. Lines + /// longer than this value will still push diagnostics further to the right. + /// + /// Default: 0 + pub min_column: u32, + + pub max_severity: DiagnosticSeverity, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct LspPullDiagnosticsSettings { + /// Whether to pull for diagnostics or not. /// /// Default: true - pub restore_unsaved_buffers: bool, + pub enabled: bool, + /// Minimum time to wait before pulling diagnostics from the language server(s). + /// 0 turns the debounce off. + /// + /// Default: 50 + pub debounce_ms: u64, } -impl Default for SessionSettings { - fn default() -> Self { +impl Settings for ProjectSettings { + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let project = &content.project.clone(); + let diagnostics = content.diagnostics.as_ref().unwrap(); + let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap(); + let inline_diagnostics = diagnostics.inline.as_ref().unwrap(); + + let git = content.git.as_ref().unwrap(); + let git_settings = GitSettings { + git_gutter: git.git_gutter.unwrap(), + gutter_debounce: git.gutter_debounce.unwrap(), + inline_blame: { + let inline = git.inline_blame.unwrap(); + InlineBlameSettings { + enabled: inline.enabled.unwrap(), + delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()), + padding: inline.padding.unwrap(), + min_column: inline.min_column.unwrap(), + show_commit_summary: inline.show_commit_summary.unwrap(), + } + }, + branch_picker: { + let branch_picker = git.branch_picker.unwrap(); + BranchPickerSettings { + show_author_name: branch_picker.show_author_name.unwrap(), + } + }, + hunk_style: git.hunk_style.unwrap(), + }; Self { - restore_unsaved_buffers: true, + context_servers: project + .context_servers + .clone() + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(), + lsp: project + .lsp + .clone() + .into_iter() + .map(|(key, value)| (LanguageServerName(key.into()), value.into())) + .collect(), + global_lsp_settings: GlobalLspSettings { + button: content.global_lsp_settings.unwrap().button.unwrap(), + }, + dap: project + .dap + .clone() + .into_iter() + .map(|(key, value)| { + ( + DebugAdapterName(key.into()), + DapSettings { + binary: value.binary.unwrap(), + args: value.args, + }, + ) + }) + .collect(), + diagnostics: DiagnosticsSettings { + button: diagnostics.button.unwrap(), + include_warnings: diagnostics.include_warnings.unwrap(), + lsp_pull_diagnostics: LspPullDiagnosticsSettings { + enabled: lsp_pull_diagnostics.enabled.unwrap(), + debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(), + }, + inline: InlineDiagnosticsSettings { + enabled: inline_diagnostics.enabled.unwrap(), + update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(), + padding: inline_diagnostics.padding.unwrap(), + min_column: inline_diagnostics.min_column.unwrap(), + max_severity: inline_diagnostics.max_severity.unwrap().into(), + }, + }, + git: git_settings, + node: content.node.clone(), + load_direnv: project.load_direnv.unwrap(), + session: content.session.clone(), } } -} -impl Settings for ProjectSettings { - type FileContent = Self; + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + let project = &content.project; + self.context_servers.extend( + project + .context_servers + .clone() + .into_iter() + .map(|(key, value)| (key, value.into())), + ); + self.dap + .extend(project.dap.clone().into_iter().filter_map(|(key, value)| { + Some(( + DebugAdapterName(key.into()), + DapSettings { + binary: value.binary?, + args: value.args, + }, + )) + })); + if let Some(diagnostics) = content.diagnostics.as_ref() { + if let Some(inline) = &diagnostics.inline { + self.diagnostics.inline.enabled.merge_from(&inline.enabled); + self.diagnostics + .inline + .update_debounce_ms + .merge_from(&inline.update_debounce_ms); + self.diagnostics.inline.padding.merge_from(&inline.padding); + self.diagnostics + .inline + .min_column + .merge_from(&inline.min_column); + self.diagnostics + .inline + .max_severity + .merge_from(&inline.max_severity.map(Into::into)); + } - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - sources.json_merge() + self.diagnostics.button.merge_from(&diagnostics.button); + self.diagnostics + .include_warnings + .merge_from(&diagnostics.include_warnings); + if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics { + self.diagnostics + .lsp_pull_diagnostics + .enabled + .merge_from(&pull_diagnostics.enabled); + self.diagnostics + .lsp_pull_diagnostics + .debounce_ms + .merge_from(&pull_diagnostics.debounce_ms); + } + } + if let Some(git) = content.git.as_ref() { + if let Some(branch_picker) = git.branch_picker.as_ref() { + self.git + .branch_picker + .show_author_name + .merge_from(&branch_picker.show_author_name); + } + if let Some(inline_blame) = git.inline_blame.as_ref() { + self.git + .inline_blame + .enabled + .merge_from(&inline_blame.enabled); + self.git + .inline_blame + .delay_ms + .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis)); + self.git + .inline_blame + .padding + .merge_from(&inline_blame.padding); + self.git + .inline_blame + .min_column + .merge_from(&inline_blame.min_column); + self.git + .inline_blame + .show_commit_summary + .merge_from(&inline_blame.show_commit_summary); + } + self.git.git_gutter.merge_from(&git.git_gutter); + self.git.hunk_style.merge_from(&git.hunk_style); + self.git.gutter_debounce.merge_from(&git.gutter_debounce); + } + self.global_lsp_settings = content.global_lsp_settings.clone(); + self.load_direnv = content.project.load_direnv.clone(); + self.lsp.extend( + content + .project + .lsp + .clone() + .into_iter() + .map(|(key, value)| (key, lsp_settings)), + ); } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { // this just sets the binary name instead of a full path so it relies on path lookup // resolving to the one you want - vscode.enum_setting( - "npm.packageManager", - &mut current.node.npm_path, - |s| match s { - v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), - _ => None, - }, - ); + let npm_path = vscode.read_enum_setting("npm.packageManager", |s| match s { + v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), + _ => None, + }); + if npm_path.is_some() { + current.node.get_or_insert_default().npm_path = npm_path; + } if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") { - if let Some(blame) = current.git.inline_blame.as_mut() { - blame.enabled = b - } else { - current.git.inline_blame = Some(InlineBlameSettings { - enabled: b, - ..Default::default() - }) - } + // todo! get_or_insert_default is risky considering defaults probably don't correspond + // to default.json, + // probably need to pass in "defaults" for this type as additional arg, and + // use unwrap on those keys + current + .git + .get_or_insert_default() + .inline_blame + .get_or_insert_default() + .enabled = Some(b); } #[derive(Deserialize)] @@ -627,29 +643,27 @@ impl Settings for ProjectSettings { env: Option>, // note: we don't support envFile and type } - impl From for ContextServerCommand { - fn from(cmd: VsCodeContextServerCommand) -> Self { - Self { - path: cmd.command, - args: cmd.args.unwrap_or_default(), - env: cmd.env, - timeout: None, - } - } - } if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) { current + .project .context_servers .extend(mcp.iter().filter_map(|(k, v)| { Some(( k.clone().into(), - ContextServerSettings::Custom { + settings::ContextServerSettingsContent::Custom { enabled: true, command: serde_json::from_value::( v.clone(), ) - .ok()? - .into(), + .ok() + .map(|cmd| { + settings::ContextServerCommand { + path: cmd.command, + args: cmd.args.unwrap_or_default(), + env: cmd.env, + timeout: None, + } + })?, }, )) })); @@ -740,7 +754,7 @@ impl SettingsObserver { if Some(new_settings) != user_settings.as_ref() { if let Some(new_settings_string) = serde_json::to_string(new_settings).ok() { - user_settings = Some(new_settings.clone()); + user_settings = new_settings.clone(); upstream_client .send(proto::UpdateUserSettings { project_id: REMOTE_SERVER_PROJECT_ID, diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index ca1e20a30aa8373e84cf618ad03d1966e8a7865c..3945db5c9ec391273a464b32ee055599ffcf30c2 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,19 +1,21 @@ mod agent; mod language; +mod project; mod terminal; mod theme; pub use agent::*; pub use language::*; +pub use project::*; pub use terminal::*; pub use theme::*; -use std::env; - use collections::HashMap; use gpui::{App, SharedString}; use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::env; +pub use util::serde::default_true; use crate::ActiveSettingsProfileName; @@ -37,9 +39,18 @@ pub struct SettingsContent { pub debugger: Option, + /// Configuration for Diagnostics-related features. + pub diagnostics: Option, + + /// Configuration for Git-related features + pub git: Option, + /// The list of custom Git hosting providers. pub git_hosting_providers: Option>, + /// Common language server settings. + pub global_lsp_settings: Option, + /// Whether or not to enable Helix mode. /// /// Default: false @@ -50,11 +61,16 @@ pub struct SettingsContent { /// Example: {"log": {"client": "warn"}} pub log: Option>, + /// Configuration for Node-related features + pub node: Option, + pub proxy: Option, /// The URL of the Zed server to connect to. pub server_url: Option, + /// Configuration for session-related features + pub session: Option, /// Control what info is collected by Zed. pub telemetry: Option, @@ -67,6 +83,9 @@ pub struct SettingsContent { /// /// Default: false pub vim_mode: Option, + + // Settings related to calls in Zed + pub calls: Option, } impl SettingsContent { @@ -147,17 +166,6 @@ pub enum BaseKeymapContent { None, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct ProjectSettingsContent { - #[serde(flatten)] - pub all_languages: AllLanguageSettingsContent, - - #[serde(flatten)] - pub worktree: WorktreeSettingsContent, - - pub disable_ai: Option, -} - #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct TitleBarSettingsContent { /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". @@ -246,42 +254,6 @@ pub enum GitHostingProviderKind { Bitbucket, } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct WorktreeSettingsContent { - /// The displayed name of this project. If not set, the root directory name - /// will be displayed. - /// - /// Default: none - pub project_name: Option, - - /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides - /// `file_scan_inclusions`. - /// - /// Default: [ - /// "**/.git", - /// "**/.svn", - /// "**/.hg", - /// "**/.jj", - /// "**/CVS", - /// "**/.DS_Store", - /// "**/Thumbs.db", - /// "**/.classpath", - /// "**/.settings" - /// ] - pub file_scan_exclusions: Option>, - - /// Always include files that match these globs when scanning for files, even if they're - /// ignored by git. This setting is overridden by `file_scan_exclusions`. - /// Default: [ - /// ".env*", - /// "docker-compose.*.yml", - /// ] - pub file_scan_inclusions: Option>, - - /// Treat the files matching these globs as `.env` files. - /// Default: [ "**/.env*" ] - pub private_files: Option>, -} /// Control what info is collected by Zed. #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)] pub struct TelemetrySettingsContent { @@ -348,3 +320,31 @@ pub enum DockPosition { Bottom, Right, } + +/// Settings for slash commands. +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +pub struct SlashCommandSettings { + /// Settings for the `/cargo-workspace` slash command. + pub cargo_workspace: Option, +} + +/// Settings for the `/cargo-workspace` slash command. +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +pub struct CargoWorkspaceCommandSettings { + /// Whether `/cargo-workspace` is enabled. + pub enabled: Option, +} + +/// Configuration of voice calls in Zed. +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct CallSettingsContent { + /// Whether the microphone should be muted when joining a channel or a call. + /// + /// Default: false + pub mute_on_join: Option, + + /// Whether your current project should be shared when joining an empty channel. + /// + /// Default: false + pub share_on_join: Option, +} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs new file mode 100644 index 0000000000000000000000000000000000000000..b35c829085135ab48909b763e313ad0919c0f9f5 --- /dev/null +++ b/crates/settings/src/settings_content/project.rs @@ -0,0 +1,384 @@ +use std::{path::PathBuf, sync::Arc}; + +use collections::{BTreeMap, HashMap}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use util::serde::default_true; + +use crate::AllLanguageSettingsContent; + +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ProjectSettingsContent { + #[serde(flatten)] + pub all_languages: AllLanguageSettingsContent, + + #[serde(flatten)] + pub worktree: WorktreeSettingsContent, + + /// Configuration for language servers. + /// + /// The following settings can be overridden for specific language servers: + /// - initialization_options + /// + /// To override settings for a language, add an entry for that language server's + /// name to the lsp value. + /// Default: null + #[serde(default)] + pub lsp: HashMap, LspSettingsContent>, + + /// Configuration for Debugger-related features + #[serde(default)] + pub dap: HashMap, DapSettingsContent>, + + /// Settings for context servers used for AI-related features. + #[serde(default)] + pub context_servers: HashMap, ContextServerSettingsContent>, + + /// Configuration for how direnv configuration should be loaded + pub load_direnv: Option, + + /// Settings for slash commands. + pub slash_commands: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct WorktreeSettingsContent { + /// The displayed name of this project. If not set, the root directory name + /// will be displayed. + /// + /// Default: none + pub project_name: Option, + + /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides + /// `file_scan_inclusions`. + /// + /// Default: [ + /// "**/.git", + /// "**/.svn", + /// "**/.hg", + /// "**/.jj", + /// "**/CVS", + /// "**/.DS_Store", + /// "**/Thumbs.db", + /// "**/.classpath", + /// "**/.settings" + /// ] + pub file_scan_exclusions: Option>, + + /// Always include files that match these globs when scanning for files, even if they're + /// ignored by git. This setting is overridden by `file_scan_exclusions`. + /// Default: [ + /// ".env*", + /// "docker-compose.*.yml", + /// ] + pub file_scan_inclusions: Option>, + + /// Treat the files matching these globs as `.env` files. + /// Default: [ "**/.env*" ] + pub private_files: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] +#[serde(rename_all = "snake_case")] +pub struct LspSettingsContent { + pub binary: Option, + pub initialization_options: Option, + pub settings: Option, + /// If the server supports sending tasks over LSP extensions, + /// this setting can be used to enable or disable them in Zed. + /// Default: true + #[serde(default = "default_true")] + pub enable_lsp_tasks: bool, + pub fetch: Option, +} + +impl Default for LspSettingsContent { + fn default() -> Self { + Self { + binary: None, + initialization_options: None, + settings: None, + enable_lsp_tasks: true, + fetch: None, + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] +pub struct BinarySettings { + pub path: Option, + pub arguments: Option>, + pub env: Option>, + pub ignore_system_version: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] +pub struct FetchSettings { + // Whether to consider pre-releases for fetching + pub pre_release: Option, +} + +/// Common language server settings. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct GlobalLspSettingsContent { + /// Whether to show the LSP servers button in the status bar. + /// + /// Default: `true` + pub button: Option, +} + +// todo! binary is actually just required, shouldn't be an option +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct DapSettingsContent { + pub binary: Option, + #[serde(default)] + pub args: Vec, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SessionSettings { + /// Whether or not to restore unsaved buffers on restart. + /// + /// If this is true, user won't be prompted whether to save/discard + /// dirty files when closing the application. + /// + /// Default: true + pub restore_unsaved_buffers: bool, +} + +impl Default for SessionSettings { + fn default() -> Self { + Self { + restore_unsaved_buffers: true, + } + } +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +#[serde(tag = "source", rename_all = "snake_case")] +pub enum ContextServerSettingsContent { + Custom { + /// Whether the context server is enabled. + #[serde(default = "default_true")] + enabled: bool, + + #[serde(flatten)] + command: ContextServerCommand, + }, + Extension { + /// Whether the context server is enabled. + #[serde(default = "default_true")] + enabled: bool, + /// The settings for this context server specified by the extension. + /// + /// Consult the documentation for the context server to see what settings + /// are supported. + settings: serde_json::Value, + }, +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] +pub struct ContextServerCommand { + #[serde(rename = "command")] + pub path: PathBuf, + pub args: Vec, + pub env: Option>, + /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified. + pub timeout: Option, +} + +impl std::fmt::Debug for ContextServerCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let filtered_env = self.env.as_ref().map(|env| { + env.iter() + .map(|(k, v)| { + ( + k, + if util::redact::should_redact(k) { + "[REDACTED]" + } else { + v + }, + ) + }) + .collect::>() + }); + + f.debug_struct("ContextServerCommand") + .field("path", &self.path) + .field("args", &self.args) + .field("env", &filtered_env) + .finish() + } +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + /// Whether or not to show the git gutter. + /// + /// Default: tracked_files + pub git_gutter: Option, + /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. + /// + /// Default: null + pub gutter_debounce: Option, + /// Whether or not to show git blame data inline in + /// the currently focused line. + /// + /// Default: on + pub inline_blame: Option, + /// Which information to show in the branch picker. + /// + /// Default: on + pub branch_picker: Option, + /// How hunks are displayed visually in the editor. + /// + /// Default: staged_hollow + pub hunk_style: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + /// Show git gutter in tracked files. + #[default] + TrackedFiles, + /// Hide git gutter + Hide, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InlineBlameSettings { + /// Whether or not to show git blame data inline in + /// the currently focused line. + /// + /// Default: true + pub enabled: Option, + /// Whether to only show the inline blame information + /// after a delay once the cursor stops moving. + /// + /// Default: 0 + pub delay_ms: Option, + /// The amount of padding between the end of the source line and the start + /// of the inline blame in units of columns. + /// + /// Default: 7 + pub padding: Option, + /// The minimum column number to show the inline blame information at + /// + /// Default: 0 + pub min_column: Option, + /// Whether to show commit summary as part of the inline blame. + /// + /// Default: false + pub show_commit_summary: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct BranchPickerSettingsContent { + /// Whether to show author name as part of the commit information. + /// + /// Default: false + pub show_author_name: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitHunkStyleSetting { + /// Show unstaged hunks with a filled background and staged hunks hollow. + #[default] + StagedHollow, + /// Show unstaged hunks hollow and staged hunks with a filled background. + UnstagedHollow, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct DiagnosticsSettingsContent { + /// Whether to show the project diagnostics button in the status bar. + pub button: Option, + + /// Whether or not to include warning diagnostics. + pub include_warnings: Option, + + /// Settings for using LSP pull diagnostics mechanism in Zed. + pub lsp_pull_diagnostics: Option, + + /// Settings for showing inline diagnostics. + pub inline: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct LspPullDiagnosticsSettingsContent { + /// Whether to pull for diagnostics or not. + /// + /// Default: true + pub enabled: Option, + /// Minimum time to wait before pulling diagnostics from the language server(s). + /// 0 turns the debounce off. + /// + /// Default: 50 + pub debounce_ms: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Eq)] +pub struct InlineDiagnosticsSettingsContent { + /// Whether or not to show inline diagnostics + /// + /// Default: false + pub enabled: Option, + /// Whether to only show the inline diagnostics after a delay after the + /// last editor event. + /// + /// Default: 150 + pub update_debounce_ms: Option, + /// The amount of padding between the end of the source line and the start + /// of the inline diagnostic in units of columns. + /// + /// Default: 4 + pub padding: Option, + /// The minimum column to display inline diagnostics. This setting can be + /// used to horizontally align inline diagnostics at some position. Lines + /// longer than this value will still push diagnostics further to the right. + /// + /// Default: 0 + pub min_column: Option, + + pub max_severity: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct NodeBinarySettings { + /// The path to the Node binary. + pub path: Option, + /// The path to the npm binary Zed should use (defaults to `.path/../npm`). + pub npm_path: Option, + /// If enabled, Zed will download its own copy of Node. + pub ignore_system_version: Option, +} + +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DirenvSettings { + /// Load direnv configuration through a shell hook + ShellHook, + /// Load direnv configuration directly using `direnv export json` + #[default] + Direct, +} + +#[derive( + Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum DiagnosticSeverityContent { + // No diagnostics are shown. + Off, + Error, + Warning, + Info, + #[serde(alias = "all")] + Hint, +} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 5792dcc12bd687faf50c91530f027b1f90d7ff92..0c63b4ad304761d7d088a80baa9dbe281c2d266d 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -135,4 +135,9 @@ impl VsCodeSettings { *setting = Some(s) } } + + // todo! replace enum_setting + pub fn read_enum_setting(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { + self.content.get(key).and_then(Value::as_str).and_then(f) + } } From d9e7cec281ddfb2c26ace0911cdfeb44e746f41f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 15:29:34 -0600 Subject: [PATCH 032/117] project --- crates/context_server/src/context_server.rs | 4 - crates/git_hosting_providers/src/settings.rs | 4 +- crates/project/src/context_server_store.rs | 37 ++-- crates/project/src/project.rs | 49 +++-- crates/project/src/project_settings.rs | 184 +++++++++++------- crates/project/src/project_tests.rs | 4 +- crates/settings/src/settings_content.rs | 49 ++--- crates/settings/src/settings_content/agent.rs | 10 +- .../settings/src/settings_content/project.rs | 70 ++++--- crates/settings/src/settings_content/theme.rs | 2 +- 10 files changed, 234 insertions(+), 179 deletions(-) diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 92b1b2277239a876f35d9d430beb2276361d8f90..52ed524220947430df3e63fced367ca4eb223fff 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -12,13 +12,9 @@ use std::{fmt::Display, path::PathBuf}; use anyhow::Result; use client::Client; -use collections::HashMap; use gpui::AsyncApp; use parking_lot::RwLock; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; pub use settings::ContextServerCommand; -use util::redact::should_redact; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ContextServerId(pub Arc); diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index fd706adf73cfe5890dac4133fd5be1d4ac967a05..b6aabc47f3fba9fca6c9b908de87ca7319afa616 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -60,12 +60,12 @@ pub struct GitHostingProviderSettings { impl Settings for GitHostingProviderSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { Self { - git_hosting_providers: content.git_hosting_providers.clone().unwrap(), + git_hosting_providers: content.project.git_hosting_providers.clone().unwrap(), } } fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) { - if let Some(more) = &content.git_hosting_providers { + if let Some(more) = &content.project.git_hosting_providers { self.git_hosting_providers.extend_from_slice(&more.clone()); } } diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 20188df5c4ae38b2ae305daee5b3eecc25319951..40a14ed37b6db3e5f25dfc5e6926f7a38bc2b510 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -915,7 +915,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -934,7 +934,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -961,7 +961,7 @@ mod tests { vec![ ( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -970,7 +970,7 @@ mod tests { ), ( server_2_id.0.clone(), - ContextServerSettings::Custom { + settings::ContextServerSettingsContent::Custom { enabled: true, command: ContextServerCommand { path: "somebinary".into(), @@ -1002,7 +1002,7 @@ mod tests { vec![ ( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -1011,7 +1011,7 @@ mod tests { ), ( server_2_id.0.clone(), - ContextServerSettings::Custom { + settings::ContextServerSettingsContent::Custom { enabled: true, command: ContextServerCommand { path: "somebinary".into(), @@ -1027,6 +1027,7 @@ mod tests { cx.run_until_parked(); } + dbg!("hi"); // Ensure that mcp-2 is removed once it is removed from the settings { @@ -1038,7 +1039,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -1054,6 +1055,7 @@ mod tests { assert_eq!(store.read(cx).status_for_server(&server_2_id), None); }); } + dbg!("bye"); // Ensure that nothing happens if the settings do not change { @@ -1061,7 +1063,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Extension { + settings::ContextServerSettingsContent::Extension { enabled: true, settings: json!({ "somevalue": false @@ -1147,7 +1149,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Custom { + settings::ContextServerSettingsContent::Custom { enabled: false, command: ContextServerCommand { path: "somebinary".into(), @@ -1176,7 +1178,7 @@ mod tests { set_context_server_configuration( vec![( server_1_id.0.clone(), - ContextServerSettings::Custom { + settings::ContextServerSettingsContent::Custom { enabled: true, command: ContextServerCommand { path: "somebinary".into(), @@ -1194,18 +1196,17 @@ mod tests { } fn set_context_server_configuration( - context_servers: Vec<(Arc, ContextServerSettings)>, + context_servers: Vec<(Arc, settings::ContextServerSettingsContent)>, cx: &mut TestAppContext, ) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - let mut settings = ProjectSettings::default(); - for (id, config) in context_servers { - settings.context_servers.insert(id, config); - } - store - .set_user_settings(&serde_json::to_string(&settings).unwrap(), cx) - .unwrap(); + store.update_user_settings(cx, |content| { + content.project.context_servers.clear(); + for (id, config) in context_servers { + content.project.context_servers.insert(id, config); + } + }); }) }); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e880144368160ae445db7c79141a2c016b84dd1a..571a7b5db48eaf3660aadf268d61bf1864140066 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -985,7 +985,7 @@ impl settings::Settings for DisableAiSettings { fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { // If disable_ai is true *in any file*, it is disabled. - self.disable_ai = self.disable_ai || content.project.disable_ai.unwrap_or(false); + self.disable_ai = self.disable_ai || content.disable_ai.unwrap_or(false); } fn import_from_vscode( @@ -3244,9 +3244,22 @@ impl Project { ) { self.buffers_needing_diff.insert(buffer.downgrade()); let first_insertion = self.buffers_needing_diff.len() == 1; - let settings = ProjectSettings::get_global(cx); - let delay = settings.git.gutter_debounce; + let delay = if let Some(delay) = settings.git.gutter_debounce { + delay + } else { + if first_insertion { + let this = cx.weak_entity(); + cx.defer(move |cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + this.recalculate_buffer_diffs(cx).detach(); + }); + } + }); + } + return; + }; const MIN_DELAY: u64 = 50; let delay = delay.max(MIN_DELAY); @@ -5632,32 +5645,42 @@ mod disable_ai_settings_tests { #[gpui::test] async fn test_disable_ai_settings_security(cx: &mut TestAppContext) { cx.update(|cx| { - let mut store = SettingsStore::new(cx, &settings::test_settings()); - store.register_setting::(cx); + settings::init(cx); + Project::init_settings(cx); + // Test 1: Default is false (AI enabled) assert!( !DisableAiSettings::get_global(cx).disable_ai, "Default should allow AI" ); + }); - let disable_true = serde_json::json!({ - "disable_ai": true - }) - .to_string(); - let disable_false = serde_json::json!({ - "disable_ai": false - }) - .to_string(); + let disable_true = serde_json::json!({ + "disable_ai": true + }) + .to_string(); + let disable_false = serde_json::json!({ + "disable_ai": false + }) + .to_string(); + cx.update_global::(|store, cx| { store.set_user_settings(&disable_false, cx).unwrap(); store.set_global_settings(&disable_true, cx).unwrap(); + }); + cx.update(|cx| { assert!( DisableAiSettings::get_global(cx).disable_ai, "Local false cannot override global true" ); + }); + cx.update_global::(|store, cx| { store.set_global_settings(&disable_false, cx).unwrap(); store.set_user_settings(&disable_true, cx).unwrap(); + }); + + cx.update(|cx| { assert!( DisableAiSettings::get_global(cx).disable_ai, "Local false cannot override global true" diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 212d3262e6fff1907cf2bd6ac91af8300cc1c739..b9bd386de361c77edbba0fb4570652983b6ed0fa 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1,14 +1,10 @@ use anyhow::Context as _; -use clock::Global; use collections::HashMap; use context_server::ContextServerCommand; use dap::adapters::DebugAdapterName; use fs::Fs; use futures::StreamExt as _; -use gpui::{ - App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, SharedString, Subscription, - Task, -}; +use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task}; use lsp::LanguageServerName; use paths::{ EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path, @@ -17,16 +13,17 @@ use paths::{ }; use rpc::{ AnyProtoClient, TypedEnvelope, - proto::{self, FromProto, Message, REMOTE_SERVER_PROJECT_ID, ToProto}, + proto::{self, FromProto, REMOTE_SERVER_PROJECT_ID, ToProto}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub use settings::DirenvSettings; +pub use settings::LspSettings; use settings::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, - SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, + InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsStore, SettingsUi, + parse_json_with_comments, watch_config_file, }; use std::{ - collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, time::Duration, @@ -51,13 +48,13 @@ pub struct ProjectSettings { /// name to the lsp value. /// Default: null // todo! should these hash map types be Map or Map - pub lsp: HashMap, + pub lsp: HashMap, /// Common language server settings. pub global_lsp_settings: GlobalLspSettings, /// Configuration for Debugger-related features - pub dap: HashMap, + pub dap: HashMap, /// Settings for context servers used for AI-related features. pub context_servers: HashMap, ContextServerSettings>, @@ -78,10 +75,35 @@ pub struct ProjectSettings { pub session: SessionSettings, } -#[derive(Debug, Clone, Default, PartialEq)] -pub struct DapSettings { - pub binary: String, - pub args: Vec, +#[derive(Copy, Clone, Debug)] +pub struct SessionSettings { + /// Whether or not to restore unsaved buffers on restart. + /// + /// If this is true, user won't be prompted whether to save/discard + /// dirty files when closing the application. + /// + /// Default: true + pub restore_unsaved_buffers: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct NodeBinarySettings { + /// The path to the Node binary. + pub path: Option, + /// The path to the npm binary Zed should use (defaults to `.path/../npm`). + pub npm_path: Option, + /// If enabled, Zed will download its own copy of Node. + pub ignore_system_version: bool, +} + +impl From for NodeBinarySettings { + fn from(settings: settings::NodeBinarySettings) -> Self { + Self { + path: settings.path, + npm_path: settings.npm_path, + ignore_system_version: settings.ignore_system_version.unwrap_or(false), + } + } } /// Common language server settings. @@ -281,7 +303,7 @@ pub struct GitSettings { /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. /// /// Default: null - pub gutter_debounce: u64, + pub gutter_debounce: Option, /// Whether or not to show git blame data inline in /// the currently focused line. /// @@ -415,7 +437,7 @@ pub struct InlineDiagnosticsSettings { /// Default: 0 pub min_column: u32, - pub max_severity: DiagnosticSeverity, + pub max_severity: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -432,7 +454,7 @@ pub struct LspPullDiagnosticsSettings { } impl Settings for ProjectSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { let project = &content.project.clone(); let diagnostics = content.diagnostics.as_ref().unwrap(); let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap(); @@ -441,7 +463,7 @@ impl Settings for ProjectSettings { let git = content.git.as_ref().unwrap(); let git_settings = GitSettings { git_gutter: git.git_gutter.unwrap(), - gutter_debounce: git.gutter_debounce.unwrap(), + gutter_debounce: git.gutter_debounce, inline_blame: { let inline = git.inline_blame.unwrap(); InlineBlameSettings { @@ -474,21 +496,18 @@ impl Settings for ProjectSettings { .map(|(key, value)| (LanguageServerName(key.into()), value.into())) .collect(), global_lsp_settings: GlobalLspSettings { - button: content.global_lsp_settings.unwrap().button.unwrap(), + button: content + .global_lsp_settings + .as_ref() + .unwrap() + .button + .unwrap(), }, dap: project .dap .clone() .into_iter() - .map(|(key, value)| { - ( - DebugAdapterName(key.into()), - DapSettings { - binary: value.binary.unwrap(), - args: value.args, - }, - ) - }) + .map(|(key, value)| (DebugAdapterName(key.into()), value)) .collect(), diagnostics: DiagnosticsSettings { button: diagnostics.button.unwrap(), @@ -502,17 +521,19 @@ impl Settings for ProjectSettings { update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(), padding: inline_diagnostics.padding.unwrap(), min_column: inline_diagnostics.min_column.unwrap(), - max_severity: inline_diagnostics.max_severity.unwrap().into(), + max_severity: inline_diagnostics.max_severity.map(Into::into), }, }, git: git_settings, - node: content.node.clone(), - load_direnv: project.load_direnv.unwrap(), - session: content.session.clone(), + node: content.node.clone().unwrap().into(), + load_direnv: project.load_direnv.clone().unwrap(), + session: SessionSettings { + restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(), + }, } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let project = &content.project; self.context_servers.extend( project @@ -521,16 +542,14 @@ impl Settings for ProjectSettings { .into_iter() .map(|(key, value)| (key, value.into())), ); - self.dap - .extend(project.dap.clone().into_iter().filter_map(|(key, value)| { - Some(( - DebugAdapterName(key.into()), - DapSettings { - binary: value.binary?, - args: value.args, - }, - )) - })); + dbg!(&self.context_servers); + self.dap.extend( + project + .dap + .clone() + .into_iter() + .filter_map(|(key, value)| Some((DebugAdapterName(key.into()), value))), + ); if let Some(diagnostics) = content.diagnostics.as_ref() { if let Some(inline) = &diagnostics.inline { self.diagnostics.inline.enabled.merge_from(&inline.enabled); @@ -543,10 +562,9 @@ impl Settings for ProjectSettings { .inline .min_column .merge_from(&inline.min_column); - self.diagnostics - .inline - .max_severity - .merge_from(&inline.max_severity.map(Into::into)); + if let Some(max_severity) = inline.max_severity { + self.diagnostics.inline.max_severity = Some(max_severity.into()) + } } self.diagnostics.button.merge_from(&diagnostics.button); @@ -595,18 +613,37 @@ impl Settings for ProjectSettings { } self.git.git_gutter.merge_from(&git.git_gutter); self.git.hunk_style.merge_from(&git.hunk_style); - self.git.gutter_debounce.merge_from(&git.gutter_debounce); + if let Some(debounce) = git.gutter_debounce { + self.git.gutter_debounce = Some(debounce); + } } - self.global_lsp_settings = content.global_lsp_settings.clone(); - self.load_direnv = content.project.load_direnv.clone(); - self.lsp.extend( - content - .project - .lsp - .clone() - .into_iter() - .map(|(key, value)| (key, lsp_settings)), + self.global_lsp_settings.button.merge_from( + &content + .global_lsp_settings + .as_ref() + .and_then(|settings| settings.button), ); + self.load_direnv + .merge_from(&content.project.load_direnv.clone()); + + for (key, value) in content.project.lsp.clone() { + self.lsp.insert(LanguageServerName(key.into()), value); + } + + if let Some(node) = content.node.as_ref() { + self.node + .ignore_system_version + .merge_from(&node.ignore_system_version); + if let Some(path) = node.path.clone() { + self.node.path = Some(path); + } + if let Some(npm_path) = node.npm_path.clone() { + self.node.npm_path = Some(npm_path); + } + } + self.session + .restore_unsaved_buffers + .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers)); } fn import_from_vscode( @@ -750,17 +787,19 @@ impl SettingsObserver { if let Some(upstream_client) = upstream_client { let mut user_settings = None; user_settings_watcher = Some(cx.observe_global::(move |_, cx| { - let new_settings = cx.global::().raw_user_settings(); - if Some(new_settings) != user_settings.as_ref() { - if let Some(new_settings_string) = serde_json::to_string(new_settings).ok() - { - user_settings = new_settings.clone(); - upstream_client - .send(proto::UpdateUserSettings { - project_id: REMOTE_SERVER_PROJECT_ID, - contents: new_settings_string, - }) - .log_err(); + if let Some(new_settings) = cx.global::().raw_user_settings() { + if Some(new_settings) != user_settings.as_ref() { + if let Some(new_settings_string) = + serde_json::to_string(new_settings).ok() + { + user_settings = Some(new_settings.clone()); + upstream_client + .send(proto::UpdateUserSettings { + project_id: REMOTE_SERVER_PROJECT_ID, + contents: new_settings_string, + }) + .log_err(); + } } } })); @@ -870,10 +909,9 @@ impl SettingsObserver { envelope: TypedEnvelope, cx: AsyncApp, ) -> anyhow::Result<()> { - let new_settings = serde_json::from_str::(&envelope.payload.contents) - .with_context(|| { - format!("deserializing {} user settings", envelope.payload.contents) - })?; + let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| { + format!("deserializing {} user settings", envelope.payload.contents) + })?; cx.update_global(|settings_store: &mut SettingsStore, cx| { settings_store .set_raw_user_settings(new_settings, cx) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5daaa09b9bafc7aa6419e017f2c378a020bdba91..915ae07230bdae01bc16f2cd4318658b42dcdfb7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -8769,8 +8769,8 @@ async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) { init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(Vec::new()); + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(Vec::new()); }); }); }); diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 3945db5c9ec391273a464b32ee055599ffcf30c2..3267d7cab3d13bd58f66193bbeb01d093b465cbe 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -19,7 +19,7 @@ pub use util::serde::default_true; use crate::ActiveSettingsProfileName; -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct SettingsContent { #[serde(flatten)] pub project: ProjectSettingsContent, @@ -45,9 +45,6 @@ pub struct SettingsContent { /// Configuration for Git-related features pub git: Option, - /// The list of custom Git hosting providers. - pub git_hosting_providers: Option>, - /// Common language server settings. pub global_lsp_settings: Option, @@ -70,7 +67,7 @@ pub struct SettingsContent { pub server_url: Option, /// Configuration for session-related features - pub session: Option, + pub session: Option, /// Control what info is collected by Zed. pub telemetry: Option, @@ -86,6 +83,11 @@ pub struct SettingsContent { // Settings related to calls in Zed pub calls: Option, + + /// Whether to disable all AI features in Zed. + /// + /// Default: false + pub disable_ai: Option, } impl SettingsContent { @@ -101,7 +103,7 @@ pub struct ServerSettingsContent { pub project: ProjectSettingsContent, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct UserSettingsContent { #[serde(flatten)] pub content: SettingsContent, @@ -211,7 +213,7 @@ pub enum TitleBarVisibilityContent { } /// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AudioSettingsContent { /// Opt into the new audio system. #[serde(rename = "experimental.rodio_audio", default)] @@ -231,31 +233,8 @@ pub struct AudioSettingsContent { pub control_output_volume: Option, } -/// A custom Git hosting provider. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct GitHostingProviderConfig { - /// The type of the provider. - /// - /// Must be one of `github`, `gitlab`, or `bitbucket`. - pub provider: GitHostingProviderKind, - - /// The base URL for the provider (e.g., "https://code.corp.big.com"). - pub base_url: String, - - /// The display name for the provider (e.g., "BigCorp GitHub"). - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitHostingProviderKind { - Github, - Gitlab, - Bitbucket, -} - /// Control what info is collected by Zed. -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug)] pub struct TelemetrySettingsContent { /// Send debug info like crash reports. /// @@ -267,7 +246,7 @@ pub struct TelemetrySettingsContent { pub metrics: Option, } -#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)] pub struct DebuggerSettingsContent { /// Determines the stepping granularity. /// @@ -322,21 +301,21 @@ pub enum DockPosition { } /// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. pub cargo_workspace: Option, } /// Settings for the `/cargo-workspace` slash command. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)] pub struct CargoWorkspaceCommandSettings { /// Whether `/cargo-workspace` is enabled. pub enabled: Option, } /// Configuration of voice calls in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct CallSettingsContent { /// Whether the microphone should be muted when joining a channel or a call. /// diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index c051491a6537808241cc2eea30c25fa0dfd40a91..1c6296f67ec4e4dbfce3e16522f5a255324ba10c 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -4,7 +4,7 @@ use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, path::PathBuf, sync::Arc}; -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)] +#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct AgentSettingsContent { /// Whether the Agent is enabled. /// @@ -174,7 +174,7 @@ pub struct ContextServerPresetContent { pub tools: IndexMap, bool>, } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AgentDockPosition { Left, @@ -183,7 +183,7 @@ pub enum AgentDockPosition { Bottom, } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DefaultAgentView { #[default] @@ -191,7 +191,7 @@ pub enum DefaultAgentView { TextThread, } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub enum NotifyWhenAgentWaiting { #[default] @@ -263,7 +263,7 @@ impl From<&str> for LanguageModelProviderSetting { } } -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index b35c829085135ab48909b763e313ad0919c0f9f5..541b700950be45b83d54e93427f236b867cfb136 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -5,7 +5,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use util::serde::default_true; -use crate::AllLanguageSettingsContent; +use crate::{AllLanguageSettingsContent, SlashCommandSettings}; #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettingsContent { @@ -24,11 +24,11 @@ pub struct ProjectSettingsContent { /// name to the lsp value. /// Default: null #[serde(default)] - pub lsp: HashMap, LspSettingsContent>, + pub lsp: HashMap, LspSettings>, /// Configuration for Debugger-related features #[serde(default)] - pub dap: HashMap, DapSettingsContent>, + pub dap: HashMap, DapSettings>, /// Settings for context servers used for AI-related features. #[serde(default)] @@ -39,6 +39,9 @@ pub struct ProjectSettingsContent { /// Settings for slash commands. pub slash_commands: Option, + + /// The list of custom Git hosting providers. + pub git_hosting_providers: Option>, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -80,7 +83,7 @@ pub struct WorktreeSettingsContent { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] -pub struct LspSettingsContent { +pub struct LspSettings { pub binary: Option, pub initialization_options: Option, pub settings: Option, @@ -92,7 +95,7 @@ pub struct LspSettingsContent { pub fetch: Option, } -impl Default for LspSettingsContent { +impl Default for LspSettings { fn default() -> Self { Self { binary: None, @@ -119,7 +122,7 @@ pub struct FetchSettings { } /// Common language server settings. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct GlobalLspSettingsContent { /// Whether to show the LSP servers button in the status bar. /// @@ -128,31 +131,23 @@ pub struct GlobalLspSettingsContent { } // todo! binary is actually just required, shouldn't be an option -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct DapSettingsContent { +pub struct DapSettings { pub binary: Option, #[serde(default)] pub args: Vec, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct SessionSettings { +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SessionSettingsContent { /// Whether or not to restore unsaved buffers on restart. /// /// If this is true, user won't be prompted whether to save/discard /// dirty files when closing the application. /// /// Default: true - pub restore_unsaved_buffers: bool, -} - -impl Default for SessionSettings { - fn default() -> Self { - Self { - restore_unsaved_buffers: true, - } - } + pub restore_unsaved_buffers: Option, } #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] @@ -213,7 +208,7 @@ impl std::fmt::Debug for ContextServerCommand { } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { /// Whether or not to show the git gutter. /// @@ -238,7 +233,7 @@ pub struct GitSettings { pub hunk_style: Option, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GitGutterSetting { /// Show git gutter in tracked files. @@ -248,7 +243,7 @@ pub enum GitGutterSetting { Hide, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InlineBlameSettings { /// Whether or not to show git blame data inline in @@ -276,7 +271,7 @@ pub struct InlineBlameSettings { pub show_commit_summary: Option, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct BranchPickerSettingsContent { /// Whether to show author name as part of the commit information. @@ -285,7 +280,7 @@ pub struct BranchPickerSettingsContent { pub show_author_name: Option, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GitHunkStyleSetting { /// Show unstaged hunks with a filled background and staged hunks hollow. @@ -295,7 +290,7 @@ pub enum GitHunkStyleSetting { UnstagedHollow, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct DiagnosticsSettingsContent { /// Whether to show the project diagnostics button in the status bar. pub button: Option, @@ -349,7 +344,7 @@ pub struct InlineDiagnosticsSettingsContent { pub max_severity: Option, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct NodeBinarySettings { /// The path to the Node binary. pub path: Option, @@ -382,3 +377,26 @@ pub enum DiagnosticSeverityContent { #[serde(alias = "all")] Hint, } + +/// A custom Git hosting provider. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] +pub struct GitHostingProviderConfig { + /// The type of the provider. + /// + /// Must be one of `github`, `gitlab`, or `bitbucket`. + pub provider: GitHostingProviderKind, + + /// The base URL for the provider (e.g., "https://code.corp.big.com"). + pub base_url: String, + + /// The display name for the provider (e.g., "BigCorp GitHub"). + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitHostingProviderKind { + Github, + Gitlab, + Bitbucket, +} diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index 640726869b6592a890937c63e1a256d7f09b7594..b0798e7c7b359b8c1d9890212d9e2b31982fe440 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -7,7 +7,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::sync::Arc; /// Settings for rendering text in UI and text buffers. -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { /// The default font size for text in the UI. #[serde(default)] From debd8701cda982965662a146fb2d643749fa8bd4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 15:37:34 -0600 Subject: [PATCH 033/117] Extension host --- crates/call/src/call_settings.rs | 21 +++++------- .../extension_host/src/extension_settings.rs | 34 +++++++++---------- crates/languages/src/lib.rs | 4 +-- crates/settings/src/settings_content.rs | 18 ++++++++++ 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 6fa10a77da327a2e02f15c2793e9d35b9a1f583e..cce8142903c17ec41244d346a79feb8f4cad70e6 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -1,35 +1,32 @@ -use anyhow::Result; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; use util::MergeFrom; -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct CallSettings { pub mute_on_join: bool, pub share_on_join: bool, } impl Settings for CallSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { - let call = content.call.unwrap(); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let call = content.calls.clone().unwrap(); CallSettings { mute_on_join: call.mute_on_join.unwrap(), share_on_join: call.share_on_join.unwrap(), } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { - if let Some(call) = content.call.clone() { - self.mute_on_join.merge_from(call.mute_on_join); - self.share_on_join.merge_from(call.share_on_join); + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + if let Some(call) = content.calls.clone() { + self.mute_on_join.merge_from(&call.mute_on_join); + self.share_on_join.merge_from(&call.share_on_join); } } fn import_from_vscode( _vscode: &settings::VsCodeSettings, - _current: &settings::SettingsContent, + _current: &mut settings::SettingsContent, ) { } } diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index fa5a613c55a76a0b5660b114d49acc17fcf79120..b9e1609b42d6e7fa2f4dab63486a77a13f29824f 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -1,13 +1,9 @@ -use anyhow::Result; use collections::HashMap; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; use std::sync::Arc; -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] +#[derive(Debug, Default, Clone)] pub struct ExtensionSettings { /// The extensions that should be automatically installed by Zed. /// @@ -15,9 +11,7 @@ pub struct ExtensionSettings { /// available out-of-the-box. /// /// Default: { "html": true } - #[serde(default)] pub auto_install_extensions: HashMap, bool>, - #[serde(default)] pub auto_update_extensions: HashMap, bool>, } @@ -39,18 +33,24 @@ impl ExtensionSettings { } impl Settings for ExtensionSettings { - type FileContent = Self; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + auto_install_extensions: content.extension.auto_install_extensions.clone(), + auto_update_extensions: content.extension.auto_update_extensions.clone(), + } + } - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - SettingsSources::::json_merge_with( - [sources.default] - .into_iter() - .chain(sources.user) - .chain(sources.server), - ) + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + self.auto_install_extensions + .extend(content.extension.auto_install_extensions.clone()); + self.auto_update_extensions + .extend(content.extension.auto_update_extensions.clone()); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { // settingsSync.ignoredExtensions controls autoupdate for vscode extensions, but we // don't have a mapping to zed-extensions. there's also extensions.autoCheckUpdates // and extensions.autoUpdate which are global switches, we don't support those yet diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index f83e9b3b8620dc9887d49313522eec2603c4273e..8ab84ed835a448649c732251fb4715fa8a776a85 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -312,8 +312,8 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime SettingsStore::update_global(cx, |settings, cx| { settings .set_extension_settings( - ExtensionsSettingsContent { - languages: language_settings.clone(), + settings::ExtensionsSettingsContent { + all_languages: language_settings.clone(), }, cx, ) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 3267d7cab3d13bd58f66193bbeb01d093b465cbe..8f279e5e4db2bfd2d17d78d7de4d8a550dd848e2 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -15,6 +15,7 @@ use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::env; +use std::sync::Arc; pub use util::serde::default_true; use crate::ActiveSettingsProfileName; @@ -27,6 +28,9 @@ pub struct SettingsContent { #[serde(flatten)] pub theme: ThemeSettingsContent, + #[serde(flatten)] + pub extension: ExtensionSettingsContent, + pub agent: Option, pub agent_servers: Option, @@ -327,3 +331,17 @@ pub struct CallSettingsContent { /// Default: false pub share_on_join: Option, } + +#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema)] +pub struct ExtensionSettingsContent { + /// The extensions that should be automatically installed by Zed. + /// + /// This is used to make functionality provided by extensions (e.g., language support) + /// available out-of-the-box. + /// + /// Default: { "html": true } + #[serde(default)] + pub auto_install_extensions: HashMap, bool>, + #[serde(default)] + pub auto_update_extensions: HashMap, bool>, +} From ed270c8646d3a0b9922ce1c8f5e47719891ff0ab Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 16:02:53 -0600 Subject: [PATCH 034/117] workspace pt 1 --- crates/markdown/examples/markdown.rs | 5 +- crates/markdown/examples/markdown_as_child.rs | 5 +- crates/project/src/direnv.rs | 2 +- crates/settings/src/settings_content.rs | 9 + .../src/settings_content/workspace.rs | 305 ++++++++++++++++++ crates/workspace/src/item.rs | 213 ++++++------ crates/workspace/src/workspace.rs | 4 +- crates/workspace/src/workspace_settings.rs | 225 ------------- 8 files changed, 410 insertions(+), 358 deletions(-) create mode 100644 crates/settings/src/settings_content/workspace.rs diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index c019bad1d081e59077bedb05cb36673a48f294bc..b4cb2a2503dcb6e097c34e4c8aad718f89e30272 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -1,6 +1,6 @@ use assets::Assets; use gpui::{Application, Entity, KeyBinding, StyleRefinement, WindowOptions, prelude::*, rgb}; -use language::{LanguageRegistry, language_settings::AllLanguageSettings}; +use language::LanguageRegistry; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use node_runtime::NodeRuntime; use settings::SettingsStore; @@ -39,9 +39,6 @@ pub fn main() { let store = SettingsStore::test(cx); cx.set_global(store); language::init(cx); - SettingsStore::update(cx, |store, cx| { - store.update_user_settings::(cx, |_| {}); - }); cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); let node_runtime = NodeRuntime::unavailable(); diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 6c0960c88b3021b6012b769aef8ffb22050b1abd..784047dc3dbd4130f0464b3e13be940b6634fedc 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -1,6 +1,6 @@ use assets::Assets; use gpui::{Application, Entity, KeyBinding, Length, StyleRefinement, WindowOptions, rgb}; -use language::{LanguageRegistry, language_settings::AllLanguageSettings}; +use language::LanguageRegistry; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use node_runtime::NodeRuntime; use settings::SettingsStore; @@ -23,9 +23,6 @@ pub fn main() { let store = SettingsStore::test(cx); cx.set_global(store); language::init(cx); - SettingsStore::update(cx, |store, cx| { - store.update_user_settings::(cx, |_| {}); - }); cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); let node_runtime = NodeRuntime::unavailable(); diff --git a/crates/project/src/direnv.rs b/crates/project/src/direnv.rs index 9ba0ad10e3173a1930186db28f7efb5d9a8267f7..32f4963fd19805e369c696cfd30c8ef599bd06f3 100644 --- a/crates/project/src/direnv.rs +++ b/crates/project/src/direnv.rs @@ -65,7 +65,7 @@ pub async fn load_direnv_environment( let output = String::from_utf8_lossy(&direnv_output.stdout); if output.is_empty() { // direnv outputs nothing when it has no changes to apply to environment variables - return Ok(HashMap::new()); + return Ok(HashMap::default()); } match serde_json::from_str(&output) { diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 8f279e5e4db2bfd2d17d78d7de4d8a550dd848e2..6e128e50b52958e8e24feb699ee9541782556574 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -3,11 +3,13 @@ mod language; mod project; mod terminal; mod theme; +mod workspace; pub use agent::*; pub use language::*; pub use project::*; pub use terminal::*; pub use theme::*; +pub use workspace::*; use collections::HashMap; use gpui::{App, SharedString}; @@ -31,6 +33,13 @@ pub struct SettingsContent { #[serde(flatten)] pub extension: ExtensionSettingsContent, + #[serde(flatten)] + pub workspace: WorkspaceSettingsContent, + + pub tabs: Option, + + pub preview_tabs: Option, + pub agent: Option, pub agent_servers: Option, diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9ecd867c4b56907c7ee189dcfe5f79aebabcc12 --- /dev/null +++ b/crates/settings/src/settings_content/workspace.rs @@ -0,0 +1,305 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +pub struct WorkspaceSettingsContent { + /// Active pane styling settings. + pub active_pane_modifiers: Option, + /// Layout mode for the bottom dock + /// + /// Default: contained + pub bottom_dock_layout: Option, + /// Direction to split horizontally. + /// + /// Default: "up" + pub pane_split_direction_horizontal: Option, + /// Direction to split vertically. + /// + /// Default: "left" + pub pane_split_direction_vertical: Option, + /// Centered layout related settings. + pub centered_layout: Option, + /// Whether or not to prompt the user to confirm before closing the application. + /// + /// Default: false + pub confirm_quit: Option, + /// Whether or not to show the call status icon in the status bar. + /// + /// Default: true + pub show_call_status_icon: Option, + /// When to automatically save edited buffers. + /// + /// Default: off + pub autosave: Option, + /// Controls previous session restoration in freshly launched Zed instance. + /// Values: none, last_workspace, last_session + /// Default: last_session + pub restore_on_startup: Option, + /// Whether to attempt to restore previous file's state when opening it again. + /// The state is stored per pane. + /// When disabled, defaults are applied instead of the state restoration. + /// + /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. + /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. + /// + /// Default: true + pub restore_on_file_reopen: Option, + /// The size of the workspace split drop targets on the outer edges. + /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. + /// + /// Default: `0.2` (20% of the smaller dimension of the workspace) + pub drop_target_size: Option, + /// Whether to close the window when using 'close active item' on a workspace with no tabs + /// + /// Default: auto ("on" on macOS, "off" otherwise) + pub when_closing_with_no_tabs: Option, + /// Whether to use the system provided dialogs for Open and Save As. + /// When set to false, Zed will use the built-in keyboard-first pickers. + /// + /// Default: true + pub use_system_path_prompts: Option, + /// Whether to use the system provided prompts. + /// When set to false, Zed will use the built-in prompts. + /// Note that this setting has no effect on Linux, where Zed will always + /// use the built-in prompts. + /// + /// Default: true + pub use_system_prompts: Option, + /// Aliases for the command palette. When you type a key in this map, + /// it will be assumed to equal the value. + /// + /// Default: true + pub command_aliases: Option>, + /// Maximum open tabs in a pane. Will not close an unsaved + /// tab. Set to `None` for unlimited tabs. + /// + /// Default: none + pub max_tabs: Option, + /// What to do when the last window is closed + /// + /// Default: auto (nothing on macOS, "app quit" otherwise) + pub on_last_window_closed: Option, + /// Whether to resize all the panels in a dock when resizing the dock. + /// + /// Default: ["left"] + pub resize_all_panels_in_dock: Option>, + /// Whether to automatically close files that have been deleted on disk. + /// + /// Default: false + pub close_on_file_delete: Option, + /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). + /// + /// Default: false + pub use_system_window_tabs: Option, + /// Whether to show padding for zoomed panels. + /// When enabled, zoomed bottom panels will have some top padding, + /// while zoomed left/right panels will have padding to the right/left (respectively). + /// + /// Default: true + pub zoomed_padding: Option, +} + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ItemSettingsContent { + /// Whether to show the Git file status on a tab item. + /// + /// Default: false + pub git_status: Option, + /// Position of the close button in a tab. + /// + /// Default: right + pub close_position: Option, + /// Whether to show the file icon for a tab. + /// + /// Default: false + pub file_icons: Option, + /// What to do after closing the current tab. + /// + /// Default: history + pub activate_on_close: Option, + /// Which files containing diagnostic errors/warnings to mark in the tabs. + /// This setting can take the following three values: + /// + /// Default: off + pub show_diagnostics: Option, + /// Whether to always show the close button on tabs. + /// + /// Default: false + pub show_close_button: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct PreviewTabsSettingsContent { + /// Whether to show opened editors as preview tabs. + /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. + /// + /// Default: true + pub enabled: Option, + /// Whether to open tabs in preview mode when selected from the file finder. + /// + /// Default: false + pub enable_preview_from_file_finder: Option, + /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab. + /// + /// Default: false + pub enable_preview_from_code_navigation: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ClosePosition { + Left, + #[default] + Right, +} + +#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ShowCloseButton { + Always, + #[default] + Hover, + Hidden, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowDiagnostics { + #[default] + Off, + Errors, + All, +} + +#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateOnClose { + #[default] + History, + Neighbour, + LeftNeighbour, +} + +#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct ActivePanelModifiers { + /// Size of the border surrounding the active pane. + /// When set to 0, the active pane doesn't have any border. + /// The border is drawn inset. + /// + /// Default: `0.0` + pub border_size: Option, + /// Opacity of inactive panels. + /// When set to 1.0, the inactive panes have the same opacity as the active one. + /// If set to 0, the inactive panes content will not be visible at all. + /// Values are clamped to the [0.0, 1.0] range. + /// + /// Default: `1.0` + pub inactive_opacity: Option, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum BottomDockLayout { + /// Contained between the left and right docks + #[default] + Contained, + /// Takes up the full width of the window + Full, + /// Extends under the left dock while snapping to the right dock + LeftAligned, + /// Extends under the right dock while snapping to the left dock + RightAligned, +} + +#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CloseWindowWhenNoItems { + /// Match platform conventions by default, so "on" on macOS and "off" everywhere else + #[default] + PlatformDefault, + /// Close the window when there are no tabs + CloseWindow, + /// Leave the window open when there are no tabs + KeepWindowOpen, +} + +impl CloseWindowWhenNoItems { + pub fn should_close(&self) -> bool { + match self { + CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"), + CloseWindowWhenNoItems::CloseWindow => true, + CloseWindowWhenNoItems::KeepWindowOpen => false, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum RestoreOnStartupBehavior { + /// Always start with an empty editor + None, + /// Restore the workspace that was closed last. + LastWorkspace, + /// Restore all workspaces that were open when quitting Zed. + #[default] + LastSession, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct TabBarSettingsContent { + /// Whether or not to show the tab bar in the editor. + /// + /// Default: true + pub show: Option, + /// Whether or not to show the navigation history buttons in the tab bar. + /// + /// Default: true + pub show_nav_history_buttons: Option, + /// Whether or not to show the tab bar buttons. + /// + /// Default: true + pub show_tab_bar_buttons: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AutosaveSetting { + /// Disable autosave. + Off, + /// Save after inactivity period of `milliseconds`. + AfterDelay { milliseconds: u64 }, + /// Autosave when focus changes. + OnFocusChange, + /// Autosave when the active window changes. + OnWindowChange, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaneSplitDirectionHorizontal { + Up, + Down, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum PaneSplitDirectionVertical { + Left, + Right, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[serde(rename_all = "snake_case")] +pub struct CenteredLayoutSettings { + /// The relative width of the left padding of the central pane from the + /// workspace when the centered layout is used. + /// + /// Default: 0.2 + pub left_padding: Option, + // The relative width of the right padding of the central pane from the + // workspace when the centered layout is used. + /// + /// Default: 0.2 + pub right_padding: Option, +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 23fbec470c4d2e305bf7b51679bbe56f6dfeaa95..055859bec395445de91978397905d2162723fce2 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -15,9 +15,9 @@ use gpui::{ Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window, }; use project::{Project, ProjectEntryId, ProjectPath}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsUi}; +use settings::{ + ActivateOnClose, ClosePosition, Settings, SettingsLocation, ShowCloseButton, ShowDiagnostics, +}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -30,7 +30,7 @@ use std::{ }; use theme::Theme; use ui::{Color, Icon, IntoElement, Label, LabelCommon}; -use util::ResultExt; +use util::{MergeFrom as _, ResultExt}; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); @@ -49,7 +49,6 @@ impl Default for SaveOptions { } } -#[derive(Deserialize)] pub struct ItemSettings { pub git_status: bool, pub close_position: ClosePosition, @@ -59,150 +58,120 @@ pub struct ItemSettings { pub show_close_button: ShowCloseButton, } -#[derive(Deserialize)] pub struct PreviewTabsSettings { pub enabled: bool, pub enable_preview_from_file_finder: bool, pub enable_preview_from_code_navigation: bool, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum ClosePosition { - Left, - #[default] - Right, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum ShowCloseButton { - Always, - #[default] - Hover, - Hidden, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowDiagnostics { - #[default] - Off, - Errors, - All, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ActivateOnClose { - #[default] - History, - Neighbour, - LeftNeighbour, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "tabs")] -pub struct ItemSettingsContent { - /// Whether to show the Git file status on a tab item. - /// - /// Default: false - git_status: Option, - /// Position of the close button in a tab. - /// - /// Default: right - close_position: Option, - /// Whether to show the file icon for a tab. - /// - /// Default: false - file_icons: Option, - /// What to do after closing the current tab. - /// - /// Default: history - pub activate_on_close: Option, - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// This setting can take the following three values: - /// - /// Default: off - show_diagnostics: Option, - /// Whether to always show the close button on tabs. - /// - /// Default: false - show_close_button: Option, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "preview_tabs")] -pub struct PreviewTabsSettingsContent { - /// Whether to show opened editors as preview tabs. - /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. - /// - /// Default: true - enabled: Option, - /// Whether to open tabs in preview mode when selected from the file finder. - /// - /// Default: false - enable_preview_from_file_finder: Option, - /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab. - /// - /// Default: false - enable_preview_from_code_navigation: Option, -} - impl Settings for ItemSettings { - type FileContent = ItemSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let tabs = content.tabs.as_ref().unwrap(); + Self { + git_status: tabs.git_status.unwrap(), + close_position: tabs.close_position.unwrap(), + activate_on_close: tabs.activate_on_close.unwrap(), + file_icons: tabs.file_icons.unwrap(), + show_diagnostics: tabs.show_diagnostics.unwrap(), + show_close_button: tabs.show_close_button.unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(tabs) = content.tabs.as_ref() else { + return; + }; + self.git_status.merge_from(&tabs.git_status); + self.close_position.merge_from(&tabs.close_position); + self.activate_on_close.merge_from(&tabs.activate_on_close); + self.file_icons.merge_from(&tabs.file_icons); + self.show_diagnostics.merge_from(&tabs.show_diagnostics); + self.show_close_button.merge_from(&tabs.show_close_button); } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") { - current.show_close_button = Some(if b { + current.tabs.get_or_insert_default().show_close_button = Some(if b { ShowCloseButton::Always } else { ShowCloseButton::Hidden }) } - vscode.enum_setting( - "workbench.editor.tabActionLocation", - &mut current.close_position, - |s| match s { - "right" => Some(ClosePosition::Right), - "left" => Some(ClosePosition::Left), - _ => None, - }, - ); + if let Some(s) = vscode.read_enum_setting("workbench.editor.tabActionLocation", |s| match s + { + "right" => Some(ClosePosition::Right), + "left" => Some(ClosePosition::Left), + _ => None, + }) { + current.tabs.get_or_insert_default().close_position = Some(s) + } if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") { - current.activate_on_close = Some(if b { + current.tabs.get_or_insert_default().activate_on_close = Some(if b { ActivateOnClose::History } else { ActivateOnClose::LeftNeighbour }) } - vscode.bool_setting("workbench.editor.showIcons", &mut current.file_icons); - vscode.bool_setting("git.decorations.enabled", &mut current.git_status); + if let Some(b) = vscode.read_bool("workbench.editor.showIcons") { + current.tabs.get_or_insert_default().file_icons = Some(b); + }; + if let Some(b) = vscode.read_bool("git.decorations.enabled") { + current.tabs.get_or_insert_default().git_status = Some(b); + } } } impl Settings for PreviewTabsSettings { - type FileContent = PreviewTabsSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.bool_setting("workbench.editor.enablePreview", &mut current.enabled); - vscode.bool_setting( - "workbench.editor.enablePreviewFromCodeNavigation", - &mut current.enable_preview_from_code_navigation, - ); - vscode.bool_setting( - "workbench.editor.enablePreviewFromQuickOpen", - &mut current.enable_preview_from_file_finder, - ); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let preview_tabs = content.preview_tabs.as_ref().unwrap(); + Self { + enabled: preview_tabs.enabled.unwrap(), + enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(), + enable_preview_from_code_navigation: preview_tabs + .enable_preview_from_code_navigation + .unwrap(), + } + } + + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(preview_tabs) = content.preview_tabs.as_ref() else { + return; + }; + + self.enabled.merge_from(&preview_tabs.enabled); + self.enable_preview_from_file_finder + .merge_from(&preview_tabs.enable_preview_from_file_finder); + self.enable_preview_from_code_navigation + .merge_from(&preview_tabs.enable_preview_from_code_navigation); + } + + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { + if let Some(enabled) = vscode.read_bool("workbench.editor.enablePreview") { + current.preview_tabs.get_or_insert_default().enabled = Some(enabled); + } + if let Some(enable_preview_from_code_navigation) = + vscode.read_bool("workbench.editor.enablePreviewFromCodeNavigation") + { + current + .preview_tabs + .get_or_insert_default() + .enable_preview_from_code_navigation = Some(enable_preview_from_code_navigation) + } + if let Some(enable_preview_from_file_finder) = + vscode.read_bool("workbench.editor.enablePreviewFromQuickOpen") + { + current + .preview_tabs + .get_or_insert_default() + .enable_preview_from_file_finder = Some(enable_preview_from_file_finder) + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 23d611964c3cd1b0284521c9444b6952748a5db9..34aa0da8896fd66b00ffa7a5ce9c32ec74dd8b99 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9889,8 +9889,8 @@ mod tests { // Enable the close_on_file_delete setting cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.close_on_file_delete = Some(true); + store.update_user_settings(cx, |settings| { + settings.tabs.get_or_insert_default().close_on_file_delete = Some(true); }); }); diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 8868f3190575ac4b861e0619732890f477d83b69..4108bec78384d49ecfbff83e5cbc2f6edcc11368 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -52,237 +52,12 @@ impl OnLastWindowClosed { } } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ActivePanelModifiers { - /// Size of the border surrounding the active pane. - /// When set to 0, the active pane doesn't have any border. - /// The border is drawn inset. - /// - /// Default: `0.0` - pub border_size: Option, - /// Opacity of inactive panels. - /// When set to 1.0, the inactive panes have the same opacity as the active one. - /// If set to 0, the inactive panes content will not be visible at all. - /// Values are clamped to the [0.0, 1.0] range. - /// - /// Default: `1.0` - pub inactive_opacity: Option, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum BottomDockLayout { - /// Contained between the left and right docks - #[default] - Contained, - /// Takes up the full width of the window - Full, - /// Extends under the left dock while snapping to the right dock - LeftAligned, - /// Extends under the right dock while snapping to the left dock - RightAligned, -} - -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum CloseWindowWhenNoItems { - /// Match platform conventions by default, so "on" on macOS and "off" everywhere else - #[default] - PlatformDefault, - /// Close the window when there are no tabs - CloseWindow, - /// Leave the window open when there are no tabs - KeepWindowOpen, -} - -impl CloseWindowWhenNoItems { - pub fn should_close(&self) -> bool { - match self { - CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"), - CloseWindowWhenNoItems::CloseWindow => true, - CloseWindowWhenNoItems::KeepWindowOpen => false, - } - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RestoreOnStartupBehavior { - /// Always start with an empty editor - None, - /// Restore the workspace that was closed last. - LastWorkspace, - /// Restore all workspaces that were open when quitting Zed. - #[default] - LastSession, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] -pub struct WorkspaceSettingsContent { - /// Active pane styling settings. - pub active_pane_modifiers: Option, - /// Layout mode for the bottom dock - /// - /// Default: contained - pub bottom_dock_layout: Option, - /// Direction to split horizontally. - /// - /// Default: "up" - pub pane_split_direction_horizontal: Option, - /// Direction to split vertically. - /// - /// Default: "left" - pub pane_split_direction_vertical: Option, - /// Centered layout related settings. - pub centered_layout: Option, - /// Whether or not to prompt the user to confirm before closing the application. - /// - /// Default: false - pub confirm_quit: Option, - /// Whether or not to show the call status icon in the status bar. - /// - /// Default: true - pub show_call_status_icon: Option, - /// When to automatically save edited buffers. - /// - /// Default: off - pub autosave: Option, - /// Controls previous session restoration in freshly launched Zed instance. - /// Values: none, last_workspace, last_session - /// Default: last_session - pub restore_on_startup: Option, - /// Whether to attempt to restore previous file's state when opening it again. - /// The state is stored per pane. - /// When disabled, defaults are applied instead of the state restoration. - /// - /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. - /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. - /// - /// Default: true - pub restore_on_file_reopen: Option, - /// The size of the workspace split drop targets on the outer edges. - /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. - /// - /// Default: `0.2` (20% of the smaller dimension of the workspace) - pub drop_target_size: Option, - /// Whether to close the window when using 'close active item' on a workspace with no tabs - /// - /// Default: auto ("on" on macOS, "off" otherwise) - pub when_closing_with_no_tabs: Option, - /// Whether to use the system provided dialogs for Open and Save As. - /// When set to false, Zed will use the built-in keyboard-first pickers. - /// - /// Default: true - pub use_system_path_prompts: Option, - /// Whether to use the system provided prompts. - /// When set to false, Zed will use the built-in prompts. - /// Note that this setting has no effect on Linux, where Zed will always - /// use the built-in prompts. - /// - /// Default: true - pub use_system_prompts: Option, - /// Aliases for the command palette. When you type a key in this map, - /// it will be assumed to equal the value. - /// - /// Default: true - pub command_aliases: Option>, - /// Maximum open tabs in a pane. Will not close an unsaved - /// tab. Set to `None` for unlimited tabs. - /// - /// Default: none - pub max_tabs: Option, - /// What to do when the last window is closed - /// - /// Default: auto (nothing on macOS, "app quit" otherwise) - pub on_last_window_closed: Option, - /// Whether to resize all the panels in a dock when resizing the dock. - /// - /// Default: ["left"] - pub resize_all_panels_in_dock: Option>, - /// Whether to automatically close files that have been deleted on disk. - /// - /// Default: false - pub close_on_file_delete: Option, - /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - /// - /// Default: false - pub use_system_window_tabs: Option, - /// Whether to show padding for zoomed panels. - /// When enabled, zoomed bottom panels will have some top padding, - /// while zoomed left/right panels will have padding to the right/left (respectively). - /// - /// Default: true - pub zoomed_padding: Option, -} - #[derive(Deserialize)] pub struct TabBarSettings { pub show: bool, pub show_nav_history_buttons: bool, pub show_tab_bar_buttons: bool, } - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "tab_bar")] -pub struct TabBarSettingsContent { - /// Whether or not to show the tab bar in the editor. - /// - /// Default: true - pub show: Option, - /// Whether or not to show the navigation history buttons in the tab bar. - /// - /// Default: true - pub show_nav_history_buttons: Option, - /// Whether or not to show the tab bar buttons. - /// - /// Default: true - pub show_tab_bar_buttons: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AutosaveSetting { - /// Disable autosave. - Off, - /// Save after inactivity period of `milliseconds`. - AfterDelay { milliseconds: u64 }, - /// Autosave when focus changes. - OnFocusChange, - /// Autosave when the active window changes. - OnWindowChange, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionHorizontal { - Up, - Down, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionVertical { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub struct CenteredLayoutSettings { - /// The relative width of the left padding of the central pane from the - /// workspace when the centered layout is used. - /// - /// Default: 0.2 - pub left_padding: Option, - // The relative width of the right padding of the central pane from the - // workspace when the centered layout is used. - /// - /// Default: 0.2 - pub right_padding: Option, -} - impl Settings for WorkspaceSettings { type FileContent = WorkspaceSettingsContent; From 27da40941d90e1d4c8ff9b48a2bca49cc53c9b5a Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 16 Sep 2025 18:34:01 -0500 Subject: [PATCH 035/117] workspace pt 2 --- Cargo.lock | 1 + crates/project/src/project_settings.rs | 2 +- .../src/settings_content/workspace.rs | 51 +++- crates/settings/src/vscode_import.rs | 2 +- crates/theme_importer/Cargo.toml | 1 + crates/theme_importer/src/main.rs | 4 +- crates/theme_importer/src/vscode/converter.rs | 4 +- crates/title_bar/src/title_bar_settings.rs | 7 +- crates/workspace/src/dock.rs | 15 +- crates/workspace/src/item.rs | 5 +- crates/workspace/src/pane.rs | 14 +- crates/workspace/src/workspace.rs | 12 +- crates/workspace/src/workspace_settings.rs | 260 +++++++++++++----- 13 files changed, 276 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdc3adb1f170eb23bc02abd511b9cc6c093fcc53..5a9ca9a409c622b5cc7b5316b92f43399946e092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16600,6 +16600,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "collections", "gpui", "indexmap", "log", diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index b9bd386de361c77edbba0fb4570652983b6ed0fa..192728ac66af21b02c8562b2f14a4a0f8b484364 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -652,7 +652,7 @@ impl Settings for ProjectSettings { ) { // this just sets the binary name instead of a full path so it relies on path lookup // resolving to the one you want - let npm_path = vscode.read_enum_setting("npm.packageManager", |s| match s { + let npm_path = vscode.read_enum("npm.packageManager", |s| match s { v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), _ => None, }); diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index c9ecd867c4b56907c7ee189dcfe5f79aebabcc12..6a981ae0f9c3376536e4f23cd3ddccad5f765688 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -1,7 +1,10 @@ +use std::num::NonZeroUsize; + +use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceSettingsContent { /// Active pane styling settings. pub active_pane_modifiers: Option, @@ -69,7 +72,8 @@ pub struct WorkspaceSettingsContent { /// it will be assumed to equal the value. /// /// Default: true - pub command_aliases: Option>, + #[serde(default)] + pub command_aliases: HashMap, /// Maximum open tabs in a pane. Will not close an unsaved /// tab. Set to `None` for unlimited tabs. /// @@ -82,7 +86,8 @@ pub struct WorkspaceSettingsContent { /// Whether to resize all the panels in a dock when resizing the dock. /// /// Default: ["left"] - pub resize_all_panels_in_dock: Option>, + #[serde(default)] + pub resize_all_panels_in_dock: Vec, /// Whether to automatically close files that have been deleted on disk. /// /// Default: false @@ -97,6 +102,11 @@ pub struct WorkspaceSettingsContent { /// /// Default: true pub zoomed_padding: Option, + + // Settings related to the editor's tab bar. + pub tab_bar: Option, + + pub tabs: Option, } #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] @@ -212,7 +222,7 @@ pub enum BottomDockLayout { RightAligned, } -#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum CloseWindowWhenNoItems { /// Match platform conventions by default, so "on" on macOS and "off" everywhere else @@ -234,7 +244,7 @@ impl CloseWindowWhenNoItems { } } -#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum RestoreOnStartupBehavior { /// Always start with an empty editor @@ -246,7 +256,7 @@ pub enum RestoreOnStartupBehavior { LastSession, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct TabBarSettingsContent { /// Whether or not to show the tab bar in the editor. /// @@ -289,7 +299,7 @@ pub enum PaneSplitDirectionVertical { Right, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub struct CenteredLayoutSettings { /// The relative width of the left padding of the central pane from the @@ -303,3 +313,30 @@ pub struct CenteredLayoutSettings { /// Default: 0.2 pub right_padding: Option, } + +#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum OnLastWindowClosed { + /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms + #[default] + PlatformDefault, + /// Quit the application the last window is closed + QuitApp, +} + +impl OnLastWindowClosed { + pub fn is_quit_app(&self) -> bool { + match self { + OnLastWindowClosed::PlatformDefault => false, + OnLastWindowClosed::QuitApp => true, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DockPosition { + Left, + Bottom, + Right, +} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 0c63b4ad304761d7d088a80baa9dbe281c2d266d..c0a055c3b6d33f741610852fab620fbb97f5c05b 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -137,7 +137,7 @@ impl VsCodeSettings { } // todo! replace enum_setting - pub fn read_enum_setting(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { + pub fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { self.content.get(key).and_then(Value::as_str).and_then(f) } } diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index f9f7daa5b3bd7d48ce0631d26d6a3c21767e5d5e..2fef5a62498d9ac0abfef3913edbd1dc711e5e64 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } +collections.workspace = true gpui.workspace = true indexmap.workspace = true log.workspace = true diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index 339c88adea016dc260867ebce1128962bcd563e1..e10d21e4e297fef1ec96d98dea0e45de0ec7e73f 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use anyhow::{Context as _, Result}; use clap::Parser; -use indexmap::IndexMap; +use collections::IndexMap; use log::LevelFilter; use serde::Deserialize; use simplelog::ColorChoice; @@ -137,7 +137,7 @@ fn main() -> Result<()> { file_name: "".to_string(), }; - let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new()); + let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::default()); let theme = converter.convert()?; let mut theme = serde_json::to_value(theme).unwrap(); diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index b0fd312fff7cb45ed80743187ca313e3957b3fba..e4a9769978e7f2b987c1404474d0f029bec7977b 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use indexmap::IndexMap; +use collections::IndexMap; use strum::IntoEnumIterator; use theme::{ FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, @@ -212,7 +212,7 @@ impl VsCodeThemeConverter { } fn convert_syntax_theme(&self) -> Result> { - let mut highlight_styles = IndexMap::new(); + let mut highlight_styles = IndexMap::default(); for syntax_token in ZedSyntaxToken::iter() { let override_match = self diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 0b386d496788547a91255ec5f175a99c1391bda4..5dce89b0cea0413a9c7096ad85c4d39e257d55ce 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -40,12 +40,13 @@ impl Settings for TitleBarSettings { fn refine(&mut self, s: &SettingsContent, _: &mut App) { let Some(content) = s.title_bar else { - return - } + return; + }; self.show.refine(&content.show); self.show_branch_icon.refine(content.show_branch_icon); - self.show_onboarding_banner.refine(content.show_onboarding_banner); + self.show_onboarding_banner + .refine(content.show_onboarding_banner); self.show_user_picture.refine(content.show_user_picture); self.show_branch_name.refine(content.show_branch_name); self.show_project_items.refine(content.show_project_items); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 149a122c0c6a31b7c4713601acca5091accb96ac..e706a949b2ec713e17381ccd51497018fc2642c7 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -9,8 +9,6 @@ use gpui::{ Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div, px, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::SettingsStore; use std::sync::Arc; use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; @@ -210,14 +208,23 @@ impl Focusable for Dock { } } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(rename_all = "lowercase")] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum DockPosition { Left, Bottom, Right, } +impl From for DockPosition { + fn from(value: settings::DockPosition) -> Self { + match value { + settings::DockPosition::Left => Self::Left, + settings::DockPosition::Bottom => Self::Bottom, + settings::DockPosition::Right => Self::Right, + } + } +} + impl DockPosition { fn label(&self) -> &'static str { match self { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 055859bec395445de91978397905d2162723fce2..da4e557776072e33bd691c38d73b0c6623ac5a4c 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -15,7 +15,7 @@ use gpui::{ Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window, }; use project::{Project, ProjectEntryId, ProjectPath}; -use settings::{ +pub use settings::{ ActivateOnClose, ClosePosition, Settings, SettingsLocation, ShowCloseButton, ShowDiagnostics, }; use smallvec::SmallVec; @@ -100,8 +100,7 @@ impl Settings for ItemSettings { ShowCloseButton::Hidden }) } - if let Some(s) = vscode.read_enum_setting("workbench.editor.tabActionLocation", |s| match s - { + if let Some(s) = vscode.read_enum("workbench.editor.tabActionLocation", |s| match s { "right" => Some(ClosePosition::Right), "left" => Some(ClosePosition::Left), _ => None, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4ce8a890237f97db5596f55f607a603a2695ab7b..37706cc3985612777e26ac0d12e08cfbb4fb9b83 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -5818,8 +5818,9 @@ mod tests { async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) { init_test(cx); cx.update_global::(|s, cx| { - s.update_user_settings::(cx, |s| { - s.activate_on_close = Some(ActivateOnClose::Neighbour); + s.update_user_settings(cx, |s| { + s.workspace.tabs.get_or_insert_default().activate_on_close = + Some(ActivateOnClose::Neighbour); }); }); let fs = FakeFs::new(cx.executor()); @@ -5907,8 +5908,9 @@ mod tests { async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) { init_test(cx); cx.update_global::(|s, cx| { - s.update_user_settings::(cx, |s| { - s.activate_on_close = Some(ActivateOnClose::LeftNeighbour); + s.update_user_settings(cx, |s| { + s.workspace.tabs.get_or_insert_default().activate_on_close = + Some(ActivateOnClose::LeftNeighbour); }); }); let fs = FakeFs::new(cx.executor()); @@ -6560,8 +6562,8 @@ mod tests { fn set_max_tabs(cx: &mut TestAppContext, value: Option) { cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.max_tabs = value.map(|v| NonZero::new(v).unwrap()) + store.update_user_settings(cx, |settings| { + settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) }); }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 34aa0da8896fd66b00ffa7a5ce9c32ec74dd8b99..11425472a20ef8ac3f4639942be1f4f35ec88e47 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1695,8 +1695,8 @@ impl Workspace { cx: &mut Context, ) { let fs = self.project().read(cx).fs(); - settings::update_settings_file::(fs.clone(), cx, move |content, _cx| { - content.bottom_dock_layout = Some(layout); + settings::update_settings_file(fs.clone(), cx, move |content, _cx| { + content.workspace.bottom_dock_layout = Some(layout); }); cx.notify(); @@ -6014,8 +6014,8 @@ impl Workspace { ) { let fs = self.project().read(cx).fs().clone(); let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file::(fs, cx, move |file, _| { - file.defaults.show_edit_predictions = Some(!show_edit_predictions) + update_settings_file(fs, cx, move |file, _| { + file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions) }); } } @@ -8678,8 +8678,8 @@ mod tests { // Autosave on window change. item.update(cx, |item, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.autosave = Some(AutosaveSetting::OnWindowChange); + settings.update_user_settings(cx, |settings| { + settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange); }) }); item.is_dirty = true; diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 4108bec78384d49ecfbff83e5cbc2f6edcc11368..64e6c94e10a89c5ed03e5faa54488d11fea93ff1 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -4,52 +4,70 @@ use crate::DockPosition; use anyhow::Result; use collections::HashMap; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use serde::Deserialize; +pub use settings::AutosaveSetting; +use settings::Settings; +pub use settings::{ + BottomDockLayout, PaneSplitDirectionHorizontal, PaneSplitDirectionVertical, + RestoreOnStartupBehavior, +}; +use util::MergeFrom as _; -#[derive(Deserialize)] pub struct WorkspaceSettings { pub active_pane_modifiers: ActivePanelModifiers, - pub bottom_dock_layout: BottomDockLayout, - pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal, - pub pane_split_direction_vertical: PaneSplitDirectionVertical, - pub centered_layout: CenteredLayoutSettings, + pub bottom_dock_layout: settings::BottomDockLayout, + pub pane_split_direction_horizontal: settings::PaneSplitDirectionHorizontal, + pub pane_split_direction_vertical: settings::PaneSplitDirectionVertical, + pub centered_layout: settings::CenteredLayoutSettings, // <- This one is hard to describe, especially as it has pub confirm_quit: bool, pub show_call_status_icon: bool, pub autosave: AutosaveSetting, - pub restore_on_startup: RestoreOnStartupBehavior, + pub restore_on_startup: settings::RestoreOnStartupBehavior, pub restore_on_file_reopen: bool, pub drop_target_size: f32, pub use_system_path_prompts: bool, pub use_system_prompts: bool, pub command_aliases: HashMap, pub max_tabs: Option, - pub when_closing_with_no_tabs: CloseWindowWhenNoItems, - pub on_last_window_closed: OnLastWindowClosed, - pub resize_all_panels_in_dock: Vec, + pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems, + pub on_last_window_closed: settings::OnLastWindowClosed, + pub resize_all_panels_in_dock: Vec, // <- This one is not an overwrite merge, it is an extend merge pub close_on_file_delete: bool, pub use_system_window_tabs: bool, pub zoomed_padding: bool, } -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum OnLastWindowClosed { - /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms - #[default] - PlatformDefault, - /// Quit the application the last window is closed - QuitApp, +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct CenteredLayoutSettings { + /// The relative width of the left padding of the central pane from the + /// workspace when the centered layout is used. + /// + /// Default: 0.2 + pub left_padding: f32, + // The relative width of the right padding of the central pane from the + // workspace when the centered layout is used. + /// + /// Default: 0.2 + pub right_padding: f32, } -impl OnLastWindowClosed { - pub fn is_quit_app(&self) -> bool { - match self { - OnLastWindowClosed::PlatformDefault => false, - OnLastWindowClosed::QuitApp => true, - } - } +#[derive(Copy, Clone, PartialEq, Debug, Default)] +pub struct ActivePanelModifiers { + /// Size of the border surrounding the active pane. + /// When set to 0, the active pane doesn't have any border. + /// The border is drawn inset. + /// + /// Default: `0.0` + // TODO: make this not an option, it is never None + pub border_size: Option, + /// Opacity of inactive panels. + /// When set to 1.0, the inactive panes have the same opacity as the active one. + /// If set to 0, the inactive panes content will not be visible at all. + /// Values are clamped to the [0.0, 1.0] range. + /// + /// Default: `1.0` + // TODO: make this not an option, it is never None + pub inactive_opacity: Option, } #[derive(Deserialize)] @@ -58,14 +76,122 @@ pub struct TabBarSettings { pub show_nav_history_buttons: bool, pub show_tab_bar_buttons: bool, } + impl Settings for WorkspaceSettings { - type FileContent = WorkspaceSettingsContent; + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let workspace = &content.workspace; + Self { + active_pane_modifiers: ActivePanelModifiers { + border_size: Some( + workspace + .active_pane_modifiers + .unwrap() + .border_size + .unwrap(), + ), + inactive_opacity: Some( + workspace + .active_pane_modifiers + .unwrap() + .inactive_opacity + .unwrap(), + ), + }, + bottom_dock_layout: workspace.bottom_dock_layout.clone().unwrap(), + pane_split_direction_horizontal: workspace + .pane_split_direction_horizontal + .clone() + .unwrap(), + pane_split_direction_vertical: workspace.pane_split_direction_vertical.clone().unwrap(), + centered_layout: workspace.centered_layout.clone().unwrap(), + confirm_quit: workspace.confirm_quit.clone().unwrap(), + show_call_status_icon: workspace.show_call_status_icon.clone().unwrap(), + autosave: workspace.autosave.clone().unwrap(), + restore_on_startup: workspace.restore_on_startup.clone().unwrap(), + restore_on_file_reopen: workspace.restore_on_file_reopen.clone().unwrap(), + drop_target_size: workspace.drop_target_size.clone().unwrap(), + use_system_path_prompts: workspace.use_system_path_prompts.clone().unwrap(), + use_system_prompts: workspace.use_system_prompts.clone().unwrap(), + command_aliases: workspace.command_aliases.clone(), + max_tabs: workspace.max_tabs.clone(), + when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.clone().unwrap(), + on_last_window_closed: workspace.on_last_window_closed.clone().unwrap(), + resize_all_panels_in_dock: workspace + .resize_all_panels_in_dock + .iter() + .copied() + .map(Into::into) + .collect(), + close_on_file_delete: workspace.close_on_file_delete.clone().unwrap(), + use_system_window_tabs: workspace.use_system_window_tabs.clone().unwrap(), + zoomed_padding: workspace.zoomed_padding.clone().unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + let workspace = &content.workspace; + if let Some(border_size) = *&workspace + .active_pane_modifiers + .and_then(|modifier| modifier.border_size) + { + self.active_pane_modifiers.border_size = Some(border_size); + } + + if let Some(inactive_opacity) = *&workspace + .active_pane_modifiers + .and_then(|modifier| modifier.inactive_opacity) + { + self.active_pane_modifiers.inactive_opacity = Some(inactive_opacity); + } + + self.bottom_dock_layout + .merge_from(&workspace.bottom_dock_layout); + self.pane_split_direction_horizontal + .merge_from(&workspace.pane_split_direction_horizontal); + self.pane_split_direction_vertical + .merge_from(&workspace.pane_split_direction_vertical); + self.centered_layout.merge_from(&workspace.centered_layout); + self.confirm_quit.merge_from(&workspace.confirm_quit); + self.show_call_status_icon + .merge_from(&workspace.show_call_status_icon); + self.autosave.merge_from(&workspace.autosave); + self.restore_on_startup + .merge_from(&workspace.restore_on_startup); + self.restore_on_file_reopen + .merge_from(&workspace.restore_on_file_reopen); + self.drop_target_size + .merge_from(&workspace.drop_target_size); + self.use_system_path_prompts + .merge_from(&workspace.use_system_path_prompts); + self.use_system_prompts + .merge_from(&workspace.use_system_prompts); + self.command_aliases + .extend(workspace.command_aliases.clone()); + if let Some(max_tabs) = workspace.max_tabs { + self.max_tabs = Some(max_tabs); + } + self.when_closing_with_no_tabs + .merge_from(&workspace.when_closing_with_no_tabs); + self.on_last_window_closed + .merge_from(&workspace.on_last_window_closed); + self.resize_all_panels_in_dock.extend( + workspace + .resize_all_panels_in_dock + .iter() + .copied() + .map(Into::::into), + ); + self.close_on_file_delete + .merge_from(&workspace.close_on_file_delete); + self.use_system_window_tabs + .merge_from(&workspace.use_system_window_tabs); + self.zoomed_padding.merge_from(&workspace.zoomed_padding); } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { if vscode .read_bool("accessibility.dimUnfocused.enabled") .unwrap_or_default() @@ -73,19 +199,16 @@ impl Settings for WorkspaceSettings { .read_value("accessibility.dimUnfocused.opacity") .and_then(|v| v.as_f64()) { - if let Some(settings) = current.active_pane_modifiers.as_mut() { - settings.inactive_opacity = Some(opacity as f32) - } else { - current.active_pane_modifiers = Some(ActivePanelModifiers { - inactive_opacity: Some(opacity as f32), - ..Default::default() - }) - } + current + .workspace + .active_pane_modifiers + .get_or_insert_default() + .inactive_opacity = Some(opacity as f32); } vscode.enum_setting( "window.confirmBeforeClose", - &mut current.confirm_quit, + &mut current.workspace.confirm_quit, |s| match s { "always" | "keyboardOnly" => Some(true), "never" => Some(false), @@ -95,22 +218,22 @@ impl Settings for WorkspaceSettings { vscode.bool_setting( "workbench.editor.restoreViewState", - &mut current.restore_on_file_reopen, + &mut current.workspace.restore_on_file_reopen, ); if let Some(b) = vscode.read_bool("window.closeWhenEmpty") { - current.when_closing_with_no_tabs = Some(if b { - CloseWindowWhenNoItems::CloseWindow + current.workspace.when_closing_with_no_tabs = Some(if b { + settings::CloseWindowWhenNoItems::CloseWindow } else { - CloseWindowWhenNoItems::KeepWindowOpen - }) + settings::CloseWindowWhenNoItems::KeepWindowOpen + }); } if let Some(b) = vscode.read_bool("files.simpleDialog.enable") { - current.use_system_path_prompts = Some(!b); + current.workspace.use_system_path_prompts = Some(!b); } - vscode.enum_setting("files.autoSave", &mut current.autosave, |s| match s { + if let Some(v) = vscode.read_enum("files.autoSave", |s| match s { "off" => Some(AutosaveSetting::Off), "afterDelay" => Some(AutosaveSetting::AfterDelay { milliseconds: vscode @@ -121,7 +244,9 @@ impl Settings for WorkspaceSettings { "onFocusChange" => Some(AutosaveSetting::OnFocusChange), "onWindowChange" => Some(AutosaveSetting::OnWindowChange), _ => None, - }); + }) { + current.workspace.autosave = Some(v); + } // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" // our semantics match if those are set to true, some N, and true respectively. @@ -134,10 +259,12 @@ impl Settings for WorkspaceSettings { .read_bool("workbench.editor.limit.enabled") .unwrap_or_default() { - current.max_tabs = Some(n) + current.workspace.max_tabs = Some(n) } - vscode.bool_setting("window.nativeTabs", &mut current.use_system_window_tabs); + if let Some(b) = vscode.read_bool("window.nativeTabs") { + current.workspace.use_system_window_tabs = Some(b); + } // some combination of "window.restoreWindows" and "workbench.startupEditor" might // map to our "restore_on_startup" @@ -148,24 +275,23 @@ impl Settings for WorkspaceSettings { } impl Settings for TabBarSettings { - type FileContent = TabBarSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.enum_setting( - "workbench.editor.showTabs", - &mut current.show, - |s| match s { - "multiple" => Some(true), - "single" | "none" => Some(false), - _ => None, - }, - ); + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { + if let Some(b) = vscode.read_enum("workbench.editor.showTabs", |s| match s { + "multiple" => Some(true), + "single" | "none" => Some(false), + _ => None, + }) { + current.workspace.tab_bar.get_or_insert_default().show = Some(b); + } if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") { - current.show_tab_bar_buttons = Some(false) + current + .workspace + .tab_bar + .get_or_insert_default() + .show_tab_bar_buttons = Some(false) } } } From 034ff4ec9fda622408346bce2e7eb7e4fb2ed588 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 21:51:09 -0600 Subject: [PATCH 036/117] TEMP --- crates/settings/src/settings_content/workspace.rs | 10 ++-------- crates/workspace/src/dock.rs | 8 +------- crates/workspace/src/workspace_settings.rs | 7 +------ 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 6a981ae0f9c3376536e4f23cd3ddccad5f765688..24a7b9a2be624ff48c200893c878dbff9b66b3c6 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -4,6 +4,8 @@ use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::DockPosition; + #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceSettingsContent { /// Active pane styling settings. @@ -332,11 +334,3 @@ impl OnLastWindowClosed { } } } - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum DockPosition { - Left, - Bottom, - Right, -} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index e706a949b2ec713e17381ccd51497018fc2642c7..77ea068a147e0d3c69efdeace3fb6541cfab25dd 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -9,6 +9,7 @@ use gpui::{ Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div, px, }; +pub use settings::DockPosition; use settings::SettingsStore; use std::sync::Arc; use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; @@ -208,13 +209,6 @@ impl Focusable for Dock { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum DockPosition { - Left, - Bottom, - Right, -} - impl From for DockPosition { fn from(value: settings::DockPosition) -> Self { match value { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 64e6c94e10a89c5ed03e5faa54488d11fea93ff1..0f56bea96650a645160b46d27a3fb2bed88e7a57 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -116,12 +116,7 @@ impl Settings for WorkspaceSettings { max_tabs: workspace.max_tabs.clone(), when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.clone().unwrap(), on_last_window_closed: workspace.on_last_window_closed.clone().unwrap(), - resize_all_panels_in_dock: workspace - .resize_all_panels_in_dock - .iter() - .copied() - .map(Into::into) - .collect(), + resize_all_panels_in_dock: workspace.resize_all_panels_in_dock.iter().collect(), close_on_file_delete: workspace.close_on_file_delete.clone().unwrap(), use_system_window_tabs: workspace.use_system_window_tabs.clone().unwrap(), zoomed_padding: workspace.zoomed_padding.clone().unwrap(), From 8195364b36d2ba8c5a337c85d89449f858567915 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 22:59:22 -0600 Subject: [PATCH 037/117] Workspace --- crates/project/src/context_server_store.rs | 2 - crates/project/src/project_settings.rs | 1 - crates/settings/src/settings_content.rs | 33 ++++----- .../src/settings_content/workspace.rs | 8 +-- crates/settings/src/settings_store.rs | 33 +++++---- crates/workspace/src/dock.rs | 8 ++- crates/workspace/src/pane.rs | 5 +- crates/workspace/src/workspace.rs | 33 ++++----- crates/workspace/src/workspace_settings.rs | 68 ++++++++++--------- 9 files changed, 95 insertions(+), 96 deletions(-) diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 40a14ed37b6db3e5f25dfc5e6926f7a38bc2b510..9444d51581fe4116fd06692a977ace6ce052ebdd 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -1027,7 +1027,6 @@ mod tests { cx.run_until_parked(); } - dbg!("hi"); // Ensure that mcp-2 is removed once it is removed from the settings { @@ -1055,7 +1054,6 @@ mod tests { assert_eq!(store.read(cx).status_for_server(&server_2_id), None); }); } - dbg!("bye"); // Ensure that nothing happens if the settings do not change { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 192728ac66af21b02c8562b2f14a4a0f8b484364..3b88261e8122fabb9c56118b21482c78ebb63873 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -542,7 +542,6 @@ impl Settings for ProjectSettings { .into_iter() .map(|(key, value)| (key, value.into())), ); - dbg!(&self.context_servers); self.dap.extend( project .dap diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 6e128e50b52958e8e24feb699ee9541782556574..432dbb2f8bf4b713a3d74a4c2fb21ff8797bd4c2 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -28,7 +28,7 @@ pub struct SettingsContent { pub project: ProjectSettingsContent, #[serde(flatten)] - pub theme: ThemeSettingsContent, + pub theme: Box, #[serde(flatten)] pub extension: ExtensionSettingsContent, @@ -37,6 +37,7 @@ pub struct SettingsContent { pub workspace: WorkspaceSettingsContent, pub tabs: Option, + pub tab_bar: Option, pub preview_tabs: Option, @@ -119,16 +120,16 @@ pub struct ServerSettingsContent { #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct UserSettingsContent { #[serde(flatten)] - pub content: SettingsContent, + pub content: Box, - pub dev: Option, - pub nightly: Option, - pub preview: Option, - pub stable: Option, + pub dev: Option>, + pub nightly: Option>, + pub preview: Option>, + pub stable: Option>, - pub macos: Option, - pub windows: Option, - pub linux: Option, + pub macos: Option>, + pub windows: Option>, + pub linux: Option>, #[serde(default)] pub profiles: HashMap, @@ -141,18 +142,18 @@ pub struct ExtensionsSettingsContent { impl UserSettingsContent { pub 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(), + ReleaseChannel::Dev => self.dev.as_deref(), + ReleaseChannel::Nightly => self.nightly.as_deref(), + ReleaseChannel::Preview => self.preview.as_deref(), + ReleaseChannel::Stable => self.stable.as_deref(), } } pub 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(), + "macos" => self.macos.as_deref(), + "linux" => self.linux.as_deref(), + "windows" => self.windows.as_deref(), _ => None, } } diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 24a7b9a2be624ff48c200893c878dbff9b66b3c6..a5fe638df3c60380abc761b51f9976d6aa82d8a3 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -88,8 +88,7 @@ pub struct WorkspaceSettingsContent { /// Whether to resize all the panels in a dock when resizing the dock. /// /// Default: ["left"] - #[serde(default)] - pub resize_all_panels_in_dock: Vec, + pub resize_all_panels_in_dock: Option>, /// Whether to automatically close files that have been deleted on disk. /// /// Default: false @@ -104,11 +103,6 @@ pub struct WorkspaceSettingsContent { /// /// Default: true pub zoomed_padding: Option, - - // Settings related to the editor's tab bar. - pub tab_bar: Option, - - pub tabs: Option, } #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 7f6d332c4f6e5084c759dd475886fe454f2375a6..4bcb984f87bd62d7f6ab7e89d272651035cce20e 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -140,12 +140,12 @@ pub struct SettingsLocation<'a> { /// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, - default_settings: SettingsContent, + default_settings: Box, user_settings: Option, - global_settings: Option, + global_settings: Option>, - extension_settings: Option, - server_settings: Option, + extension_settings: Option>, + server_settings: Option>, local_settings: BTreeMap<(WorktreeId, Arc), SettingsContent>, raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, @@ -260,11 +260,11 @@ impl SettingsStore { let mut refinements = Vec::default(); - if let Some(extension_settings) = self.extension_settings.as_ref() { + if let Some(extension_settings) = self.extension_settings.as_deref() { refinements.push(extension_settings) } - if let Some(global_settings) = self.global_settings.as_ref() { + if let Some(global_settings) = self.global_settings.as_deref() { refinements.push(global_settings) } @@ -619,7 +619,7 @@ impl SettingsStore { parse_json_with_comments(global_settings_content)? }; - self.global_settings = Some(settings); + self.global_settings = Some(Box::new(settings)); self.recompute_values(None, cx)?; Ok(()) } @@ -636,9 +636,11 @@ impl SettingsStore { }; // Rewrite the server settings into a content type - self.server_settings = settings.map(|settings| SettingsContent { - project: settings.project, - ..Default::default() + self.server_settings = settings.map(|settings| { + Box::new(SettingsContent { + project: settings.project, + ..Default::default() + }) }); self.recompute_values(None, cx)?; @@ -762,13 +764,13 @@ impl SettingsStore { content: ExtensionsSettingsContent, cx: &mut App, ) -> Result<()> { - self.extension_settings = Some(SettingsContent { + self.extension_settings = Some(Box::new(SettingsContent { project: ProjectSettingsContent { all_languages: content.all_languages, ..Default::default() }, ..Default::default() - }); + })); self.recompute_values(None, cx)?; Ok(()) } @@ -839,11 +841,11 @@ impl SettingsStore { let mut refinements = Vec::default(); - if let Some(extension_settings) = self.extension_settings.as_ref() { + if let Some(extension_settings) = self.extension_settings.as_deref() { refinements.push(extension_settings) } - if let Some(global_settings) = self.global_settings.as_ref() { + if let Some(global_settings) = self.global_settings.as_deref() { refinements.push(global_settings) } @@ -1286,7 +1288,6 @@ mod tests { ) { store.set_user_settings(&old_json, cx).ok(); let edits = store.edits_for_update(&old_json, update); - dbg!(&edits); let mut new_json = old_json; for (range, replacement) in edits.into_iter() { new_json.replace_range(range, &replacement); @@ -1401,9 +1402,7 @@ mod tests { }"# .unindent(), |settings| { - dbg!(&settings.title_bar); settings.title_bar.as_mut().unwrap().show = Some(TitleBarVisibilityContent::Never); - dbg!(&settings.title_bar); }, r#"{ "title_bar": { "show": "never", "name": "Max" } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 77ea068a147e0d3c69efdeace3fb6541cfab25dd..09fa6f068f92dda29beac98eff0dcf41e93dc1bf 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -9,7 +9,6 @@ use gpui::{ Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div, px, }; -pub use settings::DockPosition; use settings::SettingsStore; use std::sync::Arc; use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; @@ -209,6 +208,13 @@ impl Focusable for Dock { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum DockPosition { + Left, + Bottom, + Right, +} + impl From for DockPosition { fn from(value: settings::DockPosition) -> Self { match value { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 37706cc3985612777e26ac0d12e08cfbb4fb9b83..04898de15e6245ea4a9a0e270ea0f7391109017e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -5819,8 +5819,7 @@ mod tests { init_test(cx); cx.update_global::(|s, cx| { s.update_user_settings(cx, |s| { - s.workspace.tabs.get_or_insert_default().activate_on_close = - Some(ActivateOnClose::Neighbour); + s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::Neighbour); }); }); let fs = FakeFs::new(cx.executor()); @@ -5909,7 +5908,7 @@ mod tests { init_test(cx); cx.update_global::(|s, cx| { s.update_user_settings(cx, |s| { - s.workspace.tabs.get_or_insert_default().activate_on_close = + s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::LeftNeighbour); }); }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 11425472a20ef8ac3f4639942be1f4f35ec88e47..f4d54b82aee9966b5b593f29e9d488e90863179b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -52,10 +52,7 @@ pub use item::{ ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, }; use itertools::Itertools; -use language::{ - Buffer, LanguageRegistry, Rope, - language_settings::{AllLanguageSettings, all_language_settings}, -}; +use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings}; pub use modal_layer::*; use node_runtime::NodeRuntime; use notifications::{ @@ -8698,13 +8695,12 @@ mod tests { item.update_in(cx, |item, window, cx| { cx.focus_self(window); SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.autosave = Some(AutosaveSetting::OnFocusChange); + settings.update_user_settings(cx, |settings| { + settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); }) }); item.is_dirty = true; }); - // Blurring the item saves the file. item.update_in(cx, |_, window, _| window.blur()); cx.executor().run_until_parked(); @@ -8721,8 +8717,9 @@ mod tests { // Autosave after delay. item.update(cx, |item, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); + settings.update_user_settings(cx, |settings| { + settings.workspace.autosave = + Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); }) }); item.is_dirty = true; @@ -8740,8 +8737,8 @@ mod tests { // Autosave on focus change, ensuring closing the tab counts as such. item.update(cx, |item, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.autosave = Some(AutosaveSetting::OnFocusChange); + settings.update_user_settings(cx, |settings| { + settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); }) }); item.is_dirty = true; @@ -9744,8 +9741,8 @@ mod tests { // Enable the close_on_disk_deletion setting cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.close_on_file_delete = Some(true); + store.update_user_settings(cx, |settings| { + settings.workspace.close_on_file_delete = Some(true); }); }); @@ -9812,8 +9809,8 @@ mod tests { // Ensure close_on_disk_deletion is disabled (default) cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.close_on_file_delete = Some(false); + store.update_user_settings(cx, |settings| { + settings.workspace.close_on_file_delete = Some(false); }); }); @@ -9890,7 +9887,7 @@ mod tests { // Enable the close_on_file_delete setting cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings(cx, |settings| { - settings.tabs.get_or_insert_default().close_on_file_delete = Some(true); + settings.workspace.close_on_file_delete = Some(true); }); }); @@ -9962,8 +9959,8 @@ mod tests { // Enable the close_on_file_delete setting cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.close_on_file_delete = Some(true); + store.update_user_settings(cx, |settings| { + settings.workspace.close_on_file_delete = Some(true); }); }); diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 0f56bea96650a645160b46d27a3fb2bed88e7a57..165ec776febc939997d878fc69dfd6af4043d9ad 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -1,7 +1,6 @@ use std::num::NonZeroUsize; use crate::DockPosition; -use anyhow::Result; use collections::HashMap; use gpui::App; use serde::Deserialize; @@ -18,7 +17,7 @@ pub struct WorkspaceSettings { pub bottom_dock_layout: settings::BottomDockLayout, pub pane_split_direction_horizontal: settings::PaneSplitDirectionHorizontal, pub pane_split_direction_vertical: settings::PaneSplitDirectionVertical, - pub centered_layout: settings::CenteredLayoutSettings, // <- This one is hard to describe, especially as it has + pub centered_layout: settings::CenteredLayoutSettings, pub confirm_quit: bool, pub show_call_status_icon: bool, pub autosave: AutosaveSetting, @@ -31,26 +30,12 @@ pub struct WorkspaceSettings { pub max_tabs: Option, pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems, pub on_last_window_closed: settings::OnLastWindowClosed, - pub resize_all_panels_in_dock: Vec, // <- This one is not an overwrite merge, it is an extend merge + pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, pub use_system_window_tabs: bool, pub zoomed_padding: bool, } -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct CenteredLayoutSettings { - /// The relative width of the left padding of the central pane from the - /// workspace when the centered layout is used. - /// - /// Default: 0.2 - pub left_padding: f32, - // The relative width of the right padding of the central pane from the - // workspace when the centered layout is used. - /// - /// Default: 0.2 - pub right_padding: f32, -} - #[derive(Copy, Clone, PartialEq, Debug, Default)] pub struct ActivePanelModifiers { /// Size of the border surrounding the active pane. @@ -78,7 +63,7 @@ pub struct TabBarSettings { } impl Settings for WorkspaceSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { let workspace = &content.workspace; Self { active_pane_modifiers: ActivePanelModifiers { @@ -116,14 +101,20 @@ impl Settings for WorkspaceSettings { max_tabs: workspace.max_tabs.clone(), when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.clone().unwrap(), on_last_window_closed: workspace.on_last_window_closed.clone().unwrap(), - resize_all_panels_in_dock: workspace.resize_all_panels_in_dock.iter().collect(), + resize_all_panels_in_dock: workspace + .resize_all_panels_in_dock + .clone() + .unwrap() + .into_iter() + .map(Into::into) + .collect(), close_on_file_delete: workspace.close_on_file_delete.clone().unwrap(), use_system_window_tabs: workspace.use_system_window_tabs.clone().unwrap(), zoomed_padding: workspace.zoomed_padding.clone().unwrap(), } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let workspace = &content.workspace; if let Some(border_size) = *&workspace .active_pane_modifiers @@ -169,12 +160,11 @@ impl Settings for WorkspaceSettings { .merge_from(&workspace.when_closing_with_no_tabs); self.on_last_window_closed .merge_from(&workspace.on_last_window_closed); - self.resize_all_panels_in_dock.extend( - workspace + self.resize_all_panels_in_dock.merge_from( + &workspace .resize_all_panels_in_dock - .iter() - .copied() - .map(Into::::into), + .as_ref() + .map(|resize| resize.clone().into_iter().map(Into::into).collect()), ); self.close_on_file_delete .merge_from(&workspace.close_on_file_delete); @@ -270,6 +260,26 @@ impl Settings for WorkspaceSettings { } impl Settings for TabBarSettings { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let tab_bar = content.tab_bar.clone().unwrap(); + TabBarSettings { + show: tab_bar.show.unwrap(), + show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(), + show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(), + } + } + + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(tab_bar) = &content.tab_bar else { + return; + }; + self.show.merge_from(&tab_bar.show); + self.show_nav_history_buttons + .merge_from(&tab_bar.show_nav_history_buttons); + self.show_tab_bar_buttons + .merge_from(&tab_bar.show_tab_bar_buttons); + } + fn import_from_vscode( vscode: &settings::VsCodeSettings, current: &mut settings::SettingsContent, @@ -279,14 +289,10 @@ impl Settings for TabBarSettings { "single" | "none" => Some(false), _ => None, }) { - current.workspace.tab_bar.get_or_insert_default().show = Some(b); + current.tab_bar.get_or_insert_default().show = Some(b); } if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") { - current - .workspace - .tab_bar - .get_or_insert_default() - .show_tab_bar_buttons = Some(false) + current.tab_bar.get_or_insert_default().show_tab_bar_buttons = Some(false) } } } From 6e93d7060ba2de1ca66481f7ad7bffea93319db4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 23:05:57 -0600 Subject: [PATCH 038/117] Auto-update! --- crates/auto_update/src/auto_update.rs | 48 ++++++++----------------- crates/settings/src/settings_content.rs | 4 +++ 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index c1e318ee84f5228156826c340c3463fde2ef1bb8..b1e7c68cbd93fa929b82d442c45bc565ed8220cf 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -10,7 +10,7 @@ use paths::remote_servers_dir; use release_channel::{AppCommitSha, ReleaseChannel}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi}; +use settings::{Settings, SettingsContent, SettingsStore}; use smol::{fs, io::AsyncReadExt}; use smol::{fs::File, process::Command}; use std::{ @@ -113,47 +113,27 @@ impl Drop for MacOsUnmounter { } } +#[derive(Clone, Copy, Debug)] struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates. /// /// Default: true -#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)] -#[settings_key(None)] -#[settings_ui(group = "Auto Update")] -struct AutoUpdateSettingContent { - pub auto_update: Option, -} - impl Settings for AutoUpdateSetting { - type FileContent = AutoUpdateSettingContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - let auto_update = [ - sources.server, - sources.release_channel, - sources.operating_system, - sources.user, - ] - .into_iter() - .find_map(|value| value.and_then(|val| val.auto_update)) - .or(sources.default.auto_update) - .ok_or_else(Self::missing_default)?; - - Ok(Self(auto_update)) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + debug_assert_eq!(content.auto_update.unwrap(), true); + Self(content.auto_update.unwrap()) } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - let mut cur = &mut Some(*current); - vscode.enum_setting("update.mode", &mut cur, |s| match s { - "none" | "manual" => Some(AutoUpdateSettingContent { - auto_update: Some(false), - }), - _ => Some(AutoUpdateSettingContent { - auto_update: Some(true), - }), - }); - *current = cur.unwrap(); + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + if let Some(auto_update) = content.auto_update { + self.0 = auto_update; + } + } + + fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) { + // We could match on vscode's update.mode here, but + // I think it's more important to have more people updating zed by default. } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 432dbb2f8bf4b713a3d74a4c2fb21ff8797bd4c2..b5f54022c24085f68a6d4e6764a83aad86a4191f 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -46,6 +46,10 @@ pub struct SettingsContent { /// Configuration of audio in Zed. pub audio: Option, + + /// Whether or not to automatically check for updates. + /// + /// Default: true pub auto_update: Option, // todo!() comments?! From fc692b46d72518e9b697ada6318134f9f1f135de Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 23:10:24 -0600 Subject: [PATCH 039/117] title_bar! --- crates/auto_update/src/auto_update.rs | 1 - crates/settings/src/settings_content.rs | 4 +- crates/settings/src/settings_store.rs | 22 ++++---- crates/title_bar/src/title_bar_settings.rs | 58 ++++++++++------------ 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index b1e7c68cbd93fa929b82d442c45bc565ed8220cf..f5b211bf8f7099c6e29f5a5e68c49e335426b668 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -8,7 +8,6 @@ use gpui::{ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use paths::remote_servers_dir; use release_channel::{AppCommitSha, ReleaseChannel}; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsContent, SettingsStore}; use smol::{fs, io::AsyncReadExt}; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index b5f54022c24085f68a6d4e6764a83aad86a4191f..85aba4d3e04c0378c7ddb6dc0a09dc49216bbeef 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -191,7 +191,7 @@ pub struct TitleBarSettingsContent { /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". /// /// Default: "always" - pub show: Option, + pub show: Option, /// Whether to show the branch icon beside branch switcher in the title bar. /// /// Default: false @@ -224,7 +224,7 @@ pub struct TitleBarSettingsContent { #[derive(Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] -pub enum TitleBarVisibilityContent { +pub enum TitleBarVisibility { Always, Never, HideInFullScreen, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 4bcb984f87bd62d7f6ab7e89d272651035cce20e..54f1c10cf720d737e16dc128724280fbebcecf04 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1050,7 +1050,7 @@ mod tests { use std::num::NonZeroU32; use crate::{ - TitleBarSettingsContent, TitleBarVisibilityContent, VsCodeSettingsSource, default_settings, + TitleBarSettingsContent, TitleBarVisibility, VsCodeSettingsSource, default_settings, settings_content::LanguageSettingsContent, test_settings, }; @@ -1081,7 +1081,7 @@ mod tests { #[derive(Debug, PartialEq)] struct TitleBarSettings { - show: TitleBarVisibilityContent, + show: TitleBarVisibility, show_branch_name: bool, } @@ -1105,8 +1105,8 @@ mod tests { let mut show = None; vscode.enum_setting("window.titleBarStyle", &mut show, |value| match value { - "never" => Some(TitleBarVisibilityContent::Never), - "always" => Some(TitleBarVisibilityContent::Always), + "never" => Some(TitleBarVisibility::Never), + "always" => Some(TitleBarVisibility::Always), _ => None, }); if let Some(show) = show { @@ -1163,7 +1163,7 @@ mod tests { ); assert_eq!( store.get::(None).show, - TitleBarVisibilityContent::Always + TitleBarVisibility::Always ); store @@ -1184,7 +1184,7 @@ mod tests { ); assert_eq!( store.get::(None).show, - TitleBarVisibilityContent::Never + TitleBarVisibility::Never ); // todo!() @@ -1253,7 +1253,7 @@ mod tests { path: Path::new("/root2/something") })), &TitleBarSettings { - show: TitleBarVisibilityContent::Never, + show: TitleBarVisibility::Never, show_branch_name: true, } ); @@ -1274,7 +1274,7 @@ mod tests { ); assert_eq!( store.get::(None).show, - TitleBarVisibilityContent::Always, + TitleBarVisibility::Always, ); } @@ -1402,7 +1402,7 @@ mod tests { }"# .unindent(), |settings| { - settings.title_bar.as_mut().unwrap().show = Some(TitleBarVisibilityContent::Never); + settings.title_bar.as_mut().unwrap().show = Some(TitleBarVisibility::Never); }, r#"{ "title_bar": { "show": "never", "name": "Max" } @@ -1594,7 +1594,7 @@ mod tests { assert_eq!( store.get::(None), &TitleBarSettings { - show: TitleBarVisibilityContent::Never, + show: TitleBarVisibility::Never, show_branch_name: true, } ); @@ -1615,7 +1615,7 @@ mod tests { assert_eq!( store.get::(None), &TitleBarSettings { - show: TitleBarVisibilityContent::Always, + show: TitleBarVisibility::Always, show_branch_name: true, // Staff from global settings } ); diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index 5dce89b0cea0413a9c7096ad85c4d39e257d55ce..670d085756c5e27c45f8640e2d053a7f6929aead 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,15 +1,7 @@ -use db::anyhow; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsContent, SettingsKey, SettingsSources, SettingsUi}; - -#[derive(Copy, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum TitleBarVisibility { - Always, - Never, - HideInFullScreen, -} +pub use settings::TitleBarVisibility; +use settings::{Settings, SettingsContent}; +use ui::App; +use util::MergeFrom; #[derive(Copy, Clone, Debug)] pub struct TitleBarSettings { @@ -24,35 +16,37 @@ pub struct TitleBarSettings { } impl Settings for TitleBarSettings { - fn from_defaults(s: &SettingsContent) -> Option { - let content = s.title_bar?; + fn from_defaults(s: &SettingsContent, _: &mut App) -> Self { + let content = s.title_bar.clone().unwrap(); TitleBarSettings { - show: content.show?, - show_branch_icon: content.show_branch_icon?, - show_onboarding_banner: content.show_onboarding_banner?, - show_user_picture: content.show_user_picture?, - show_branch_name: content.show_branch_name?, - show_project_items: content.show_project_items?, - show_sign_in: content.show_sign_in?, - show_menus: content.show_menus?, + show: content.show.unwrap(), + show_branch_icon: content.show_branch_icon.unwrap(), + show_onboarding_banner: content.show_onboarding_banner.unwrap(), + show_user_picture: content.show_user_picture.unwrap(), + show_branch_name: content.show_branch_name.unwrap(), + show_project_items: content.show_project_items.unwrap(), + show_sign_in: content.show_sign_in.unwrap(), + show_menus: content.show_menus.unwrap(), } } fn refine(&mut self, s: &SettingsContent, _: &mut App) { - let Some(content) = s.title_bar else { + let Some(content) = &s.title_bar else { return; }; - self.show.refine(&content.show); - self.show_branch_icon.refine(content.show_branch_icon); + self.show.merge_from(&content.show); + self.show_branch_icon.merge_from(&content.show_branch_icon); self.show_onboarding_banner - .refine(content.show_onboarding_banner); - self.show_user_picture.refine(content.show_user_picture); - self.show_branch_name.refine(content.show_branch_name); - self.show_project_items.refine(content.show_project_items); - self.show_sign_in.refine(content.show_sign_in); - self.show_menus.refine(content.show_menus); + .merge_from(&content.show_onboarding_banner); + self.show_user_picture + .merge_from(&content.show_user_picture); + self.show_branch_name.merge_from(&content.show_branch_name); + self.show_project_items + .merge_from(&content.show_project_items); + self.show_sign_in.merge_from(&content.show_sign_in); + self.show_menus.merge_from(&content.show_menus); } - fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut Self::FileContent) {} + fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) {} } From b61c73aac67246024183229cd5e58a250d3c0144 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Sep 2025 23:13:11 -0600 Subject: [PATCH 040/117] languages! --- crates/languages/src/bash.rs | 6 +++--- crates/languages/src/c.rs | 6 +++--- crates/languages/src/python.rs | 6 +++--- crates/languages/src/rust.rs | 5 ++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs index 0c6ab8cc14eca2ba87fa0b876946bee0b21d479b..8fabd4cf43aa4a79fa868854064942252deb4117 100644 --- a/crates/languages/src/bash.rs +++ b/crates/languages/src/bash.rs @@ -19,7 +19,7 @@ pub(super) fn bash_task_context() -> ContextProviderWithTasks { #[cfg(test)] mod tests { use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext}; - use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings}; + use language::{AutoindentMode, Buffer}; use settings::SettingsStore; use std::num::NonZeroU32; use unindent::Unindent; @@ -34,8 +34,8 @@ mod tests { cx.set_global(test_settings); language::init(cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |s| { - s.defaults.tab_size = NonZeroU32::new(2) + store.update_user_settings(cx, |s| { + s.project.all_languages.defaults.tab_size = NonZeroU32::new(2) }); }); }); diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 1e3a1f805885e850884c2e5f5ec18ca71801301f..0af467bcaa8f9b4e6b1094d937358a55db402c60 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -394,7 +394,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option(|store, cx| { - store.update_user_settings::(cx, |s| { - s.defaults.tab_size = NonZeroU32::new(2); + store.update_user_settings(cx, |s| { + s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); }); }); }); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 34dfe4a4287c80a91efd285dc020657f7e0a0fe8..b893a44d523c7f34821056fea243124ded3e8ece 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2125,7 +2125,7 @@ impl LspInstaller for RuffLspAdapter { #[cfg(test)] mod tests { use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext}; - use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings}; + use language::{AutoindentMode, Buffer}; use settings::SettingsStore; use std::num::NonZeroU32; @@ -2138,8 +2138,8 @@ mod tests { cx.set_global(test_settings); language::init(cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |s| { - s.defaults.tab_size = NonZeroU32::new(2); + store.update_user_settings(cx, |s| { + s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); }); }); }); diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index e35d870610669f0e09cb28e15841d1105d129cca..f5401f448c324f11c2de3a6379c704df5441b3db 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -1051,7 +1051,6 @@ mod tests { use super::*; use crate::language; use gpui::{BorrowAppContext, Hsla, TestAppContext}; - use language::language_settings::AllLanguageSettings; use lsp::CompletionItemLabelDetails; use settings::SettingsStore; use theme::SyntaxTheme; @@ -1381,8 +1380,8 @@ mod tests { cx.set_global(test_settings); language::init(cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |s| { - s.defaults.tab_size = NonZeroU32::new(2); + store.update_user_settings(cx, |s| { + s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); }); }); }); From 7de2084bcf9f9a7df1f91a191aaa7c2180ae9201 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 00:26:50 -0600 Subject: [PATCH 041/117] EDITOR!! --- crates/editor/src/code_completion_tests.rs | 3 +- crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/display_map.rs | 22 +- crates/editor/src/editor.rs | 31 +- crates/editor/src/editor_settings.rs | 949 ++++++------------ crates/editor/src/editor_settings_controls.rs | 87 +- crates/editor/src/editor_tests.rs | 64 +- crates/editor/src/element.rs | 54 +- crates/editor/src/hover_links.rs | 2 +- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 6 +- crates/editor/src/jsx_tag_auto_close.rs | 13 +- crates/language/src/buffer.rs | 19 +- .../settings/src/editable_setting_control.rs | 5 +- crates/settings/src/settings_content.rs | 5 + .../settings/src/settings_content/editor.rs | 586 +++++++++++ .../settings/src/settings_content/language.rs | 2 +- .../settings/src/settings_content/terminal.rs | 2 +- crates/ui/src/components/scrollbar.rs | 11 + 19 files changed, 1052 insertions(+), 813 deletions(-) create mode 100644 crates/settings/src/settings_content/editor.rs diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index a1d9f04a9c590ef1f20779bf19c2fe0be8905709..ec97c0ebb31952da9ad8e9e6f4f75b4b0078c4a3 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -1,9 +1,10 @@ -use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder}; +use crate::code_context_menus::CompletionsMenu; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::TestAppContext; use language::CodeLabel; use lsp::{CompletionItem, CompletionItemKind, LanguageServerId}; use project::{Completion, CompletionSource}; +use settings::SnippetSortOrder; use std::sync::Arc; use std::sync::atomic::AtomicBool; use text::Anchor; diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 9384e2efed162e1a21b0b31fdc0e7f65ba25b130..a89125a3aa6aebe23665469f34962dbacddc52d6 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -32,7 +32,6 @@ use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*}; use util::ResultExt; use crate::CodeActionSource; -use crate::editor_settings::SnippetSortOrder; use crate::hover_popover::{hover_markdown_style, open_markdown_url}; use crate::{ CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor, @@ -40,6 +39,7 @@ use crate::{ actions::{ConfirmCodeAction, ConfirmCompletion}, split_words, styled_runs_for_code_label, }; +use settings::SnippetSortOrder; pub const MENU_GAP: Pixels = px(4.); pub const MENU_ASIDE_X_PADDING: Pixels = px(16.); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1acbdab7a6646fe46b9ad9d9cb09c1549d64bb1a..cc6bb3571bc11c836fa4d13abb76f6cd4a554755 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1529,12 +1529,11 @@ pub mod tests { use language::{ Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig, LanguageMatcher, - language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, }; use lsp::LanguageServerId; use project::Project; use rand::{Rng, prelude::*}; - use settings::SettingsStore; + use settings::{SettingsContent, SettingsStore}; use smol::stream::StreamExt; use std::{env, sync::Arc}; use text::PointUtf16; @@ -1564,7 +1563,9 @@ pub mod tests { log::info!("wrap width: {:?}", wrap_width); cx.update(|cx| { - init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); + init_test(cx, |s| { + s.project.all_languages.defaults.tab_size = NonZeroU32::new(tab_size) + }); }); let buffer = cx.update(|cx| { @@ -1623,8 +1624,9 @@ pub mod tests { log::info!("setting tab size to {:?}", tab_size); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |s| { - s.defaults.tab_size = NonZeroU32::new(tab_size); + store.update_user_settings(cx, |s| { + s.project.all_languages.defaults.tab_size = + NonZeroU32::new(tab_size); }); }); }); @@ -2084,7 +2086,11 @@ pub mod tests { ); language.set_theme(&theme); - cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); + cx.update(|cx| { + init_test(cx, |s| { + s.project.all_languages.defaults.tab_size = Some(2.try_into().unwrap()) + }) + }); let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); cx.condition(&buffer, |buf, _| !buf.is_parsing()).await; @@ -2910,7 +2916,7 @@ pub mod tests { chunks } - fn init_test(cx: &mut App, f: impl Fn(&mut AllLanguageSettingsContent)) { + fn init_test(cx: &mut App, f: impl Fn(&mut SettingsContent)) { let settings = SettingsStore::test(cx); cx.set_global(settings); workspace::init_settings(cx); @@ -2919,7 +2925,7 @@ pub mod tests { Project::init_settings(cx); theme::init(LoadThemes::JustBase, cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, f); + store.update_user_settings(cx, f); }); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00ae0bcbac1e49e193739ed8b8af77a8c02dc845..28dd71c94b341e42fe6a68de6efcf097c7536e6c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -126,8 +126,8 @@ use language::{ Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, - all_language_settings, language_settings, + self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, + language_settings, }, point_from_lsp, point_to_lsp, text_diff_with_options, }; @@ -160,9 +160,7 @@ use project::{ }, git_store::{GitStoreEvent, RepositoryEvent}, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, - project_settings::{ - DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings, - }, + project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings}, }; use rand::seq::SliceRandom; use rpc::{ErrorCode, ErrorExt, proto::PeerId}; @@ -171,7 +169,10 @@ use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, }; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; +use settings::{ + GitGutterSetting, InlayHintSettings, Settings, SettingsLocation, SettingsStore, + update_settings_file, +}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; use std::{ @@ -5587,8 +5588,9 @@ impl Editor { .language_at(buffer_position) .map(|language| language.name()); - let completion_settings = - language_settings(language.clone(), buffer_snapshot.file(), cx).completions; + let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx) + .completions + .clone(); let show_completion_documentation = buffer_snapshot .settings_at(buffer_position, cx) @@ -18990,8 +18992,8 @@ impl Editor { }; let fs = workspace.read(cx).app_state().fs.clone(); let current_show = TabBarSettings::get_global(cx).show; - update_settings_file::(fs, cx, move |setting, _| { - setting.show = Some(!current_show); + update_settings_file(fs, cx, move |setting, _| { + setting.tab_bar.get_or_insert_default().show = Some(!current_show); }); } @@ -21659,11 +21661,12 @@ impl Editor { } } +// todo(settings_refactor) this should not be! fn vim_enabled(cx: &App) -> bool { cx.global::() .raw_user_settings() - .get("vim_mode") - == Some(&serde_json::Value::Bool(true)) + .and_then(|settings| settings.content.vim_mode) + == Some(true) } fn process_completion_for_edit( @@ -22896,7 +22899,7 @@ fn inlay_hint_settings( ) -> InlayHintSettings { let file = snapshot.file_at(location); let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints + language_settings(language, file, cx).inlay_hints.clone() } fn consume_contiguous_rows( @@ -23103,7 +23106,7 @@ impl EditorSnapshot { let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { matches!( ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) + GitGutterSetting::TrackedFiles ) }); let gutter_settings = EditorSettings::get_global(cx).gutter; diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 09f496637498b2535f8836282ffed4ea30950a4f..f184aed90b85071418a006a36293d235d9a580a4 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -4,15 +4,19 @@ use std::num::NonZeroU32; use gpui::App; use language::CursorShape; use project::project_settings::DiagnosticSeverity; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings}; +pub use settings::{ + CurrentLineHighlight, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, + GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, + ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, + VsCodeSettings, +}; +use settings::{Settings, SettingsContent}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; -use util::serde::default_true; +use util::MergeFrom; /// Imports from the VSCode settings at /// https://code.visualstudio.com/docs/reference/default-settings -#[derive(Deserialize, Clone)] +#[derive(Clone)] pub struct EditorSettings { pub cursor_blink: bool, pub cursor_shape: Option, @@ -41,83 +45,22 @@ pub struct EditorSettings { pub expand_excerpt_lines: u32, pub excerpt_context_lines: u32, pub middle_click_paste: bool, - #[serde(default)] pub double_click_in_multibuffer: DoubleClickInMultibuffer, pub search_wrap: bool, - #[serde(default)] pub search: SearchSettings, pub auto_signature_help: bool, pub show_signature_help_after_edits: bool, - #[serde(default)] pub go_to_definition_fallback: GoToDefinitionFallback, pub jupyter: Jupyter, pub hide_mouse: Option, pub snippet_sort_order: SnippetSortOrder, - #[serde(default)] pub diagnostics_max_severity: Option, pub inline_code_actions: bool, pub drag_and_drop_selection: DragAndDropSelection, pub lsp_document_colors: DocumentColorsRenderMode, pub minimum_contrast_for_highlights: f32, } - -/// How to render LSP `textDocument/documentColor` colors in the editor. -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum DocumentColorsRenderMode { - /// Do not query and render document colors. - None, - /// Render document colors as inlay hints near the color text. - #[default] - Inlay, - /// Draw a border around the color text. - Border, - /// Draw a background behind the color text. - Background, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum CurrentLineHighlight { - // Don't highlight the current line. - None, - // Highlight the gutter area. - Gutter, - // Highlight the editor area. - Line, - // Highlight the full line. - All, -} - -/// When to populate a new search's query based on the text under the cursor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum SeedQuerySetting { - /// Always populate the search query with the word under the cursor. - Always, - /// Only populate the search query when there is text selected. - Selection, - /// Never populate the search query - Never, -} - -/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers). -#[derive( - Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum DoubleClickInMultibuffer { - /// Behave as a regular buffer and select the whole word. - #[default] - Select, - /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click. - /// Otherwise, behave as a regular buffer and select the whole word. - Open, -} - -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct Jupyter { /// Whether the Jupyter feature is enabled. /// @@ -125,18 +68,7 @@ pub struct Jupyter { pub enabled: bool, } -#[derive( - Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub struct JupyterContent { - /// Whether the Jupyter feature is enabled. - /// - /// Default: true - pub enabled: Option, -} - -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct StatusBar { /// Whether to display the active language button in the status bar. /// @@ -148,7 +80,7 @@ pub struct StatusBar { pub cursor_position_button: bool, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, pub quick_actions: bool, @@ -157,7 +89,7 @@ pub struct Toolbar { pub code_actions: bool, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Scrollbar { pub show: ShowScrollbar, pub git_diff: bool, @@ -169,7 +101,7 @@ pub struct Scrollbar { pub axes: ScrollbarAxes, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct Minimap { pub show: ShowMinimap, pub display_in: DisplayIn, @@ -197,7 +129,7 @@ impl Minimap { } } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Gutter { pub min_line_number_digits: usize, pub line_numbers: bool, @@ -206,69 +138,8 @@ pub struct Gutter { pub folds: bool, } -/// When to show the minimap in the editor. -/// -/// Default: never -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowMinimap { - /// Follow the visibility of the scrollbar. - Auto, - /// Always show the minimap. - Always, - /// Never show the minimap. - #[default] - Never, -} - -/// Where to show the minimap in the editor. -/// -/// Default: all_editors -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum DisplayIn { - /// Show on all open editors. - AllEditors, - /// Show the minimap on the active editor only. - #[default] - ActiveEditor, -} - -/// When to show the minimap thumb. -/// -/// Default: always -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumb { - /// Show the minimap thumb only when the mouse is hovering over the minimap. - Hover, - /// Always show the minimap thumb. - #[default] - Always, -} - -/// Defines the border style for the minimap's scrollbar thumb. -/// -/// Default: left_open -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumbBorder { - /// Displays a border on all sides of the thumb. - Full, - /// Displays a border on all sides except the left side of the thumb. - #[default] - LeftOpen, - /// Displays a border on all sides except the right side of the thumb. - RightOpen, - /// Displays a border only on the left side of the thumb. - LeftOnly, - /// Displays the thumb without any border. - None, -} - /// Forcefully enable or disable the scrollbar for each axis -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ScrollbarAxes { /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. /// @@ -282,480 +153,29 @@ pub struct ScrollbarAxes { } /// Whether to allow drag and drop text selection in buffer. -#[derive( - Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, -)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct DragAndDropSelection { /// When true, enables drag and drop text selection in buffer. /// /// Default: true - #[serde(default = "default_true")] pub enabled: bool, /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. /// /// Default: 300 - #[serde(default = "default_drag_and_drop_selection_delay_ms")] pub delay: u64, } -fn default_drag_and_drop_selection_delay_ms() -> u64 { - 300 -} - -/// Which diagnostic indicators to show in the scrollbar. -/// -/// Default: all -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum ScrollbarDiagnostics { - /// Show all diagnostic levels: hint, information, warnings, error. - All, - /// Show only the following diagnostic levels: information, warning, error. - Information, - /// Show only the following diagnostic levels: warning, error. - Warning, - /// Show only the following diagnostic level: error. - Error, - /// Do not show diagnostics. - None, -} - -/// The key to use for adding multiple cursors -/// -/// Default: alt -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum MultiCursorModifier { - Alt, - #[serde(alias = "cmd", alias = "ctrl")] - CmdOrCtrl, -} - -/// Whether the editor will scroll beyond the last line. -/// -/// Default: one_page -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -#[serde(rename_all = "snake_case")] -pub enum ScrollBeyondLastLine { - /// The editor will not scroll beyond the last line. - Off, - - /// The editor will scroll beyond the last line by one page. - OnePage, - - /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin. - VerticalScrollMargin, -} - /// Default options for buffer and project search items. -#[derive( - Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, -)] +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct SearchSettings { /// Whether to show the project search button in the status bar. - #[serde(default = "default_true")] pub button: bool, - #[serde(default)] pub whole_word: bool, - #[serde(default)] pub case_sensitive: bool, - #[serde(default)] pub include_ignored: bool, - #[serde(default)] pub regex: bool, } - -/// What to do when go to definition yields no results. -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum GoToDefinitionFallback { - /// Disables the fallback. - None, - /// Looks up references of the same symbol instead. - #[default] - FindAllReferences, -} - -/// Determines when the mouse cursor should be hidden in an editor or input box. -/// -/// Default: on_typing_and_movement -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum HideMouseMode { - /// Never hide the mouse cursor - Never, - /// Hide only when typing - OnTyping, - /// Hide on both typing and cursor movement - #[default] - OnTypingAndMovement, -} - -/// Determines how snippets are sorted relative to other completion items. -/// -/// Default: inline -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] -pub enum SnippetSortOrder { - /// Place snippets at the top of the completion list - Top, - /// Sort snippets normally using the default comparison logic - #[default] - Inline, - /// Place snippets at the bottom of the completion list - Bottom, - /// Do not show snippets in the completion list - None, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_ui(group = "Editor")] -#[settings_key(None)] -pub struct EditorSettingsContent { - /// Whether the cursor blinks in the editor. - /// - /// Default: true - pub cursor_blink: Option, - /// Cursor shape for the default editor. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: bar - pub cursor_shape: Option, - /// Determines when the mouse cursor should be hidden in an editor or input box. - /// - /// Default: on_typing_and_movement - pub hide_mouse: Option, - /// Determines how snippets are sorted relative to other completion items. - /// - /// Default: inline - pub snippet_sort_order: Option, - /// How to highlight the current line in the editor. - /// - /// Default: all - pub current_line_highlight: Option, - /// Whether to highlight all occurrences of the selected text in an editor. - /// - /// Default: true - pub selection_highlight: Option, - /// Whether the text selection should have rounded corners. - /// - /// Default: true - pub rounded_selection: Option, - /// The debounce delay before querying highlights from the language - /// server based on the current cursor location. - /// - /// Default: 75 - pub lsp_highlight_debounce: Option, - /// Whether to show the informational hover box when moving the mouse - /// over symbols in the editor. - /// - /// Default: true - pub hover_popover_enabled: Option, - /// Time to wait in milliseconds before showing the informational hover box. - /// - /// Default: 300 - pub hover_popover_delay: Option, - /// Status bar related settings - pub status_bar: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar related settings - pub scrollbar: Option, - /// Minimap related settings - pub minimap: Option, - /// Gutter related settings - pub gutter: Option, - /// Whether the editor will scroll beyond the last line. - /// - /// Default: one_page - pub scroll_beyond_last_line: Option, - /// The number of lines to keep above/below the cursor when auto-scrolling. - /// - /// Default: 3. - pub vertical_scroll_margin: Option, - /// Whether to scroll when clicking near the edge of the visible text area. - /// - /// Default: false - pub autoscroll_on_clicks: Option, - /// The number of characters to keep on either side when scrolling with the mouse. - /// - /// Default: 5. - pub horizontal_scroll_margin: Option, - /// Scroll sensitivity multiplier. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. - /// - /// Default: 1.0 - pub scroll_sensitivity: Option, - /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. Fast scrolling - /// happens when a user holds the alt or option key while scrolling. - /// - /// Default: 4.0 - pub fast_scroll_sensitivity: Option, - /// Whether the line numbers on editors gutter are relative or not. - /// - /// Default: false - pub relative_line_numbers: Option, - /// When to populate a new search's query based on the text under the cursor. - /// - /// Default: always - pub seed_search_query_from_cursor: Option, - pub use_smartcase_search: Option, - /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. - /// - /// Default: alt - pub multi_cursor_modifier: Option, - /// Hide the values of variables in `private` files, as defined by the - /// private_files setting. This only changes the visual representation, - /// the values are still present in the file and can be selected / copied / pasted - /// - /// Default: false - pub redact_private_values: Option, - - /// How many lines to expand the multibuffer excerpts by default - /// - /// Default: 3 - pub expand_excerpt_lines: Option, - - /// How many lines of context to provide in multibuffer excerpts by default - /// - /// Default: 2 - pub excerpt_context_lines: Option, - - /// Whether to enable middle-click paste on Linux - /// - /// Default: true - pub middle_click_paste: Option, - - /// What to do when multibuffer is double clicked in some of its excerpts - /// (parts of singleton buffers). - /// - /// Default: select - pub double_click_in_multibuffer: Option, - /// Whether the editor search results will loop - /// - /// Default: true - pub search_wrap: Option, - - /// Defaults to use when opening a new buffer and project search items. - /// - /// Default: nothing is enabled - pub search: Option, - - /// Whether to automatically show a signature help pop-up or not. - /// - /// Default: false - pub auto_signature_help: Option, - - /// Whether to show the signature help pop-up after completions or bracket pairs inserted. - /// - /// Default: false - pub show_signature_help_after_edits: Option, - /// The minimum APCA perceptual contrast to maintain when - /// rendering text over highlight backgrounds in the editor. - /// - /// Values range from 0 to 106. Set to 0 to disable adjustments. - /// Default: 45 - pub minimum_contrast_for_highlights: Option, - - /// Whether to follow-up empty go to definition responses from the language server or not. - /// `FindAllReferences` allows to look up references of the same symbol instead. - /// `None` disables the fallback. - /// - /// Default: FindAllReferences - pub go_to_definition_fallback: Option, - - /// Jupyter REPL settings. - pub jupyter: Option, - - /// Which level to use to filter out diagnostics displayed in the editor. - /// - /// Affects the editor rendering only, and does not interrupt - /// the functionality of diagnostics fetching and project diagnostics editor. - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// Diagnostics are only shown when file icons are also active. - /// - /// Shows all diagnostics if not specified. - /// - /// Default: warning - #[serde(default)] - pub diagnostics_max_severity: Option, - - /// Whether to show code action button at start of buffer line. - /// - /// Default: true - pub inline_code_actions: Option, - - /// Drag and drop related settings - pub drag_and_drop_selection: Option, - - /// How to render LSP `textDocument/documentColor` colors in the editor. - /// - /// Default: [`DocumentColorsRenderMode::Inlay`] - pub lsp_document_colors: Option, -} - -// Status bar related settings -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -pub struct StatusBarContent { - /// Whether to display the active language button in the status bar. - /// - /// Default: true - pub active_language_button: Option, - /// Whether to show the cursor position button in the status bar. - /// - /// Default: true - pub cursor_position_button: Option, -} - -// Toolbar related settings -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] -pub struct ToolbarContent { - /// Whether to display breadcrumbs in the editor toolbar. - /// - /// Default: true - pub breadcrumbs: Option, - /// Whether to display quick action buttons in the editor toolbar. - /// - /// Default: true - pub quick_actions: Option, - /// Whether to show the selections menu in the editor toolbar. - /// - /// Default: true - pub selections_menu: Option, - /// Whether to display Agent review buttons in the editor toolbar. - /// Only applicable while reviewing a file edited by the Agent. - /// - /// Default: true - pub agent_review: Option, - /// Whether to display code action buttons in the editor toolbar. - /// - /// Default: false - pub code_actions: Option, -} - -/// Scrollbar related settings -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi, -)] -pub struct ScrollbarContent { - /// When to show the scrollbar in the editor. - /// - /// Default: auto - pub show: Option, - /// Whether to show git diff indicators in the scrollbar. - /// - /// Default: true - pub git_diff: Option, - /// Whether to show buffer search result indicators in the scrollbar. - /// - /// Default: true - pub search_results: Option, - /// Whether to show selected text occurrences in the scrollbar. - /// - /// Default: true - pub selected_text: Option, - /// Whether to show selected symbol occurrences in the scrollbar. - /// - /// Default: true - pub selected_symbol: Option, - /// Which diagnostic indicators to show in the scrollbar: - /// - /// Default: all - pub diagnostics: Option, - /// Whether to show cursor positions in the scrollbar. - /// - /// Default: true - pub cursors: Option, - /// Forcefully enable or disable the scrollbar for each axis - pub axes: Option, -} - -/// Minimap related settings -#[derive( - Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi, -)] -pub struct MinimapContent { - /// When to show the minimap in the editor. - /// - /// Default: never - pub show: Option, - - /// Where to show the minimap in the editor. - /// - /// Default: [`DisplayIn::ActiveEditor`] - pub display_in: Option, - - /// When to show the minimap thumb. - /// - /// Default: always - pub thumb: Option, - - /// Defines the border style for the minimap's scrollbar thumb. - /// - /// Default: left_open - pub thumb_border: Option, - - /// How to highlight the current line in the minimap. - /// - /// Default: inherits editor line highlights setting - pub current_line_highlight: Option>, - - /// Maximum number of columns to display in the minimap. - /// - /// Default: 80 - pub max_width_columns: Option, -} - -/// Forcefully enable or disable the scrollbar for each axis -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] -pub struct ScrollbarAxesContent { - /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. - /// - /// Default: true - horizontal: Option, - - /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - /// - /// Default: true - vertical: Option, -} - -/// Gutter related settings -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi, -)] -#[settings_ui(group = "Gutter")] -pub struct GutterContent { - /// Whether to show line numbers in the gutter. - /// - /// Default: true - pub line_numbers: Option, - /// Minimum number of characters to reserve space for in the gutter. - /// - /// Default: 4 - pub min_line_number_digits: Option, - /// Whether to show runnable buttons in the gutter. - /// - /// Default: true - pub runnables: Option, - /// Whether to show breakpoints in the gutter. - /// - /// Default: true - pub breakpoints: Option, - /// Whether to show fold buttons in the gutter. - /// - /// Default: true - pub folds: Option, -} - impl EditorSettings { pub fn jupyter_enabled(cx: &App) -> bool { EditorSettings::get_global(cx).jupyter.enabled @@ -769,16 +189,266 @@ impl ScrollbarVisibility for EditorSettings { } impl Settings for EditorSettings { - type FileContent = EditorSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let editor = content.editor.clone(); + let scrollbar = editor.scrollbar.unwrap(); + let minimap = editor.minimap.unwrap(); + let gutter = editor.gutter.unwrap(); + let axes = scrollbar.axes.unwrap(); + let status_bar = editor.status_bar.unwrap(); + let toolbar = editor.toolbar.unwrap(); + let search = editor.search.unwrap(); + let drag_and_drop_selection = editor.drag_and_drop_selection.unwrap(); + Self { + cursor_blink: editor.cursor_blink.unwrap(), + cursor_shape: editor.cursor_shape.map(Into::into), + current_line_highlight: editor.current_line_highlight.unwrap(), + selection_highlight: editor.selection_highlight.unwrap(), + rounded_selection: editor.rounded_selection.unwrap(), + lsp_highlight_debounce: editor.lsp_highlight_debounce.unwrap(), + hover_popover_enabled: editor.hover_popover_enabled.unwrap(), + hover_popover_delay: editor.hover_popover_delay.unwrap(), + status_bar: StatusBar { + active_language_button: status_bar.active_language_button.unwrap(), + cursor_position_button: status_bar.cursor_position_button.unwrap(), + }, + toolbar: Toolbar { + breadcrumbs: toolbar.breadcrumbs.unwrap(), + quick_actions: toolbar.quick_actions.unwrap(), + selections_menu: toolbar.selections_menu.unwrap(), + agent_review: toolbar.agent_review.unwrap(), + code_actions: toolbar.code_actions.unwrap(), + }, + scrollbar: Scrollbar { + show: scrollbar.show.map(Into::into).unwrap(), + git_diff: scrollbar.git_diff.unwrap(), + selected_text: scrollbar.selected_text.unwrap(), + selected_symbol: scrollbar.selected_symbol.unwrap(), + search_results: scrollbar.search_results.unwrap(), + diagnostics: scrollbar.diagnostics.unwrap(), + cursors: scrollbar.cursors.unwrap(), + axes: ScrollbarAxes { + horizontal: axes.horizontal.unwrap(), + vertical: axes.vertical.unwrap(), + }, + }, + minimap: Minimap { + show: minimap.show.unwrap(), + display_in: minimap.display_in.unwrap(), + thumb: minimap.thumb.unwrap(), + thumb_border: minimap.thumb_border.unwrap(), + current_line_highlight: minimap.current_line_highlight.flatten(), + max_width_columns: minimap.max_width_columns.unwrap(), + }, + gutter: Gutter { + min_line_number_digits: gutter.min_line_number_digits.unwrap(), + line_numbers: gutter.line_numbers.unwrap(), + runnables: gutter.runnables.unwrap(), + breakpoints: gutter.breakpoints.unwrap(), + folds: gutter.folds.unwrap(), + }, + scroll_beyond_last_line: editor.scroll_beyond_last_line.unwrap(), + vertical_scroll_margin: editor.vertical_scroll_margin.unwrap(), + autoscroll_on_clicks: editor.autoscroll_on_clicks.unwrap(), + horizontal_scroll_margin: editor.horizontal_scroll_margin.unwrap(), + scroll_sensitivity: editor.scroll_sensitivity.unwrap(), + fast_scroll_sensitivity: editor.fast_scroll_sensitivity.unwrap(), + relative_line_numbers: editor.relative_line_numbers.unwrap(), + seed_search_query_from_cursor: editor.seed_search_query_from_cursor.unwrap(), + use_smartcase_search: editor.use_smartcase_search.unwrap(), + multi_cursor_modifier: editor.multi_cursor_modifier.unwrap(), + redact_private_values: editor.redact_private_values.unwrap(), + expand_excerpt_lines: editor.expand_excerpt_lines.unwrap(), + excerpt_context_lines: editor.excerpt_context_lines.unwrap(), + middle_click_paste: editor.middle_click_paste.unwrap(), + double_click_in_multibuffer: editor.double_click_in_multibuffer.unwrap(), + search_wrap: editor.search_wrap.unwrap(), + search: SearchSettings { + button: search.button.unwrap(), + whole_word: search.whole_word.unwrap(), + case_sensitive: search.case_sensitive.unwrap(), + include_ignored: search.include_ignored.unwrap(), + regex: search.regex.unwrap(), + }, + auto_signature_help: editor.auto_signature_help.unwrap(), + show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(), + go_to_definition_fallback: editor.go_to_definition_fallback.unwrap(), + jupyter: Jupyter { + enabled: editor.jupyter.unwrap().enabled.unwrap(), + }, + hide_mouse: editor.hide_mouse, + snippet_sort_order: editor.snippet_sort_order.unwrap(), + diagnostics_max_severity: editor.diagnostics_max_severity.map(Into::into), + inline_code_actions: editor.inline_code_actions.unwrap(), + drag_and_drop_selection: DragAndDropSelection { + enabled: drag_and_drop_selection.enabled.unwrap(), + delay: drag_and_drop_selection.delay.unwrap(), + }, + lsp_document_colors: editor.lsp_document_colors.unwrap(), + minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap(), + } + } - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let editor = &content.editor; + self.cursor_blink.merge_from(&editor.cursor_blink); + if let Some(cursor_shape) = editor.cursor_shape { + self.cursor_shape = Some(cursor_shape.into()) + } + self.current_line_highlight + .merge_from(&editor.current_line_highlight); + self.selection_highlight + .merge_from(&editor.selection_highlight); + self.rounded_selection.merge_from(&editor.rounded_selection); + self.lsp_highlight_debounce + .merge_from(&editor.lsp_highlight_debounce); + self.hover_popover_enabled + .merge_from(&editor.hover_popover_enabled); + self.hover_popover_delay + .merge_from(&editor.hover_popover_delay); + self.scroll_beyond_last_line + .merge_from(&editor.scroll_beyond_last_line); + self.vertical_scroll_margin + .merge_from(&editor.vertical_scroll_margin); + self.autoscroll_on_clicks + .merge_from(&editor.autoscroll_on_clicks); + self.horizontal_scroll_margin + .merge_from(&editor.horizontal_scroll_margin); + self.scroll_sensitivity + .merge_from(&editor.scroll_sensitivity); + self.fast_scroll_sensitivity + .merge_from(&editor.fast_scroll_sensitivity); + self.relative_line_numbers + .merge_from(&editor.relative_line_numbers); + self.seed_search_query_from_cursor + .merge_from(&editor.seed_search_query_from_cursor); + self.use_smartcase_search + .merge_from(&editor.use_smartcase_search); + self.multi_cursor_modifier + .merge_from(&editor.multi_cursor_modifier); + self.redact_private_values + .merge_from(&editor.redact_private_values); + self.expand_excerpt_lines + .merge_from(&editor.expand_excerpt_lines); + self.excerpt_context_lines + .merge_from(&editor.excerpt_context_lines); + self.middle_click_paste + .merge_from(&editor.middle_click_paste); + self.double_click_in_multibuffer + .merge_from(&editor.double_click_in_multibuffer); + self.search_wrap.merge_from(&editor.search_wrap); + self.auto_signature_help + .merge_from(&editor.auto_signature_help); + self.show_signature_help_after_edits + .merge_from(&editor.show_signature_help_after_edits); + self.go_to_definition_fallback + .merge_from(&editor.go_to_definition_fallback); + if let Some(hide_mouse) = editor.hide_mouse { + self.hide_mouse = Some(hide_mouse) + } + self.snippet_sort_order + .merge_from(&editor.snippet_sort_order); + if let Some(diagnostics_max_severity) = editor.diagnostics_max_severity { + self.diagnostics_max_severity = Some(diagnostics_max_severity.into()); + } + self.inline_code_actions + .merge_from(&editor.inline_code_actions); + self.lsp_document_colors + .merge_from(&editor.lsp_document_colors); + self.minimum_contrast_for_highlights + .merge_from(&editor.minimum_contrast_for_highlights); + + if let Some(status_bar) = &editor.status_bar { + self.status_bar + .active_language_button + .merge_from(&status_bar.active_language_button); + self.status_bar + .cursor_position_button + .merge_from(&status_bar.cursor_position_button); + } + if let Some(toolbar) = &editor.toolbar { + self.toolbar.breadcrumbs.merge_from(&toolbar.breadcrumbs); + self.toolbar + .quick_actions + .merge_from(&toolbar.quick_actions); + self.toolbar + .selections_menu + .merge_from(&toolbar.selections_menu); + self.toolbar.agent_review.merge_from(&toolbar.agent_review); + self.toolbar.code_actions.merge_from(&toolbar.code_actions); + } + if let Some(scrollbar) = &editor.scrollbar { + self.scrollbar + .show + .merge_from(&scrollbar.show.map(Into::into)); + self.scrollbar.git_diff.merge_from(&scrollbar.git_diff); + self.scrollbar + .selected_text + .merge_from(&scrollbar.selected_text); + self.scrollbar + .selected_symbol + .merge_from(&scrollbar.selected_symbol); + self.scrollbar + .search_results + .merge_from(&scrollbar.search_results); + self.scrollbar + .diagnostics + .merge_from(&scrollbar.diagnostics); + self.scrollbar.cursors.merge_from(&scrollbar.cursors); + if let Some(axes) = &scrollbar.axes { + self.scrollbar.axes.horizontal.merge_from(&axes.horizontal); + self.scrollbar.axes.vertical.merge_from(&axes.vertical); + } + } + if let Some(minimap) = &editor.minimap { + self.minimap.show.merge_from(&minimap.show); + self.minimap.display_in.merge_from(&minimap.display_in); + self.minimap.thumb.merge_from(&minimap.thumb); + self.minimap.thumb_border.merge_from(&minimap.thumb_border); + self.minimap + .current_line_highlight + .merge_from(&minimap.current_line_highlight); + self.minimap + .max_width_columns + .merge_from(&minimap.max_width_columns); + } + if let Some(gutter) = &editor.gutter { + self.gutter + .min_line_number_digits + .merge_from(&gutter.min_line_number_digits); + self.gutter.line_numbers.merge_from(&gutter.line_numbers); + self.gutter.runnables.merge_from(&gutter.runnables); + self.gutter.breakpoints.merge_from(&gutter.breakpoints); + self.gutter.folds.merge_from(&gutter.folds); + } + if let Some(search) = &editor.search { + self.search.button.merge_from(&search.button); + self.search.whole_word.merge_from(&search.whole_word); + self.search + .case_sensitive + .merge_from(&search.case_sensitive); + self.search + .include_ignored + .merge_from(&search.include_ignored); + self.search.regex.merge_from(&search.regex); + } + if let Some(enabled) = editor.jupyter.as_ref().and_then(|jupyter| jupyter.enabled) { + self.jupyter.enabled = enabled; + } + if let Some(drag_and_drop_selection) = &editor.drag_and_drop_selection { + self.drag_and_drop_selection + .enabled + .merge_from(&drag_and_drop_selection.enabled); + self.drag_and_drop_selection + .delay + .merge_from(&drag_and_drop_selection.delay); + } } - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent) { vscode.enum_setting( "editor.cursorBlinking", - &mut current.cursor_blink, + &mut current.editor.cursor_blink, |s| match s { "blink" | "phase" | "expand" | "smooth" => Some(true), "solid" => Some(false), @@ -787,19 +457,19 @@ impl Settings for EditorSettings { ); vscode.enum_setting( "editor.cursorStyle", - &mut current.cursor_shape, + &mut current.editor.cursor_shape, |s| match s { - "block" => Some(CursorShape::Block), - "block-outline" => Some(CursorShape::Hollow), - "line" | "line-thin" => Some(CursorShape::Bar), - "underline" | "underline-thin" => Some(CursorShape::Underline), + "block" => Some(settings::CursorShape::Block), + "block-outline" => Some(settings::CursorShape::Hollow), + "line" | "line-thin" => Some(settings::CursorShape::Bar), + "underline" | "underline-thin" => Some(settings::CursorShape::Underline), _ => None, }, ); vscode.enum_setting( "editor.renderLineHighlight", - &mut current.current_line_highlight, + &mut current.editor.current_line_highlight, |s| match s { "gutter" => Some(CurrentLineHighlight::Gutter), "line" => Some(CurrentLineHighlight::Line), @@ -810,13 +480,22 @@ impl Settings for EditorSettings { vscode.bool_setting( "editor.selectionHighlight", - &mut current.selection_highlight, + &mut current.editor.selection_highlight, + ); + vscode.bool_setting( + "editor.roundedSelection", + &mut current.editor.rounded_selection, + ); + vscode.bool_setting( + "editor.hover.enabled", + &mut current.editor.hover_popover_enabled, + ); + vscode.u64_setting( + "editor.hover.delay", + &mut current.editor.hover_popover_delay, ); - vscode.bool_setting("editor.roundedSelection", &mut current.rounded_selection); - vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled); - vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay); - let mut gutter = GutterContent::default(); + let mut gutter = settings::GutterContent::default(); vscode.enum_setting( "editor.showFoldingControls", &mut gutter.folds, @@ -835,25 +514,25 @@ impl Settings for EditorSettings { _ => None, }, ); - if let Some(old_gutter) = current.gutter.as_mut() { + if let Some(old_gutter) = current.editor.gutter.as_mut() { if gutter.folds.is_some() { old_gutter.folds = gutter.folds } if gutter.line_numbers.is_some() { old_gutter.line_numbers = gutter.line_numbers } - } else if gutter != GutterContent::default() { - current.gutter = Some(gutter) + } else if gutter != settings::GutterContent::default() { + current.editor.gutter = Some(gutter) } if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { - current.scroll_beyond_last_line = Some(if b { + current.editor.scroll_beyond_last_line = Some(if b { ScrollBeyondLastLine::OnePage } else { ScrollBeyondLastLine::Off }) } - let mut scrollbar_axes = ScrollbarAxesContent::default(); + let mut scrollbar_axes = settings::ScrollbarAxesContent::default(); vscode.enum_setting( "editor.scrollbar.horizontal", &mut scrollbar_axes.horizontal, @@ -873,8 +552,8 @@ impl Settings for EditorSettings { }, ); - if scrollbar_axes != ScrollbarAxesContent::default() { - let scrollbar_settings = current.scrollbar.get_or_insert_default(); + if scrollbar_axes != settings::ScrollbarAxesContent::default() { + let scrollbar_settings = current.editor.scrollbar.get_or_insert_default(); let axes_settings = scrollbar_settings.axes.get_or_insert_default(); if let Some(vertical) = scrollbar_axes.vertical { @@ -888,23 +567,23 @@ impl Settings for EditorSettings { // TODO: check if this does the int->float conversion? vscode.f32_setting( "editor.cursorSurroundingLines", - &mut current.vertical_scroll_margin, + &mut current.editor.vertical_scroll_margin, ); vscode.f32_setting( "editor.mouseWheelScrollSensitivity", - &mut current.scroll_sensitivity, + &mut current.editor.scroll_sensitivity, ); vscode.f32_setting( "editor.fastScrollSensitivity", - &mut current.fast_scroll_sensitivity, + &mut current.editor.fast_scroll_sensitivity, ); if Some("relative") == vscode.read_string("editor.lineNumbers") { - current.relative_line_numbers = Some(true); + current.editor.relative_line_numbers = Some(true); } vscode.enum_setting( "editor.find.seedSearchStringFromSelection", - &mut current.seed_search_query_from_cursor, + &mut current.editor.seed_search_query_from_cursor, |s| match s { "always" => Some(SeedQuerySetting::Always), "selection" => Some(SeedQuerySetting::Selection), @@ -912,10 +591,10 @@ impl Settings for EditorSettings { _ => None, }, ); - vscode.bool_setting("search.smartCase", &mut current.use_smartcase_search); + vscode.bool_setting("search.smartCase", &mut current.editor.use_smartcase_search); vscode.enum_setting( "editor.multiCursorModifier", - &mut current.multi_cursor_modifier, + &mut current.editor.multi_cursor_modifier, |s| match s { "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), "alt" => Some(MultiCursorModifier::Alt), @@ -925,19 +604,19 @@ impl Settings for EditorSettings { vscode.bool_setting( "editor.parameterHints.enabled", - &mut current.auto_signature_help, + &mut current.editor.auto_signature_help, ); vscode.bool_setting( "editor.parameterHints.enabled", - &mut current.show_signature_help_after_edits, + &mut current.editor.show_signature_help_after_edits, ); if let Some(use_ignored) = vscode.read_bool("search.useIgnoreFiles") { - let search = current.search.get_or_insert_default(); - search.include_ignored = use_ignored; + let search = current.editor.search.get_or_insert_default(); + search.include_ignored = Some(use_ignored); } - let mut minimap = MinimapContent::default(); + let mut minimap = settings::MinimapContent::default(); let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true); let autohide = vscode.read_bool("editor.minimap.autohide"); let mut max_width_columns: Option = None; @@ -965,8 +644,8 @@ impl Settings for EditorSettings { }, ); - if minimap != MinimapContent::default() { - current.minimap = Some(minimap) + if minimap != settings::MinimapContent::default() { + current.editor.minimap = Some(minimap) } } } diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 91022d94a8843a2e9b7e9c77137d4d2ba57bfa7f..a26af2739924152a972e00a660b03f72e3eff8d6 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use gpui::{App, FontFeatures, FontWeight}; -use project::project_settings::{InlineBlameSettings, ProjectSettings}; -use settings::{EditableSettingControl, Settings}; +use project::project_settings::ProjectSettings; +use settings::{EditableSettingControl, Settings, SettingsContent}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, @@ -59,7 +59,6 @@ struct BufferFontFamilyControl; impl EditableSettingControl for BufferFontFamilyControl { type Value = SharedString; - type Settings = ThemeSettings; fn name(&self) -> SharedString { "Buffer Font Family".into() @@ -70,12 +69,8 @@ impl EditableSettingControl for BufferFontFamilyControl { settings.buffer_font.family.clone() } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.buffer_font_family = Some(FontFamilyName(value.into())); + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings.theme.buffer_font_family = Some(FontFamilyName(value.into())); } } @@ -118,7 +113,6 @@ struct BufferFontSizeControl; impl EditableSettingControl for BufferFontSizeControl { type Value = Pixels; - type Settings = ThemeSettings; fn name(&self) -> SharedString { "Buffer Font Size".into() @@ -128,12 +122,8 @@ impl EditableSettingControl for BufferFontSizeControl { ThemeSettings::get_global(cx).buffer_font_size(cx) } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.buffer_font_size = Some(value.into()); + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings.theme.buffer_font_size = Some(value.into()); } } @@ -162,7 +152,6 @@ struct BufferFontWeightControl; impl EditableSettingControl for BufferFontWeightControl { type Value = FontWeight; - type Settings = ThemeSettings; fn name(&self) -> SharedString { "Buffer Font Weight".into() @@ -173,12 +162,8 @@ impl EditableSettingControl for BufferFontWeightControl { settings.buffer_font.weight } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.buffer_font_weight = Some(value.0); + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings.theme.buffer_font_weight = Some(value.0); } } @@ -215,7 +200,6 @@ struct BufferFontLigaturesControl; impl EditableSettingControl for BufferFontLigaturesControl { type Value = bool; - type Settings = ThemeSettings; fn name(&self) -> SharedString { "Buffer Font Ligatures".into() @@ -230,14 +214,11 @@ impl EditableSettingControl for BufferFontLigaturesControl { .unwrap_or(true) } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { let value = if value { 1 } else { 0 }; let mut features = settings + .theme .buffer_font_features .as_ref() .map(|features| features.tag_value_list().to_vec()) @@ -249,7 +230,7 @@ impl EditableSettingControl for BufferFontLigaturesControl { features.push(("calt".into(), value)); } - settings.buffer_font_features = Some(FontFeatures(Arc::new(features))); + settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features))); } } @@ -279,7 +260,6 @@ struct InlineGitBlameControl; impl EditableSettingControl for InlineGitBlameControl { type Value = bool; - type Settings = ProjectSettings; fn name(&self) -> SharedString { "Inline Git Blame".into() @@ -290,19 +270,13 @@ impl EditableSettingControl for InlineGitBlameControl { settings.git.inline_blame_enabled() } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - if let Some(inline_blame) = settings.git.inline_blame.as_mut() { - inline_blame.enabled = value; - } else { - settings.git.inline_blame = Some(InlineBlameSettings { - enabled: false, - ..Default::default() - }); - } + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings + .git + .get_or_insert_default() + .inline_blame + .get_or_insert_default() + .enabled = Some(value) } } @@ -332,7 +306,6 @@ struct LineNumbersControl; impl EditableSettingControl for LineNumbersControl { type Value = bool; - type Settings = EditorSettings; fn name(&self) -> SharedString { "Line Numbers".into() @@ -343,19 +316,8 @@ impl EditableSettingControl for LineNumbersControl { settings.gutter.line_numbers } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - if let Some(gutter) = settings.gutter.as_mut() { - gutter.line_numbers = Some(value); - } else { - settings.gutter = Some(crate::editor_settings::GutterContent { - line_numbers: Some(value), - ..Default::default() - }); - } + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings.editor.gutter.get_or_insert_default().line_numbers = Some(value); } } @@ -385,7 +347,6 @@ struct RelativeLineNumbersControl; impl EditableSettingControl for RelativeLineNumbersControl { type Value = bool; - type Settings = EditorSettings; fn name(&self) -> SharedString { "Relative Line Numbers".into() @@ -396,12 +357,8 @@ impl EditableSettingControl for RelativeLineNumbersControl { settings.relative_line_numbers } - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.relative_line_numbers = Some(value); + fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { + settings.editor.relative_line_numbers = Some(value); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 131b03c61b9a36ca73bf91fbc3bdf35b0000e3ee..4973e0d9b98ac1e362518d68f081912ac13144a4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25,8 +25,8 @@ use language::{ DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList, - LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter, + CompletionSettings, FormatterList, LanguageSettingsContent, LspInsertMode, + SelectedFormatter, }, tree_sitter_python, }; @@ -38,9 +38,10 @@ use pretty_assertions::{assert_eq, assert_ne}; use project::{ FakeFs, debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}, - project_settings::{LspSettings, ProjectSettings}, + project_settings::LspSettings, }; use serde_json::{self, json}; +use settings::{AllLanguageSettingsContent, ProjectSettingsContent}; use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; use std::{ iter, @@ -11699,10 +11700,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { update_test_language_settings(cx, |settings| { // Enable Prettier formatting for the same buffer, and ensure // LSP is called instead of Prettier. - settings.defaults.prettier = Some(PrettierSettings { - allowed: true, - ..PrettierSettings::default() - }); + settings.defaults.prettier.get_or_insert_default().allowed = true; }); let mut fake_servers = language_registry.register_fake_lsp( "Rust", @@ -12088,10 +12086,7 @@ async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), ))); update_test_language_settings(cx, |settings| { - settings.defaults.prettier = Some(PrettierSettings { - allowed: true, - ..PrettierSettings::default() - }); + settings.defaults.prettier.get_or_insert_default().allowed = true; }); let mut fake_servers = language_registry.register_fake_lsp( "TypeScript", @@ -12402,8 +12397,8 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true( cx.update(|cx| { cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.auto_signature_help = Some(true); + settings.update_user_settings(cx, |settings| { + settings.editor.auto_signature_help = Some(true); }); }); }); @@ -12542,9 +12537,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA cx.update(|cx| { cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.auto_signature_help = Some(false); - settings.show_signature_help_after_edits = Some(false); + settings.update_user_settings(cx, |settings| { + settings.editor.auto_signature_help = Some(false); + settings.editor.show_signature_help_after_edits = Some(false); }); }); }); @@ -12669,9 +12664,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA // Ensure that signature_help is called when enabled afte edits cx.update(|_, cx| { cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.auto_signature_help = Some(false); - settings.show_signature_help_after_edits = Some(true); + settings.update_user_settings(cx, |settings| { + settings.editor.auto_signature_help = Some(false); + settings.editor.show_signature_help_after_edits = Some(true); }); }); }); @@ -12711,9 +12706,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA // Ensure that signature_help is called when auto signature help override is enabled cx.update(|_, cx| { cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.auto_signature_help = Some(true); - settings.show_signature_help_after_edits = Some(false); + settings.update_user_settings(cx, |settings| { + settings.editor.auto_signature_help = Some(true); + settings.editor.show_signature_help_after_edits = Some(false); }); }); }); @@ -12755,8 +12750,8 @@ async fn test_signature_help(cx: &mut TestAppContext) { init_test(cx, |_| {}); cx.update(|cx| { cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.auto_signature_help = Some(true); + settings.update_user_settings(cx, |settings| { + settings.editor.auto_signature_help = Some(true); }); }); }); @@ -17067,7 +17062,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon let _fake_server = fake_servers.next().await.unwrap(); update_test_language_settings(cx, |language_settings| { language_settings.languages.0.insert( - language_name.clone(), + language_name.clone().0, LanguageSettingsContent { tab_size: NonZeroU32::new(8), ..Default::default() @@ -17991,10 +17986,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), ))); update_test_language_settings(cx, |settings| { - settings.defaults.prettier = Some(PrettierSettings { - allowed: true, - ..PrettierSettings::default() - }); + settings.defaults.prettier.get_or_insert_default().allowed = true; }); let test_plugin = "test_plugin"; @@ -23672,8 +23664,8 @@ println!("5"); }); cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.restore_on_file_reopen = Some(false); + store.update_user_settings(cx, |s| { + s.workspace.restore_on_file_reopen = Some(false); }); }); editor.update_in(cx, |editor, window, cx| { @@ -23697,8 +23689,8 @@ println!("5"); assert!(pane.active_item().is_none()); }); cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.restore_on_file_reopen = Some(true); + store.update_user_settings(cx, |s| { + s.workspace.restore_on_file_reopen = Some(true); }); }); @@ -25120,18 +25112,18 @@ pub(crate) fn update_test_language_settings( ) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, f); + store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages)); }); }); } pub(crate) fn update_test_project_settings( cx: &mut TestAppContext, - f: impl Fn(&mut ProjectSettings), + f: impl Fn(&mut ProjectSettingsContent), ) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, f); + store.update_user_settings(cx, |settings| f(&mut settings.project)); }); }); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 603dcdece22053d131441929fe4270371e803463..ed35d853865adba0aa4a18218153e93afaff89a2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -51,9 +51,7 @@ use gpui::{ transparent_black, }; use itertools::Itertools; -use language::language_settings::{ - IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting, -}; +use language::language_settings::{IndentGuideSettings, ShowWhitespaceSetting}; use markdown::Markdown; use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, @@ -63,9 +61,12 @@ use multi_buffer::{ use project::{ Entry, ProjectPath, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, - project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, + project_settings::ProjectSettings, +}; +use settings::{ + GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, + Settings, }; -use settings::Settings; use smallvec::{SmallVec, smallvec}; use std::{ any::TypeId, @@ -2095,10 +2096,7 @@ impl EditorElement { .display_diff_hunks_for_rows(display_rows, folded_buffers) .map(|hunk| (hunk, None)) .collect::>(); - let git_gutter_setting = ProjectSettings::get_global(cx) - .git - .git_gutter - .unwrap_or_default(); + let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter; if let GitGutterSetting::TrackedFiles = git_gutter_setting { for (hunk, hitbox) in &mut display_hunks { if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) { @@ -2450,11 +2448,7 @@ impl EditorElement { let padding = { const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.; - let mut padding = ProjectSettings::get_global(cx) - .git - .inline_blame - .unwrap_or_default() - .padding as f32; + let mut padding = ProjectSettings::get_global(cx).git.inline_blame.padding as f32; if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() && let EditPrediction::Edit { @@ -2488,12 +2482,10 @@ impl EditorElement { let padded_line_end = line_end + padding; - let min_column_in_pixels = ProjectSettings::get_global(cx) - .git - .inline_blame - .map(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, window)) - .unwrap_or(px(0.)); + let min_column_in_pixels = self.column_pixels( + ProjectSettings::get_global(cx).git.inline_blame.min_column as usize, + window, + ); let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; cmp::max(padded_line_end, min_start) @@ -5666,7 +5658,7 @@ impl EditorElement { for indent_guide in indent_guides { let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth); - let settings = indent_guide.settings; + let settings = &indent_guide.settings; // TODO fixed for now, expose them through themes later const INDENT_AWARE_ALPHA: f32 = 0.2; @@ -6001,7 +5993,7 @@ impl EditorElement { .unwrap_or_else(|| { matches!( ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) + GitGutterSetting::TrackedFiles ) }); if show_git_gutter { @@ -7296,10 +7288,10 @@ impl EditorElement { fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool { let unstaged = status.has_secondary_hunk(); - let unstaged_hollow = ProjectSettings::get_global(cx) - .git - .hunk_style - .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow)); + let unstaged_hollow = matches!( + ProjectSettings::get_global(cx).git.hunk_style, + GitHunkStyleSetting::UnstagedHollow + ); unstaged == unstaged_hollow } @@ -8836,13 +8828,9 @@ impl Element for EditorElement { }) .flatten()?; let mut element = render_inline_blame_entry(blame_entry, style, cx)?; - let inline_blame_padding = ProjectSettings::get_global(cx) - .git - .inline_blame - .unwrap_or_default() - .padding - as f32 - * em_advance; + let inline_blame_padding = + ProjectSettings::get_global(cx).git.inline_blame.padding as f32 + * em_advance; Some( element .layout_as_root(AvailableSpace::min_size(), window, cx) diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index ba0b6f88683969aca3818a2795aa6b8454de3bb8..bf0367c159f5e576f9f2cf2a5c32f790f9c85ea5 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -931,8 +931,8 @@ mod tests { use futures::StreamExt; use gpui::Modifiers; use indoc::indoc; - use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use settings::InlayHintSettings; use util::{assert_set_eq, path}; use workspace::item::Item; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 51be0d234eca9c2e6b908c0aba6f3746b3eff460..21b9c777e409ef2b2d8a3ecb990b5bd77d408ea5 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1004,8 +1004,8 @@ mod tests { use collections::BTreeSet; use gpui::App; use indoc::indoc; - use language::language_settings::InlayHintSettings; use markdown::parser::MarkdownEvent; + use settings::InlayHintSettings; use smol::stream::StreamExt; use std::sync::atomic; use std::sync::atomic::AtomicUsize; diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index c1b0a7640c155fff02f0b778e8996a9b68ea452e..970c533710a09c84875ce0047060ede5e3a6aa56 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -25,7 +25,7 @@ use parking_lot::RwLock; use project::{InlayHint, ResolveState}; use collections::{HashMap, HashSet, hash_map}; -use language::language_settings::InlayHintSettings; +use settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; use text::{BufferId, ToOffset, ToPoint}; @@ -1301,13 +1301,13 @@ pub mod tests { use futures::StreamExt; use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle}; use itertools::Itertools as _; - use language::{Capability, FakeLspAdapter, language_settings::AllLanguageSettingsContent}; + use language::{Capability, FakeLspAdapter}; use language::{Language, LanguageConfig, LanguageMatcher}; use lsp::FakeLanguageServer; use parking_lot::Mutex; use project::{FakeFs, Project}; use serde_json::json; - use settings::SettingsStore; + use settings::{AllLanguageSettingsContent, InlayHintSettings, SettingsStore}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use text::Point; use util::path; diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index e6c518beae3ecf3741b5f74be6087628f5231c8c..f2bdb717efe4b4d7521d4d6906b3c0f77dcb14f6 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -620,14 +620,17 @@ mod jsx_tag_autoclose_tests { use super::*; use gpui::{AppContext as _, TestAppContext}; - use language::language_settings::JsxTagAutoCloseSettings; use languages::language; use multi_buffer::ExcerptRange; use text::Selection; async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext { init_test(cx, |settings| { - settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true }); + settings + .defaults + .jsx_tag_auto_close + .get_or_insert_default() + .enabled = true; }); let mut cx = EditorTestContext::new(cx).await; @@ -789,7 +792,11 @@ mod jsx_tag_autoclose_tests { #[gpui::test] async fn test_multibuffer(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true }); + settings + .defaults + .jsx_tag_auto_close + .get_or_insert_default() + .enabled = true; }); let buffer_a = cx.new(|cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f03df08a55b2759885d133b9a7dc3556b549a184..e55d1f2d2385d41fa30643987cfd026958b9b803 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -30,10 +30,9 @@ use gpui::{ use lsp::{LanguageServerId, NumberOrString}; use parking_lot::Mutex; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use settings::{SettingsUi, WorktreeId}; +use settings::WorktreeId; use smallvec::SmallVec; use smol::future::yield_now; use std::{ @@ -174,10 +173,7 @@ pub enum IndentKind { } /// The shape of a selection cursor. -#[derive( - Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi, -)] -#[serde(rename_all = "snake_case")] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum CursorShape { /// A vertical bar #[default] @@ -190,6 +186,17 @@ pub enum CursorShape { Hollow, } +impl From for CursorShape { + fn from(shape: settings::CursorShape) -> Self { + match shape { + settings::CursorShape::Bar => CursorShape::Bar, + settings::CursorShape::Block => CursorShape::Block, + settings::CursorShape::Underline => CursorShape::Underline, + settings::CursorShape::Hollow => CursorShape::Hollow, + } + } +} + #[derive(Clone, Debug)] struct SelectionSet { line_mode: bool, diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs index 40244c10c4e5789e56a8458e2cc0e7737f14a2d9..fd9d9869620f57b9cee9c522312bb2e59222e4c0 100644 --- a/crates/settings/src/editable_setting_control.rs +++ b/crates/settings/src/editable_setting_control.rs @@ -1,16 +1,13 @@ use fs::Fs; use gpui::{App, RenderOnce, SharedString}; -use crate::{Settings, settings_content::SettingsContent, update_settings_file}; +use crate::{settings_content::SettingsContent, update_settings_file}; /// A UI control that can be used to edit a setting. pub trait EditableSettingControl: RenderOnce { /// The type of the setting value. type Value: Send; - /// The settings type to which this setting belongs. - type Settings: Settings; - /// Returns the name of this setting. fn name(&self) -> SharedString; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 85aba4d3e04c0378c7ddb6dc0a09dc49216bbeef..c01253d8b98884b430a44b194416c2d9c07279b3 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,10 +1,12 @@ mod agent; +mod editor; mod language; mod project; mod terminal; mod theme; mod workspace; pub use agent::*; +pub use editor::*; pub use language::*; pub use project::*; pub use terminal::*; @@ -36,6 +38,9 @@ pub struct SettingsContent { #[serde(flatten)] pub workspace: WorkspaceSettingsContent, + #[serde(flatten)] + pub editor: EditorSettingsContent, + pub tabs: Option, pub tab_bar: Option, diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs new file mode 100644 index 0000000000000000000000000000000000000000..5220c029697e9714dc80f31fec23a4406e8d1855 --- /dev/null +++ b/crates/settings/src/settings_content/editor.rs @@ -0,0 +1,586 @@ +use std::num; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{DiagnosticSeverityContent, ShowScrollbar}; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct EditorSettingsContent { + /// Whether the cursor blinks in the editor. + /// + /// Default: true + pub cursor_blink: Option, + /// Cursor shape for the default editor. + /// Can be "bar", "block", "underline", or "hollow". + /// + /// Default: bar + pub cursor_shape: Option, + /// Determines when the mouse cursor should be hidden in an editor or input box. + /// + /// Default: on_typing_and_movement + pub hide_mouse: Option, + /// Determines how snippets are sorted relative to other completion items. + /// + /// Default: inline + pub snippet_sort_order: Option, + /// How to highlight the current line in the editor. + /// + /// Default: all + pub current_line_highlight: Option, + /// Whether to highlight all occurrences of the selected text in an editor. + /// + /// Default: true + pub selection_highlight: Option, + /// Whether the text selection should have rounded corners. + /// + /// Default: true + pub rounded_selection: Option, + /// The debounce delay before querying highlights from the language + /// server based on the current cursor location. + /// + /// Default: 75 + pub lsp_highlight_debounce: Option, + /// Whether to show the informational hover box when moving the mouse + /// over symbols in the editor. + /// + /// Default: true + pub hover_popover_enabled: Option, + /// Time to wait in milliseconds before showing the informational hover box. + /// + /// Default: 300 + pub hover_popover_delay: Option, + /// Status bar related settings + pub status_bar: Option, + /// Toolbar related settings + pub toolbar: Option, + /// Scrollbar related settings + pub scrollbar: Option, + /// Minimap related settings + pub minimap: Option, + /// Gutter related settings + pub gutter: Option, + /// Whether the editor will scroll beyond the last line. + /// + /// Default: one_page + pub scroll_beyond_last_line: Option, + /// The number of lines to keep above/below the cursor when auto-scrolling. + /// + /// Default: 3. + pub vertical_scroll_margin: Option, + /// Whether to scroll when clicking near the edge of the visible text area. + /// + /// Default: false + pub autoscroll_on_clicks: Option, + /// The number of characters to keep on either side when scrolling with the mouse. + /// + /// Default: 5. + pub horizontal_scroll_margin: Option, + /// Scroll sensitivity multiplier. This multiplier is applied + /// to both the horizontal and vertical delta values while scrolling. + /// + /// Default: 1.0 + pub scroll_sensitivity: Option, + /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied + /// to both the horizontal and vertical delta values while scrolling. Fast scrolling + /// happens when a user holds the alt or option key while scrolling. + /// + /// Default: 4.0 + pub fast_scroll_sensitivity: Option, + /// Whether the line numbers on editors gutter are relative or not. + /// + /// Default: false + pub relative_line_numbers: Option, + /// When to populate a new search's query based on the text under the cursor. + /// + /// Default: always + pub seed_search_query_from_cursor: Option, + pub use_smartcase_search: Option, + /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. + /// + /// Default: alt + pub multi_cursor_modifier: Option, + /// Hide the values of variables in `private` files, as defined by the + /// private_files setting. This only changes the visual representation, + /// the values are still present in the file and can be selected / copied / pasted + /// + /// Default: false + pub redact_private_values: Option, + + /// How many lines to expand the multibuffer excerpts by default + /// + /// Default: 3 + pub expand_excerpt_lines: Option, + + /// How many lines of context to provide in multibuffer excerpts by default + /// + /// Default: 2 + pub excerpt_context_lines: Option, + + /// Whether to enable middle-click paste on Linux + /// + /// Default: true + pub middle_click_paste: Option, + + /// What to do when multibuffer is double clicked in some of its excerpts + /// (parts of singleton buffers). + /// + /// Default: select + pub double_click_in_multibuffer: Option, + /// Whether the editor search results will loop + /// + /// Default: true + pub search_wrap: Option, + + /// Defaults to use when opening a new buffer and project search items. + /// + /// Default: nothing is enabled + pub search: Option, + + /// Whether to automatically show a signature help pop-up or not. + /// + /// Default: false + pub auto_signature_help: Option, + + /// Whether to show the signature help pop-up after completions or bracket pairs inserted. + /// + /// Default: false + pub show_signature_help_after_edits: Option, + /// The minimum APCA perceptual contrast to maintain when + /// rendering text over highlight backgrounds in the editor. + /// + /// Values range from 0 to 106. Set to 0 to disable adjustments. + /// Default: 45 + pub minimum_contrast_for_highlights: Option, + + /// Whether to follow-up empty go to definition responses from the language server or not. + /// `FindAllReferences` allows to look up references of the same symbol instead. + /// `None` disables the fallback. + /// + /// Default: FindAllReferences + pub go_to_definition_fallback: Option, + + /// Jupyter REPL settings. + pub jupyter: Option, + + /// Which level to use to filter out diagnostics displayed in the editor. + /// + /// Affects the editor rendering only, and does not interrupt + /// the functionality of diagnostics fetching and project diagnostics editor. + /// Which files containing diagnostic errors/warnings to mark in the tabs. + /// Diagnostics are only shown when file icons are also active. + /// + /// Shows all diagnostics if not specified. + /// + /// Default: warning + pub diagnostics_max_severity: Option, + + /// Whether to show code action button at start of buffer line. + /// + /// Default: true + pub inline_code_actions: Option, + + /// Drag and drop related settings + pub drag_and_drop_selection: Option, + + /// How to render LSP `textDocument/documentColor` colors in the editor. + /// + /// Default: [`DocumentColorsRenderMode::Inlay`] + pub lsp_document_colors: Option, +} + +// Status bar related settings +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct StatusBarContent { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: Option, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: Option, +} + +// Toolbar related settings +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ToolbarContent { + /// Whether to display breadcrumbs in the editor toolbar. + /// + /// Default: true + pub breadcrumbs: Option, + /// Whether to display quick action buttons in the editor toolbar. + /// + /// Default: true + pub quick_actions: Option, + /// Whether to show the selections menu in the editor toolbar. + /// + /// Default: true + pub selections_menu: Option, + /// Whether to display Agent review buttons in the editor toolbar. + /// Only applicable while reviewing a file edited by the Agent. + /// + /// Default: true + pub agent_review: Option, + /// Whether to display code action buttons in the editor toolbar. + /// + /// Default: false + pub code_actions: Option, +} + +/// Scrollbar related settings +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] +pub struct ScrollbarContent { + /// When to show the scrollbar in the editor. + /// + /// Default: auto + pub show: Option, + /// Whether to show git diff indicators in the scrollbar. + /// + /// Default: true + pub git_diff: Option, + /// Whether to show buffer search result indicators in the scrollbar. + /// + /// Default: true + pub search_results: Option, + /// Whether to show selected text occurrences in the scrollbar. + /// + /// Default: true + pub selected_text: Option, + /// Whether to show selected symbol occurrences in the scrollbar. + /// + /// Default: true + pub selected_symbol: Option, + /// Which diagnostic indicators to show in the scrollbar: + /// + /// Default: all + pub diagnostics: Option, + /// Whether to show cursor positions in the scrollbar. + /// + /// Default: true + pub cursors: Option, + /// Forcefully enable or disable the scrollbar for each axis + pub axes: Option, +} + +/// Minimap related settings +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct MinimapContent { + /// When to show the minimap in the editor. + /// + /// Default: never + pub show: Option, + + /// Where to show the minimap in the editor. + /// + /// Default: [`DisplayIn::ActiveEditor`] + pub display_in: Option, + + /// When to show the minimap thumb. + /// + /// Default: always + pub thumb: Option, + + /// Defines the border style for the minimap's scrollbar thumb. + /// + /// Default: left_open + pub thumb_border: Option, + + /// How to highlight the current line in the minimap. + /// + /// Default: inherits editor line highlights setting + pub current_line_highlight: Option>, + + /// Maximum number of columns to display in the minimap. + /// + /// Default: 80 + pub max_width_columns: Option, +} + +/// Forcefully enable or disable the scrollbar for each axis +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] +pub struct ScrollbarAxesContent { + /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. + /// + /// Default: true + pub horizontal: Option, + + /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. + /// + /// Default: true + pub vertical: Option, +} + +/// Gutter related settings +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct GutterContent { + /// Whether to show line numbers in the gutter. + /// + /// Default: true + pub line_numbers: Option, + /// Minimum number of characters to reserve space for in the gutter. + /// + /// Default: 4 + pub min_line_number_digits: Option, + /// Whether to show runnable buttons in the gutter. + /// + /// Default: true + pub runnables: Option, + /// Whether to show breakpoints in the gutter. + /// + /// Default: true + pub breakpoints: Option, + /// Whether to show fold buttons in the gutter. + /// + /// Default: true + pub folds: Option, +} + +/// How to render LSP `textDocument/documentColor` colors in the editor. +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DocumentColorsRenderMode { + /// Do not query and render document colors. + None, + /// Render document colors as inlay hints near the color text. + #[default] + Inlay, + /// Draw a border around the color text. + Border, + /// Draw a background behind the color text. + Background, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CurrentLineHighlight { + // Don't highlight the current line. + None, + // Highlight the gutter area. + Gutter, + // Highlight the editor area. + Line, + // Highlight the full line. + All, +} + +/// When to populate a new search's query based on the text under the cursor. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SeedQuerySetting { + /// Always populate the search query with the word under the cursor. + Always, + /// Only populate the search query when there is text selected. + Selection, + /// Never populate the search query + Never, +} + +/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers). +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DoubleClickInMultibuffer { + /// Behave as a regular buffer and select the whole word. + #[default] + Select, + /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click. + /// Otherwise, behave as a regular buffer and select the whole word. + Open, +} + +/// When to show the minimap thumb. +/// +/// Default: always +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MinimapThumb { + /// Show the minimap thumb only when the mouse is hovering over the minimap. + Hover, + /// Always show the minimap thumb. + #[default] + Always, +} + +/// Defines the border style for the minimap's scrollbar thumb. +/// +/// Default: left_open +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MinimapThumbBorder { + /// Displays a border on all sides of the thumb. + Full, + /// Displays a border on all sides except the left side of the thumb. + #[default] + LeftOpen, + /// Displays a border on all sides except the right side of the thumb. + RightOpen, + /// Displays a border only on the left side of the thumb. + LeftOnly, + /// Displays the thumb without any border. + None, +} + +/// Which diagnostic indicators to show in the scrollbar. +/// +/// Default: all +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ScrollbarDiagnostics { + /// Show all diagnostic levels: hint, information, warnings, error. + All, + /// Show only the following diagnostic levels: information, warning, error. + Information, + /// Show only the following diagnostic levels: warning, error. + Warning, + /// Show only the following diagnostic level: error. + Error, + /// Do not show diagnostics. + None, +} + +/// The key to use for adding multiple cursors +/// +/// Default: alt +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MultiCursorModifier { + Alt, + #[serde(alias = "cmd", alias = "ctrl")] + CmdOrCtrl, +} + +/// Whether the editor will scroll beyond the last line. +/// +/// Default: one_page +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ScrollBeyondLastLine { + /// The editor will not scroll beyond the last line. + Off, + + /// The editor will scroll beyond the last line by one page. + OnePage, + + /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin. + VerticalScrollMargin, +} + +/// The shape of a selection cursor. +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CursorShape { + /// A vertical bar + #[default] + Bar, + /// A block that surrounds the following character + Block, + /// An underline that runs along the following character + Underline, + /// A box drawn around the following character + Hollow, +} + +/// What to do when go to definition yields no results. +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GoToDefinitionFallback { + /// Disables the fallback. + None, + /// Looks up references of the same symbol instead. + #[default] + FindAllReferences, +} + +/// Determines when the mouse cursor should be hidden in an editor or input box. +/// +/// Default: on_typing_and_movement +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HideMouseMode { + /// Never hide the mouse cursor + Never, + /// Hide only when typing + OnTyping, + /// Hide on both typing and cursor movement + #[default] + OnTypingAndMovement, +} + +/// Determines how snippets are sorted relative to other completion items. +/// +/// Default: inline +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SnippetSortOrder { + /// Place snippets at the top of the completion list + Top, + /// Sort snippets normally using the default comparison logic + #[default] + Inline, + /// Place snippets at the bottom of the completion list + Bottom, + /// Do not show snippets in the completion list + None, +} + +/// Default options for buffer and project search items. +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct SearchSettingsContent { + /// Whether to show the project search button in the status bar. + pub button: Option, + pub whole_word: Option, + pub case_sensitive: Option, + pub include_ignored: Option, + pub regex: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct JupyterContent { + /// Whether the Jupyter feature is enabled. + /// + /// Default: true + pub enabled: Option, +} + +/// Whether to allow drag and drop text selection in buffer. +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct DragAndDropSelectionContent { + /// When true, enables drag and drop text selection in buffer. + /// + /// Default: true + pub enabled: Option, + + /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + /// + /// Default: 300 + pub delay: Option, +} + +/// When to show the minimap in the editor. +/// +/// Default: never +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowMinimap { + /// Follow the visibility of the scrollbar. + Auto, + /// Always show the minimap. + Always, + /// Never show the minimap. + #[default] + Never, +} + +/// Where to show the minimap in the editor. +/// +/// Default: all_editors +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DisplayIn { + /// Show on all open editors. + AllEditors, + /// Show the minimap on the active editor only. + #[default] + ActiveEditor, +} diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index c86c36954a022c20aac184c993be2b59117c9958..e9588ac8de21181d2414fcf15f179b4b9f2966f7 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -379,7 +379,7 @@ pub struct JsxTagAutoCloseSettings { } /// The settings for inlay hints. -/// todo(settings_refactor) the fields of this struct should likely be optional, +/// todo!() the fields of this struct should likely be optional, /// and a similar struct exposed from the language crate. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettings { diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index 351ee41402ee9aea77282d23ea6fd0a35f8b9e8e..8a98265c564f17be39888a4b8ec8adbdd5e7eddb 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -192,7 +192,7 @@ impl TerminalLineHeight { } } -/// When to show the scrollbar in the terminal. +/// When to show the scrollbar. /// /// Default: auto #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index f3cc37d2f55356f05af3f1644dc4292a6add2660..b00cbc5441c92626ff117fc96fbf5bca3891ed3b 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -44,6 +44,17 @@ pub mod scrollbars { Never, } + impl From for ShowScrollbar { + fn from(value: settings::ShowScrollbar) -> Self { + match value { + settings::ShowScrollbar::Auto => ShowScrollbar::Auto, + settings::ShowScrollbar::System => ShowScrollbar::System, + settings::ShowScrollbar::Always => ShowScrollbar::Always, + settings::ShowScrollbar::Never => ShowScrollbar::Never, + } + } + } + impl ShowScrollbar { pub(super) fn show(&self) -> bool { *self != Self::Never From 6eeffe93a94fbe2bfe24babc27055773bce0d9d8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 00:32:11 -0600 Subject: [PATCH 042/117] TerminalView --- crates/agent/src/agent_profile.rs | 2 +- crates/terminal/src/terminal_settings.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 27 ++++++++-------------- crates/terminal_view/src/terminal_view.rs | 11 +++------ 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 76f0199afb170e9843f32d2c3cd93d0a0c7b1b5e..40ba2f07db7ad425a5d0e9befe91499eb746b74e 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -49,7 +49,7 @@ impl AgentProfile { .unwrap_or_default(), }; - update_settings_file::(fs, cx, { + update_settings_file(fs, cx, { let id = id.clone(); move |settings, _cx| { profile_settings.save_to_settings(id, settings).log_err(); diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 9d4037154a92307ac3088e936d3903f0db8c8703..e39ded2dc4145e7d68edea0c54311216598ebc3e 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -108,7 +108,7 @@ impl settings::Settings for TerminalSettings { breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(), }, scrollbar: ScrollbarSettings { - show: content.scrollbar.unwrap().show.unwrap(), + show: content.scrollbar.unwrap().show.flatten(), }, minimum_contrast: content.minimum_contrast.unwrap(), } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 5308b230f78b334c9a39cadbcbb3117ac4e3e11f..c327978201a6b330ee2afbe4856d96ae511dff73 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -18,12 +18,9 @@ use gpui::{ use itertools::Itertools; use project::{Fs, Project, ProjectEntryId}; use search::{BufferSearchBar, buffer_search::DivRegistrar}; -use settings::Settings; +use settings::{Settings, TerminalDockPosition}; use task::{RevealStrategy, RevealTarget, SpawnInTerminal, TaskId}; -use terminal::{ - Terminal, - terminal_settings::{TerminalDockPosition, TerminalSettings}, -}; +use terminal::{Terminal, terminal_settings::TerminalSettings}; use ui::{ ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip, prelude::*, @@ -1433,18 +1430,14 @@ impl Panel for TerminalPanel { _window: &mut Window, cx: &mut Context, ) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| { - let dock = match position { - DockPosition::Left => TerminalDockPosition::Left, - DockPosition::Bottom => TerminalDockPosition::Bottom, - DockPosition::Right => TerminalDockPosition::Right, - }; - settings.dock = Some(dock); - }, - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + let dock = match position { + DockPosition::Left => TerminalDockPosition::Left, + DockPosition::Bottom => TerminalDockPosition::Bottom, + DockPosition::Right => TerminalDockPosition::Right, + }; + settings.terminal.get_or_insert_default().dock = Some(dock); + }); } fn size(&self, window: &Window, cx: &App) -> Pixels { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5fdefc6a66224587bc746d21ecddb1a66a2bbe4f..45ecbb4d0ff38e144b1d1c4806b19921f6b4e30b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -25,7 +25,7 @@ use terminal::{ index::Point, term::{TermMode, point_to_viewport, search::RegexSearch}, }, - terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory}, + terminal_settings::{CursorShape, TerminalSettings}, }; use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; @@ -50,7 +50,7 @@ use workspace::{ }; use serde::Deserialize; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory}; use smol::Timer; use zed_actions::assistant::InlineAssist; @@ -997,12 +997,7 @@ impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper { TerminalSettings::get_global(cx) .scrollbar .show - .map(|value| match value { - terminal_settings::ShowScrollbar::Auto => scrollbars::ShowScrollbar::Auto, - terminal_settings::ShowScrollbar::System => scrollbars::ShowScrollbar::System, - terminal_settings::ShowScrollbar::Always => scrollbars::ShowScrollbar::Always, - terminal_settings::ShowScrollbar::Never => scrollbars::ShowScrollbar::Never, - }) + .map(Into::into) .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) } } From 0a8a82e3b6a042aa1f1d6049d8023120009fb0ed Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 00:35:41 -0600 Subject: [PATCH 043/117] Agent --- assets/settings/default.json | 8 +++++++- crates/agent/src/thread.rs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index b544e83a0af27b42b2848d47f289c610b6f4d4f9..3a0c0754c9ee0b48275e76c12f1ad91f7945e53f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -821,6 +821,8 @@ "default_width": 640, // Default height when the agent panel is docked to the bottom. "default_height": 320, + // The view to use by default (thread, or text_thread) + "default_view": "thread", // The default model to use when creating new threads. "default_model": { // The provider to use. @@ -929,7 +931,11 @@ /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. /// /// Default: true - "expand_terminal_card": true + "expand_terminal_card": true, + /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. + /// + /// Default: false + "use_modifier_to_send": false }, // The settings for slash commands. "slash_commands": { diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56ab1e7acb6705aeace82f142dc28a9f3..8b9d489ccf472ca16435934e48a12b70dc783c40 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -3272,7 +3272,7 @@ mod tests { // Test-specific constants const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30; - use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters}; + use agent_settings::{AgentProfileId, AgentSettings}; use assistant_tool::ToolRegistry; use assistant_tools; use futures::StreamExt; @@ -3289,7 +3289,7 @@ mod tests { use project::{FakeFs, Project}; use prompt_store::PromptBuilder; use serde_json::json; - use settings::{Settings, SettingsStore}; + use settings::{LanguageModelParameters, Settings, SettingsStore}; use std::sync::Arc; use std::time::Duration; use theme::ThemeSettings; From 7d7e67a20a541d4061e38c34ccf3459afb9907cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 00:47:53 -0600 Subject: [PATCH 044/117] WIP --- crates/git_ui/src/branch_picker.rs | 1 - crates/git_ui/src/git_panel.rs | 13 ++- crates/git_ui/src/git_panel_settings.rs | 121 ++++++++++-------------- crates/settings/src/settings_content.rs | 56 +++++++++++ crates/workspace/src/dock.rs | 10 ++ 5 files changed, 121 insertions(+), 80 deletions(-) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 42558b7b79bca1892b9d36ae4b39bb3cdb196d4f..f425af646d6c38227bb82b3185ab7b0192fdea6c 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -556,7 +556,6 @@ impl PickerDelegate for BranchListDelegate { let show_author_name = ProjectSettings::get_global(cx) .git .branch_picker - .unwrap_or_default() .show_author_name; subject.map_or("no commits found".into(), |subject| { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 3063206723021d00aa21e0f8163b4fe3b941fcb7..4f626ee7e83b89ab863cdcadcb0a32471905c4c7 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2439,8 +2439,9 @@ impl GitPanel { let workspace = workspace.read(cx); let fs = workspace.app_state().fs.clone(); cx.update_global::(|store, _cx| { - store.update_settings_file::(fs, move |settings, _cx| { - settings.sort_by_path = Some(!current_setting); + store.update_settings_file(fs, move |settings, _cx| { + settings.git_panel.get_or_insert_default().sort_by_path = + Some(!current_setting); }); }); } @@ -4353,11 +4354,9 @@ impl Panel for GitPanel { } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| settings.dock = Some(position), - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.git_panel.get_or_insert_default().dock = Some(position.into()) + }); } fn size(&self, _: &Window, cx: &App) -> Pixels { diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index be26061b0d2cbeee9a0a75f52daf56fb9fcb94f4..9b389945b90fbae5e2ada0a64ee7b3d463c98a5f 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -2,8 +2,12 @@ use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; -use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; +use settings::{Settings, SettingsContent, SettingsKey, SettingsSources, SettingsUi, StatusStyle}; +use ui::{ + px, + scrollbars::{ScrollbarVisibility, ShowScrollbar}, +}; +use util::MergeFrom; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -19,67 +23,7 @@ pub struct ScrollbarSettings { pub show: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -// Style of the git status indicator in the panel. -// -// Default: icon -pub enum StatusStyleContent { - Icon, - LabelColor, -} - -#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum StatusStyle { - #[default] - Icon, - LabelColor, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "git_panel")] -pub struct GitPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 360 - pub default_width: Option, - /// How entry statuses are displayed. - /// - /// Default: icon - pub status_style: Option, - /// How and when the scrollbar should be displayed. - /// - /// Default: inherits editor scrollbar settings - pub scrollbar: Option, - - /// What the default branch name should be when - /// `init.defaultBranch` is not set in git - /// - /// Default: main - pub fallback_branch_name: Option, - - /// Whether to sort entries in the panel by path - /// or by status (the default). - /// - /// Default: false - pub sort_by_path: Option, - - /// Whether to collapse untracked files in the diff panel. - /// - /// Default: false - pub collapse_untracked_diff: Option, -} - -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct GitPanelSettings { pub button: bool, pub dock: DockPosition, @@ -108,17 +52,50 @@ impl ScrollbarVisibility for GitPanelSettings { } impl Settings for GitPanelSettings { - type FileContent = GitPanelSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { + let git_panel = content.git_panel.clone().unwrap(); + Self { + button: git_panel.button.unwrap(), + dock: git_panel.dock.unwrap().into(), + default_width: px(git_panel.default_width.unwrap()), + status_style: git_panel.status_style.unwrap(), + scrollbar: ScrollbarSettings { + show: git_panel.scrollbar.unwrap().show.map(Into::into), + }, + fallback_branch_name: git_panel.fallback_branch_name.unwrap(), + sort_by_path: git_panel.sort_by_path.unwrap(), + collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(), + } + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { + let Some(git_panel) = &content.git_panel else { + return; + }; + self.button.merge_from(&git_panel.button); + self.dock.merge_from(&git_panel.dock.map(Into::into)); + self.default_width + .merge_from(&git_panel.default_width.map(px)); + self.status_style.merge_from(&git_panel.status_style); + self.fallback_branch_name + .merge_from(&git_panel.fallback_branch_name); + self.sort_by_path.merge_from(&git_panel.sort_by_path); + self.collapse_untracked_diff + .merge_from(&git_panel.collapse_untracked_diff); + if let Some(show) = git_panel.scrollbar.as_ref().and_then(|s| s.show) { + self.scrollbar.show = Some(show.into()) + } } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.bool_setting("git.enabled", &mut current.button); - vscode.string_setting("git.defaultBranchName", &mut current.fallback_branch_name); + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { + if let Some(git_enabled) = vscode.read_bool("git.enabled") { + current.git_panel.get_or_insert_default().button = Some(git_enabled); + } + if let Some(default_branch) = vscode.read_string("git.defaultBranchName") { + current + .git_panel + .get_or_insert_default() + .fallback_branch_name = Some(default_branch.to_string()); + } } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index c01253d8b98884b430a44b194416c2d9c07279b3..8d0d99b49ed9e53a30490ee6c01a425b4ac98ba9 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -41,6 +41,8 @@ pub struct SettingsContent { #[serde(flatten)] pub editor: EditorSettingsContent, + pub git_panel: Option, + pub tabs: Option, pub tab_bar: Option, @@ -364,3 +366,57 @@ pub struct ExtensionSettingsContent { #[serde(default)] pub auto_update_extensions: HashMap, bool>, } + +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct GitPanelSettingsContent { + /// Whether to show the panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Where to dock the panel. + /// + /// Default: left + pub dock: Option, + /// Default width of the panel in pixels. + /// + /// Default: 360 + pub default_width: Option, + /// How entry statuses are displayed. + /// + /// Default: icon + pub status_style: Option, + /// How and when the scrollbar should be displayed. + /// + /// Default: inherits editor scrollbar settings + pub scrollbar: Option, + + /// What the default branch name should be when + /// `init.defaultBranch` is not set in git + /// + /// Default: main + pub fallback_branch_name: Option, + + /// Whether to sort entries in the panel by path + /// or by status (the default). + /// + /// Default: false + pub sort_by_path: Option, + + /// Whether to collapse untracked files in the diff panel. + /// + /// Default: false + pub collapse_untracked_diff: Option, +} + +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum StatusStyle { + #[default] + Icon, + LabelColor, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ScrollbarSettings { + pub show: Option, +} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 09fa6f068f92dda29beac98eff0dcf41e93dc1bf..ec33b9af59c2cb070866c0a1c4a5b8670a8d58e6 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -225,6 +225,16 @@ impl From for DockPosition { } } +impl Into for DockPosition { + fn into(self) -> settings::DockPosition { + match self { + Self::Left => settings::DockPosition::Left, + Self::Bottom => settings::DockPosition::Bottom, + Self::Right => settings::DockPosition::Right, + } + } +} + impl DockPosition { fn label(&self) -> &'static str { match self { From 83d9f07547f596d945db5d2a65ad6e35318c51b6 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 17 Sep 2025 11:44:16 +0200 Subject: [PATCH 045/117] Add WSL opening UI (#38260) This PR adds an option to open WSL machines from the UI. - [x] Open wsl from open remote - [ ] Open local folder in wsl action - [ ] Open wsl shortcut (shortcuts to open remote) Release Notes: - N/A --- Cargo.lock | 54 +- crates/recent_projects/Cargo.toml | 3 + .../recent_projects/src/remote_connections.rs | 83 +- crates/recent_projects/src/remote_servers.rs | 1503 ++++++++++++----- 4 files changed, 1216 insertions(+), 427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43a2fc4041fbf76b57e62d335e94c695ef07fc12..ee14efd2d800aeabb29a75959f9a929d06b78f81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,7 +2977,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -13790,6 +13790,7 @@ dependencies = [ "theme", "ui", "util", + "windows-registry 0.6.0", "workspace", "workspace-hack", "zed_actions", @@ -19613,7 +19614,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -19683,7 +19684,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -19695,7 +19696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -19770,6 +19771,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -19777,7 +19784,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -19797,11 +19804,22 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" dependencies = [ - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.2", "windows-strings 0.4.0", ] +[[package]] +name = "windows-registry" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f91f87ce112ffb7275000ea98eb1940912c21c1567c9312fde20261f3eadd29" +dependencies = [ + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -19826,7 +19844,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -19845,7 +19872,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -19854,7 +19881,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index d48beeaab6bfbe54e3cac6d7f836248cc0ff2f3e..91879c5d4175ad66428a255655f3c8bd4a5059e3 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -44,6 +44,9 @@ workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true +[target.'cfg(target_os = "windows")'.dependencies] +windows-registry = "0.6.0" + [dev-dependencies] dap.workspace = true editor = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 3e6810239c80c72d74624bcc243157290fcd93fa..72e2844d501f8f8860d62964d22430af80bab4b6 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -17,7 +17,7 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; use remote::{ ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform, - SshConnectionOptions, SshPortForwardOption, + SshConnectionOptions, SshPortForwardOption, WslConnectionOptions, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -33,6 +33,7 @@ use workspace::{AppState, ModalView, Workspace}; #[derive(Deserialize)] pub struct SshSettings { pub ssh_connections: Option>, + pub wsl_connections: Option>, /// Whether to read ~/.ssh/config for ssh connection sources. #[serde(default = "default_true")] pub read_ssh_config: bool, @@ -43,6 +44,10 @@ impl SshSettings { self.ssh_connections.clone().into_iter().flatten() } + pub fn wsl_connections(&self) -> impl Iterator + use<> { + self.wsl_connections.clone().into_iter().flatten() + } + pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { for conn in self.ssh_connections() { if conn.host == options.host @@ -116,6 +121,51 @@ impl From for SshConnectionOptions { } } +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct WslConnection { + pub distro_name: SharedString, + #[serde(default)] + pub user: Option, + #[serde(default)] + pub projects: BTreeSet, +} + +impl From for WslConnectionOptions { + fn from(val: WslConnection) -> Self { + WslConnectionOptions { + distro_name: val.distro_name.into(), + user: val.user, + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +pub enum Connection { + Ssh(SshConnection), + Wsl(WslConnection), +} + +impl From for RemoteConnectionOptions { + fn from(val: Connection) -> Self { + match val { + Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()), + Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()), + } + } +} + +impl From for Connection { + fn from(val: SshConnection) -> Self { + Connection::Ssh(val) + } +} + +impl From for Connection { + fn from(val: WslConnection) -> Self { + Connection::Wsl(val) + } +} + #[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)] pub struct SshProject { pub paths: Vec, @@ -125,6 +175,7 @@ pub struct SshProject { #[settings_key(None)] pub struct RemoteSettingsContent { pub ssh_connections: Option>, + pub wsl_connections: Option>, pub read_ssh_config: Option, } @@ -561,6 +612,36 @@ pub fn connect_over_ssh( ) } +pub fn connect( + unique_identifier: ConnectionIdentifier, + connection_options: RemoteConnectionOptions, + ui: Entity, + window: &mut Window, + cx: &mut App, +) -> Task>>> { + let window = window.window_handle(); + let known_password = match &connection_options { + RemoteConnectionOptions::Ssh(ssh_connection_options) => { + ssh_connection_options.password.clone() + } + _ => None, + }; + let (tx, rx) = oneshot::channel(); + ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); + + remote::RemoteClient::new( + unique_identifier, + connection_options, + rx, + Arc::new(RemoteClientDelegate { + window, + ui: ui.downgrade(), + known_password, + }), + cx, + ) +} + pub async fn open_remote_project( connection_options: RemoteConnectionOptions, paths: Vec, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 39032642b887350730c16a12d696253c256cfd72..d7e7505851a2b0cd2f86c807d6850937096dac7c 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,7 +1,8 @@ use crate::{ remote_connections::{ - RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection, - SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project, + Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, + SshConnection, SshConnectionHeader, SshProject, SshSettings, connect, connect_over_ssh, + open_remote_project, }, ssh_config::parse_ssh_config_hosts, }; @@ -13,11 +14,12 @@ use gpui::{ FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, canvas, }; +use log::info; use paths::{global_ssh_config_file, user_ssh_config_file}; use picker::Picker; use project::{Fs, Project}; use remote::{ - RemoteClient, RemoteConnectionOptions, SshConnectionOptions, + RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, remote_client::ConnectionIdentifier, }; use settings::{Settings, SettingsStore, update_settings_file, watch_config_file}; @@ -79,27 +81,253 @@ impl CreateRemoteServer { } } +#[cfg(target_os = "windows")] +struct AddWslDistro { + picker: Entity>, + connection_prompt: Option>, + _creating: Option>, +} + +#[cfg(target_os = "windows")] +impl AddWslDistro { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let delegate = WslPickerDelegate::new(); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); + + cx.subscribe_in( + &picker, + window, + |this, _, _: &WslDistroSelected, window, cx| { + this.confirm(&menu::Confirm, window, cx); + }, + ) + .detach(); + + cx.subscribe_in( + &picker, + window, + |this, _, _: &WslPickerDismissed, window, cx| { + this.cancel(&menu::Cancel, window, cx); + }, + ) + .detach(); + + AddWslDistro { + picker, + connection_prompt: None, + _creating: None, + } + } +} + +#[cfg(target_os = "windows")] +#[derive(Clone, Debug)] +pub struct WslDistroSelected(pub String); + +#[cfg(target_os = "windows")] +#[derive(Clone, Debug)] +pub struct WslPickerDismissed; + +#[cfg(target_os = "windows")] +struct WslPickerDelegate { + selected_index: usize, + distro_list: Option>, + matches: Vec, +} + +#[cfg(target_os = "windows")] +impl WslPickerDelegate { + fn new() -> Self { + WslPickerDelegate { + selected_index: 0, + distro_list: None, + matches: Vec::new(), + } + } + + pub fn selected_distro(&self) -> Option { + self.matches + .get(self.selected_index) + .map(|m| m.string.clone()) + } +} + +#[cfg(target_os = "windows")] +impl WslPickerDelegate { + fn fetch_distros() -> anyhow::Result> { + use anyhow::Context; + use windows_registry::CURRENT_USER; + + let lxss_key = CURRENT_USER + .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss") + .context("failed to get lxss wsl key")?; + + let distros = lxss_key + .keys() + .context("failed to get wsl distros")? + .filter_map(|key| { + lxss_key + .open(&key) + .context("failed to open subkey for distro") + .log_err() + }) + .filter_map(|distro| distro.get_string("DistributionName").ok()) + .collect::>(); + + Ok(distros) + } +} + +#[cfg(target_os = "windows")] +impl EventEmitter for Picker {} + +#[cfg(target_os = "windows")] +impl EventEmitter for Picker {} + +#[cfg(target_os = "windows")] +impl picker::PickerDelegate for WslPickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + cx: &mut Context>, + ) { + self.selected_index = ix; + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + Arc::from("Enter WSL distro name") + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + use fuzzy::StringMatchCandidate; + + let needs_fetch = self.distro_list.is_none(); + if needs_fetch { + let distros = Self::fetch_distros().log_err(); + self.distro_list = distros; + } + + if let Some(distro_list) = &self.distro_list { + use ordered_float::OrderedFloat; + + let candidates = distro_list + .iter() + .enumerate() + .map(|(id, distro)| StringMatchCandidate::new(id, distro)) + .collect::>(); + + let query = query.trim_start(); + let smart_case = query.chars().any(|c| c.is_uppercase()); + self.matches = smol::block_on(fuzzy::match_strings( + candidates.as_slice(), + query, + smart_case, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + )); + self.matches.sort_unstable_by_key(|m| m.candidate_id); + + self.selected_index = self + .matches + .iter() + .enumerate() + .rev() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(index, _)| index) + .unwrap_or(0); + } + + Task::ready(()) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) { + if let Some(distro) = self.matches.get(self.selected_index) { + cx.emit(WslDistroSelected(distro.string.clone())); + } + } + + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + cx.emit(WslPickerDismissed); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + use ui::HighlightedLabel; + + let matched = self.matches.get(ix)?; + Some( + ListItem::new(ix) + .toggle_state(selected) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .child( + h_flex() + .flex_grow() + .gap_3() + .child(Icon::new(IconName::Server)) + .child(v_flex().child(HighlightedLabel::new( + matched.string.clone(), + matched.positions.clone(), + ))), + ), + ) + } +} + +enum ProjectPickerData { + Ssh { + connection_string: SharedString, + nickname: Option, + }, + Wsl { + distro_name: SharedString, + }, +} + struct ProjectPicker { - connection_string: SharedString, - nickname: Option, + data: ProjectPickerData, picker: Entity>, _path_task: Shared>>, } struct EditNicknameState { - index: usize, + index: SshServerIndex, editor: Entity, } impl EditNicknameState { - fn new(index: usize, window: &mut Window, cx: &mut App) -> Self { + fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self { let this = Self { index, editor: cx.new(|cx| Editor::single_line(window, cx)), }; let starting_text = SshSettings::get_global(cx) .ssh_connections() - .nth(index) + .nth(index.0) .and_then(|state| state.nickname) .filter(|text| !text.is_empty()); this.editor.update(cx, |this, cx| { @@ -122,8 +350,8 @@ impl Focusable for ProjectPicker { impl ProjectPicker { fn new( create_new_window: bool, - ix: usize, - connection: SshConnectionOptions, + index: ServerIndex, + connection: RemoteConnectionOptions, project: Entity, home_dir: RemotePathBuf, path_style: PathStyle, @@ -142,8 +370,16 @@ impl ProjectPicker { picker.set_query(home_dir.to_string(), window, cx); picker }); - let connection_string = connection.connection_string().into(); - let nickname = connection.nickname.clone().map(|nick| nick.into()); + + let data = match &connection { + RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh { + connection_string: connection.connection_string().into(), + nickname: connection.nickname.clone().map(|nick| nick.into()), + }, + RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl { + distro_name: connection.distro_name.clone().into(), + }, + }; let _path_task = cx .spawn_in(window, { let workspace = workspace; @@ -178,13 +414,24 @@ impl ProjectPicker { .iter() .map(|path| path.to_string_lossy().to_string()) .collect(); - move |setting, _| { - if let Some(server) = setting - .ssh_connections - .as_mut() - .and_then(|connections| connections.get_mut(ix)) - { - server.projects.insert(SshProject { paths }); + move |setting, _| match index { + ServerIndex::Ssh(index) => { + if let Some(server) = setting + .ssh_connections + .as_mut() + .and_then(|connections| connections.get_mut(index.0)) + { + server.projects.insert(SshProject { paths }); + }; + } + ServerIndex::Wsl(index) => { + if let Some(server) = setting + .wsl_connections + .as_mut() + .and_then(|connections| connections.get_mut(index.0)) + { + server.projects.insert(SshProject { paths }); + }; } } }); @@ -204,12 +451,7 @@ impl ProjectPicker { .log_err()?; open_remote_project_with_existing_connection( - RemoteConnectionOptions::Ssh(connection), - project, - paths, - app_state, - window, - cx, + connection, project, paths, app_state, window, cx, ) .await .log_err(); @@ -225,8 +467,7 @@ impl ProjectPicker { cx.new(|_| Self { _path_task, picker, - connection_string, - nickname, + data, }) } } @@ -234,14 +475,23 @@ impl ProjectPicker { impl gpui::Render for ProjectPicker { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() - .child( - SshConnectionHeader { - connection_string: self.connection_string.clone(), + .child(match &self.data { + ProjectPickerData::Ssh { + connection_string, + nickname, + } => SshConnectionHeader { + connection_string: connection_string.clone(), paths: Default::default(), - nickname: self.nickname.clone(), + nickname: nickname.clone(), } .render(window, cx), - ) + ProjectPickerData::Wsl { distro_name } => SshConnectionHeader { + connection_string: distro_name.clone(), + paths: Default::default(), + nickname: None, + } + .render(window, cx), + }) .child( div() .border_t_1() @@ -251,13 +501,48 @@ impl gpui::Render for ProjectPicker { } } +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +struct SshServerIndex(usize); +impl std::fmt::Display for SshServerIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +struct WslServerIndex(usize); +impl std::fmt::Display for WslServerIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +enum ServerIndex { + Ssh(SshServerIndex), + Wsl(WslServerIndex), +} +impl From for ServerIndex { + fn from(index: SshServerIndex) -> Self { + Self::Ssh(index) + } +} +impl From for ServerIndex { + fn from(index: WslServerIndex) -> Self { + Self::Wsl(index) + } +} + #[derive(Clone)] enum RemoteEntry { Project { open_folder: NavigableEntry, projects: Vec<(NavigableEntry, SshProject)>, configure: NavigableEntry, - connection: SshConnection, + connection: Connection, + index: ServerIndex, }, SshConfig { open_folder: NavigableEntry, @@ -270,13 +555,16 @@ impl RemoteEntry { matches!(self, Self::Project { .. }) } - fn connection(&self) -> Cow<'_, SshConnection> { + fn connection(&self) -> Cow<'_, Connection> { match self { Self::Project { connection, .. } => Cow::Borrowed(connection), - Self::SshConfig { host, .. } => Cow::Owned(SshConnection { - host: host.clone(), - ..SshConnection::default() - }), + Self::SshConfig { host, .. } => Cow::Owned( + SshConnection { + host: host.clone(), + ..SshConnection::default() + } + .into(), + ), } } } @@ -285,6 +573,7 @@ impl RemoteEntry { struct DefaultState { scroll_handle: ScrollHandle, add_new_server: NavigableEntry, + add_new_wsl: NavigableEntry, servers: Vec, } @@ -292,13 +581,15 @@ impl DefaultState { fn new(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { let handle = ScrollHandle::new(); let add_new_server = NavigableEntry::new(&handle, cx); + let add_new_wsl = NavigableEntry::new(&handle, cx); let ssh_settings = SshSettings::get_global(cx); let read_ssh_config = ssh_settings.read_ssh_config; - let mut servers: Vec = ssh_settings + let ssh_servers = ssh_settings .ssh_connections() - .map(|connection| { + .enumerate() + .map(|(index, connection)| { let open_folder = NavigableEntry::new(&handle, cx); let configure = NavigableEntry::new(&handle, cx); let projects = connection @@ -310,16 +601,42 @@ impl DefaultState { open_folder, configure, projects, - connection, + index: ServerIndex::Ssh(SshServerIndex(index)), + connection: connection.into(), } - }) - .collect(); + }); + + let wsl_servers = ssh_settings + .wsl_connections() + .enumerate() + .map(|(index, connection)| { + let open_folder = NavigableEntry::new(&handle, cx); + let configure = NavigableEntry::new(&handle, cx); + let projects = connection + .projects + .iter() + .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) + .collect(); + RemoteEntry::Project { + open_folder, + configure, + projects, + index: ServerIndex::Wsl(WslServerIndex(index)), + connection: connection.into(), + } + }); + + let mut servers = ssh_servers.chain(wsl_servers).collect::>(); if read_ssh_config { let mut extra_servers_from_config = ssh_config_servers.clone(); for server in &servers { - if let RemoteEntry::Project { connection, .. } = server { - extra_servers_from_config.remove(&connection.host); + if let RemoteEntry::Project { + connection: Connection::Ssh(ssh_options), + .. + } = server + { + extra_servers_from_config.remove(&SharedString::new(ssh_options.host.clone())); } } servers.extend(extra_servers_from_config.into_iter().map(|host| { @@ -333,23 +650,43 @@ impl DefaultState { Self { scroll_handle: handle, add_new_server, + add_new_wsl, servers, } } } #[derive(Clone)] -struct ViewServerOptionsState { - server_index: usize, - connection: SshConnection, - entries: [NavigableEntry; 4], +enum ViewServerOptionsState { + Ssh { + connection: SshConnectionOptions, + server_index: SshServerIndex, + entries: [NavigableEntry; 4], + }, + Wsl { + connection: WslConnectionOptions, + server_index: WslServerIndex, + entries: [NavigableEntry; 2], + }, } + +impl ViewServerOptionsState { + fn entries(&self) -> &[NavigableEntry] { + match self { + Self::Ssh { entries, .. } => entries, + Self::Wsl { entries, .. } => entries, + } + } +} + enum Mode { Default(DefaultState), ViewServerOptions(ViewServerOptionsState), EditNickname(EditNicknameState), ProjectPicker(Entity), CreateRemoteServer(CreateRemoteServer), + #[cfg(target_os = "windows")] + AddWslDistro(AddWslDistro), } impl Mode { @@ -357,6 +694,7 @@ impl Mode { Self::Default(DefaultState::new(ssh_config_servers, cx)) } } + impl RemoteServerProjects { pub fn new( create_new_window: bool, @@ -405,10 +743,10 @@ impl RemoteServerProjects { } } - pub fn project_picker( + fn project_picker( create_new_window: bool, - ix: usize, - connection_options: remote::SshConnectionOptions, + index: ServerIndex, + connection_options: remote::RemoteConnectionOptions, project: Entity, home_dir: RemotePathBuf, path_style: PathStyle, @@ -420,7 +758,7 @@ impl RemoteServerProjects { let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx); this.mode = Mode::ProjectPicker(ProjectPicker::new( create_new_window, - ix, + index, connection_options, project, home_dir, @@ -480,6 +818,7 @@ impl RemoteServerProjects { match connection.await { Some(Some(client)) => this .update_in(cx, |this, window, cx| { + info!("ssh server created"); telemetry::event!("SSH Server Created"); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); @@ -517,25 +856,100 @@ impl RemoteServerProjects { }); } + #[cfg(target_os = "windows")] + fn connect_wsl_distro( + &mut self, + picker: Entity>, + distro: String, + window: &mut Window, + cx: &mut Context, + ) { + let connection_options = WslConnectionOptions { + distro_name: distro, + user: None, + }; + + let prompt = cx.new(|cx| { + RemoteConnectionPrompt::new(connection_options.distro_name.clone(), None, window, cx) + }); + let connection = connect( + ConnectionIdentifier::setup(), + connection_options.clone().into(), + prompt.clone(), + window, + cx, + ) + .prompt_err("Failed to connect", window, cx, |_, _, _| None); + + let wsl_picker = picker.clone(); + let creating = cx.spawn_in(window, async move |this, cx| { + match connection.await { + Some(Some(client)) => this + .update_in(cx, |this, window, cx| { + telemetry::event!("WSL Distro Added"); + this.retained_connections.push(client); + this.add_wsl_distro(connection_options, cx); + this.mode = Mode::default_mode(&BTreeSet::new(), cx); + this.focus_handle(cx).focus(window); + cx.notify() + }) + .log_err(), + _ => this + .update(cx, |this, cx| { + this.mode = Mode::AddWslDistro(AddWslDistro { + picker: wsl_picker, + connection_prompt: None, + _creating: None, + }); + cx.notify() + }) + .log_err(), + }; + () + }); + + self.mode = Mode::AddWslDistro(AddWslDistro { + picker, + connection_prompt: Some(prompt), + _creating: Some(creating), + }); + } + fn view_server_options( &mut self, - (server_index, connection): (usize, SshConnection), + (server_index, connection): (ServerIndex, RemoteConnectionOptions), window: &mut Window, cx: &mut Context, ) { - self.mode = Mode::ViewServerOptions(ViewServerOptionsState { - server_index, - connection, - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), + self.mode = Mode::ViewServerOptions(match (server_index, connection) { + (ServerIndex::Ssh(server_index), RemoteConnectionOptions::Ssh(connection)) => { + ViewServerOptionsState::Ssh { + connection, + server_index, + entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), + } + } + (ServerIndex::Wsl(server_index), RemoteConnectionOptions::Wsl(connection)) => { + ViewServerOptionsState::Wsl { + connection, + server_index, + entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), + } + } + _ => { + log::error!("server index and connection options mismatch"); + self.mode = Mode::default_mode(&BTreeSet::default(), cx); + return; + } }); self.focus_handle(cx).focus(window); cx.notify(); } - fn create_ssh_project( + fn create_remote_project( &mut self, - ix: usize, - ssh_connection: SshConnection, + index: ServerIndex, + connection_options: RemoteConnectionOptions, window: &mut Window, cx: &mut Context, ) { @@ -544,17 +958,11 @@ impl RemoteServerProjects { }; let create_new_window = self.create_new_window; - let connection_options: SshConnectionOptions = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { let app_state = workspace.app_state().clone(); workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new( - &RemoteConnectionOptions::Ssh(connection_options.clone()), - Vec::new(), - window, - cx, - ) + RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) }); let prompt = workspace .active_modal::(cx) @@ -563,7 +971,7 @@ impl RemoteServerProjects { .prompt .clone(); - let connect = connect_over_ssh( + let connect = connect( ConnectionIdentifier::setup(), connection_options.clone(), prompt, @@ -624,7 +1032,7 @@ impl RemoteServerProjects { workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::project_picker( create_new_window, - ix, + index, connection_options, project, home_dir, @@ -662,7 +1070,7 @@ impl RemoteServerProjects { let index = state.index; self.update_settings_file(cx, move |setting, _| { if let Some(connections) = setting.ssh_connections.as_mut() - && let Some(connection) = connections.get_mut(index) + && let Some(connection) = connections.get_mut(index.0) { connection.nickname = text; } @@ -670,6 +1078,12 @@ impl RemoteServerProjects { self.mode = Mode::default_mode(&self.ssh_config_servers, cx); self.focus_handle.focus(window); } + #[cfg(target_os = "windows")] + Mode::AddWslDistro(state) => { + let delegate = &state.picker.read(cx).delegate; + let distro = delegate.selected_distro().unwrap(); + self.connect_wsl_distro(state.picker.clone(), distro, window, cx); + } } } @@ -702,11 +1116,19 @@ impl RemoteServerProjects { cx: &mut Context, ) -> impl IntoElement { let connection = ssh_server.connection().into_owned(); - let (main_label, aux_label) = if let Some(nickname) = connection.nickname.clone() { - let aux_label = SharedString::from(format!("({})", connection.host)); - (nickname.into(), Some(aux_label)) - } else { - (connection.host.clone(), None) + + let (main_label, aux_label, is_wsl) = match &connection { + Connection::Ssh(connection) => { + if let Some(nickname) = connection.nickname.clone() { + let aux_label = SharedString::from(format!("({})", connection.host)); + (nickname.into(), Some(aux_label), false) + } else { + (connection.host.clone(), None, false) + } + } + Connection::Wsl(wsl_connection_options) => { + (wsl_connection_options.distro_name.clone(), None, true) + } }; v_flex() .w_full() @@ -720,11 +1142,23 @@ impl RemoteServerProjects { .gap_1() .overflow_hidden() .child( - div().max_w_96().overflow_hidden().text_ellipsis().child( - Label::new(main_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), + h_flex() + .gap_1() + .max_w_96() + .overflow_hidden() + .text_ellipsis() + .when(is_wsl, |this| { + this.child( + Label::new("WSL:") + .size(LabelSize::Small) + .color(Color::Muted), + ) + }) + .child( + Label::new(main_label) + .size(LabelSize::Small) + .color(Color::Muted), + ), ) .children( aux_label.map(|label| { @@ -738,98 +1172,114 @@ impl RemoteServerProjects { projects, configure, connection, - } => List::new() - .empty_message("No projects.") - .children(projects.iter().enumerate().map(|(pix, p)| { - v_flex().gap_0p5().child(self.render_ssh_project( - ix, - ssh_server.clone(), - pix, - p, - window, - cx, - )) - })) - .child( - h_flex() - .id(("new-remote-project-container", ix)) - .track_focus(&open_folder.focus_handle) - .anchor_scroll(open_folder.scroll_anchor.clone()) - .on_action(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.create_ssh_project(ix, ssh_connection.clone(), window, cx); - } - })) - .child( - ListItem::new(("new-remote-project", ix)) - .toggle_state( - open_folder.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _, window, cx| { - this.create_ssh_project( - ix, - ssh_connection.clone(), - window, - cx, - ); - } - })), - ), - ) - .child( - h_flex() - .id(("server-options-container", ix)) - .track_focus(&configure.focus_handle) - .anchor_scroll(configure.scroll_anchor.clone()) - .on_action(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.view_server_options( - (ix, ssh_connection.clone()), - window, - cx, - ); - } - })) - .child( - ListItem::new(("server-options", ix)) - .toggle_state( - configure.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Settings).color(Color::Muted)) - .child(Label::new("View Server Options")) - .on_click(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _, window, cx| { - this.view_server_options( - (ix, ssh_connection.clone()), - window, - cx, - ); - } - })), - ), - ), + index, + } => { + let index = *index; + List::new() + .empty_message("No projects.") + .children(projects.iter().enumerate().map(|(pix, p)| { + v_flex().gap_0p5().child(self.render_ssh_project( + index, + ssh_server.clone(), + pix, + p, + window, + cx, + )) + })) + .child( + h_flex() + .id(("new-remote-project-container", ix)) + .track_focus(&open_folder.focus_handle) + .anchor_scroll(open_folder.scroll_anchor.clone()) + .on_action(cx.listener({ + let connection = connection.clone(); + move |this, _: &menu::Confirm, window, cx| { + this.create_remote_project( + index, + connection.clone().into(), + window, + cx, + ); + } + })) + .child( + ListItem::new(("new-remote-project", ix)) + .toggle_state( + open_folder.focus_handle.contains_focused(window, cx), + ) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) + .child(Label::new("Open Folder")) + .on_click(cx.listener({ + let connection = connection.clone(); + move |this, _, window, cx| { + this.create_remote_project( + index, + connection.clone().into(), + window, + cx, + ); + } + })), + ), + ) + .child( + h_flex() + .id(("server-options-container", ix)) + .track_focus(&configure.focus_handle) + .anchor_scroll(configure.scroll_anchor.clone()) + .on_action(cx.listener({ + let connection = connection.clone(); + move |this, _: &menu::Confirm, window, cx| { + this.view_server_options( + (index, connection.clone().into()), + window, + cx, + ); + } + })) + .child( + ListItem::new(("server-options", ix)) + .toggle_state( + configure.focus_handle.contains_focused(window, cx), + ) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot( + Icon::new(IconName::Settings).color(Color::Muted), + ) + .child(Label::new("View Server Options")) + .on_click(cx.listener({ + let ssh_connection = connection.clone(); + move |this, _, window, cx| { + this.view_server_options( + (index, ssh_connection.clone().into()), + window, + cx, + ); + } + })), + ), + ) + } RemoteEntry::SshConfig { open_folder, host } => List::new().child( h_flex() .id(("new-remote-project-container", ix)) .track_focus(&open_folder.focus_handle) .anchor_scroll(open_folder.scroll_anchor.clone()) .on_action(cx.listener({ - let ssh_connection = connection.clone(); + let connection = connection.clone(); let host = host.clone(); move |this, _: &menu::Confirm, window, cx| { let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_ssh_project(new_ix, ssh_connection.clone(), window, cx); + this.create_remote_project( + new_ix.into(), + connection.clone().into(), + window, + cx, + ); } })) .child( @@ -840,13 +1290,12 @@ impl RemoteServerProjects { .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) .child(Label::new("Open Folder")) .on_click(cx.listener({ - let ssh_connection = connection; let host = host.clone(); move |this, _, window, cx| { let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_ssh_project( - new_ix, - ssh_connection.clone(), + this.create_remote_project( + new_ix.into(), + connection.clone().into(), window, cx, ); @@ -859,7 +1308,7 @@ impl RemoteServerProjects { fn render_ssh_project( &mut self, - server_ix: usize, + server_ix: ServerIndex, server: RemoteEntry, ix: usize, (navigation, project): &(NavigableEntry, SshProject), @@ -868,7 +1317,13 @@ impl RemoteServerProjects { ) -> impl IntoElement { let create_new_window = self.create_new_window; let is_from_zed = server.is_from_zed(); - let element_id_base = SharedString::from(format!("remote-project-{server_ix}")); + let element_id_base = SharedString::from(format!( + "remote-project-{}", + match server_ix { + ServerIndex::Ssh(index) => format!("ssh-{index}"), + ServerIndex::Wsl(index) => format!("wsl-{index}"), + } + )); let container_element_id_base = SharedString::from(format!("remote-project-container-{element_id_base}")); @@ -896,7 +1351,7 @@ impl RemoteServerProjects { cx.spawn_in(window, async move |_, cx| { let result = open_remote_project( - RemoteConnectionOptions::Ssh(server.into()), + server.into(), project.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions { @@ -953,25 +1408,31 @@ impl RemoteServerProjects { let secondary_confirm = e.modifiers().platform; callback(this, secondary_confirm, window, cx) })) - .when(is_from_zed, |server_list_item| { - server_list_item.end_hover_slot::(Some( - div() - .mr_2() - .child({ - let project = project.clone(); - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::Trash) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(Tooltip::text("Delete Remote Project")) - .on_click(cx.listener(move |this, _, _, cx| { - this.delete_ssh_project(server_ix, &project, cx) - })) - }) - .into_any_element(), - )) - }), + .when( + is_from_zed && matches!(server_ix, ServerIndex::Ssh(_)), + |server_list_item| { + let ServerIndex::Ssh(server_ix) = server_ix else { + unreachable!() + }; + server_list_item.end_hover_slot::(Some( + div() + .mr_2() + .child({ + let project = project.clone(); + // Right-margin to offset it from the Scrollbar + IconButton::new("remove-remote-project", IconName::Trash) + .icon_size(IconSize::Small) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(Tooltip::text("Delete Remote Project")) + .on_click(cx.listener(move |this, _, _, cx| { + this.delete_ssh_project(server_ix, &project, cx) + })) + }) + .into_any_element(), + )) + }, + ), ) } @@ -990,27 +1451,58 @@ impl RemoteServerProjects { update_settings_file::(fs, cx, move |setting, cx| f(setting, cx)); } - fn delete_ssh_server(&mut self, server: usize, cx: &mut Context) { + fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context) { self.update_settings_file(cx, move |setting, _| { if let Some(connections) = setting.ssh_connections.as_mut() { - connections.remove(server); + connections.remove(server.0); } }); } - fn delete_ssh_project(&mut self, server: usize, project: &SshProject, cx: &mut Context) { + fn delete_ssh_project( + &mut self, + server: SshServerIndex, + project: &SshProject, + cx: &mut Context, + ) { let project = project.clone(); self.update_settings_file(cx, move |setting, _| { if let Some(server) = setting .ssh_connections .as_mut() - .and_then(|connections| connections.get_mut(server)) + .and_then(|connections| connections.get_mut(server.0)) { server.projects.remove(&project); } }); } + #[cfg(target_os = "windows")] + fn add_wsl_distro( + &mut self, + connection_options: remote::WslConnectionOptions, + cx: &mut Context, + ) { + self.update_settings_file(cx, move |setting, _| { + setting + .wsl_connections + .get_or_insert(Default::default()) + .push(crate::remote_connections::WslConnection { + distro_name: SharedString::from(connection_options.distro_name), + user: connection_options.user, + projects: BTreeSet::new(), + }) + }); + } + + fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context) { + self.update_settings_file(cx, move |setting, _| { + if let Some(connections) = setting.wsl_connections.as_mut() { + connections.remove(server.0); + } + }); + } + fn add_ssh_server( &mut self, connection_options: remote::SshConnectionOptions, @@ -1108,222 +1600,94 @@ impl RemoteServerProjects { ) } + #[cfg(target_os = "windows")] + fn render_add_wsl_distro( + &self, + state: &AddWslDistro, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let connection_prompt = state.connection_prompt.clone(); + + state.picker.update(cx, |picker, cx| { + picker.focus_handle(cx).focus(window); + }); + + v_flex() + .id("add-wsl-distro") + .overflow_hidden() + .size_full() + .flex_1() + .map(|this| { + if let Some(connection_prompt) = connection_prompt { + this.child(connection_prompt) + } else { + this.child(state.picker.clone()) + } + }) + } + fn render_view_options( &mut self, - ViewServerOptionsState { - server_index, - connection, - entries, - }: ViewServerOptionsState, + options: ViewServerOptionsState, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let connection_string = connection.host.clone(); + let last_entry = options.entries().last().unwrap(); let mut view = Navigable::new( div() .track_focus(&self.focus_handle(cx)) .size_full() - .child( - SshConnectionHeader { - connection_string: connection_string.clone(), + .child(match &options { + ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader { + connection_string: connection.host.clone().into(), paths: Default::default(), nickname: connection.nickname.clone().map(|s| s.into()), } - .render(window, cx), - ) + .render(window, cx) + .into_any_element(), + ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader { + connection_string: connection.distro_name.clone().into(), + paths: Default::default(), + nickname: None, + } + .render(window, cx) + .into_any_element(), + }) .child( v_flex() .pb_1() .child(ListSeparator) - .child({ - let label = if connection.nickname.is_some() { - "Edit Nickname" - } else { - "Add Nickname to Server" - }; - div() - .id("ssh-options-add-nickname") - .track_focus(&entries[0].focus_handle) - .on_action(cx.listener( - move |this, _: &menu::Confirm, window, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new( - server_index, - window, - cx, - )); - cx.notify(); - }, - )) - .child( - ListItem::new("add-nickname") - .toggle_state( - entries[0].focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child(Label::new(label)) - .on_click(cx.listener(move |this, _, window, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new( - server_index, - window, - cx, - )); - cx.notify(); - })), - ) - }) - .child({ - let workspace = self.workspace.clone(); - fn callback( - workspace: WeakEntity, - connection_string: SharedString, - cx: &mut App, - ) { - cx.write_to_clipboard(ClipboardItem::new_string( - connection_string.to_string(), - )); - workspace - .update(cx, |this, cx| { - struct SshServerAddressCopiedToClipboard; - let notification = format!( - "Copied server address ({}) to clipboard", - connection_string - ); - - this.show_toast( - Toast::new( - NotificationId::composite::< - SshServerAddressCopiedToClipboard, - >( - connection_string.clone() - ), - notification, - ) - .autohide(), - cx, - ); - }) - .ok(); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[1].focus_handle) - .on_action({ - let connection_string = connection_string.clone(); - let workspace = self.workspace.clone(); - move |_: &menu::Confirm, _, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }) - .child( - ListItem::new("copy-server-address") - .toggle_state( - entries[1].focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) - .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()) - .color(Color::Muted), - ) - .on_click({ - let connection_string = connection_string.clone(); - move |_, _, cx| { - callback( - workspace.clone(), - connection_string.clone(), - cx, - ); - } - }), - ) - }) - .child({ - fn remove_ssh_server( - remote_servers: Entity, - index: usize, - connection_string: SharedString, - window: &mut Window, - cx: &mut App, - ) { - let prompt_message = - format!("Remove server `{}`?", connection_string); - - let confirmation = window.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - cx, - ); - - cx.spawn(async move |cx| { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(cx, |this, cx| { - this.delete_ssh_server(index, cx); - }) - .ok(); - remote_servers - .update(cx, |this, cx| { - this.mode = Mode::default_mode( - &this.ssh_config_servers, - cx, - ); - cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[2].focus_handle) - .on_action(cx.listener({ - let connection_string = connection_string.clone(); - move |_, _: &menu::Confirm, window, cx| { - remove_ssh_server( - cx.entity(), - server_index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - } - })) - .child( - ListItem::new("remove-server") - .toggle_state( - entries[2].focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Server").color(Color::Error)) - .on_click(cx.listener(move |_, _, window, cx| { - remove_ssh_server( - cx.entity(), - server_index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - })), - ) + .map(|this| match &options { + ViewServerOptionsState::Ssh { + connection, + entries, + server_index, + } => this.child(self.render_edit_ssh( + connection, + *server_index, + entries, + window, + cx, + )), + ViewServerOptionsState::Wsl { + connection, + entries, + server_index, + } => this.child(self.render_edit_wsl( + connection, + *server_index, + entries, + window, + cx, + )), }) .child(ListSeparator) .child({ div() .id("ssh-options-copy-server-address") - .track_focus(&entries[3].focus_handle) + .track_focus(&last_entry.focus_handle) .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { this.mode = Mode::default_mode(&this.ssh_config_servers, cx); cx.focus_self(window); @@ -1332,7 +1696,7 @@ impl RemoteServerProjects { .child( ListItem::new("go-back") .toggle_state( - entries[3].focus_handle.contains_focused(window, cx), + last_entry.focus_handle.contains_focused(window, cx), ) .inset(true) .spacing(ui::ListItemSpacing::Sparse) @@ -1351,13 +1715,253 @@ impl RemoteServerProjects { ) .into_any_element(), ); - for entry in entries { - view = view.entry(entry); + + for entry in options.entries() { + view = view.entry(entry.clone()); } view.render(window, cx).into_any_element() } + fn render_edit_wsl( + &self, + connection: &WslConnectionOptions, + index: WslServerIndex, + entries: &[NavigableEntry], + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let distro_name = SharedString::new(connection.distro_name.clone()); + + v_flex().child({ + fn remove_wsl_distro( + remote_servers: Entity, + index: WslServerIndex, + distro_name: SharedString, + window: &mut Window, + cx: &mut App, + ) { + let prompt_message = format!("Remove WSL distro `{}`?", distro_name); + + let confirmation = window.prompt( + PromptLevel::Warning, + &prompt_message, + None, + &["Yes, remove it", "No, keep it"], + cx, + ); + + cx.spawn(async move |cx| { + if confirmation.await.ok() == Some(0) { + remote_servers + .update(cx, |this, cx| { + this.delete_wsl_distro(index, cx); + }) + .ok(); + remote_servers + .update(cx, |this, cx| { + this.mode = Mode::default_mode(&this.ssh_config_servers, cx); + cx.notify(); + }) + .ok(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + div() + .id("wsl-options-remove-distro") + .track_focus(&entries[0].focus_handle) + .on_action(cx.listener({ + let distro_name = distro_name.clone(); + move |_, _: &menu::Confirm, window, cx| { + remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); + cx.focus_self(window); + } + })) + .child( + ListItem::new("remove-distro") + .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Trash).color(Color::Error)) + .child(Label::new("Remove Distro").color(Color::Error)) + .on_click(cx.listener(move |_, _, window, cx| { + remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); + cx.focus_self(window); + })), + ) + }) + } + + fn render_edit_ssh( + &self, + connection: &SshConnectionOptions, + index: SshServerIndex, + entries: &[NavigableEntry], + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let connection_string = SharedString::new(connection.host.clone()); + + v_flex() + .child({ + let label = if connection.nickname.is_some() { + "Edit Nickname" + } else { + "Add Nickname to Server" + }; + div() + .id("ssh-options-add-nickname") + .track_focus(&entries[0].focus_handle) + .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| { + this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx)); + cx.notify(); + })) + .child( + ListItem::new("add-nickname") + .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) + .child(Label::new(label)) + .on_click(cx.listener(move |this, _, window, cx| { + this.mode = + Mode::EditNickname(EditNicknameState::new(index, window, cx)); + cx.notify(); + })), + ) + }) + .child({ + let workspace = self.workspace.clone(); + fn callback( + workspace: WeakEntity, + connection_string: SharedString, + cx: &mut App, + ) { + cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string())); + workspace + .update(cx, |this, cx| { + struct SshServerAddressCopiedToClipboard; + let notification = format!( + "Copied server address ({}) to clipboard", + connection_string + ); + + this.show_toast( + Toast::new( + NotificationId::composite::( + connection_string.clone(), + ), + notification, + ) + .autohide(), + cx, + ); + }) + .ok(); + } + div() + .id("ssh-options-copy-server-address") + .track_focus(&entries[1].focus_handle) + .on_action({ + let connection_string = connection_string.clone(); + let workspace = self.workspace.clone(); + move |_: &menu::Confirm, _, cx| { + callback(workspace.clone(), connection_string.clone(), cx); + } + }) + .child( + ListItem::new("copy-server-address") + .toggle_state(entries[1].focus_handle.contains_focused(window, cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) + .child(Label::new("Copy Server Address")) + .end_hover_slot( + Label::new(connection_string.clone()).color(Color::Muted), + ) + .on_click({ + let connection_string = connection_string.clone(); + move |_, _, cx| { + callback(workspace.clone(), connection_string.clone(), cx); + } + }), + ) + }) + .child({ + fn remove_ssh_server( + remote_servers: Entity, + index: SshServerIndex, + connection_string: SharedString, + window: &mut Window, + cx: &mut App, + ) { + let prompt_message = format!("Remove server `{}`?", connection_string); + + let confirmation = window.prompt( + PromptLevel::Warning, + &prompt_message, + None, + &["Yes, remove it", "No, keep it"], + cx, + ); + + cx.spawn(async move |cx| { + if confirmation.await.ok() == Some(0) { + remote_servers + .update(cx, |this, cx| { + this.delete_ssh_server(index, cx); + }) + .ok(); + remote_servers + .update(cx, |this, cx| { + this.mode = Mode::default_mode(&this.ssh_config_servers, cx); + cx.notify(); + }) + .ok(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + div() + .id("ssh-options-copy-server-address") + .track_focus(&entries[2].focus_handle) + .on_action(cx.listener({ + let connection_string = connection_string.clone(); + move |_, _: &menu::Confirm, window, cx| { + remove_ssh_server( + cx.entity(), + index, + connection_string.clone(), + window, + cx, + ); + cx.focus_self(window); + } + })) + .child( + ListItem::new("remove-server") + .toggle_state(entries[2].focus_handle.contains_focused(window, cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Trash).color(Color::Error)) + .child(Label::new("Remove Server").color(Color::Error)) + .on_click(cx.listener(move |_, _, window, cx| { + remove_ssh_server( + cx.entity(), + index, + connection_string.clone(), + window, + cx, + ); + cx.focus_self(window); + })), + ) + }) + } + fn render_edit_nickname( &self, state: &EditNicknameState, @@ -1366,7 +1970,7 @@ impl RemoteServerProjects { ) -> impl IntoElement { let Some(connection) = SshSettings::get_global(cx) .ssh_connections() - .nth(state.index) + .nth(state.index.0) else { return v_flex() .id("ssh-edit-nickname") @@ -1405,20 +2009,43 @@ impl RemoteServerProjects { let ssh_settings = SshSettings::get_global(cx); let mut should_rebuild = false; - if ssh_settings - .ssh_connections - .as_ref() - .is_some_and(|connections| { - state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { connection, .. } => Some(connection), - RemoteEntry::SshConfig { .. } => None, - }) - .ne(connections.iter()) - }) - { + let ssh_connections_changed = + ssh_settings + .ssh_connections + .as_ref() + .is_some_and(|connections| { + state + .servers + .iter() + .filter_map(|server| match server { + RemoteEntry::Project { + connection: Connection::Ssh(connection), + .. + } => Some(connection), + _ => None, + }) + .ne(connections.iter()) + }); + + let wsl_connections_changed = + ssh_settings + .wsl_connections + .as_ref() + .is_some_and(|connections| { + state + .servers + .iter() + .filter_map(|server| match server { + RemoteEntry::Project { + connection: Connection::Wsl(connection), + .. + } => Some(connection), + _ => None, + }) + .ne(connections.iter()) + }); + + if ssh_connections_changed || wsl_connections_changed { should_rebuild = true; }; @@ -1433,7 +2060,11 @@ impl RemoteServerProjects { .collect(); let mut expected_ssh_hosts = self.ssh_config_servers.clone(); for server in &state.servers { - if let RemoteEntry::Project { connection, .. } = server { + if let RemoteEntry::Project { + connection: Connection::Ssh(connection), + .. + } = server + { expected_ssh_hosts.remove(&connection.host); } } @@ -1477,14 +2108,47 @@ impl RemoteServerProjects { cx.notify(); })); + #[cfg(target_os = "windows")] + let wsl_connect_button = div() + .id("wsl-connect-new-server") + .track_focus(&state.add_new_wsl.focus_handle) + .anchor_scroll(state.add_new_wsl.scroll_anchor.clone()) + .child( + ListItem::new("wsl-add-new-server") + .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) + .child(Label::new("Add WSL Distro")) + .on_click(cx.listener(|this, _, window, cx| { + let state = AddWslDistro::new(window, cx); + this.mode = Mode::AddWslDistro(state); + + cx.notify(); + })), + ) + .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { + let state = AddWslDistro::new(window, cx); + this.mode = Mode::AddWslDistro(state); + + cx.notify(); + })); + + let modal_section = v_flex() + .track_focus(&self.focus_handle(cx)) + .id("ssh-server-list") + .overflow_y_scroll() + .track_scroll(&state.scroll_handle) + .size_full() + .child(connect_button); + + #[cfg(target_os = "windows")] + let modal_section = modal_section.child(wsl_connect_button); + #[cfg(not(target_os = "windows"))] + let modal_section = modal_section; + let mut modal_section = Navigable::new( - v_flex() - .track_focus(&self.focus_handle(cx)) - .id("ssh-server-list") - .overflow_y_scroll() - .track_scroll(&state.scroll_handle) - .size_full() - .child(connect_button) + modal_section .child( List::new() .empty_message( @@ -1504,7 +2168,8 @@ impl RemoteServerProjects { ) .into_any_element(), ) - .entry(state.add_new_server.clone()); + .entry(state.add_new_server.clone()) + .entry(state.add_new_wsl.clone()); for server in &state.servers { match server { @@ -1587,7 +2252,7 @@ impl RemoteServerProjects { &mut self, ssh_config_host: &SharedString, cx: &mut Context<'_, Self>, - ) -> usize { + ) -> SshServerIndex { let new_ix = Arc::new(AtomicUsize::new(0)); let update_new_ix = new_ix.clone(); @@ -1609,7 +2274,7 @@ impl RemoteServerProjects { cx, ); self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - new_ix.load(atomic::Ordering::Acquire) + SshServerIndex(new_ix.load(atomic::Ordering::Acquire)) } } @@ -1719,6 +2384,10 @@ impl Render for RemoteServerProjects { Mode::EditNickname(state) => self .render_edit_nickname(state, window, cx) .into_any_element(), + #[cfg(target_os = "windows")] + Mode::AddWslDistro(state) => self + .render_add_wsl_distro(state, window, cx) + .into_any_element(), }) } } From 28800c2a3b5571348f67ffaabc5e9bcc0c17185b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 12:09:15 +0200 Subject: [PATCH 046/117] languages: Fix panic in python lsp adapters assuming settings shape (#38309) Fixes ZED-1EV Fixes ZED-S0 Fixes ZED-Q9 Release Notes: - N/A --- crates/languages/src/python.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d989f4d6dc22d2e58752d6d635b62091d8bd63d6..e0273250c91af8f437bff435d4a0f2f0c6467951 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -461,7 +461,7 @@ impl LspAdapter for PyrightLspAdapter { pet_core::python_environment::PythonEnvironment, >(toolchain.as_json.clone()) { - if user_settings.is_null() { + if !user_settings.is_object() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); @@ -492,9 +492,13 @@ impl LspAdapter for PyrightLspAdapter { // Get or create the python section let python = object .entry("python") - .or_insert(Value::Object(serde_json::Map::default())) - .as_object_mut() - .unwrap(); + .and_modify(|v| { + if !v.is_object() { + *v = Value::Object(serde_json::Map::default()); + } + }) + .or_insert(Value::Object(serde_json::Map::default())); + let python = python.as_object_mut().unwrap(); // Set both pythonPath and defaultInterpreterPath for compatibility python.insert( @@ -1465,7 +1469,7 @@ impl LspAdapter for PyLspAdapter { // If user did not explicitly modify their python venv, use one from picker. if let Some(toolchain) = toolchain { - if user_settings.is_null() { + if !user_settings.is_object() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); @@ -1787,7 +1791,7 @@ impl LspAdapter for BasedPyrightLspAdapter { pet_core::python_environment::PythonEnvironment, >(toolchain.as_json.clone()) { - if user_settings.is_null() { + if !user_settings.is_object() { user_settings = Value::Object(serde_json::Map::default()); } let object = user_settings.as_object_mut().unwrap(); From d74b8bcf4c1716b935e6f527908b6d2630d693ef Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 17 Sep 2025 15:46:53 +0530 Subject: [PATCH 047/117] docs: Fix macOS development docs typo (#38311) Release Notes: - N/A --- docs/src/development/macos.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index 851e2efdd7cdf15b9617445fe065149da8a5721f..f3adf2e44b06647f07d5b2069f70e9d23e2856b0 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -118,8 +118,8 @@ cargo run This error seems to be caused by OS resource constraints. Installing and running tests with `cargo-nextest` should resolve the issue. -- `cargo install cargo-nexttest --locked` -- `cargo nexttest run --workspace --no-fail-fast` +- `cargo install cargo-nextest --locked` +- `cargo nextest run --workspace --no-fail-fast` ## Tips & Tricks From 5ca3b998f34d66dbd08c9ec23479a731c33d57f3 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 12:28:05 +0200 Subject: [PATCH 048/117] fs: Do panic when failing to query `modified` timestamps (#38312) Fixes ZED-1EW Release Notes: - N/A --- crates/fs/src/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index bd7c94c1d71dd64b5c6caec6f2ffaa4517ac2db7..198299617619363fa9d486042d1b803c3ede6f88 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -693,7 +693,7 @@ impl Fs for RealFs { Ok(Some(Metadata { inode, - mtime: MTime(metadata.modified().unwrap()), + mtime: MTime(metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH)), len: metadata.len(), is_symlink, is_dir: metadata.file_type().is_dir(), From 399118f461840b102ee93eb03224f44602b9814f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 12:38:49 +0200 Subject: [PATCH 049/117] denoise: Fix LICENSE-GPL symlink (#38313) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/denoise/LICENSE-GPL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/denoise/LICENSE-GPL b/crates/denoise/LICENSE-GPL index e0f9dbd5d63fef1630c297edc4ceba4790be6f02..89e542f750cd3860a0598eff0dc34b56d7336dc4 120000 --- a/crates/denoise/LICENSE-GPL +++ b/crates/denoise/LICENSE-GPL @@ -1 +1 @@ -LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file From 574b943081c1645c0c0f2eed57f99d24e113d353 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 17 Sep 2025 12:47:09 +0200 Subject: [PATCH 050/117] Add wsl specific icon (#38316) Release Notes: - N/A --- assets/icons/linux.svg | 11 +++++++++ crates/icons/src/icons.rs | 1 + crates/recent_projects/src/recent_projects.rs | 11 +++++---- .../recent_projects/src/remote_connections.rs | 24 ++++++++++++++----- crates/recent_projects/src/remote_servers.rs | 16 +++++++++++-- crates/title_bar/src/title_bar.rs | 13 +++++----- 6 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 assets/icons/linux.svg diff --git a/assets/icons/linux.svg b/assets/icons/linux.svg new file mode 100644 index 0000000000000000000000000000000000000000..fc76742a3f236650cb8c514c8263ec2c3b2d4521 --- /dev/null +++ b/assets/icons/linux.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f3609f7ea8706f33eb07eaaf456731e14c85555a..0f05e58c27c48c37043fe90f64b4f03968b22752 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -263,6 +263,7 @@ pub enum IconName { ZedPredictError, ZedPredictUp, ZedXCopilot, + Linux, } impl IconName { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index aa0ce7661b29123c25fdf20cbde5f53e6525d2d6..2fc57a52fcb55f62b213cd7bb842009384b6ec91 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -417,10 +417,13 @@ impl PickerDelegate for RecentProjectsDelegate { SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) .color(Color::Muted) .into_any_element(), - SerializedWorkspaceLocation::Remote(_) => { - Icon::new(IconName::Server) - .color(Color::Muted) - .into_any_element() + SerializedWorkspaceLocation::Remote(options) => { + Icon::new(match options { + RemoteConnectionOptions::Ssh { .. } => IconName::Server, + RemoteConnectionOptions::Wsl { .. } => IconName::Linux, + }) + .color(Color::Muted) + .into_any_element() } }) }) diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 72e2844d501f8f8860d62964d22430af80bab4b6..8b47cbfc0f031f6f7013d9f105dd496223a67cec 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -192,6 +192,7 @@ impl Settings for SshSettings { pub struct RemoteConnectionPrompt { connection_string: SharedString, nickname: Option, + is_wsl: bool, status_message: Option, prompt: Option<(Entity, oneshot::Sender)>, cancellation: Option>, @@ -216,12 +217,14 @@ impl RemoteConnectionPrompt { pub(crate) fn new( connection_string: String, nickname: Option, + is_wsl: bool, window: &mut Window, cx: &mut Context, ) -> Self { Self { connection_string: connection_string.into(), nickname: nickname.map(|nickname| nickname.into()), + is_wsl, editor: cx.new(|cx| Editor::single_line(window, cx)), status_message: None, cancellation: None, @@ -350,15 +353,16 @@ impl RemoteConnectionModal { window: &mut Window, cx: &mut Context, ) -> Self { - let (connection_string, nickname) = match connection_options { + let (connection_string, nickname, is_wsl) = match connection_options { RemoteConnectionOptions::Ssh(options) => { - (options.connection_string(), options.nickname.clone()) + (options.connection_string(), options.nickname.clone(), false) } - RemoteConnectionOptions::Wsl(options) => (options.distro_name.clone(), None), + RemoteConnectionOptions::Wsl(options) => (options.distro_name.clone(), None, true), }; Self { - prompt: cx - .new(|cx| RemoteConnectionPrompt::new(connection_string, nickname, window, cx)), + prompt: cx.new(|cx| { + RemoteConnectionPrompt::new(connection_string, nickname, is_wsl, window, cx) + }), finished: false, paths, } @@ -389,6 +393,7 @@ pub(crate) struct SshConnectionHeader { pub(crate) connection_string: SharedString, pub(crate) paths: Vec, pub(crate) nickname: Option, + pub(crate) is_wsl: bool, } impl RenderOnce for SshConnectionHeader { @@ -404,6 +409,11 @@ impl RenderOnce for SshConnectionHeader { (self.connection_string, None) }; + let icon = match self.is_wsl { + true => IconName::Linux, + false => IconName::Server, + }; + h_flex() .px(DynamicSpacing::Base12.rems(cx)) .pt(DynamicSpacing::Base08.rems(cx)) @@ -411,7 +421,7 @@ impl RenderOnce for SshConnectionHeader { .rounded_t_sm() .w_full() .gap_1p5() - .child(Icon::new(IconName::Server).size(IconSize::Small)) + .child(Icon::new(icon).size(IconSize::Small)) .child( h_flex() .gap_1() @@ -443,6 +453,7 @@ impl Render for RemoteConnectionModal { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let nickname = self.prompt.read(cx).nickname.clone(); let connection_string = self.prompt.read(cx).connection_string.clone(); + let is_wsl = self.prompt.read(cx).is_wsl; let theme = cx.theme().clone(); let body_color = theme.colors().editor_background; @@ -461,6 +472,7 @@ impl Render for RemoteConnectionModal { paths: self.paths.clone(), connection_string, nickname, + is_wsl, } .render(window, cx), ) diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index d7e7505851a2b0cd2f86c807d6850937096dac7c..1ef9ab671f35dc477d1113e1b924c1272e13de2f 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -288,7 +288,7 @@ impl picker::PickerDelegate for WslPickerDelegate { h_flex() .flex_grow() .gap_3() - .child(Icon::new(IconName::Server)) + .child(Icon::new(IconName::Linux)) .child(v_flex().child(HighlightedLabel::new( matched.string.clone(), matched.positions.clone(), @@ -483,12 +483,14 @@ impl gpui::Render for ProjectPicker { connection_string: connection_string.clone(), paths: Default::default(), nickname: nickname.clone(), + is_wsl: false, } .render(window, cx), ProjectPickerData::Wsl { distro_name } => SshConnectionHeader { connection_string: distro_name.clone(), paths: Default::default(), nickname: None, + is_wsl: true, } .render(window, cx), }) @@ -799,6 +801,7 @@ impl RemoteServerProjects { RemoteConnectionPrompt::new( connection_options.connection_string(), connection_options.nickname.clone(), + false, window, cx, ) @@ -870,7 +873,13 @@ impl RemoteServerProjects { }; let prompt = cx.new(|cx| { - RemoteConnectionPrompt::new(connection_options.distro_name.clone(), None, window, cx) + RemoteConnectionPrompt::new( + connection_options.distro_name.clone(), + None, + true, + window, + cx, + ) }); let connection = connect( ConnectionIdentifier::setup(), @@ -1644,6 +1653,7 @@ impl RemoteServerProjects { connection_string: connection.host.clone().into(), paths: Default::default(), nickname: connection.nickname.clone().map(|s| s.into()), + is_wsl: false, } .render(window, cx) .into_any_element(), @@ -1651,6 +1661,7 @@ impl RemoteServerProjects { connection_string: connection.distro_name.clone().into(), paths: Default::default(), nickname: None, + is_wsl: true, } .render(window, cx) .into_any_element(), @@ -1988,6 +1999,7 @@ impl RemoteServerProjects { connection_string, paths: Default::default(), nickname, + is_wsl: false, } .render(window, cx), ) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 129b5645641a01ba22d6993621b92a17664f5c8a..9f00b0ffeffe6b9744ffa67a0f52795e31e5737f 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -349,10 +349,11 @@ impl TitleBar { let options = self.project.read(cx).remote_connection_options(cx)?; let host: SharedString = options.display_name().into(); - let nickname = if let RemoteConnectionOptions::Ssh(options) = options { - options.nickname.map(|nick| nick.into()) - } else { - None + let (nickname, icon) = match options { + RemoteConnectionOptions::Ssh(options) => { + (options.nickname.map(|nick| nick.into()), IconName::Server) + } + RemoteConnectionOptions::Wsl(_) => (None, IconName::Linux), }; let nickname = nickname.unwrap_or_else(|| host.clone()); @@ -390,9 +391,7 @@ impl TitleBar { .max_w_32() .child( IconWithIndicator::new( - Icon::new(IconName::Server) - .size(IconSize::Small) - .color(icon_color), + Icon::new(icon).size(IconSize::Small).color(icon_color), Some(Indicator::dot().color(indicator_color)), ) .indicator_border_color(Some(cx.theme().colors().title_bar_background)) From a5c29176a360dd410e68bc1f0810687838e86c65 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 14:02:39 +0200 Subject: [PATCH 051/117] editor: Fix incorrect offset passed to acp completion provider (#38321) Might fix | ZED-15G Release Notes: - N/A *or* Added/Fixed/Improved ... --- .../src/context_picker/completion_provider.rs | 19 ++++++++------- crates/editor/src/editor.rs | 23 ++++++++++++------- crates/text/src/text.rs | 2 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index c9cd69bf8e49b2e4f20148640cd029caea51264f..01a7a51316eee4709eaf9c17c8840e3cd637a62b 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -743,15 +743,15 @@ impl CompletionProvider for ContextPickerCompletionProvider { _window: &mut Window, cx: &mut Context, ) -> Task>> { - let state = buffer.update(cx, |buffer, _cx| { - let position = buffer_position.to_point(buffer); - let line_start = Point::new(position.row, 0); - let offset_to_line = buffer.point_to_offset(line_start); - let mut lines = buffer.text_for_range(line_start..position).lines(); - let line = lines.next()?; - MentionCompletion::try_parse(line, offset_to_line) - }); - let Some(state) = state else { + let snapshot = buffer.read(cx).snapshot(); + let position = buffer_position.to_point(&snapshot); + let line_start = Point::new(position.row, 0); + let offset_to_line = snapshot.point_to_offset(line_start); + let mut lines = snapshot.text_for_range(line_start..position).lines(); + let Some(line) = lines.next() else { + return Task::ready(Ok(Vec::new())); + }; + let Some(state) = MentionCompletion::try_parse(line, offset_to_line) else { return Task::ready(Ok(Vec::new())); }; @@ -761,7 +761,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { return Task::ready(Ok(Vec::new())); }; - let snapshot = buffer.read(cx).snapshot(); let source_range = snapshot.anchor_before(state.source_range.start) ..snapshot.anchor_after(state.source_range.end); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49a09385d521e2b79c01acc4b2ff2ad9db3be936..bdf1ae16474c647abd7ecb0593fb272b37ef9c54 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5473,16 +5473,18 @@ impl Editor { if position.diff_base_anchor.is_some() { return; } - let (buffer, buffer_position) = - if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { - output - } else { - return; - }; + let buffer_position = multibuffer_snapshot.anchor_before(position); + let Some(buffer) = buffer_position + .buffer_id + .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) + else { + return; + }; let buffer_snapshot = buffer.read(cx).snapshot(); let query: Option> = - Self::completion_query(&multibuffer_snapshot, position).map(|query| query.into()); + Self::completion_query(&multibuffer_snapshot, buffer_position) + .map(|query| query.into()); drop(multibuffer_snapshot); @@ -5568,6 +5570,11 @@ impl Editor { } }; + let Anchor { + excerpt_id: buffer_excerpt_id, + text_anchor: buffer_position, + .. + } = buffer_position; let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = buffer_snapshot.surrounding_word(buffer_position, false) { @@ -5623,7 +5630,7 @@ impl Editor { let (mut words, provider_responses) = match &provider { Some(provider) => { let provider_responses = provider.completions( - position.excerpt_id, + buffer_excerpt_id, &buffer, buffer_position, completion_context, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index db282e5c30de562441f5076157a8db4a269aea9d..590c30c8a73c13180e4d09dda1b3a071ef46ad7f 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -3078,7 +3078,7 @@ impl ToOffset for usize { fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { assert!( *self <= snapshot.len(), - "offset {} is out of range, max allowed is {}", + "offset {} is out of range, snapshot length is {}", self, snapshot.len() ); From 86834887da048b504373acc184135011aa3390b2 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 17 Sep 2025 18:13:57 +0530 Subject: [PATCH 052/117] editor: Fix completions menu flashes on every keystroke in TSX files with emmet (#38320) Closes https://github.com/zed-industries/zed/issues/37774 Bug in https://github.com/zed-industries/zed/pull/32927 Instead of using trigger characters to clear cached completions items, now we check if the query is empty to clear it. Turns out Emmet defines whole [alphanumeric as trigger characters](https://github.com/olrtg/emmet-language-server/blob/279be108725fb391c167690b697ce154fd32657b/index.ts#L116) which causes flickering. Clear on trigger characters was introduced to get rid of cached completions like in the case of "Parent.Foo.Bar", where "." is one of the trigger characters. This works still since "." is not part of `completion_query_characters` and hence we use it as a boundary while building the current query. i.e in this case, the query would be empty after typing ".", clearing cached completions. Release Notes: - Fixed issue where completions menu flashed on every keystroke in TSX files with emmet extension installed. --- crates/editor/src/editor.rs | 61 ++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bdf1ae16474c647abd7ecb0593fb272b37ef9c54..999e66efdcf7a39774cb29a6dba28473cd84fa81 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5488,6 +5488,18 @@ impl Editor { drop(multibuffer_snapshot); + // Hide the current completions menu when query is empty. Without this, cached + // completions from before the trigger char may be reused (#32774). + if query.is_none() { + let menu_is_open = matches!( + self.context_menu.borrow().as_ref(), + Some(CodeContextMenu::Completions(_)) + ); + if menu_is_open { + self.hide_context_menu(window, cx); + } + } + let mut ignore_word_threshold = false; let provider = match requested_source { Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(), @@ -5509,37 +5521,6 @@ impl Editor { .as_ref() .is_none_or(|provider| provider.filter_completions()); - let trigger_kind = match trigger { - Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { - CompletionTriggerKind::TRIGGER_CHARACTER - } - _ => CompletionTriggerKind::INVOKED, - }; - let completion_context = CompletionContext { - trigger_character: trigger.and_then(|trigger| { - if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { - Some(String::from(trigger)) - } else { - None - } - }), - trigger_kind, - }; - - // Hide the current completions menu when a trigger char is typed. Without this, cached - // completions from before the trigger char may be reused (#32774). Snippet choices could - // involve trigger chars, so this is skipped in that case. - if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER && self.snippet_stack.is_empty() - { - let menu_is_open = matches!( - self.context_menu.borrow().as_ref(), - Some(CodeContextMenu::Completions(_)) - ); - if menu_is_open { - self.hide_context_menu(window, cx); - } - } - if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { if filter_completions { menu.filter(query.clone(), provider.clone(), window, cx); @@ -5570,11 +5551,29 @@ impl Editor { } }; + let trigger_kind = match trigger { + Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { + CompletionTriggerKind::TRIGGER_CHARACTER + } + _ => CompletionTriggerKind::INVOKED, + }; + let completion_context = CompletionContext { + trigger_character: trigger.and_then(|trigger| { + if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { + Some(String::from(trigger)) + } else { + None + } + }), + trigger_kind, + }; + let Anchor { excerpt_id: buffer_excerpt_id, text_anchor: buffer_position, .. } = buffer_position; + let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = buffer_snapshot.surrounding_word(buffer_position, false) { From c54e294965df8e05b76773f04e879e68a0cc3687 Mon Sep 17 00:00:00 2001 From: itsaphel Date: Wed, 17 Sep 2025 14:08:29 +0100 Subject: [PATCH 053/117] Autosave files on close, when setting is `afterDelay` (#36929) Closes https://github.com/zed-industries/zed/issues/12149 Closes #35524 Release Notes: - Improved autosave behavior, to prevent a confirmation dialog when quickly closing files and using the `afterDelay` setting --------- Co-authored-by: MrSubidubi --- crates/search/src/project_search.rs | 8 +---- crates/workspace/src/pane.rs | 6 ++-- crates/workspace/src/workspace.rs | 36 ++++++++++++++++++++-- crates/workspace/src/workspace_settings.rs | 11 +++++++ docs/src/configuring-zed.md | 2 ++ 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index eaad5dad65b75c1fffcabb400eb6a2dea1fb1811..33ccd095687c448abc5d8b685da22e89ab59cbc8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1071,18 +1071,12 @@ impl ProjectSearchView { window: &mut Window, cx: &mut Context, ) -> Task> { - use workspace::AutosaveSetting; - let project = self.entity.read(cx).project.clone(); let can_autosave = self.results_editor.can_autosave(cx); let autosave_setting = self.results_editor.workspace_settings(cx).autosave; - let will_autosave = can_autosave - && matches!( - autosave_setting, - AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - ); + let will_autosave = can_autosave && autosave_setting.should_save_on_close(); let is_dirty = self.is_dirty(cx); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4ce8a890237f97db5596f55f607a603a2695ab7b..0418be7a2fbc858c8623400e65fed0f96e2cdb61 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2082,10 +2082,8 @@ impl Pane { } else if is_dirty && (can_save || can_save_as) { if save_intent == SaveIntent::Close { let will_autosave = cx.update(|_window, cx| { - matches!( - item.workspace_settings(cx).autosave, - AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - ) && item.can_autosave(cx) + item.can_autosave(cx) + && item.workspace_settings(cx).autosave.should_save_on_close() })?; if !will_autosave { let item_id = item.item_id(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 23d611964c3cd1b0284521c9444b6952748a5db9..5772695310e1258bee2a62953ad4bcc3620cd4ee 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8737,6 +8737,36 @@ mod tests { cx.executor().advance_clock(Duration::from_millis(250)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); + // Autosave after delay, should save earlier than delay if tab is closed + item.update(cx, |item, cx| { + item.is_dirty = true; + cx.emit(ItemEvent::Edit); + }); + cx.executor().advance_clock(Duration::from_millis(250)); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); + + // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out. + pane.update_in(cx, |pane, window, cx| { + pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) + }) + .await + .unwrap(); + assert!(!cx.has_pending_prompt()); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + + // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. + workspace.update_in(cx, |workspace, window, cx| { + workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); + }); + item.update_in(cx, |item, _window, cx| { + item.is_dirty = true; + for project_item in &mut item.project_items { + project_item.update(cx, |project_item, _| project_item.is_dirty = true); + } + }); + cx.run_until_parked(); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + // Autosave on focus change, ensuring closing the tab counts as such. item.update(cx, |item, cx| { SettingsStore::update_global(cx, |settings, cx| { @@ -8756,7 +8786,7 @@ mod tests { .await .unwrap(); assert!(!cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. workspace.update_in(cx, |workspace, window, cx| { @@ -8770,7 +8800,7 @@ mod tests { window.blur(); }); cx.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); // Ensure autosave is prevented for deleted files also when closing the buffer. let _close_items = pane.update_in(cx, |pane, window, cx| { @@ -8778,7 +8808,7 @@ mod tests { }); cx.run_until_parked(); assert!(cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); + item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); } #[gpui::test] diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 8868f3190575ac4b861e0619732890f477d83b69..a86f81f442b0437494d5cc2915a0a0b7d15ca7f3 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -254,6 +254,17 @@ pub enum AutosaveSetting { OnWindowChange, } +impl AutosaveSetting { + pub fn should_save_on_close(&self) -> bool { + matches!( + &self, + AutosaveSetting::OnFocusChange + | AutosaveSetting::OnWindowChange + | AutosaveSetting::AfterDelay { .. } + ) + } +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum PaneSplitDirectionHorizontal { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a7b89dc5207e0acea422401b0ce77946c7d484c6..6c25d62e291f91f3faf4dc77e5a6dab3b8637ca8 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -246,6 +246,8 @@ Define extensions which should be installed (`true`) or never installed (`false` } ``` +Note that a save will be triggered when an unsaved tab is closed, even if this is earlier than the configured inactivity period. + ## Autoscroll on Clicks - Description: Whether to scroll when clicking near the edge of the visible text area. From 405a8eaf78e2cac41b5b13aff6befcda7ef5bd02 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 15:47:00 +0200 Subject: [PATCH 054/117] editor: Fix `BlockMapWriter::blocks_intersecting_buffer_range` creating invalid indexing ranges (#38325) Fixes ZED-113 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/display_map/block_map.rs | 30 +++++++++------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 03d04e7010248293604d10c2f3e553430e74c9c6..2d16e6af8b469ff6e94b1b9fc7d11f7186e7b3c3 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1264,36 +1264,30 @@ impl BlockMapWriter<'_> { range: Range, inclusive: bool, ) -> &[Arc] { + if range.is_empty() && !inclusive { + return &[]; + } let wrap_snapshot = self.0.wrap_snapshot.borrow(); let buffer = wrap_snapshot.buffer_snapshot(); let start_block_ix = match self.0.custom_blocks.binary_search_by(|block| { let block_end = block.end().to_offset(buffer); - block_end.cmp(&range.start).then_with(|| { - if inclusive || (range.is_empty() && block.start().to_offset(buffer) == block_end) { - Ordering::Greater - } else { - Ordering::Less - } - }) + block_end.cmp(&range.start).then(Ordering::Greater) }) { Ok(ix) | Err(ix) => ix, }; - let end_block_ix = match self.0.custom_blocks.binary_search_by(|block| { - block - .start() - .to_offset(buffer) - .cmp(&range.end) - .then(if inclusive { - Ordering::Less - } else { - Ordering::Greater - }) + let end_block_ix = match self.0.custom_blocks[start_block_ix..].binary_search_by(|block| { + let block_start = block.start().to_offset(buffer); + block_start.cmp(&range.end).then(if inclusive { + Ordering::Less + } else { + Ordering::Greater + }) }) { Ok(ix) | Err(ix) => ix, }; - &self.0.custom_blocks[start_block_ix..end_block_ix] + &self.0.custom_blocks[start_block_ix..][..end_block_ix] } } From 0f6dd84c98bff0c486a6957091c8097dfa493fee Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 17 Sep 2025 10:45:25 -0400 Subject: [PATCH 055/117] Bump Zed to v0.206 (#38327) Release Notes: -N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee14efd2d800aeabb29a75959f9a929d06b78f81..cdf4109334af2e6f98a028970d124c7b9221371d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21105,7 +21105,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.205.0" +version = "0.206.0" dependencies = [ "acp_tools", "activity_indicator", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e53dedafd2f27699645662155f154f1220c6cd85..1c5dc3ddddb11279ae0d79717aadc18f51350675 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.205.0" +version = "0.206.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From f3b8c619e357c658e2ec42b054582eef0741d82a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 17 Sep 2025 17:10:05 +0200 Subject: [PATCH 056/117] editor: Fix `unwrap_syntax_node` panicking by not setting selections (#38329) Fixes ZED-11T Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/text_thread_editor.rs | 4 +- crates/editor/src/editor.rs | 90 +++++++++++-------- crates/editor/src/element.rs | 12 ++- crates/editor/src/items.rs | 8 +- crates/editor/src/jsx_tag_auto_close.rs | 2 +- crates/editor/src/lsp_ext.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 7 +- crates/editor/src/rust_analyzer_ext.rs | 6 +- crates/editor/src/selections_collection.rs | 42 +++++---- crates/editor/src/test/editor_test_context.rs | 2 +- crates/git_ui/src/text_diff_view.rs | 2 +- crates/vim/src/command.rs | 4 +- crates/vim/src/normal/mark.rs | 2 +- crates/vim/src/surrounds.rs | 2 +- crates/vim/src/vim.rs | 10 +-- crates/vim/src/visual.rs | 2 +- 16 files changed, 112 insertions(+), 85 deletions(-) diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index d979db5e0468b696d32ed755aec1ef47e2fd3df3..df904b2416cc9b66371a3557b04ff97246f25a41 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -477,7 +477,7 @@ impl TextThreadEditor { return; } - let selections = self.editor.read(cx).selections.disjoint_anchors(); + let selections = self.editor.read(cx).selections.disjoint_anchors_arc(); let mut commands_by_range = HashMap::default(); let workspace = self.workspace.clone(); self.context.update(cx, |context, cx| { @@ -1823,7 +1823,7 @@ impl TextThreadEditor { fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context) { self.context.update(cx, |context, cx| { - let selections = self.editor.read(cx).selections.disjoint_anchors(); + let selections = self.editor.read(cx).selections.disjoint_anchors_arc(); for selection in selections.as_ref() { let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); let range = selection diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 999e66efdcf7a39774cb29a6dba28473cd84fa81..8492ba60e7d302ab82509a3ece849ce59a6c2a2b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2414,14 +2414,10 @@ impl Editor { pub fn is_range_selected(&mut self, range: &Range, cx: &mut Context) -> bool { if self .selections - .pending - .as_ref() + .pending_anchor() .is_some_and(|pending_selection| { let snapshot = self.buffer().read(cx).snapshot(cx); - pending_selection - .selection - .range() - .includes(range, &snapshot) + pending_selection.range().includes(range, &snapshot) }) { return true; @@ -3054,7 +3050,7 @@ impl Editor { } } - let selection_anchors = self.selections.disjoint_anchors(); + let selection_anchors = self.selections.disjoint_anchors_arc(); if self.focus_handle.is_focused(window) && self.leader_id.is_none() { self.buffer.update(cx, |buffer, cx| { @@ -3170,7 +3166,7 @@ impl Editor { self.blink_manager.update(cx, BlinkManager::pause_blinking); cx.emit(EditorEvent::SelectionsChanged { local }); - let selections = &self.selections.disjoint; + let selections = &self.selections.disjoint_anchors_arc(); if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } @@ -3282,14 +3278,14 @@ impl Editor { other: Entity, cx: &mut Context, ) -> gpui::Subscription { - let other_selections = other.read(cx).selections.disjoint.to_vec(); + let other_selections = other.read(cx).selections.disjoint_anchors().to_vec(); self.selections.change_with(cx, |selections| { selections.select_anchors(other_selections); }); let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| { if let EditorEvent::SelectionsChanged { local: true } = other_evt { - let other_selections = other.read(cx).selections.disjoint.to_vec(); + let other_selections = other.read(cx).selections.disjoint_anchors().to_vec(); if other_selections.is_empty() { return; } @@ -3301,7 +3297,7 @@ impl Editor { let this_subscription = cx.subscribe_self::(move |this, this_evt, cx| { if let EditorEvent::SelectionsChanged { local: true } = this_evt { - let these_selections = this.selections.disjoint.to_vec(); + let these_selections = this.selections.disjoint_anchors().to_vec(); if these_selections.is_empty() { return; } @@ -3339,7 +3335,7 @@ impl Editor { effects, old_cursor_position: self.selections.newest_anchor().head(), history_entry: SelectionHistoryEntry { - selections: self.selections.disjoint_anchors(), + selections: self.selections.disjoint_anchors_arc(), select_next_state: self.select_next_state.clone(), select_prev_state: self.select_prev_state.clone(), add_selections_state: self.add_selections_state.clone(), @@ -3499,6 +3495,7 @@ impl Editor { let mut pending_selection = self .selections .pending_anchor() + .cloned() .expect("extend_selection not called with pending selection"); if position >= tail { pending_selection.start = tail_anchor; @@ -3520,7 +3517,7 @@ impl Editor { }; self.change_selections(effects, window, cx, |s| { - s.set_pending(pending_selection, pending_mode) + s.set_pending(pending_selection.clone(), pending_mode) }); } @@ -3595,7 +3592,7 @@ impl Editor { Some(selected_points[0].id) } else { let clicked_point_already_selected = - self.selections.disjoint.iter().find(|selection| { + self.selections.disjoint_anchors().iter().find(|selection| { selection.start.to_point(buffer) == start.to_point(buffer) || selection.end.to_point(buffer) == end.to_point(buffer) }); @@ -3700,7 +3697,7 @@ impl Editor { if self.columnar_selection_state.is_some() { self.select_columns(position, goal_column, &display_map, window, cx); - } else if let Some(mut pending) = self.selections.pending_anchor() { + } else if let Some(mut pending) = self.selections.pending_anchor().cloned() { let buffer = &display_map.buffer_snapshot; let head; let tail; @@ -3776,7 +3773,7 @@ impl Editor { } self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.set_pending(pending, mode); + s.set_pending(pending.clone(), mode); }); } else { log::error!("update_selection dispatched with no pending selection"); @@ -3885,7 +3882,8 @@ impl Editor { }; pending_nonempty_selection - || (self.columnar_selection_state.is_some() && self.selections.disjoint.len() > 1) + || (self.columnar_selection_state.is_some() + && self.selections.disjoint_anchors().len() > 1) } pub fn has_pending_selection(&self) -> bool { @@ -6065,7 +6063,7 @@ impl Editor { editor.refresh_edit_prediction(true, false, window, cx); }); - self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors_arc(), &snapshot); let show_new_completions_on_confirm = completion .confirm @@ -7472,7 +7470,7 @@ impl Editor { s.select_anchor_ranges([last_edit_end..last_edit_end]); }); - let selections = self.selections.disjoint_anchors(); + let selections = self.selections.disjoint_anchors_arc(); if let Some(transaction_id_now) = self.buffer.read(cx).last_transaction_id(cx) { let has_new_transaction = transaction_id_prev != Some(transaction_id_now); if has_new_transaction { @@ -7717,7 +7715,7 @@ impl Editor { let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else { return; }; - if self.selections.pending.is_none() { + if self.selections.pending_anchor().is_none() { return; } @@ -10517,7 +10515,7 @@ impl Editor { fn enable_wrap_selections_in_tag(&self, cx: &App) -> bool { let snapshot = self.buffer.read(cx).snapshot(cx); - for selection in self.selections.disjoint_anchors().iter() { + for selection in self.selections.disjoint_anchors_arc().iter() { if snapshot .language_at(selection.start) .and_then(|lang| lang.config().wrap_characters.as_ref()) @@ -10851,7 +10849,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let cursors = self .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|selection| { let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); @@ -14542,7 +14540,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); + let selections = self.selections.disjoint_anchors_arc(); match selections.first() { Some(first) if selections.len() >= 2 => { self.change_selections(Default::default(), window, cx, |s| { @@ -14566,7 +14564,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); + let selections = self.selections.disjoint_anchors_arc(); match selections.last() { Some(last) if selections.len() >= 2 => { self.change_selections(Default::default(), window, cx, |s| { @@ -15123,11 +15121,9 @@ impl Editor { let full_edits = selections .into_iter() .filter_map(|selection| { - // Only requires two branches once if-let-chains stabilize (#53667) - let child = if !selection.is_empty() { - selection.range() - } else if let Some((_, ancestor_range)) = - buffer.syntax_ancestor(selection.start..selection.end) + let child = if selection.is_empty() + && let Some((_, ancestor_range)) = + buffer.syntax_ancestor(selection.start..selection.end) { match ancestor_range { MultiOrSingleBufferOffsetRange::Single(range) => range, @@ -15155,6 +15151,9 @@ impl Editor { Some((selection.id, parent, text)) }) .collect::>(); + if full_edits.is_empty() { + return; + } self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |buffer, cx| { @@ -15663,7 +15662,7 @@ impl Editor { cx: &mut Context, ) { - let selections = self.selections.disjoint_anchors(); + let selections = self.selections.disjoint_anchors_arc(); let lines = if lines == 0 { EditorSettings::get_global(cx).expand_excerpt_lines @@ -17122,7 +17121,7 @@ impl Editor { .transaction(transaction_id_prev) .map(|t| t.0.clone()) }) - .unwrap_or_else(|| self.selections.disjoint_anchors()); + .unwrap_or_else(|| self.selections.disjoint_anchors_arc()); let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { @@ -17662,7 +17661,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) { self.selection_history - .insert_transaction(tx_id, self.selections.disjoint_anchors()); + .insert_transaction(tx_id, self.selections.disjoint_anchors_arc()); cx.emit(EditorEvent::TransactionBegun { transaction_id: tx_id, }); @@ -17684,7 +17683,7 @@ impl Editor { if let Some((_, end_selections)) = self.selection_history.transaction_mut(transaction_id) { - *end_selections = Some(self.selections.disjoint_anchors()); + *end_selections = Some(self.selections.disjoint_anchors_arc()); } else { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); } @@ -18354,7 +18353,12 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + let ranges: Vec<_> = self + .selections + .disjoint_anchors() + .iter() + .map(|s| s.range()) + .collect(); self.toggle_diff_hunks_in_ranges(ranges, cx); } @@ -18392,7 +18396,12 @@ impl Editor { cx: &mut Context, ) { let snapshot = self.buffer.read(cx).snapshot(cx); - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + let ranges: Vec<_> = self + .selections + .disjoint_anchors() + .iter() + .map(|s| s.range()) + .collect(); let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); self.stage_or_unstage_diff_hunks(stage, ranges, cx); } @@ -18556,7 +18565,12 @@ impl Editor { } pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + let ranges: Vec<_> = self + .selections + .disjoint_anchors() + .iter() + .map(|s| s.range()) + .collect(); self.buffer .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) } @@ -21276,7 +21290,7 @@ impl Editor { buffer.finalize_last_transaction(cx); if self.leader_id.is_none() { buffer.set_active_selections( - &self.selections.disjoint_anchors(), + &self.selections.disjoint_anchors_arc(), self.selections.line_mode, self.cursor_shape, cx, @@ -23572,7 +23586,7 @@ impl EntityInputHandler for Editor { let marked_ranges = { let snapshot = this.buffer.read(cx).read(cx); this.selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|selection| { selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 230c3160fb616984f71ce12b7b3b6b8d459d6356..2212507e38b2e577eed1cf140eea362425152623 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1060,7 +1060,7 @@ impl EditorElement { ); if mouse_down_time.elapsed() >= drag_and_drop_delay { let drop_cursor = Selection { - id: post_inc(&mut editor.selections.next_selection_id), + id: post_inc(&mut editor.selections.next_selection_id()), start: drop_anchor, end: drop_anchor, reversed: false, @@ -1547,9 +1547,13 @@ impl EditorElement { // Local cursors if !skip_local { let color = cx.theme().players().local().cursor; - editor.selections.disjoint.iter().for_each(|selection| { - add_cursor(selection.head(), color); - }); + editor + .selections + .disjoint_anchors() + .iter() + .for_each(|selection| { + add_cursor(selection.head(), color); + }); if let Some(ref selection) = editor.selections.pending_anchor() { add_cursor(selection.head(), color); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 253d0c27518107dc1cad3733cefbfef5bc12b807..bf21d6b461e6fdc082fdd1431f13b8daae730824 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -187,7 +187,7 @@ impl FollowableItem for Editor { } else if self.focus_handle.is_focused(window) { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( - &self.selections.disjoint_anchors(), + &self.selections.disjoint_anchors_arc(), self.selections.line_mode, self.cursor_shape, cx, @@ -231,7 +231,7 @@ impl FollowableItem for Editor { scroll_y: scroll_anchor.offset.y, selections: self .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|s| serialize_selection(s, &snapshot)) .collect(), @@ -310,7 +310,7 @@ impl FollowableItem for Editor { let snapshot = self.buffer.read(cx).snapshot(cx); update.selections = self .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|s| serialize_selection(s, &snapshot)) .collect(); @@ -1675,7 +1675,7 @@ impl SearchableItem for Editor { cx: &mut Context, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); - let current_index_position = if self.selections.disjoint_anchors().len() == 1 { + let current_index_position = if self.selections.disjoint_anchors_arc().len() == 1 { self.selections.newest_anchor().head() } else { matches[current_index].start diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index e6c518beae3ecf3741b5f74be6087628f5231c8c..c1c6ccf2f549042a3defc84d8628f7f614244d44 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -507,7 +507,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors()) + .read_with(cx, |this, _| this.selections.disjoint_anchors_arc()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 18ad2d71c835e5ec7e3bbd540de21f7e38425c39..0c4760f5684acf450b793a1deac54be983dcafd0 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -35,7 +35,7 @@ where let project = editor.project.clone()?; editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .filter_map(|selection| Some((selection.head(), selection.head().buffer_id?))) .unique_by(|(_, buffer_id)| *buffer_id) diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 3bc334c54c2f58e6dda2b404039369907c275422..78b12945afd1c2fcd359181afb030fc235c60a18 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -130,12 +130,9 @@ fn display_ranges<'a>( display_map: &'a DisplaySnapshot, selections: &'a SelectionsCollection, ) -> impl Iterator> + 'a { - let pending = selections - .pending - .as_ref() - .map(|pending| &pending.selection); + let pending = selections.pending_anchor(); selections - .disjoint + .disjoint_anchors() .iter() .chain(pending) .map(move |s| s.start.to_display_point(display_map)..s.end.to_display_point(display_map)) diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index f4059ca03d2ad70106aa958b4fe0c545cb4988ea..ffa0c017c0eb157df776cc49e0dba51e617e3379 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -319,7 +319,7 @@ fn cancel_flycheck_action( }; let buffer_id = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .find_map(|selection| { let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; @@ -344,7 +344,7 @@ fn run_flycheck_action( }; let buffer_id = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .find_map(|selection| { let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; @@ -369,7 +369,7 @@ fn clear_flycheck_action( }; let buffer_id = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .find_map(|selection| { let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 0a02390b641e1020aff8d9cf0167b44485baf489..e562be10e92344c1c892878ab674cba39beb74c2 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -28,13 +28,13 @@ pub struct PendingSelection { pub struct SelectionsCollection { display_map: Entity, buffer: Entity, - pub next_selection_id: usize, + next_selection_id: usize, pub line_mode: bool, /// The non-pending, non-overlapping selections. /// The [SelectionsCollection::pending] selection could possibly overlap these - pub disjoint: Arc<[Selection]>, + disjoint: Arc<[Selection]>, /// A pending selection, such as when the mouse is being dragged - pub pending: Option, + pending: Option, } impl SelectionsCollection { @@ -84,20 +84,27 @@ impl SelectionsCollection { /// The non-pending, non-overlapping selections. There could be a pending selection that /// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a /// pending selection. Returned as selections over Anchors. - pub fn disjoint_anchors(&self) -> Arc<[Selection]> { + pub fn disjoint_anchors_arc(&self) -> Arc<[Selection]> { self.disjoint.clone() } + /// The non-pending, non-overlapping selections. There could be a pending selection that + /// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a + /// pending selection. Returned as selections over Anchors. + pub fn disjoint_anchors(&self) -> &[Selection] { + &self.disjoint + } + pub fn disjoint_anchor_ranges(&self) -> impl Iterator> { // Mapping the Arc slice would borrow it, whereas indexing captures it. - let disjoint = self.disjoint_anchors(); + let disjoint = self.disjoint_anchors_arc(); (0..disjoint.len()).map(move |ix| disjoint[ix].range()) } /// Non-overlapping selections using anchors, including the pending selection. pub fn all_anchors(&self, cx: &mut App) -> Arc<[Selection]> { if self.pending.is_none() { - self.disjoint_anchors() + self.disjoint_anchors_arc() } else { let all_offset_selections = self.all::(cx); let buffer = self.buffer(cx); @@ -108,10 +115,12 @@ impl SelectionsCollection { } } - pub fn pending_anchor(&self) -> Option> { - self.pending - .as_ref() - .map(|pending| pending.selection.clone()) + pub fn pending_anchor(&self) -> Option<&Selection> { + self.pending.as_ref().map(|pending| &pending.selection) + } + + pub fn pending_anchor_mut(&mut self) -> Option<&mut Selection> { + self.pending.as_mut().map(|pending| &mut pending.selection) } pub fn pending>( @@ -120,7 +129,7 @@ impl SelectionsCollection { ) -> Option> { let map = self.display_map(cx); - resolve_selections(self.pending_anchor().as_ref(), &map).next() + resolve_selections(self.pending_anchor(), &map).next() } pub(crate) fn pending_mode(&self) -> Option { @@ -234,8 +243,7 @@ impl SelectionsCollection { let map = self.display_map(cx); let disjoint_anchors = &self.disjoint; let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable(); - let mut pending_opt = - resolve_selections_display(self.pending_anchor().as_ref(), &map).next(); + let mut pending_opt = resolve_selections_display(self.pending_anchor(), &map).next(); let selections = iter::from_fn(move || { if let Some(pending) = pending_opt.as_mut() { while let Some(next_selection) = disjoint.peek() { @@ -343,9 +351,9 @@ impl SelectionsCollection { #[cfg(any(test, feature = "test-support"))] pub fn display_ranges(&self, cx: &mut App) -> Vec> { let display_map = self.display_map(cx); - self.disjoint_anchors() + self.disjoint_anchors_arc() .iter() - .chain(self.pending_anchor().as_ref()) + .chain(self.pending_anchor()) .map(|s| { if s.reversed { s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) @@ -412,6 +420,10 @@ impl SelectionsCollection { ); (mutable_collection.selections_changed, result) } + + pub fn next_selection_id(&self) -> usize { + self.next_selection_id + } } pub struct MutableSelectionsCollection<'a> { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8c54c265edf7a19af9d17e982a5f4cb6a0079cc3..fbf7a312fe56600ad78e13c278c85e29b8ca5aa5 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -396,7 +396,7 @@ impl EditorTestContext { let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| { let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); - let selections = editor.selections.disjoint_anchors(); + let selections = editor.selections.disjoint_anchors_arc(); let excerpts = multibuffer_snapshot .excerpts() .map(|(e_id, snapshot, range)| (e_id, snapshot.clone(), range)) diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index ebf32d1b994814fa277201176b555efed5e85e66..bd46a067dc8e6c3aeec4de878709024f66a819f2 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -416,7 +416,7 @@ impl Item for TextDiffView { pub fn selection_location_text(editor: &Editor, cx: &App) -> Option { let buffer = editor.buffer().read(cx); let buffer_snapshot = buffer.snapshot(cx); - let first_selection = editor.selections.disjoint.first()?; + let first_selection = editor.selections.disjoint_anchors().first()?; let selection_start = first_selection.start.to_point(&buffer_snapshot); let selection_end = first_selection.end.to_point(&buffer_snapshot); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 5fee0b95f11d94e8a448a8a11a43cc158786d190..53855c2c929ed44085b27bd22f80eba21e2e831d 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -463,7 +463,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { .collect(); vim.switch_mode(Mode::Normal, true, window, cx); let initial_selections = - vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors()); + vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc()); if let Some(range) = &action.range { let result = vim.update_editor(cx, |vim, editor, cx| { let range = range.buffer_range(vim, editor, window, cx)?; @@ -515,7 +515,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { .buffer() .update(cx, |multi, cx| multi.last_transaction_id(cx)) { - let last_sel = editor.selections.disjoint_anchors(); + let last_sel = editor.selections.disjoint_anchors_arc(); editor.modify_transaction_selection_history(tx_id, |old| { old.0 = first_sel; old.1 = Some(last_sel); diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 619769d41adc690014a2872eff9a18d6f0250ae6..acc4ef8d3c311892e864589fb998ffced7e47867 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -22,7 +22,7 @@ impl Vim { self.update_editor(cx, |vim, editor, cx| { let anchors = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|s| s.head()) .collect::>(); diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 7c36ebe6747488376d2264e4984175fb536fed4f..8b3359c8f08046cf995db077a9a5ff0d36a97b95 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -326,7 +326,7 @@ impl Vim { let stable_anchors = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|selection| { let start = selection.start.bias_left(&display_map.buffer_snapshot); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 7faff54e73b179bdfa944798a9c87fafa245f732..5fb1f90a08e7a4346619c089cfe93c826bedf156 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1075,16 +1075,16 @@ impl Vim { } let snapshot = s.display_map(); - if let Some(pending) = s.pending.as_mut() - && pending.selection.reversed + if let Some(pending) = s.pending_anchor_mut() + && pending.reversed && mode.is_visual() && !last_mode.is_visual() { - let mut end = pending.selection.end.to_point(&snapshot.buffer_snapshot); + let mut end = pending.end.to_point(&snapshot.buffer_snapshot); end = snapshot .buffer_snapshot .clip_point(end + Point::new(0, 1), Bias::Right); - pending.selection.end = snapshot.buffer_snapshot.anchor_before(end); + pending.end = snapshot.buffer_snapshot.anchor_before(end); } s.move_with(|map, selection| { @@ -1332,7 +1332,7 @@ impl Vim { self.update_editor(cx, |_, editor, _| { editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|selection| selection.tail()..selection.head()) .collect() diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 5fbc04fbee9570db95cc95a4ce023e8e82c3183c..35bc1eba2c900cd7c8f370629e0585584bc92d59 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -748,7 +748,7 @@ impl Vim { // after the change let stable_anchors = editor .selections - .disjoint_anchors() + .disjoint_anchors_arc() .iter() .map(|selection| { let start = selection.start.bias_left(&display_map.buffer_snapshot); From 4a7784cf67fc677360bfa08e14841416a5131202 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 17 Sep 2025 17:39:47 +0200 Subject: [PATCH 057/117] Allow opening a local folder inside WSL (#38335) This PR adds an option to allow opening local folders inside WSL containers. (wsl_actions::OpenFolderInWsl). It is accessible via the command palette and should be available to keybind. - [x] Open wsl from open remote - [x] Open local folder in wsl action - [ ] Open wsl shortcut (shortcuts to open remote) Release Notes: - N/A --- Cargo.lock | 1 + crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/recent_projects.rs | 59 ++++ crates/recent_projects/src/remote_servers.rs | 184 +---------- crates/recent_projects/src/wsl_picker.rs | 295 ++++++++++++++++++ crates/util/src/paths.rs | 21 ++ crates/zed_actions/src/lib.rs | 16 + 7 files changed, 397 insertions(+), 180 deletions(-) create mode 100644 crates/recent_projects/src/wsl_picker.rs diff --git a/Cargo.lock b/Cargo.lock index cdf4109334af2e6f98a028970d124c7b9221371d..298ef0a4b260d57166f46674817f7717a25c185c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13770,6 +13770,7 @@ dependencies = [ "futures 0.3.31", "fuzzy", "gpui", + "indoc", "language", "log", "markdown", diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 91879c5d4175ad66428a255655f3c8bd4a5059e3..2ba6293ad2cf63c7ca664dba43f53d7facc70a57 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -43,6 +43,7 @@ util.workspace = true workspace.workspace = true zed_actions.workspace = true workspace-hack.workspace = true +indoc.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.0" diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 2fc57a52fcb55f62b213cd7bb842009384b6ec91..35ef024743475fc2036600724224f9f3c06bca4a 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -3,6 +3,9 @@ mod remote_connections; mod remote_servers; mod ssh_config; +#[cfg(target_os = "windows")] +mod wsl_picker; + use remote::RemoteConnectionOptions; pub use remote_connections::open_remote_project; @@ -31,6 +34,62 @@ use zed_actions::{OpenRecent, OpenRemote}; pub fn init(cx: &mut App) { SshSettings::register(cx); + + #[cfg(target_os = "windows")] + cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenFolderInWsl, cx| { + let create_new_window = open_wsl.create_new_window; + with_active_or_new_workspace(cx, move |workspace, window, cx| { + use gpui::PathPromptOptions; + use project::DirectoryLister; + + let paths = workspace.prompt_for_open_path( + PathPromptOptions { + files: true, + directories: true, + multiple: false, + prompt: None, + }, + DirectoryLister::Local( + workspace.project().clone(), + workspace.app_state().fs.clone(), + ), + window, + cx, + ); + + cx.spawn_in(window, async move |workspace, cx| { + use util::paths::SanitizedPath; + + let Some(paths) = paths.await.log_err().flatten() else { + return; + }; + + let paths = paths + .into_iter() + .filter_map(|path| SanitizedPath::new(&path).local_to_wsl()) + .collect::>(); + + if paths.is_empty() { + let message = indoc::indoc! { r#" + Invalid path specified when trying to open a folder inside WSL. + + Please note that Zed currently does not support opening network share folders inside wsl. + "#}; + + let _ = cx.prompt(gpui::PromptLevel::Critical, "Invalid path", Some(&message), &["Ok"]).await; + return; + } + + workspace.update_in(cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + crate::wsl_picker::WslOpenModal::new(paths, create_new_window, window, cx) + }); + }).log_err(); + }) + .detach(); + }); + }); + cx.on_action(|open_recent: &OpenRecent, cx| { let create_new_window = open_recent.create_new_window; with_active_or_new_workspace(cx, move |workspace, window, cx| { diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 1ef9ab671f35dc477d1113e1b924c1272e13de2f..f7b9001444dea0371009a2ca878d12ab808a8823 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -83,7 +83,7 @@ impl CreateRemoteServer { #[cfg(target_os = "windows")] struct AddWslDistro { - picker: Entity>, + picker: Entity>, connection_prompt: Option>, _creating: Option>, } @@ -91,6 +91,8 @@ struct AddWslDistro { #[cfg(target_os = "windows")] impl AddWslDistro { fn new(window: &mut Window, cx: &mut Context) -> Self { + use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed}; + let delegate = WslPickerDelegate::new(); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); @@ -120,184 +122,6 @@ impl AddWslDistro { } } -#[cfg(target_os = "windows")] -#[derive(Clone, Debug)] -pub struct WslDistroSelected(pub String); - -#[cfg(target_os = "windows")] -#[derive(Clone, Debug)] -pub struct WslPickerDismissed; - -#[cfg(target_os = "windows")] -struct WslPickerDelegate { - selected_index: usize, - distro_list: Option>, - matches: Vec, -} - -#[cfg(target_os = "windows")] -impl WslPickerDelegate { - fn new() -> Self { - WslPickerDelegate { - selected_index: 0, - distro_list: None, - matches: Vec::new(), - } - } - - pub fn selected_distro(&self) -> Option { - self.matches - .get(self.selected_index) - .map(|m| m.string.clone()) - } -} - -#[cfg(target_os = "windows")] -impl WslPickerDelegate { - fn fetch_distros() -> anyhow::Result> { - use anyhow::Context; - use windows_registry::CURRENT_USER; - - let lxss_key = CURRENT_USER - .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss") - .context("failed to get lxss wsl key")?; - - let distros = lxss_key - .keys() - .context("failed to get wsl distros")? - .filter_map(|key| { - lxss_key - .open(&key) - .context("failed to open subkey for distro") - .log_err() - }) - .filter_map(|distro| distro.get_string("DistributionName").ok()) - .collect::>(); - - Ok(distros) - } -} - -#[cfg(target_os = "windows")] -impl EventEmitter for Picker {} - -#[cfg(target_os = "windows")] -impl EventEmitter for Picker {} - -#[cfg(target_os = "windows")] -impl picker::PickerDelegate for WslPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::from("Enter WSL distro name") - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - use fuzzy::StringMatchCandidate; - - let needs_fetch = self.distro_list.is_none(); - if needs_fetch { - let distros = Self::fetch_distros().log_err(); - self.distro_list = distros; - } - - if let Some(distro_list) = &self.distro_list { - use ordered_float::OrderedFloat; - - let candidates = distro_list - .iter() - .enumerate() - .map(|(id, distro)| StringMatchCandidate::new(id, distro)) - .collect::>(); - - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by_key(|m| m.candidate_id); - - self.selected_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(index, _)| index) - .unwrap_or(0); - } - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) { - if let Some(distro) = self.matches.get(self.selected_index) { - cx.emit(WslDistroSelected(distro.string.clone())); - } - } - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - cx.emit(WslPickerDismissed); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - _: &mut Context>, - ) -> Option { - use ui::HighlightedLabel; - - let matched = self.matches.get(ix)?; - Some( - ListItem::new(ix) - .toggle_state(selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .child( - h_flex() - .flex_grow() - .gap_3() - .child(Icon::new(IconName::Linux)) - .child(v_flex().child(HighlightedLabel::new( - matched.string.clone(), - matched.positions.clone(), - ))), - ), - ) - } -} - enum ProjectPickerData { Ssh { connection_string: SharedString, @@ -862,7 +686,7 @@ impl RemoteServerProjects { #[cfg(target_os = "windows")] fn connect_wsl_distro( &mut self, - picker: Entity>, + picker: Entity>, distro: String, window: &mut Window, cx: &mut Context, diff --git a/crates/recent_projects/src/wsl_picker.rs b/crates/recent_projects/src/wsl_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..e386b723fa43777e496565c11b8308f16031d837 --- /dev/null +++ b/crates/recent_projects/src/wsl_picker.rs @@ -0,0 +1,295 @@ +use std::{path::PathBuf, sync::Arc}; + +use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Subscription, Task}; +use picker::Picker; +use remote::{RemoteConnectionOptions, WslConnectionOptions}; +use ui::{ + App, Context, HighlightedLabel, Icon, IconName, InteractiveElement, ListItem, ParentElement, + Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex, +}; +use util::ResultExt as _; +use workspace::{ModalView, Workspace}; + +use crate::open_remote_project; + +#[derive(Clone, Debug)] +pub struct WslDistroSelected { + pub secondary: bool, + pub distro: String, +} + +#[derive(Clone, Debug)] +pub struct WslPickerDismissed; + +pub(crate) struct WslPickerDelegate { + selected_index: usize, + distro_list: Option>, + matches: Vec, +} + +impl WslPickerDelegate { + pub fn new() -> Self { + WslPickerDelegate { + selected_index: 0, + distro_list: None, + matches: Vec::new(), + } + } + + pub fn selected_distro(&self) -> Option { + self.matches + .get(self.selected_index) + .map(|m| m.string.clone()) + } +} + +impl WslPickerDelegate { + fn fetch_distros() -> anyhow::Result> { + use anyhow::Context; + use windows_registry::CURRENT_USER; + + let lxss_key = CURRENT_USER + .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss") + .context("failed to get lxss wsl key")?; + + let distros = lxss_key + .keys() + .context("failed to get wsl distros")? + .filter_map(|key| { + lxss_key + .open(&key) + .context("failed to open subkey for distro") + .log_err() + }) + .filter_map(|distro| distro.get_string("DistributionName").ok()) + .collect::>(); + + Ok(distros) + } +} + +impl EventEmitter for Picker {} + +impl EventEmitter for Picker {} + +impl picker::PickerDelegate for WslPickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + cx: &mut Context>, + ) { + self.selected_index = ix; + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + Arc::from("Enter WSL distro name") + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + use fuzzy::StringMatchCandidate; + + let needs_fetch = self.distro_list.is_none(); + if needs_fetch { + let distros = Self::fetch_distros().log_err(); + self.distro_list = distros; + } + + if let Some(distro_list) = &self.distro_list { + use ordered_float::OrderedFloat; + + let candidates = distro_list + .iter() + .enumerate() + .map(|(id, distro)| StringMatchCandidate::new(id, distro)) + .collect::>(); + + let query = query.trim_start(); + let smart_case = query.chars().any(|c| c.is_uppercase()); + self.matches = smol::block_on(fuzzy::match_strings( + candidates.as_slice(), + query, + smart_case, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + )); + self.matches.sort_unstable_by_key(|m| m.candidate_id); + + self.selected_index = self + .matches + .iter() + .enumerate() + .rev() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(index, _)| index) + .unwrap_or(0); + } + + Task::ready(()) + } + + fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context>) { + if let Some(distro) = self.matches.get(self.selected_index) { + cx.emit(WslDistroSelected { + secondary, + distro: distro.string.clone(), + }); + } + } + + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + cx.emit(WslPickerDismissed); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + let matched = self.matches.get(ix)?; + Some( + ListItem::new(ix) + .toggle_state(selected) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .child( + h_flex() + .flex_grow() + .gap_3() + .child(Icon::new(IconName::Linux)) + .child(v_flex().child(HighlightedLabel::new( + matched.string.clone(), + matched.positions.clone(), + ))), + ), + ) + } +} + +pub(crate) struct WslOpenModal { + paths: Vec, + create_new_window: bool, + picker: Entity>, + _subscriptions: [Subscription; 2], +} + +impl WslOpenModal { + pub fn new( + paths: Vec, + create_new_window: bool, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let delegate = WslPickerDelegate::new(); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); + + let selected = cx.subscribe_in( + &picker, + window, + |this, _, event: &WslDistroSelected, window, cx| { + this.confirm(&event.distro, event.secondary, window, cx); + }, + ); + + let dismissed = cx.subscribe_in( + &picker, + window, + |this, _, _: &WslPickerDismissed, window, cx| { + this.cancel(&menu::Cancel, window, cx); + }, + ); + + WslOpenModal { + paths, + create_new_window, + picker, + _subscriptions: [selected, dismissed], + } + } + + fn confirm( + &mut self, + distro: &str, + secondary: bool, + window: &mut Window, + cx: &mut Context, + ) { + let app_state = workspace::AppState::global(cx); + let Some(app_state) = app_state.upgrade() else { + return; + }; + + let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions { + distro_name: distro.to_string(), + user: None, + }); + + let replace_current_window = match self.create_new_window { + true => secondary, + false => !secondary, + }; + let replace_window = match replace_current_window { + true => window.window_handle().downcast::(), + false => None, + }; + + let paths = self.paths.clone(); + let open_options = workspace::OpenOptions { + replace_window, + ..Default::default() + }; + + cx.emit(DismissEvent); + cx.spawn_in(window, async move |_, cx| { + open_remote_project(connection_options, paths, app_state, open_options, cx).await + }) + .detach(); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } +} + +impl ModalView for WslOpenModal {} + +impl Focusable for WslOpenModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for WslOpenModal {} + +impl Render for WslOpenModal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + div() + .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))) + .on_action(cx.listener(Self::cancel)) + .elevation_3(cx) + .w(rems(34.)) + .flex_1() + .overflow_hidden() + .child(self.picker.clone()) + } +} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 1658052e6f4894b54c83fecf29e729959c9cfe6e..72753b026e2194e0b083acb1f9d6d69864286c6b 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -65,6 +65,7 @@ pub trait PathExt { .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}")) } } + fn local_to_wsl(&self) -> Option; } impl> PathExt for T { @@ -118,6 +119,26 @@ impl> PathExt for T { self.as_ref().to_string_lossy().to_string() } } + + /// Converts a local path to one that can be used inside of WSL. + /// Returns `None` if the path cannot be converted into a WSL one (network share). + fn local_to_wsl(&self) -> Option { + let mut new_path = PathBuf::new(); + for component in self.as_ref().components() { + match component { + std::path::Component::Prefix(prefix) => { + let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase(); + let drive_letter = drive_letter.strip_suffix(':')?; + + new_path.push(format!("/mnt/{}", drive_letter)); + } + std::path::Component::RootDir => {} + _ => new_path.push(component), + } + } + + Some(new_path) + } } /// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index fd979b3648b9a84aa89039386f8ac300e28d4771..2015f6db3b6c5754fe6b7e433866f05f43de440f 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -497,3 +497,19 @@ actions!( OpenProjectDebugTasks, ] ); + +#[cfg(target_os = "windows")] +pub mod wsl_actions { + use gpui::Action; + use schemars::JsonSchema; + use serde::Deserialize; + + /// Opens a folder inside Wsl. + #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] + #[action(namespace = projects)] + #[serde(deny_unknown_fields)] + pub struct OpenFolderInWsl { + #[serde(default)] + pub create_new_window: bool, + } +} From 52521efc7b898ad0431810ecb89660129bf11dfe Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 17 Sep 2025 17:41:46 +0200 Subject: [PATCH 058/117] acp: update to v0.4 of Rust library (#38336) Release Notes: - N/A --- Cargo.lock | 14 ++++++++------ Cargo.toml | 2 +- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/acp.rs | 15 ++++----------- tooling/workspace-hack/Cargo.toml | 8 ++------ 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 298ef0a4b260d57166f46674817f7717a25c185c..6de762f43b31405d47c4ddcfc26a2be03eaadb80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,12 +195,13 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.2.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003fb91bf1b8d6e15f72c45fb9171839af8241e81e3839fbb73536af113b7a79" +checksum = "cc2526e80463b9742afed4829aedd6ae5632d6db778c6cc1fecb80c960c3521b" dependencies = [ "anyhow", "async-broadcast", + "async-trait", "futures 0.3.31", "log", "parking_lot", @@ -293,6 +294,7 @@ dependencies = [ "agent-client-protocol", "agent_settings", "anyhow", + "async-trait", "client", "collections", "env_logger 0.11.8", @@ -2190,7 +2192,7 @@ dependencies = [ "bitflags 2.9.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -13225,7 +13227,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes 1.10.1", "heck 0.5.0", - "itertools 0.11.0", + "itertools 0.12.1", "log", "multimap", "once_cell", @@ -13258,7 +13260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.101", @@ -20622,7 +20624,7 @@ dependencies = [ "idna", "indexmap", "inout", - "itertools 0.11.0", + "itertools 0.12.1", "itertools 0.13.0", "jiff", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index e69885a835f5f579ac5ac5fa0063f5291a1d01f5..96a1fc588f03162ba10ed60c52353a1a710b3783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -437,7 +437,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = { version = "0.2.1", features = ["unstable"] } +agent-client-protocol = { version = "0.4.0", features = ["unstable"] } aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index a168da05c83482f9d5b34118a74ee5e1f15e2e37..ca6db6c663ddb2132c05d716e5b935c5855bccdb 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -23,6 +23,7 @@ action_log.workspace = true agent-client-protocol.workspace = true agent_settings.workspace = true anyhow.workspace = true +async-trait.workspace = true client.workspace = true collections.workspace = true env_logger = { workspace = true, optional = true } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index cc897d85e7b4de149a0dca84df84d2b8c2c5bc98..b8c75a01a2e2965c255e32bd3c0746b26d78ecab 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -13,7 +13,7 @@ use util::ResultExt as _; use std::path::PathBuf; use std::{any::Any, cell::RefCell}; -use std::{path::Path, rc::Rc, sync::Arc}; +use std::{path::Path, rc::Rc}; use thiserror::Error; use anyhow::{Context as _, Result}; @@ -505,6 +505,7 @@ struct ClientDelegate { cx: AsyncApp, } +#[async_trait::async_trait(?Send)] impl acp::Client for ClientDelegate { async fn request_permission( &self, @@ -638,19 +639,11 @@ impl acp::Client for ClientDelegate { Ok(Default::default()) } - async fn ext_method( - &self, - _name: Arc, - _params: Arc, - ) -> Result, acp::Error> { + async fn ext_method(&self, _args: acp::ExtRequest) -> Result { Err(acp::Error::method_not_found()) } - async fn ext_notification( - &self, - _name: Arc, - _params: Arc, - ) -> Result<(), acp::Error> { + async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> { Err(acp::Error::method_not_found()) } diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index a01989a047c6072db1719ccef6278f80b323123b..ec9629685d8366864b92a6160ece623450f72b0c 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -75,6 +75,7 @@ hmac = { version = "0.12", default-features = false, features = ["reset"] } hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] } idna = { version = "1" } indexmap = { version = "2", features = ["serde"] } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } jiff = { version = "0.2" } lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2", features = ["extra_traits"] } @@ -213,6 +214,7 @@ hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", " idna = { version = "1" } indexmap = { version = "2", features = ["serde"] } itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } +itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } jiff = { version = "0.2" } lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2", features = ["extra_traits"] } @@ -333,7 +335,6 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } @@ -395,7 +396,6 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } naga = { version = "25", features = ["msl-out", "wgsl-in"] } nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } @@ -474,7 +474,6 @@ getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } inout = { version = "0.1", default-features = false, features = ["block-padding"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } @@ -555,7 +554,6 @@ getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } inout = { version = "0.1", default-features = false, features = ["block-padding"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } @@ -614,7 +612,6 @@ foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } num = { version = "0.4" } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } @@ -690,7 +687,6 @@ getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-f gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } inout = { version = "0.1", default-features = false, features = ["block-padding"] } -itertools-a6292c17cd707f01 = { package = "itertools", version = "0.11" } linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } From e673c3657a2a98286a0f0e8c69e65375080123a4 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 17 Sep 2025 10:44:12 -0500 Subject: [PATCH 059/117] panels --- crates/collab_ui/src/collab_panel.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 4 +- crates/collab_ui/src/panel_settings.rs | 128 +++++++++--------- .../file_finder/src/file_finder_settings.rs | 31 ----- crates/settings/src/settings_content.rs | 81 +++++++++++ crates/zeta/src/onboarding_modal.rs | 6 +- 6 files changed, 154 insertions(+), 98 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 1698fcb89376787bef52c0ee69b5d799976eda3b..070ab13b271882c4e771e71285843371a9cfb77d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2996,7 +2996,7 @@ impl Panel for CollabPanel { settings::update_settings_file::( self.fs.clone(), cx, - move |settings, _| settings.dock = Some(position), + move |settings, _| settings.workspace.dock = Some(position), ); } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 9731b89521e29ebda21ad5ce2cfca6e0531ae437..91c349d8e1bc249edaff75a258184759e8e922ef 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -624,7 +624,9 @@ impl Panel for NotificationPanel { settings::update_settings_file::( self.fs.clone(), cx, - move |settings, _| settings.dock = Some(position), + move |settings, _| { + settings.notification_panel.get_or_insert_default().dock = Some(position) + }, ); } diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 98559ffd34006bf2f65427a899fd1fe5d41a4d11..c99f299e2c03c0b152221c849134613bceb9132d 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -1,102 +1,104 @@ use gpui::Pixels; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +use ui::px; +use util::MergeFrom as _; use workspace::dock::DockPosition; -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct CollaborationPanelSettings { pub button: bool, pub dock: DockPosition, pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "collaboration_panel")] -pub struct PanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 240 - pub default_width: Option, -} - -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, pub default_width: Pixels, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "notification_panel")] -pub struct NotificationPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: right - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 300 - pub default_width: Option, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "message_editor")] +#[derive(Clone, Default, Debug)] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. /// /// Default: false - pub auto_replace_emoji_shortcode: Option, + pub auto_replace_emoji_shortcode: bool, } impl Settings for CollaborationPanelSettings { - type FileContent = PanelSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { + let panel = content.collaboration_panel.as_ref().unwrap(); + + Self { + button: panel.button.unwrap(), + dock: panel.dock.unwrap().into(), + default_width: panel.default_width.map(px).unwrap(), + } + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { + if let Some(panel) = content.collaboration_panel.as_ref() { + self.button.merge_from(&panel.button); + self.default_width + .merge_from(&panel.default_width.map(Pixels::from)); + self.dock.merge_from(&panel.dock.map(Into::into)); + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _content: &mut settings::SettingsContent, + ) { + } } impl Settings for NotificationPanelSettings { - type FileContent = NotificationPanelSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { + let panel = content.notification_panel.as_ref().unwrap(); + return Self { + button: panel.button.unwrap(), + dock: panel.dock.unwrap().into(), + default_width: panel.default_width.map(px).unwrap(), + }; + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { + let Some(panel) = content.notification_panel.as_ref() else { + return; + }; + self.button.merge_from(&panel.button); + self.dock.merge_from(&panel.dock.map(Into::into)); + self.default_width.merge_from(&panel.default_width.map(px)); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { + } } impl Settings for MessageEditorSettings { - type FileContent = MessageEditorSettings; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { + let messages = content.message_editor.as_ref().unwrap(); + Self { + auto_replace_emoji_shortcode: messages.auto_replace_emoji_shortcode.unwrap(), + } + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { + let Some(messages) = content.message_editor.as_ref() else { + return; + }; + self.auto_replace_emoji_shortcode + .merge_from(&messages.auto_replace_emoji_shortcode); } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { + } } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 05f77ee55e79efb25a822d0aab4e051deeeafce7..f24b1bf2bed9e103dce2afda3daa80cca8fba26b 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -11,37 +11,6 @@ pub struct FileFinderSettings { pub include_ignored: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "file_finder")] -pub struct FileFinderSettingsContent { - /// Whether to show file icons in the file finder. - /// - /// Default: true - pub file_icons: Option, - /// Determines how much space the file finder can take up in relation to the available window width. - /// - /// Default: small - pub modal_max_width: Option, - /// Determines whether the file finder should skip focus for the active file in search results. - /// - /// Default: true - pub skip_focus_for_active_in_search: Option, - /// Determines whether to show the git status in the file finder - /// - /// Default: true - pub git_status: Option, - /// Whether to use gitignored files when searching. - /// Only the file Zed had indexed will be used, not necessary all the gitignored files. - /// - /// Can accept 3 values: - /// * `Some(true)`: Use all gitignored files - /// * `Some(false)`: Use only the files Zed had indexed - /// * `None`: Be smart and search for ignored when called from a gitignored worktree - /// - /// Default: None - pub include_ignored: Option>, -} - impl Settings for FileFinderSettings { type FileContent = FileFinderSettingsContent; diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 8d0d99b49ed9e53a30490ee6c01a425b4ac98ba9..a9765e99debd78463b6ff8dde11ba386cd34329c 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -62,6 +62,9 @@ pub struct SettingsContent { // todo!() comments?! pub base_keymap: Option, + /// Configuration for the collab panel visual settings. + pub collaboration_panel: Option, + pub debugger: Option, /// Configuration for Diagnostics-related features. @@ -77,15 +80,22 @@ pub struct SettingsContent { /// /// Default: false pub helix_mode: Option, + /// A map of log scopes to the desired log level. /// Useful for filtering out noisy logs or enabling more verbose logging. /// /// Example: {"log": {"client": "warn"}} pub log: Option>, + /// Configuration for the Message Editor + pub message_editor: Option, + /// Configuration for Node-related features pub node: Option, + /// Configuration for the Notification Panel + pub notification_panel: Option, + pub proxy: Option, /// The URL of the Zed server to connect to. @@ -420,3 +430,74 @@ pub enum StatusStyle { pub struct ScrollbarSettings { pub show: Option, } + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +pub struct NotificationPanelSettingsContent { + /// Whether to show the panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Where to dock the panel. + /// + /// Default: right + pub dock: Option, + /// Default width of the panel in pixels. + /// + /// Default: 300 + pub default_width: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +pub struct PanelSettingsContent { + /// Whether to show the panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Where to dock the panel. + /// + /// Default: left + pub dock: Option, + /// Default width of the panel in pixels. + /// + /// Default: 240 + pub default_width: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +pub struct MessageEditorSettings { + /// Whether to automatically replace emoji shortcodes with emoji characters. + /// For example: typing `:wave:` gets replaced with `👋`. + /// + /// Default: false + pub auto_replace_emoji_shortcode: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct FileFinderSettingsContent { + /// Whether to show file icons in the file finder. + /// + /// Default: true + pub file_icons: Option, + /// Determines how much space the file finder can take up in relation to the available window width. + /// + /// Default: small + pub modal_max_width: Option, + /// Determines whether the file finder should skip focus for the active file in search results. + /// + /// Default: true + pub skip_focus_for_active_in_search: Option, + /// Determines whether to show the git status in the file finder + /// + /// Default: true + pub git_status: Option, + /// Whether to use gitignored files when searching. + /// Only the file Zed had indexed will be used, not necessary all the gitignored files. + /// + /// Can accept 3 values: + /// * `Some(true)`: Use all gitignored files + /// * `Some(false)`: Use only the files Zed had indexed + /// * `None`: Be smart and search for ignored when called from a gitignored worktree + /// + /// Default: None + pub include_ignored: Option>, +} diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index 6b743d95f2d2765be0d332e1aa8a03a9647131aa..94480add3053bece5017cf478e9f74065491639b 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -9,7 +9,7 @@ use gpui::{ ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, linear_color_stop, linear_gradient, }; -use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; +use language::language_settings::EditPredictionProvider; use settings::update_settings_file; use ui::{Vector, VectorName, prelude::*}; use workspace::{ModalView, Workspace}; @@ -22,8 +22,10 @@ pub struct ZedPredictModal { pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, _| { + update_settings_file(fs, cx, move |settings, _| { settings + .project + .all_languages .features .get_or_insert(Default::default()) .edit_prediction_provider = Some(provider); From 34b57b3be7ec9f263bb8f46f772608ae21a51b1c Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 17 Sep 2025 10:57:30 -0500 Subject: [PATCH 060/117] nuke old settings UI controls --- .../src/appearance_settings_controls.rs | 387 ------------------ crates/settings_ui/src/settings_ui.rs | 28 -- 2 files changed, 415 deletions(-) delete mode 100644 crates/settings_ui/src/appearance_settings_controls.rs diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs deleted file mode 100644 index 255f5a36b5868b3fd36085e8c980eb7a17fdc163..0000000000000000000000000000000000000000 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::sync::Arc; - -use gpui::{App, FontFeatures, FontWeight}; -use settings::{EditableSettingControl, Settings}; -use theme::{ - FontFamilyCache, FontFamilyName, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings, -}; -use ui::{ - CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, - ToggleButton, prelude::*, -}; - -#[derive(IntoElement)] -pub struct AppearanceSettingsControls {} - -impl AppearanceSettingsControls { - pub fn new() -> Self { - Self {} - } -} - -impl RenderOnce for AppearanceSettingsControls { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - SettingsContainer::new() - .child( - SettingsGroup::new("Theme").child( - h_flex() - .gap_2() - .justify_between() - .child(ThemeControl) - .child(ThemeModeControl), - ), - ) - .child( - SettingsGroup::new("Font") - .child( - h_flex() - .gap_2() - .justify_between() - .child(UiFontFamilyControl) - .child(UiFontWeightControl), - ) - .child(UiFontSizeControl) - .child(UiFontLigaturesControl), - ) - } -} - -#[derive(IntoElement)] -struct ThemeControl; - -impl EditableSettingControl for ThemeControl { - type Value = String; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "Theme".into() - } - - fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - let appearance = SystemAppearance::global(cx); - settings - .theme_selection - .as_ref() - .map(|selection| selection.theme(appearance.0).to_string()) - .unwrap_or_else(|| ThemeSettings::default_theme(*appearance).to_string()) - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - cx: &App, - ) { - let appearance = SystemAppearance::global(cx); - settings.set_theme(value, appearance.0); - } -} - -impl RenderOnce for ThemeControl { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - DropdownMenu::new( - "theme", - value, - ContextMenu::build(window, cx, |mut menu, _, cx| { - let theme_registry = ThemeRegistry::global(cx); - - for theme in theme_registry.list_names() { - menu = menu.custom_entry( - { - let theme = theme.clone(); - move |_window, _cx| Label::new(theme.clone()).into_any_element() - }, - { - let theme = theme.clone(); - move |_window, cx| { - Self::write(theme.to_string(), cx); - } - }, - ) - } - - menu - }), - ) - .full_width(true) - } -} - -#[derive(IntoElement)] -struct ThemeModeControl; - -impl EditableSettingControl for ThemeModeControl { - type Value = ThemeMode; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "Theme Mode".into() - } - - fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings - .theme_selection - .as_ref() - .and_then(|selection| selection.mode()) - .unwrap_or_default() - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.set_mode(value); - } -} - -impl RenderOnce for ThemeModeControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .child( - ToggleButton::new("light", "Light") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(value == ThemeMode::Light) - .on_click(|_, _, cx| Self::write(ThemeMode::Light, cx)) - .first(), - ) - .child( - ToggleButton::new("system", "System") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(value == ThemeMode::System) - .on_click(|_, _, cx| Self::write(ThemeMode::System, cx)) - .middle(), - ) - .child( - ToggleButton::new("dark", "Dark") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .toggle_state(value == ThemeMode::Dark) - .on_click(|_, _, cx| Self::write(ThemeMode::Dark, cx)) - .last(), - ) - } -} - -#[derive(IntoElement)] -struct UiFontFamilyControl; - -impl EditableSettingControl for UiFontFamilyControl { - type Value = SharedString; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Family".into() - } - - fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font.family.clone() - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.ui_font_family = Some(FontFamilyName(value.into())); - } -} - -impl RenderOnce for UiFontFamilyControl { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .gap_2() - .child(Icon::new(IconName::Font)) - .child(DropdownMenu::new( - "ui-font-family", - value, - ContextMenu::build(window, cx, |mut menu, _, cx| { - let font_family_cache = FontFamilyCache::global(cx); - - for font_name in font_family_cache.list_font_families(cx) { - menu = menu.custom_entry( - { - let font_name = font_name.clone(); - move |_window, _cx| Label::new(font_name.clone()).into_any_element() - }, - { - let font_name = font_name.clone(); - move |_window, cx| { - Self::write(font_name.clone(), cx); - } - }, - ) - } - - menu - }), - )) - } -} - -#[derive(IntoElement)] -struct UiFontSizeControl; - -impl EditableSettingControl for UiFontSizeControl { - type Value = Pixels; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Size".into() - } - - fn read(cx: &App) -> Self::Value { - ThemeSettings::get_global(cx).ui_font_size(cx) - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.ui_font_size = Some(value.into()); - } -} - -impl RenderOnce for UiFontSizeControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .gap_2() - .child(Icon::new(IconName::FontSize)) - .child(NumericStepper::new( - "ui-font-size", - value.to_string(), - move |_, _, cx| { - Self::write(value - px(1.), cx); - }, - move |_, _, cx| { - Self::write(value + px(1.), cx); - }, - )) - } -} - -#[derive(IntoElement)] -struct UiFontWeightControl; - -impl EditableSettingControl for UiFontWeightControl { - type Value = FontWeight; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Weight".into() - } - - fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font.weight - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - settings.ui_font_weight = Some(value.0); - } -} - -impl RenderOnce for UiFontWeightControl { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .gap_2() - .child(Icon::new(IconName::FontWeight)) - .child(DropdownMenu::new( - "ui-font-weight", - value.0.to_string(), - ContextMenu::build(window, cx, |mut menu, _window, _cx| { - for weight in FontWeight::ALL { - menu = menu.custom_entry( - move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(), - { - move |_window, cx| { - Self::write(weight, cx); - } - }, - ) - } - - menu - }), - )) - } -} - -#[derive(IntoElement)] -struct UiFontLigaturesControl; - -impl EditableSettingControl for UiFontLigaturesControl { - type Value = bool; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Ligatures".into() - } - - fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font.features.is_calt_enabled().unwrap_or(true) - } - - fn apply( - settings: &mut ::FileContent, - value: Self::Value, - _cx: &App, - ) { - let value = if value { 1 } else { 0 }; - - let mut features = settings - .ui_font_features - .as_ref() - .map(|features| features.tag_value_list().to_vec()) - .unwrap_or_default(); - - if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") { - features[calt_index].1 = value; - } else { - features.push(("calt".into(), value)); - } - - settings.ui_font_features = Some(FontFeatures(Arc::new(features))); - } -} - -impl RenderOnce for UiFontLigaturesControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let value = Self::read(cx); - - CheckboxWithLabel::new( - "ui-font-ligatures", - Label::new(self.name()), - value.into(), - |selection, _, cx| { - Self::write( - match selection { - ToggleState::Selected => true, - ToggleState::Unselected | ToggleState::Indeterminate => false, - }, - cx, - ); - }, - ) - } -} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 6d79b838743bb23f1bce7b409360f04929431df6..790889cf93585b406c895e4754b814e952940f2c 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,5 +1,3 @@ -mod appearance_settings_controls; - use std::{ num::NonZeroU32, ops::{Not, Range}, @@ -25,8 +23,6 @@ use workspace::{ item::{Item, ItemEvent}, }; -use crate::appearance_settings_controls::AppearanceSettingsControls; - pub struct SettingsUiFeatureFlag; impl FeatureFlag for SettingsUiFeatureFlag { @@ -581,30 +577,6 @@ impl Render for SettingsPage { } } -// todo(settings_ui): remove, only here as inspiration -#[allow(dead_code)] -fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement { - v_flex() - .p_4() - .size_full() - .gap_4() - .child(Label::new("Settings").size(LabelSize::Large)) - .child( - v_flex().gap_1().child(Label::new("Appearance")).child( - v_flex() - .elevation_2(cx) - .child(AppearanceSettingsControls::new()), - ), - ) - .child( - v_flex().gap_1().child(Label::new("Editor")).child( - v_flex() - .elevation_2(cx) - .child(EditorSettingsControls::new()), - ), - ) -} - fn element_id_from_path(path: &[SharedString]) -> ElementId { if path.len() == 0 { panic!("Path length must not be zero"); From 50326ddc359e612e9f91cffa64565f87bc814d0a Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Wed, 17 Sep 2025 17:58:46 +0200 Subject: [PATCH 061/117] project_panel: Collapse top-level entries in `Collapse all entries` command (#38310) Closes #11760 The command `project panel: collapse all entries` currently does not collapse top-level entries (the workspaces themselves). I think this should be expected behaviour if you only have a single workspace in your project. However, if you have multiple workspaces, we should collapse their top-level folders as well. This is the expected behaviour in the screenshots in #11760. For more context: Atm the `.retain` function empties the `self.expanded_dir_ids` Hash Map, because the `expanded_entries` Vec is (almost) never empty - it contains the id of the `root_entry` of the workspace. https://github.com/zed-industries/zed/blob/d48d6a745409a8998998ed59c28493a1aa733ebb/crates/project_panel/src/project_panel.rs#L1148-L1152 We then update the `self.expanded_dir_ids` in the `update_visible_entries` function, and since the Hash Map is empty, we execute the `hash_map::Entry::Vacant` arm of the following match statement. https://github.com/zed-industries/zed/blob/d48d6a745409a8998998ed59c28493a1aa733ebb/crates/project_panel/src/project_panel.rs#L3062-L3073 This change makes sure that we do not clear the `expanded_dir_ids` HashMap and always keep the keys for all visible workspaces and therefore we run the `hash_map::Entry::Occupied` arm, which does not override the `expanded_dir_ids` anymore. https://github.com/user-attachments/assets/b607523b-2ea2-4159-8edf-aed7bca05e3a cc @MrSubidubi Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Finn Evers --- crates/project_panel/src/project_panel.rs | 26 ++++- .../project_panel/src/project_panel_tests.rs | 105 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index aed3b44515554299b3d50fbcf5e2b58123495908..ec63c5ca60ee2bdca9ba699c2af300116e5cdb3a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1147,8 +1147,32 @@ impl ProjectPanel { ) { // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids). + let multiple_worktrees = self.project.read(cx).worktrees(cx).count() > 1; + let project = self.project.read(cx); + self.expanded_dir_ids - .retain(|_, expanded_entries| expanded_entries.is_empty()); + .iter_mut() + .for_each(|(worktree_id, expanded_entries)| { + if multiple_worktrees { + *expanded_entries = Default::default(); + return; + } + + let root_entry_id = project + .worktree_for_id(*worktree_id, cx) + .map(|worktree| worktree.read(cx).snapshot()) + .and_then(|worktree_snapshot| { + worktree_snapshot.root_entry().map(|entry| entry.id) + }); + + match root_entry_id { + Some(id) => { + expanded_entries.retain(|entry_id| entry_id == &id); + } + None => *expanded_entries = Default::default(), + }; + }); + self.update_visible_entries(None, cx); cx.notify(); } diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index ad2a7d12ecce31cf1aa4458b3fd59e23f63ab08b..73c33b057807d66687c2dec13962fed5ed0412d3 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -2747,6 +2747,111 @@ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor()); + let worktree_content = json!({ + "dir_1": { + "file_1.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + } + }); + + fs.insert_tree("/project_root_1", worktree_content.clone()) + .await; + fs.insert_tree("/project_root_2", worktree_content).await; + + let project = Project::test( + fs.clone(), + ["/project_root_1".as_ref(), "/project_root_2".as_ref()], + cx, + ) + .await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root_1", "> project_root_2",] + ); +} + +#[gpui::test] +async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Open project_root/dir_1 to ensure that a nested directory is expanded + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + + // Close root directory + toggle_expand_dir(&panel, "project_root", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root <== selected"] + ); + + // Run collapse_all_entries and make sure root is not expanded + panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root <== selected"] + ); +} + #[gpui::test] async fn test_new_file_move(cx: &mut gpui::TestAppContext) { init_test(cx); From b3b71a6e5cd5202fb85980bcb97a241f55e70113 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 17 Sep 2025 10:57:30 -0500 Subject: [PATCH 062/117] wip --- crates/collab_ui/src/panel_settings.rs | 1 + .../file_finder/src/file_finder_settings.rs | 42 ++++- crates/git_ui/src/git_panel_settings.rs | 2 +- crates/go_to_line/src/cursor_position.rs | 2 +- crates/journal/src/journal.rs | 42 ++++- .../src/outline_panel_settings.rs | 158 +++++++--------- crates/settings/src/settings_content.rs | 178 +++++++++++++++++- crates/vim/src/vim.rs | 118 ++++-------- 8 files changed, 345 insertions(+), 198 deletions(-) diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index c99f299e2c03c0b152221c849134613bceb9132d..9e9a2e185bd9c24d62b075344e8fb0e61692cc46 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -19,6 +19,7 @@ pub struct NotificationPanelSettings { } #[derive(Clone, Default, Debug)] +// todo! are these settings even relevant any more? pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index f24b1bf2bed9e103dce2afda3daa80cca8fba26b..0ebdaa6f25612f6f79f465b38d8ea6d6dc7cf59d 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -1,21 +1,41 @@ use anyhow::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +use util::MergeFrom; #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct FileFinderSettings { pub file_icons: bool, - pub modal_max_width: Option, + pub modal_max_width: FileFinderWidth, pub skip_focus_for_active_in_search: bool, pub include_ignored: Option, } impl Settings for FileFinderSettings { - type FileContent = FileFinderSettingsContent; + fn from_defaults(content: &settings::SettingsContent, cx: &mut ui::App) -> Self { + let file_finder = content.file_finder.as_ref().unwrap(); - fn load(sources: SettingsSources, _: &mut gpui::App) -> Result { - sources.json_merge() + Self { + file_icons: file_finder.file_icons.unwrap(), + modal_max_width: file_finder.modal_max_width.unwrap().into(), + skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(), + include_ignored: file_finder.include_ignored.unwrap(), + } + } + + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { + let Some(file_finder) = content.file_finder.as_ref() else { + return; + }; + + self.file_icons.merge_from(&file_finder.file_icons); + self.modal_max_width + .merge_from(&file_finder.modal_max_width.map(Into::into)); + self.skip_focus_for_active_in_search + .merge_from(&file_finder.skip_focus_for_active_in_search); + self.include_ignored + .merge_from(&file_finder.include_ignored); } fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} @@ -31,3 +51,15 @@ pub enum FileFinderWidth { XLarge, Full, } + +impl From for FileFinderWidth { + fn from(content: settings::FileFinderWidthContent) -> Self { + match content { + settings::FileFinderWidthContent::Small => FileFinderWidth::Small, + settings::FileFinderWidthContent::Medium => FileFinderWidth::Medium, + settings::FileFinderWidthContent::Large => FileFinderWidth::Large, + settings::FileFinderWidthContent::XLarge => FileFinderWidth::XLarge, + settings::FileFinderWidthContent::Full => FileFinderWidth::Full, + } + } +} diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index 9b389945b90fbae5e2ada0a64ee7b3d463c98a5f..c82ff469857f084ce59201b359a79883056fee43 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -2,7 +2,7 @@ use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsContent, SettingsKey, SettingsSources, SettingsUi, StatusStyle}; +use settings::{Settings, SettingsContent, StatusStyle}; use ui::{ px, scrollbars::{ScrollbarVisibility, ShowScrollbar}, diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 5d7e0d27d362f8b6245d8ce25774342495a90427..49ab6013478ee0fa623e051ce927fde5921fa977 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -2,7 +2,7 @@ use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; use std::{fmt::Write, num::NonZeroU32, time::Duration}; use text::{Point, Selection}; use ui::{ diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 5cdfa6c1df034deaf06e1c99ea99415757b84c29..740cc2d2d8ad7149d1615995f2e3ac6b5a216f80 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -22,17 +22,16 @@ actions!( ); /// Settings specific to journaling -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "journal")] +#[derive(Clone, Debug)] pub struct JournalSettings { /// The path of the directory where journal entries are stored. /// /// Default: `~` - pub path: Option, + pub path: String, /// What format to display the hours in. /// /// Default: hour12 - pub hour_format: Option, + pub hour_format: HourFormat, } impl Default for JournalSettings { @@ -44,19 +43,44 @@ impl Default for JournalSettings { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[derive(Clone, Debug, Default)] pub enum HourFormat { #[default] Hour12, Hour24, } +impl From for HourFormat { + fn from(content: settings::HourFormatContent) -> Self { + match content { + settings::HourFormatContent::Hour12 => HourFormat::Hour12, + settings::HourFormatContent::Hour24 => HourFormat::Hour24, + } + } +} + impl settings::Settings for JournalSettings { - type FileContent = Self; + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let journal = content.journal.as_ref().unwrap(); - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + Self { + path: journal.path.unwrap(), + hour_format: journal.hour_format.unwrap().into(), + } + } + + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + let Some(journal) = content.journal.as_ref() else { + return; + }; + + if let Some(path) = journal.path { + self.path = path; + } + + if let Some(hour_format) = journal.hour_format { + self.hour_format = hour_format.into(); + } } fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index db34a318188fc10b100b88f959b1c7a77b1161cb..9cbac56e891c9d081c50cf558f00308ce777e96e 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -1,25 +1,10 @@ use editor::EditorSettings; use gpui::{App, Pixels}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +pub use settings::{OutlinePanelDockPosition, Settings, ShowIndentGuides}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; +use util::MergeFrom; -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum OutlinePanelDockPosition { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowIndentGuides { - Always, - Never, -} - -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, @@ -35,7 +20,7 @@ pub struct OutlinePanelSettings { pub expand_outlines_with_depth: usize, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ScrollbarSettings { /// When to show the scrollbar in the project panel. /// @@ -43,80 +28,17 @@ pub struct ScrollbarSettings { pub show: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettingsContent { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option>, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct IndentGuidesSettings { pub show: ShowIndentGuides, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct IndentGuidesSettingsContent { /// When to show the scrollbar in the outline panel. pub show: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "outline_panel")] -pub struct OutlinePanelSettingsContent { - /// Whether to show the outline panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Customize default width (in pixels) taken by outline panel - /// - /// Default: 240 - pub default_width: Option, - /// The position of outline panel - /// - /// Default: left - pub dock: Option, - /// Whether to show file icons in the outline panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the outline panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the outline panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - pub indent_size: Option, - /// Whether to reveal it in the outline panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Settings related to indent guides in the outline panel. - pub indent_guides: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Default depth to expand outline items in the current file. - /// The default depth to which outline entries are expanded on reveal. - /// - Set to 0 to collapse all items that have children - /// - Set to 1 or higher to collapse items at that depth or deeper - /// - /// Default: 100 - pub expand_outlines_with_depth: Option, -} - impl ScrollbarVisibility for OutlinePanelSettings { fn visibility(&self, cx: &App) -> ShowScrollbar { self.scrollbar @@ -126,21 +48,67 @@ impl ScrollbarVisibility for OutlinePanelSettings { } impl Settings for OutlinePanelSettings { - type FileContent = OutlinePanelSettingsContent; - - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let panel = content.outline_panel.as_ref().unwrap(); + Self { + button: panel.button.unwrap(), + default_width: panel.default_width.map(gpui::px).unwrap(), + dock: panel.dock.unwrap(), + file_icons: panel.file_icons.unwrap(), + folder_icons: panel.folder_icons.unwrap(), + git_status: panel.git_status.unwrap(), + indent_size: panel.indent_size.unwrap(), + indent_guides: IndentGuidesSettings { + show: panel.indent_guides.unwrap().show.unwrap(), + }, + auto_reveal_entries: panel.auto_reveal_entries.unwrap(), + auto_fold_dirs: panel.auto_fold_dirs.unwrap(), + scrollbar: ScrollbarSettings { + show: panel.scrollbar.unwrap().show.unwrap(), + }, + expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(), + } } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + let Some(panel) = content.outline_panel.as_ref() else { + return; + }; + + self.button.merge_from(&panel.button); + self.default_width + .merge_from(&panel.default_width.map(Pixels::from)); + self.dock.merge_from(&panel.dock.map(Into::into)); + self.file_icons.merge_from(&panel.file_icons); + self.folder_icons.merge_from(&panel.folder_icons); + self.git_status.merge_from(&panel.git_status); + self.indent_size.merge_from(&panel.indent_size); + + if let Some(indent_guides) = panel.indent_guides.as_ref() { + self.indent_guides.show.merge_from(&indent_guides.show); + } + + self.auto_reveal_entries + .merge_from(&panel.auto_reveal_entries); + self.auto_fold_dirs.merge_from(&panel.auto_fold_dirs); + + if let Some(scrollbar) = panel.scrollbar.as_ref() { + self.scrollbar.show.merge_from(&scrollbar.show); + } + } + fn import_from_vscode( + vscode: &settings::VsCodeSettings, + current: &mut settings::SettingsContent, + ) { if let Some(b) = vscode.read_bool("outline.icons") { - current.file_icons = Some(b); - current.folder_icons = Some(b); + let outline_panel = current.outline_panel.get_or_insert_default(); + outline_panel.file_icons = Some(b); + outline_panel.folder_icons = Some(b); } - vscode.bool_setting("git.decorations.enabled", &mut current.git_status); + if let Some(b) = vscode.read_bool("git.decorations.enabled") { + let outline_panel = current.outline_panel.get_or_insert_default(); + outline_panel.git_status = Some(b); + } } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index a9765e99debd78463b6ff8dde11ba386cd34329c..1a7a643c6b5b287f56a66373391494a3248d15f2 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -41,6 +41,9 @@ pub struct SettingsContent { #[serde(flatten)] pub editor: EditorSettingsContent, + /// Settings related to the file finder. + pub file_finder: Option, + pub git_panel: Option, pub tabs: Option, @@ -81,12 +84,16 @@ pub struct SettingsContent { /// Default: false pub helix_mode: Option, + pub journal: Option, + /// A map of log scopes to the desired log level. /// Useful for filtering out noisy logs or enabling more verbose logging. /// /// Example: {"log": {"client": "warn"}} pub log: Option>, + pub outline_panel: Option, + /// Configuration for the Message Editor pub message_editor: Option, @@ -123,6 +130,9 @@ pub struct SettingsContent { /// /// Default: false pub disable_ai: Option, + + /// Settings related to Vim mode in Zed. + pub vim: Option, } impl SettingsContent { @@ -472,7 +482,7 @@ pub struct MessageEditorSettings { pub auto_replace_emoji_shortcode: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct FileFinderSettingsContent { /// Whether to show file icons in the file finder. /// @@ -481,7 +491,7 @@ pub struct FileFinderSettingsContent { /// Determines how much space the file finder can take up in relation to the available window width. /// /// Default: small - pub modal_max_width: Option, + pub modal_max_width: Option, /// Determines whether the file finder should skip focus for the active file in search results. /// /// Default: true @@ -499,5 +509,169 @@ pub struct FileFinderSettingsContent { /// * `None`: Be smart and search for ignored when called from a gitignored worktree /// /// Default: None + /// todo!() -> Change this type to an enum pub include_ignored: Option>, } + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum FileFinderWidthContent { + #[default] + Small, + Medium, + Large, + XLarge, + Full, +} + +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema)] +pub struct VimSettingsContent { + pub default_mode: Option, + pub toggle_relative_line_numbers: Option, + pub use_system_clipboard: Option, + pub use_smartcase_find: Option, + pub custom_digraphs: Option>>, + pub highlight_on_yank_duration: Option, + pub cursor_shape: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ModeContent { + #[default] + Normal, + Insert, + Replace, + Visual, + VisualLine, + VisualBlock, + HelixNormal, +} + +/// Controls when to use system clipboard. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum UseSystemClipboard { + /// Don't use system clipboard. + Never, + /// Use system clipboard. + Always, + /// Use system clipboard for yank operations. + OnYank, +} + +/// The settings for cursor shape. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct CursorShapeSettings { + /// Cursor shape for the normal mode. + /// + /// Default: block + pub normal: Option, + /// Cursor shape for the replace mode. + /// + /// Default: underline + pub replace: Option, + /// Cursor shape for the visual mode. + /// + /// Default: block + pub visual: Option, + /// Cursor shape for the insert mode. + /// + /// The default value follows the primary cursor_shape. + pub insert: Option, +} + +/// Settings specific to journaling +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct JournalSettingsContent { + /// The path of the directory where journal entries are stored. + /// + /// Default: `~` + pub path: Option, + /// What format to display the hours in. + /// + /// Default: hour12 + pub hour_format: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum HourFormatContent { + #[default] + Hour12, + Hour24, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +pub struct OutlinePanelSettingsContent { + /// Whether to show the outline panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Customize default width (in pixels) taken by outline panel + /// + /// Default: 240 + pub default_width: Option, + /// The position of outline panel + /// + /// Default: left + pub dock: Option, + /// Whether to show file icons in the outline panel. + /// + /// Default: true + pub file_icons: Option, + /// Whether to show folder icons or chevrons for directories in the outline panel. + /// + /// Default: true + pub folder_icons: Option, + /// Whether to show the git status in the outline panel. + /// + /// Default: true + pub git_status: Option, + /// Amount of indentation (in pixels) for nested items. + /// + /// Default: 20 + pub indent_size: Option, + /// Whether to reveal it in the outline panel automatically, + /// when a corresponding project entry becomes active. + /// Gitignored entries are never auto revealed. + /// + /// Default: true + pub auto_reveal_entries: Option, + /// Whether to fold directories automatically + /// when directory has only one directory inside. + /// + /// Default: true + pub auto_fold_dirs: Option, + /// Settings related to indent guides in the outline panel. + pub indent_guides: Option, + /// Scrollbar-related settings + pub scrollbar: Option, + /// Default depth to expand outline items in the current file. + /// The default depth to which outline entries are expanded on reveal. + /// - Set to 0 to collapse all items that have children + /// - Set to 1 or higher to collapse items at that depth or deeper + /// + /// Default: 100 + pub expand_outlines_with_depth: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum OutlinePanelDockPosition { + Left, + Right, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ShowIndentGuides { + Always, + Never, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct IndentGuidesSettingsContent { + /// When to show the scrollbar in the outline panel. + pub show: Option, +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 7faff54e73b179bdfa944798a9c87fafa245f732..8bffc45b7e1cdf30781e2d685404b8fb5526f2af 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1793,39 +1793,6 @@ impl Vim { } } -/// Controls when to use system clipboard. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum UseSystemClipboard { - /// Don't use system clipboard. - Never, - /// Use system clipboard. - Always, - /// Use system clipboard for yank operations. - OnYank, -} - -/// The settings for cursor shape. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -struct CursorShapeSettings { - /// Cursor shape for the normal mode. - /// - /// Default: block - pub normal: Option, - /// Cursor shape for the replace mode. - /// - /// Default: underline - pub replace: Option, - /// Cursor shape for the visual mode. - /// - /// Default: block - pub visual: Option, - /// Cursor shape for the insert mode. - /// - /// The default value follows the primary cursor_shape. - pub insert: Option, -} - #[derive(Deserialize)] struct VimSettings { pub default_mode: Mode, @@ -1837,32 +1804,7 @@ struct VimSettings { pub cursor_shape: CursorShapeSettings, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "vim")] -struct VimSettingsContent { - pub default_mode: Option, - pub toggle_relative_line_numbers: Option, - pub use_system_clipboard: Option, - pub use_smartcase_find: Option, - pub custom_digraphs: Option>>, - pub highlight_on_yank_duration: Option, - pub cursor_shape: Option, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ModeContent { - #[default] - Normal, - Insert, - Replace, - Visual, - VisualLine, - VisualBlock, - HelixNormal, -} - -impl From for Mode { +impl From for Mode { fn from(mode: ModeContent) -> Self { match mode { ModeContent::Normal => Self::Normal, @@ -1877,34 +1819,40 @@ impl From for Mode { } impl Settings for VimSettings { - type FileContent = VimSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - let settings: VimSettingsContent = sources.json_merge()?; - - Ok(Self { - default_mode: settings - .default_mode - .ok_or_else(Self::missing_default)? - .into(), - toggle_relative_line_numbers: settings - .toggle_relative_line_numbers - .ok_or_else(Self::missing_default)?, - use_system_clipboard: settings - .use_system_clipboard - .ok_or_else(Self::missing_default)?, - use_smartcase_find: settings - .use_smartcase_find - .ok_or_else(Self::missing_default)?, - custom_digraphs: settings.custom_digraphs.ok_or_else(Self::missing_default)?, - highlight_on_yank_duration: settings - .highlight_on_yank_duration - .ok_or_else(Self::missing_default)?, - cursor_shape: settings.cursor_shape.ok_or_else(Self::missing_default)?, - }) + fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + let vim = content.vim.as_ref().unwrap(); + Self { + default_mode: vim.default_mode.unwrap().into(), + toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(), + use_system_clipboard: vim.use_system_clipboard.unwrap(), + use_smartcase_find: vim.use_smartcase_find.unwrap(), + custom_digraphs: vim.custom_digraphs.unwrap(), + highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), + cursor_shape: vim.cursor_shape.unwrap(), + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) { + fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + let Some(vim) = content.vim.as_ref() else { + return; + }; + self.default_mode + .merge_from(&vim.default_mode.map(Into::into)); + self.toggle_relative_line_numbers + .merge_from(&vim.toggle_relative_line_numbers); + self.use_system_clipboard + .merge_from(&vim.use_system_clipboard); + self.use_smartcase_find.merge_from(&vim.use_smartcase_find); + self.custom_digraphs.merge_from(&vim.custom_digraphs); + self.highlight_on_yank_duration + .merge_from(&vim.highlight_on_yank_duration); + self.cursor_shape.merge_from(&vim.cursor_shape); + } + + fn import_from_vscode( + _vscode: &settings::VsCodeSettings, + _current: &mut settings::SettingsContent, + ) { // TODO: translate vim extension settings } } From 55fa4a33b82fa0fecba678f2f058b53dc6488826 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 10:18:45 -0600 Subject: [PATCH 063/117] Git UI --- crates/git_ui/src/git_panel.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4f626ee7e83b89ab863cdcadcb0a32471905c4c7..6fb940588a83bfd910b52543489996b023267c00 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2,7 +2,6 @@ use crate::askpass_modal::AskPassModal; use crate::commit_modal::CommitModal; use crate::commit_tooltip::CommitTooltip; use crate::commit_view::CommitView; -use crate::git_panel_settings::StatusStyle; use crate::project_diff::{self, Diff, ProjectDiff}; use crate::remote_output::{self, RemoteAction, SuccessMessage}; use crate::{branch_picker, picker_prompt, render_remote_button}; @@ -51,7 +50,7 @@ use project::{ git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId}, }; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, StatusStyle}; use std::future::Future; use std::ops::Range; use std::path::{Path, PathBuf}; From 824f6953835784f1408efaa2542bb9f77e9b7145 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 17 Sep 2025 12:25:50 -0400 Subject: [PATCH 064/117] Rename Windows GitHub Issue template (#38339) Release Notes: - N/A --- .../{07_bug_windows_alpha.yml => 07_bug_windows_beta.yml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .github/ISSUE_TEMPLATE/{07_bug_windows_alpha.yml => 07_bug_windows_beta.yml} (86%) diff --git a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_beta.yml similarity index 86% rename from .github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml rename to .github/ISSUE_TEMPLATE/07_bug_windows_beta.yml index 826c2b8027144d4b658108e09c79e40490c3005d..b2b2a0f9dfcd5ddaa0dda41650864b053c5bb933 100644 --- a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml +++ b/.github/ISSUE_TEMPLATE/07_bug_windows_beta.yml @@ -1,8 +1,8 @@ -name: Bug Report (Windows Alpha) -description: Zed Windows Alpha Related Bugs +name: Bug Report (Windows Beta) +description: Zed Windows Beta Related Bugs type: "Bug" labels: ["windows"] -title: "Windows Alpha: " +title: "Windows Beta: " body: - type: textarea attributes: From 4e0a78813d2f424614087a079f475a6c0259908b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 10:30:58 -0600 Subject: [PATCH 065/117] go to line --- crates/collab_ui/src/collab_panel.rs | 8 ++-- crates/collab_ui/src/notification_panel.rs | 10 ++-- .../src/copilot_completion_provider.rs | 9 ++-- crates/debugger_ui/src/debugger_panel.rs | 24 ++-------- .../src/extension_version_selector.rs | 9 ++-- crates/extensions_ui/src/extensions_ui.rs | 16 +++---- crates/file_finder/src/file_finder.rs | 12 ++--- .../file_finder/src/file_finder_settings.rs | 3 +- crates/go_to_line/src/cursor_position.rs | 46 ++++++------------- crates/settings/src/settings_content.rs | 12 ++++- crates/settings/src/settings_store.rs | 2 +- 11 files changed, 61 insertions(+), 90 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 070ab13b271882c4e771e71285843371a9cfb77d..6290878483e69d8a7b56ee865672b7f19d74d4de 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2993,11 +2993,9 @@ impl Panel for CollabPanel { _window: &mut Window, cx: &mut Context, ) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| settings.workspace.dock = Some(position), - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.collaboration_panel.get_or_insert_default().dock = Some(position.into()) + }); } fn size(&self, _window: &Window, cx: &App) -> Pixels { diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 91c349d8e1bc249edaff75a258184759e8e922ef..3d988c4634ded9bd2c94d8a75886cf452e64eacb 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -621,13 +621,9 @@ impl Panel for NotificationPanel { } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| { - settings.notification_panel.get_or_insert_default().dock = Some(position) - }, - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.notification_panel.get_or_insert_default().dock = Some(position.into()) + }); } fn size(&self, _: &Window, cx: &App) -> Pixels { diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 52d75175e5b5ba265bb32c6c15c713e1bd8faecd..a3f88dd5ae0a0a7279bbf6d4158558d22a5c665a 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -281,14 +281,11 @@ mod tests { use indoc::indoc; use language::{ Point, - language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LspInsertMode, - WordsCompletionMode, - }, + language_settings::{CompletionSettings, LspInsertMode, WordsCompletionMode}, }; use project::Project; use serde_json::json; - use settings::SettingsStore; + use settings::{AllLanguageSettingsContent, SettingsStore}; use std::future::Future; use util::{ path, @@ -1128,7 +1125,7 @@ mod tests { Project::init_settings(cx); workspace::init_settings(cx); SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, f); + store.update_user_settings(cx, |settings| f(settings.project.all_languages)); }); }); } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 3001e21136ff2d1948ff4a7396798a03fd87c997..7c58e0e802c700d64c350147e1443f6cfbd5e25c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -12,7 +12,6 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use collections::IndexMap; use dap::adapters::DebugAdapterName; -use dap::debugger_settings::DebugPanelDockPosition; use dap::{DapRegistry, StartDebuggingRequestArguments}; use dap::{client::SessionId, debugger_settings::DebuggerSettings}; use editor::Editor; @@ -29,7 +28,7 @@ use project::debugger::session::{Session, SessionQuirks, SessionState, SessionSt use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; -use settings::{DockPosition, Settings}; +use settings::Settings; use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; @@ -1400,11 +1399,7 @@ impl Panel for DebugPanel { } fn position(&self, _window: &Window, cx: &App) -> DockPosition { - match DebuggerSettings::get_global(cx).dock { - DockPosition::Left => DockPosition::Left, - DockPosition::Bottom => DockPosition::Bottom, - DockPosition::Right => DockPosition::Right, - } + DebuggerSettings::get_global(cx).dock.into() } fn position_is_valid(&self, _: DockPosition) -> bool { @@ -1426,18 +1421,9 @@ impl Panel for DebugPanel { }); } - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| { - let dock = match position { - DockPosition::Left => DebugPanelDockPosition::Left, - DockPosition::Bottom => DebugPanelDockPosition::Bottom, - DockPosition::Right => DebugPanelDockPosition::Right, - }; - settings.dock = dock; - }, - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.debugger.get_or_insert_default().dock = position.into(); + }); } fn size(&self, _window: &Window, _: &App) -> Pixels { diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs index fe7a419fbe8001b99d5c3ebf16dfc38cda3fc713..d38c27375f6c32324d4832d308768af8473869eb 100644 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ b/crates/extensions_ui/src/extension_version_selector.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use client::ExtensionMetadata; -use extension_host::{ExtensionSettings, ExtensionStore}; +use extension_host::ExtensionStore; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*}; @@ -183,10 +183,13 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { let extension_id = extension_version.id.clone(); let version = extension_version.manifest.version.clone(); - update_settings_file::(self.fs.clone(), cx, { + update_settings_file(self.fs.clone(), cx, { let extension_id = extension_id.clone(); move |settings, _| { - settings.auto_update_extensions.insert(extension_id, false); + settings + .extension + .auto_update_extensions + .insert(extension_id, false); } }); diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index dab238dc8be1ae6f2243e94fc7c00ce363a79469..dc636d92cfc6badf5ff62513e58ebea9ad13a9b4 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -20,7 +20,7 @@ use gpui::{ use num_format::{Locale, ToFormattedString}; use project::DirectoryLister; use release_channel::ReleaseChannel; -use settings::Settings; +use settings::{Settings, SettingsContent}; use strum::IntoEnumIterator as _; use theme::ThemeSettings; use ui::{ @@ -1269,17 +1269,17 @@ impl ExtensionsPage { Label::new(message) } - fn update_settings( + fn update_settings( &mut self, selection: &ToggleState, cx: &mut Context, - callback: impl 'static + Send + Fn(&mut T::FileContent, bool), + callback: impl 'static + Send + Fn(&mut SettingsContent, bool), ) { if let Some(workspace) = self.workspace.upgrade() { let fs = workspace.read(cx).app_state().fs.clone(); let selection = *selection; - settings::update_settings_file::(fs, cx, move |settings, _| { + settings::update_settings_file(fs, cx, move |settings, _| { let value = match selection { ToggleState::Unselected => false, ToggleState::Selected => true, @@ -1340,11 +1340,9 @@ impl ExtensionsPage { }, cx.listener(move |this, selection, _, cx| { telemetry::event!("Vim Mode Toggled", source = "Feature Upsell"); - this.update_settings::( - selection, - cx, - |setting, value| setting.vim_mode = Some(value), - ); + this.update_settings(selection, cx, |setting, value| { + setting.vim_mode = Some(value) + }); }), )), Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!") diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 53cf0552f22a59c31ab2422d86eb8cbb76145908..eda01466f6dda2f90fbdbd9f92f3cf812b083026 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -347,16 +347,16 @@ impl FileFinder { }) } - pub fn modal_max_width(width_setting: Option, window: &mut Window) -> Pixels { + pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels { let window_width = window.viewport_size().width; let small_width = rems(34.).to_pixels(window.rem_size()); match width_setting { - None | Some(FileFinderWidth::Small) => small_width, - Some(FileFinderWidth::Full) => window_width, - Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width), - Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width), - Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width), + FileFinderWidth::Small => small_width, + FileFinderWidth::Full => window_width, + FileFinderWidth::XLarge => (window_width - Pixels(512.)).max(small_width), + FileFinderWidth::Large => (window_width - Pixels(768.)).max(small_width), + FileFinderWidth::Medium => (window_width - Pixels(1024.)).max(small_width), } } } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 0ebdaa6f25612f6f79f465b38d8ea6d6dc7cf59d..461a9dae45f9ac3edf46518ac9c20fce20580c3d 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -13,7 +12,7 @@ pub struct FileFinderSettings { } impl Settings for FileFinderSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut ui::App) -> Self { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { let file_finder = content.file_finder.as_ref().unwrap(); Self { diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 49ab6013478ee0fa623e051ce927fde5921fa977..9c109db8de6e092da5513317d9bc5b686146edf8 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,7 +1,5 @@ use editor::{Editor, EditorSettings, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::Settings; use std::{fmt::Write, num::NonZeroU32, time::Duration}; use text::{Point, Selection}; @@ -9,7 +7,7 @@ use ui::{ Button, ButtonCommon, Clickable, Context, FluentBuilder, IntoElement, LabelSize, ParentElement, Render, Tooltip, Window, div, }; -use util::paths::FILE_ROW_COLUMN_DELIMITER; +use util::{MergeFrom, paths::FILE_ROW_COLUMN_DELIMITER}; use workspace::{StatusItemView, Workspace, item::ItemHandle}; #[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)] @@ -293,41 +291,27 @@ impl StatusItemView for CursorPosition { } } -#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub(crate) enum LineIndicatorFormat { +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum LineIndicatorFormat { Short, - #[default] Long, } -#[derive( - Clone, Copy, Default, Debug, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey, -)] -#[settings_key(None)] -pub(crate) struct LineIndicatorFormatContent { - line_indicator_format: Option, +impl From for LineIndicatorFormat { + fn from(format: settings::LineIndicatorFormat) -> Self { + match format { + settings::LineIndicatorFormat::Short => LineIndicatorFormat::Short, + settings::LineIndicatorFormat::Long => LineIndicatorFormat::Long, + } + } } impl Settings for LineIndicatorFormat { - type FileContent = LineIndicatorFormatContent; - - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - let format = [ - sources.release_channel, - sources.profile, - sources.user, - sources.operating_system, - Some(sources.default), - ] - .into_iter() - .flatten() - .filter_map(|val| val.line_indicator_format) - .next() - .ok_or_else(Self::missing_default)?; - - Ok(format) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + content.line_indicator_format.unwrap().into() } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + self.merge_from(&content.line_indicator_format.map(Into::into)); + } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 1a7a643c6b5b287f56a66373391494a3248d15f2..c9970dd1185e6eaac2de6b893c14d0e988bc2246 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -92,6 +92,8 @@ pub struct SettingsContent { /// Example: {"log": {"client": "warn"}} pub log: Option>, + pub line_indicator_format: Option, + pub outline_panel: Option, /// Configuration for the Message Editor @@ -291,7 +293,7 @@ pub struct TelemetrySettingsContent { pub metrics: Option, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)] +#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)] pub struct DebuggerSettingsContent { /// Determines the stepping granularity. /// @@ -675,3 +677,11 @@ pub struct IndentGuidesSettingsContent { /// When to show the scrollbar in the outline panel. pub show: Option, } + +#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum LineIndicatorFormat { + Short, + #[default] + Long, +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 54f1c10cf720d737e16dc128724280fbebcecf04..fa4cb8dd0db161e39de828043a2651a97d62942e 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -74,7 +74,7 @@ pub trait Settings: 'static + Send + Sync + Sized { /// 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 SettingsContent); + fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {} #[track_caller] fn register(cx: &mut App) From 76d1b2291b38a061195bbfc4764df051eb46d4cf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 10:39:39 -0600 Subject: [PATCH 066/117] More --- Cargo.lock | 1 + .../src/copilot_completion_provider.rs | 2 +- crates/debugger_ui/src/debugger_panel.rs | 2 +- .../file_finder/src/file_finder_settings.rs | 2 - .../image_viewer/src/image_viewer_settings.rs | 42 +++++------- crates/journal/Cargo.toml | 1 + crates/journal/src/journal.rs | 64 +++++-------------- crates/settings/src/settings_content.rs | 26 +++++++- 8 files changed, 59 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a9ca9a409c622b5cc7b5316b92f43399946e092..de4bd5cc8342c6bfaee8cff3a473616de1cbc8ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8845,6 +8845,7 @@ dependencies = [ "serde", "settings", "shellexpand 2.1.2", + "util", "workspace", "workspace-hack", ] diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index a3f88dd5ae0a0a7279bbf6d4158558d22a5c665a..9184c1b196e316f045cb9790e4d0cd61d3aba0bc 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1125,7 +1125,7 @@ mod tests { Project::init_settings(cx); workspace::init_settings(cx); SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| f(settings.project.all_languages)); + store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages)); }); }); } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 7c58e0e802c700d64c350147e1443f6cfbd5e25c..12a31a7360bb6e7fb1ff6fdcaf18f73d679693a3 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1422,7 +1422,7 @@ impl Panel for DebugPanel { } settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - settings.debugger.get_or_insert_default().dock = position.into(); + settings.debugger.get_or_insert_default().dock = Some(position.into()); }); } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 461a9dae45f9ac3edf46518ac9c20fce20580c3d..22d60160f535b674ef7ab7a3d9d967e924b43bbb 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -36,8 +36,6 @@ impl Settings for FileFinderSettings { self.include_ignored .merge_from(&file_finder.include_ignored); } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] diff --git a/crates/image_viewer/src/image_viewer_settings.rs b/crates/image_viewer/src/image_viewer_settings.rs index 510de69b522fbb07cb8eedba43edfe3a95e4a591..e093a5fc826fa23c9894ecdb56466dcb71e03465 100644 --- a/crates/image_viewer/src/image_viewer_settings.rs +++ b/crates/image_viewer/src/image_viewer_settings.rs @@ -1,40 +1,30 @@ use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +pub use settings::ImageFileSizeUnit; +use settings::Settings; +use util::MergeFrom; /// The settings for the image viewer. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi, SettingsKey)] -#[settings_key(key = "image_viewer")] +#[derive(Clone, Debug, Default)] pub struct ImageViewerSettings { /// The unit to use for displaying image file sizes. /// /// Default: "binary" - #[serde(default)] pub unit: ImageFileSizeUnit, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum ImageFileSizeUnit { - /// Displays file size in binary units (e.g., KiB, MiB). - #[default] - Binary, - /// Displays file size in decimal units (e.g., KB, MB). - Decimal, -} - impl Settings for ImageViewerSettings { - type FileContent = Self; - - fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - SettingsSources::::json_merge_with( - [sources.default] - .into_iter() - .chain(sources.user) - .chain(sources.server), - ) + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + Self { + unit: content.image_viewer.clone().unwrap().unit.unwrap(), + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + self.unit.merge_from( + &content + .image_viewer + .as_ref() + .and_then(|image_viewer| image_viewer.unit), + ); + } } diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 041badd10490a8ea876eb43975a911ca6811fa05..089896cc1edafa760fbe908bcf7d8f689511edca 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -24,6 +24,7 @@ settings.workspace = true shellexpand.workspace = true workspace.workspace = true workspace-hack.workspace = true +util.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 740cc2d2d8ad7149d1615995f2e3ac6b5a216f80..4d0d1a41dafbe466abf61178c6e006b7b86161dc 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,16 +1,15 @@ -use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; use editor::scroll::Autoscroll; use editor::{Editor, SelectionEffects}; use gpui::{App, AppContext as _, Context, Window, actions}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +pub use settings::HourFormat; +use settings::Settings; use std::{ fs::OpenOptions, path::{Path, PathBuf}, sync::Arc, }; +use util::MergeFrom; use workspace::{AppState, OpenVisible, Workspace}; actions!( @@ -34,34 +33,9 @@ pub struct JournalSettings { pub hour_format: HourFormat, } -impl Default for JournalSettings { - fn default() -> Self { - Self { - path: Some("~".into()), - hour_format: Some(Default::default()), - } - } -} - -#[derive(Clone, Debug, Default)] -pub enum HourFormat { - #[default] - Hour12, - Hour24, -} - -impl From for HourFormat { - fn from(content: settings::HourFormatContent) -> Self { - match content { - settings::HourFormatContent::Hour12 => HourFormat::Hour12, - settings::HourFormatContent::Hour24 => HourFormat::Hour24, - } - } -} - impl settings::Settings for JournalSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { - let journal = content.journal.as_ref().unwrap(); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let journal = content.journal.clone().unwrap(); Self { path: journal.path.unwrap(), @@ -69,21 +43,13 @@ impl settings::Settings for JournalSettings { } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let Some(journal) = content.journal.as_ref() else { return; }; - - if let Some(path) = journal.path { - self.path = path; - } - - if let Some(hour_format) = journal.hour_format { - self.hour_format = hour_format.into(); - } + self.path.merge_from(&journal.path); + self.hour_format.merge_from(&journal.hour_format); } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } pub fn init(_: Arc, cx: &mut App) { @@ -101,7 +67,7 @@ pub fn init(_: Arc, cx: &mut App) { pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut App) { let settings = JournalSettings::get_global(cx); - let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) { + let journal_dir = match journal_dir(&settings.path) { Some(journal_dir) => journal_dir, None => { log::error!("Can't determine journal directory"); @@ -223,13 +189,13 @@ fn journal_dir(path: &str) -> Option { .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")) } -fn heading_entry(now: NaiveTime, hour_format: &Option) -> String { +fn heading_entry(now: NaiveTime, hour_format: &HourFormat) -> String { match hour_format { - Some(HourFormat::Hour24) => { + HourFormat::Hour24 => { let hour = now.hour(); format!("# {}:{:02}", hour, now.minute()) } - _ => { + HourFormat::Hour12 => { let (pm, hour) = now.hour12(); let am_or_pm = if pm { "PM" } else { "AM" }; format!("# {}:{:02} {}", hour, now.minute(), am_or_pm) @@ -245,7 +211,7 @@ mod tests { #[test] fn test_heading_entry_defaults_to_hour_12() { let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &None); + let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour12); let expected_heading_entry = "# 3:00 PM"; assert_eq!(actual_heading_entry, expected_heading_entry); @@ -254,7 +220,7 @@ mod tests { #[test] fn test_heading_entry_is_hour_12() { let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12)); + let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour12); let expected_heading_entry = "# 3:00 PM"; assert_eq!(actual_heading_entry, expected_heading_entry); @@ -263,7 +229,7 @@ mod tests { #[test] fn test_heading_entry_is_hour_24() { let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24)); + let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour24); let expected_heading_entry = "# 15:00"; assert_eq!(actual_heading_entry, expected_heading_entry); diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index c9970dd1185e6eaac2de6b893c14d0e988bc2246..652506e5937688ac0d1be8be5ddf1044db7140c9 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -79,6 +79,9 @@ pub struct SettingsContent { /// Common language server settings. pub global_lsp_settings: Option, + /// The settings for the image viewer. + pub image_viewer: Option, + /// Whether or not to enable Helix mode. /// /// Default: false @@ -593,12 +596,12 @@ pub struct JournalSettingsContent { /// What format to display the hours in. /// /// Default: hour12 - pub hour_format: Option, + pub hour_format: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] -pub enum HourFormatContent { +pub enum HourFormat { #[default] Hour12, Hour24, @@ -685,3 +688,22 @@ pub enum LineIndicatorFormat { #[default] Long, } + +/// The settings for the image viewer. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)] +pub struct ImageViewerSettingsContent { + /// The unit to use for displaying image file sizes. + /// + /// Default: "binary" + pub unit: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ImageFileSizeUnit { + /// Displays file size in binary units (e.g., KiB, MiB). + #[default] + Binary, + /// Displays file size in decimal units (e.g., KB, MB). + Decimal, +} From 43f40c60fda1a4d81b503aa0898cde4455931f90 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Sep 2025 13:02:13 -0400 Subject: [PATCH 067/117] rope: Fix spelling of `peek_with_bitmaps` (#38341) This PR fixes the spelling of the `peek_with_bitmaps` method. Release Notes: - N/A --- crates/rope/src/rope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 8bcaef20ca3bd5c79413791764a313fd1e6b75ac..3f6addb7c2394503098a213f4139fedc9757ba86 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -767,7 +767,7 @@ impl<'a> Chunks<'a> { } /// Returns bitmaps that represent character positions and tab positions - pub fn peak_with_bitmaps(&self) -> Option> { + pub fn peek_with_bitmaps(&self) -> Option> { if !self.offset_is_valid() { return None; } @@ -898,7 +898,7 @@ impl<'a> Iterator for ChunkWithBitmaps<'a> { type Item = ChunkBitmaps<'a>; fn next(&mut self) -> Option { - let chunk_bitmaps = self.0.peak_with_bitmaps()?; + let chunk_bitmaps = self.0.peek_with_bitmaps()?; if self.0.reversed { self.0.offset -= chunk_bitmaps.text.len(); if self.0.offset <= *self.0.chunks.start() { From dcbeb86253773d8a567fecc9868f6987cc40fad7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 11:30:47 -0600 Subject: [PATCH 068/117] LanguageModels --- Cargo.lock | 6 + crates/anthropic/Cargo.toml | 1 + crates/anthropic/src/anthropic.rs | 19 + crates/google_ai/Cargo.toml | 1 + crates/google_ai/src/google_ai.rs | 11 +- crates/language_model/Cargo.toml | 1 + crates/language_model/src/language_model.rs | 9 +- .../language_models/src/provider/anthropic.rs | 53 +- .../language_models/src/provider/bedrock.rs | 19 +- crates/language_models/src/provider/cloud.rs | 38 +- .../language_models/src/provider/deepseek.rs | 12 +- crates/language_models/src/provider/google.rs | 27 +- .../language_models/src/provider/lmstudio.rs | 12 +- .../language_models/src/provider/mistral.rs | 15 +- crates/language_models/src/provider/ollama.rs | 21 +- .../language_models/src/provider/open_ai.rs | 13 +- .../src/provider/open_ai_compatible.rs | 34 +- .../src/provider/open_router.rs | 54 +-- crates/language_models/src/provider/vercel.rs | 12 +- crates/language_models/src/provider/x_ai.rs | 12 +- crates/language_models/src/settings.rs | 459 +++++++----------- crates/ollama/Cargo.toml | 1 + crates/ollama/src/ollama.rs | 24 +- crates/open_ai/Cargo.toml | 1 + crates/open_ai/src/open_ai.rs | 11 +- crates/open_router/Cargo.toml | 3 +- crates/open_router/src/open_router.rs | 50 +- crates/settings/src/settings_content.rs | 5 + .../src/settings_content/language_model.rs | 393 +++++++++++++++ 29 files changed, 646 insertions(+), 671 deletions(-) create mode 100644 crates/settings/src/settings_content/language_model.rs diff --git a/Cargo.lock b/Cargo.lock index de4bd5cc8342c6bfaee8cff3a473616de1cbc8ae..f599385230bac594ad5759ae3f90b53217501979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,6 +658,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "strum 0.27.1", "thiserror 2.0.12", "workspace-hack", @@ -7368,6 +7369,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "strum 0.27.1", "workspace-hack", ] @@ -9153,6 +9155,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "smol", "telemetry_events", "thiserror 2.0.12", @@ -11134,6 +11137,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "workspace-hack", ] @@ -11242,6 +11246,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "strum 0.27.1", "workspace-hack", ] @@ -11256,6 +11261,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "strum 0.27.1", "thiserror 2.0.12", "util", diff --git a/crates/anthropic/Cargo.toml b/crates/anthropic/Cargo.toml index 8e82c7cdd6a6a1ac7dd0e8b165f4d924c85d39ab..c8103e5bfb533a0f7f8e88995ac0927073a9793f 100644 --- a/crates/anthropic/Cargo.toml +++ b/crates/anthropic/Cargo.toml @@ -23,6 +23,7 @@ http_client.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true +settings.workspace = true strum.workspace = true thiserror.workspace = true workspace-hack.workspace = true diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 7fd0fb4bc5abd983c57507522c2a37dffcbfa258..a9a4fbb6340b59cafe3bbc555efb6ad5c9e11806 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -8,6 +8,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B use http_client::http::{self, HeaderMap, HeaderValue}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode}; use serde::{Deserialize, Serialize}; +pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode}; use strum::{EnumIter, EnumString}; use thiserror::Error; @@ -31,6 +32,24 @@ pub enum AnthropicModelMode { }, } +impl From for AnthropicModelMode { + fn from(value: ModelMode) -> Self { + match value { + ModelMode::Default => AnthropicModelMode::Default, + ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens }, + } + } +} + +impl From for ModelMode { + fn from(value: AnthropicModelMode) -> Self { + match value { + AnthropicModelMode::Default => ModelMode::Default, + AnthropicModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens }, + } + } +} + #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] pub enum Model { diff --git a/crates/google_ai/Cargo.toml b/crates/google_ai/Cargo.toml index bc9213ca8f81d8f1bccfd84cbffe95aa61923cd8..ce759698ed5f986663fe1cae4a83b65cd76a8e4f 100644 --- a/crates/google_ai/Cargo.toml +++ b/crates/google_ai/Cargo.toml @@ -21,5 +21,6 @@ http_client.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true +settings.workspace = true strum.workspace = true workspace-hack.workspace = true diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index 92fd53189327fabccdc1472ac0fa2a20dc665646..9b7e5ec8d1c42fc846d131cfd063de5bba8287ae 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -4,6 +4,7 @@ use anyhow::{Result, anyhow, bail}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +pub use settings::ModelMode as GoogleModelMode; pub const API_URL: &str = "https://generativelanguage.googleapis.com"; @@ -295,16 +296,6 @@ pub struct ThinkingConfig { pub thinking_budget: u32, } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub enum GoogleModelMode { - #[default] - Default, - Thinking { - budget_tokens: Option, - }, -} - #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GenerationConfig { diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index d4513f617b0d9f79e960c6cec6ca1a5dd806cea6..a85283cf121bc10a82e1022071d6a136dd5716f5 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -35,6 +35,7 @@ proto.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true smol.workspace = true telemetry_events.workspace = true thiserror.workspace = true diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index fb35a1cae109490620268b11382c3070a6ef6da2..418a864648e10bf36073f00392a189f45d474534 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -21,6 +21,7 @@ use open_router::OpenRouterError; use parking_lot::Mutex; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; +pub use settings::LanguageModelCacheConfiguration; use std::ops::{Add, Sub}; use std::str::FromStr; use std::sync::Arc; @@ -62,14 +63,6 @@ pub fn init_settings(cx: &mut App) { registry::init(cx); } -/// Configuration for caching language model messages. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct LanguageModelCacheConfiguration { - pub max_cache_anchors: usize, - pub should_speculate: bool, - pub min_total_token: u64, -} - /// A completion event from a language model. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum LanguageModelCompletionEvent { diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index ca7763e2c5cda3c07c5cb51389cb3173a55865e2..dd122159fda1dbf8f13ebec2a01b37795d18fe75 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -22,8 +22,6 @@ use language_model::{ LanguageModelToolResultContent, MessageContent, RateLimiter, Role, }; use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr; @@ -33,6 +31,8 @@ use theme::ThemeSettings; use ui::{Icon, IconName, List, Tooltip, prelude::*}; use util::ResultExt; +pub use settings::AnthropicAvailableModel as AvailableModel; + const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID; const PROVIDER_NAME: LanguageModelProviderName = language_model::ANTHROPIC_PROVIDER_NAME; @@ -43,55 +43,6 @@ pub struct AnthropicSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The model's context window size. - pub max_tokens: u64, - /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling. - pub tool_override: Option, - /// Configuration of Anthropic's caching API. - pub cache_configuration: Option, - pub max_output_tokens: Option, - pub default_temperature: Option, - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ModelMode { - #[default] - Default, - Thinking { - /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. - budget_tokens: Option, - }, -} - -impl From for AnthropicModelMode { - fn from(value: ModelMode) -> Self { - match value { - ModelMode::Default => AnthropicModelMode::Default, - ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens }, - } - } -} - -impl From for ModelMode { - fn from(value: AnthropicModelMode) -> Self { - match value { - AnthropicModelMode::Default => ModelMode::Default, - AnthropicModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens }, - } - } -} - pub struct AnthropicLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 49a976d5b18d2c7a2ca3162c632f53706b385cb0..a81e4b30d21f6ecdefc9361017c2d6fd979acced 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -42,7 +42,7 @@ use language_model::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use settings::{Settings, SettingsStore}; +use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore}; use smol::lock::OnceCell; use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; use theme::ThemeSettings; @@ -83,15 +83,14 @@ pub enum BedrockAuthMethod { Automatic, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub cache_configuration: Option, - pub max_output_tokens: Option, - pub default_temperature: Option, - pub mode: Option, +impl From for BedrockAuthMethod { + fn from(value: settings::BedrockAuthMethodContent) -> Self { + match value { + settings::BedrockAuthMethodContent::SingleSignOn => BedrockAuthMethod::SingleSignOn, + settings::BedrockAuthMethodContent::Automatic => BedrockAuthMethod::Automatic, + settings::BedrockAuthMethodContent::NamedProfile => BedrockAuthMethod::NamedProfile, + } + } } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 421e34e658fb604e21fdfc4af52b3c4c7874fd70..292512c78687c72c5237eaa694b44e953fad0b5e 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -32,6 +32,8 @@ use release_channel::AppVersion; use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use settings::SettingsStore; +pub use settings::ZedDotDevAvailableModel as AvailableModel; +pub use settings::ZedDotDevAvailableProvider as AvailableProvider; use smol::io::{AsyncReadExt, BufReader}; use std::pin::Pin; use std::str::FromStr as _; @@ -52,42 +54,6 @@ const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVI pub struct ZedDotDevSettings { pub available_models: Vec, } - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum AvailableProvider { - Anthropic, - OpenAi, - Google, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - /// The provider of the language model. - pub provider: AvailableProvider, - /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620 - pub name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - pub display_name: Option, - /// The size of the context window, indicating the maximum number of tokens the model can process. - pub max_tokens: usize, - /// The maximum number of output tokens allowed by the model. - pub max_output_tokens: Option, - /// The maximum number of completion tokens allowed by the model (o1-* only) - pub max_completion_tokens: Option, - /// Override this model with a different Anthropic model for tool calls. - pub tool_override: Option, - /// Indicates whether this custom model supports caching. - pub cache_configuration: Option, - /// The default temperature to use for this model. - pub default_temperature: Option, - /// Any extra beta headers to provide when using the model. - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ModelMode { diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 82bf067cd475fe031630767da9e4302afa4d78ec..cd8d3f91ac9a6ddca5aeb49b20bed05286cb59a6 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -16,8 +16,7 @@ use language_model::{ LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +pub use settings::DeepseekAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr; @@ -44,15 +43,6 @@ pub struct DeepSeekSettings { pub api_url: String, pub available_models: Vec, } - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, -} - pub struct DeepSeekLanguageModelProvider { http_client: Arc, state: Entity, diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 939cf0ca60d92d713b90a5d62e8ec7f6dac7ec46..ad87ceac438b922838416f07e761f8a29f4567a2 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -23,6 +23,7 @@ use language_model::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub use settings::GoogleAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::sync::{ @@ -59,32 +60,6 @@ pub enum ModelMode { }, } -impl From for GoogleModelMode { - fn from(value: ModelMode) -> Self { - match value { - ModelMode::Default => GoogleModelMode::Default, - ModelMode::Thinking { budget_tokens } => GoogleModelMode::Thinking { budget_tokens }, - } - } -} - -impl From for ModelMode { - fn from(value: GoogleModelMode) -> Self { - match value { - GoogleModelMode::Default => ModelMode::Default, - GoogleModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens }, - } - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - name: String, - display_name: Option, - max_tokens: u64, - mode: Option, -} - pub struct GoogleLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index e6e1726ff5b1f0a2edd51d5fde32da9734e56f9d..f98906f5be1cc811e124c89cf61050fdf98c967d 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -15,8 +15,7 @@ use language_model::{ LanguageModelRequest, RateLimiter, Role, }; use lmstudio::{ModelType, get_models}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +pub use settings::LmStudioAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr; @@ -40,15 +39,6 @@ pub struct LmStudioSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub supports_tool_calls: bool, - pub supports_images: bool, -} - pub struct LmStudioLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index c9824bf89ea7a919f4517f492a5091a2cda7b43b..803049579c7f7fd68bda8da2d67fa618b7d413f5 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -15,8 +15,7 @@ use language_model::{ RateLimiter, Role, StopReason, TokenUsage, }; use mistral::StreamResponse; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +pub use settings::MistralAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::collections::HashMap; use std::pin::Pin; @@ -38,18 +37,6 @@ pub struct MistralSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub supports_thinking: Option, -} - pub struct MistralLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index a80cacfc4a02521af74b32c34cc3360e9665a7d9..86226ea9199702ee329fc6d39d227314574fe8e9 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -14,8 +14,7 @@ use ollama::{ ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall, OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +pub use settings::OllamaAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; @@ -39,24 +38,6 @@ pub struct OllamaSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - /// The model name in the Ollama API (e.g. "llama3.2:latest") - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The Context Length parameter to the model (aka num_ctx or n_ctx) - pub max_tokens: u64, - /// The number of seconds to keep the connection open after the last request - pub keep_alive: Option, - /// Whether the model supports tools - pub supports_tools: Option, - /// Whether the model supports vision - pub supports_images: Option, - /// Whether to enable think mode - pub supports_thinking: Option, -} - pub struct OllamaLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index fca1cf977cb5e3b32dc6f2335fb0d9188979bc9f..4c0de86a945de01272aef36598cf12513fa2b70d 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -15,8 +15,7 @@ use language_model::{ }; use menu; use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use settings::OpenAiAvailableModel as AvailableModel; use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr as _; @@ -38,16 +37,6 @@ pub struct OpenAiSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub reasoning_effort: Option, -} - pub struct OpenAiLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 4ebb11a07b66ec7054ca65437ec887a415fa3f5c..9407376f301c04c10b198bbb3155a76dc1537abd 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -13,8 +13,6 @@ use language_model::{ }; use menu; use open_ai::{ResponseStreamEvent, stream_completion}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::sync::Arc; @@ -24,6 +22,8 @@ use util::ResultExt; use crate::AllLanguageModelSettings; use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai}; +pub use settings::OpenAiCompatibleAvailableModel as AvailableModel; +pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities; #[derive(Default, Clone, Debug, PartialEq)] pub struct OpenAiCompatibleSettings { @@ -31,36 +31,6 @@ pub struct OpenAiCompatibleSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - #[serde(default)] - pub capabilities: ModelCapabilities, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct ModelCapabilities { - pub tools: bool, - pub images: bool, - pub parallel_tool_calls: bool, - pub prompt_cache_key: bool, -} - -impl Default for ModelCapabilities { - fn default() -> Self { - Self { - tools: true, - images: false, - parallel_tool_calls: false, - prompt_cache_key: false, - } - } -} - pub struct OpenAiCompatibleLanguageModelProvider { id: LanguageModelProviderId, name: LanguageModelProviderName, diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 0ebf379b393791558cba6ff36ab31d278162386e..ff51c4a5f8eb009d3a7043ddc99357ab24e4b661 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -15,12 +15,9 @@ use language_model::{ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; use open_router::{ - Model, ModelMode as OpenRouterModelMode, Provider, ResponseStreamEvent, list_models, - stream_completion, + Model, ModelMode as OpenRouterModelMode, ResponseStreamEvent, list_models, stream_completion, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr as _; use std::sync::Arc; @@ -39,51 +36,6 @@ pub struct OpenRouterSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub mode: Option, - pub provider: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ModelMode { - #[default] - Default, - Thinking { - budget_tokens: Option, - }, -} - -impl From for OpenRouterModelMode { - fn from(value: ModelMode) -> Self { - match value { - ModelMode::Default => OpenRouterModelMode::Default, - ModelMode::Thinking { budget_tokens } => { - OpenRouterModelMode::Thinking { budget_tokens } - } - } - } -} - -impl From for ModelMode { - fn from(value: OpenRouterModelMode) -> Self { - match value { - OpenRouterModelMode::Default => ModelMode::Default, - OpenRouterModelMode::Thinking { budget_tokens } => { - ModelMode::Thinking { budget_tokens } - } - } - } -} - pub struct OpenRouterLanguageModelProvider { http_client: Arc, state: gpui::Entity, @@ -297,7 +249,7 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { max_tokens: model.max_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, - mode: model.mode.clone().unwrap_or_default().into(), + mode: model.mode.clone().unwrap_or_default(), provider: model.provider.clone(), }); } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 84f3175d1e5493fd55cafd2ea9c4a0604d2a97b4..ba0bb78bc75eb69d79d17305a34b44f220348747 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -12,13 +12,12 @@ use language_model::{ }; use menu; use open_ai::ResponseStreamEvent; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::sync::Arc; use strum::IntoEnumIterator; use vercel::Model; +pub use settings::VercelAvailableModel as AvailableModel; use ui::{ElevationIndex, List, Tooltip, prelude::*}; use ui_input::SingleLineInput; use util::ResultExt; @@ -34,15 +33,6 @@ pub struct VercelSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, -} - pub struct VercelLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index bb17f22c7f3fdbb0296b1e0bb290fbce9a979ddf..748d77d53e4f1d708035ac8507ff32d6c52081a7 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -12,13 +12,12 @@ use language_model::{ }; use menu; use open_ai::ResponseStreamEvent; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::sync::Arc; use strum::IntoEnumIterator; use x_ai::Model; +pub use settings::XaiAvailableModel as AvailableModel; use ui::{ElevationIndex, List, Tooltip, prelude::*}; use ui_input::SingleLineInput; use util::ResultExt; @@ -34,15 +33,6 @@ pub struct XAiSettings { pub available_models: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, -} - pub struct XAiLanguageModelProvider { http_client: Arc, state: gpui::Entity, diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index cfe66c91a36d4da562cba84363f79bd1d5b4e1ce..808a1e88d7c039ea2576aae9472d4ee73437accb 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -1,27 +1,16 @@ use std::sync::Arc; -use anyhow::Result; use collections::HashMap; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +use util::MergeFrom; use crate::provider::{ - self, - anthropic::AnthropicSettings, - bedrock::AmazonBedrockSettings, - cloud::{self, ZedDotDevSettings}, - deepseek::DeepSeekSettings, - google::GoogleSettings, - lmstudio::LmStudioSettings, - mistral::MistralSettings, - ollama::OllamaSettings, - open_ai::OpenAiSettings, - open_ai_compatible::OpenAiCompatibleSettings, - open_router::OpenRouterSettings, - vercel::VercelSettings, - x_ai::XAiSettings, + anthropic::AnthropicSettings, bedrock::AmazonBedrockSettings, cloud::ZedDotDevSettings, + deepseek::DeepSeekSettings, google::GoogleSettings, lmstudio::LmStudioSettings, + mistral::MistralSettings, ollama::OllamaSettings, open_ai::OpenAiSettings, + open_ai_compatible::OpenAiCompatibleSettings, open_router::OpenRouterSettings, + vercel::VercelSettings, x_ai::XAiSettings, }; /// Initializes the language model settings. @@ -46,281 +35,197 @@ pub struct AllLanguageModelSettings { pub zed_dot_dev: ZedDotDevSettings, } -#[derive( - Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi, SettingsKey, -)] -#[settings_key(key = "language_models")] -pub struct AllLanguageModelSettingsContent { - pub anthropic: Option, - pub bedrock: Option, - pub deepseek: Option, - pub google: Option, - pub lmstudio: Option, - pub mistral: Option, - pub ollama: Option, - pub open_router: Option, - pub openai: Option, - pub openai_compatible: Option, OpenAiCompatibleSettingsContent>>, - pub vercel: Option, - pub x_ai: Option, - #[serde(rename = "zed.dev")] - pub zed_dot_dev: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct AnthropicSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct AmazonBedrockSettingsContent { - available_models: Option>, - endpoint_url: Option, - region: Option, - profile: Option, - authentication_method: Option, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct OllamaSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct LmStudioSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct DeepseekSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct MistralSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct OpenAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct OpenAiCompatibleSettingsContent { - pub api_url: String, - pub available_models: Vec, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct VercelSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct GoogleSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct XAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct ZedDotDevSettingsContent { - available_models: Option>, -} - -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct OpenRouterSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - impl settings::Settings for AllLanguageModelSettings { const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]); - type FileContent = AllLanguageModelSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let language_models = content.language_models.clone().unwrap(); + let anthropic = language_models.anthropic.unwrap(); + let bedrock = language_models.bedrock.unwrap(); + let deepseek = language_models.deepseek.unwrap(); + let google = language_models.google.unwrap(); + let lmstudio = language_models.lmstudio.unwrap(); + let mistral = language_models.mistral.unwrap(); + let ollama = language_models.ollama.unwrap(); + let open_router = language_models.open_router.unwrap(); + let openai = language_models.openai.unwrap(); + let openai_compatible = language_models.openai_compatible.unwrap(); + let vercel = language_models.vercel.unwrap(); + let x_ai = language_models.x_ai.unwrap(); + let zed_dot_dev = language_models.zed_dot_dev.unwrap(); + Self { + anthropic: AnthropicSettings { + api_url: anthropic.api_url.unwrap(), + available_models: anthropic.available_models.unwrap_or_default(), + }, + bedrock: AmazonBedrockSettings { + available_models: bedrock.available_models.unwrap_or_default(), + region: bedrock.region, + endpoint: bedrock.endpoint_url, // todo(should be api_url) + profile_name: bedrock.profile, + role_arn: None, // todo(was never a setting for this...) + authentication_method: bedrock.authentication_method.map(Into::into), + }, + deepseek: DeepSeekSettings { + api_url: deepseek.api_url.unwrap(), + available_models: deepseek.available_models.unwrap_or_default(), + }, + google: GoogleSettings { + api_url: google.api_url.unwrap(), + available_models: google.available_models.unwrap_or_default(), + }, + lmstudio: LmStudioSettings { + api_url: lmstudio.api_url.unwrap(), + available_models: lmstudio.available_models.unwrap_or_default(), + }, + mistral: MistralSettings { + api_url: mistral.api_url.unwrap(), + available_models: mistral.available_models.unwrap_or_default(), + }, + ollama: OllamaSettings { + api_url: ollama.api_url.unwrap(), + available_models: ollama.available_models.unwrap_or_default(), + }, + open_router: OpenRouterSettings { + api_url: open_router.api_url.unwrap(), + available_models: open_router.available_models.unwrap_or_default(), + }, + openai: OpenAiSettings { + api_url: openai.api_url.unwrap(), + available_models: openai.available_models.unwrap_or_default(), + }, + openai_compatible: openai_compatible + .into_iter() + .map(|(key, value)| { + ( + key, + OpenAiCompatibleSettings { + api_url: value.api_url, + available_models: value.available_models, + }, + ) + }) + .collect(), + vercel: VercelSettings { + api_url: vercel.api_url.unwrap(), + available_models: vercel.available_models.unwrap_or_default(), + }, + x_ai: XAiSettings { + api_url: x_ai.api_url.unwrap(), + available_models: x_ai.available_models.unwrap_or_default(), + }, + zed_dot_dev: ZedDotDevSettings { + available_models: zed_dot_dev.available_models.unwrap_or_default(), + }, } + } - let mut settings = AllLanguageModelSettings::default(); - - for value in sources.defaults_and_customizations() { - // Anthropic - let anthropic = value.anthropic.clone(); - merge( - &mut settings.anthropic.api_url, - anthropic.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.anthropic.available_models, - anthropic.as_ref().and_then(|s| s.available_models.clone()), - ); - - // Bedrock - let bedrock = value.bedrock.clone(); - merge( - &mut settings.bedrock.profile_name, - bedrock.as_ref().map(|s| s.profile.clone()), - ); - merge( - &mut settings.bedrock.authentication_method, - bedrock.as_ref().map(|s| s.authentication_method.clone()), - ); - merge( - &mut settings.bedrock.region, - bedrock.as_ref().map(|s| s.region.clone()), - ); - merge( - &mut settings.bedrock.endpoint, - bedrock.as_ref().map(|s| s.endpoint_url.clone()), - ); - - // Ollama - let ollama = value.ollama.clone(); - - merge( - &mut settings.ollama.api_url, - value.ollama.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.ollama.available_models, - ollama.as_ref().and_then(|s| s.available_models.clone()), - ); - - // LM Studio - let lmstudio = value.lmstudio.clone(); - - merge( - &mut settings.lmstudio.api_url, - value.lmstudio.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.lmstudio.available_models, - lmstudio.as_ref().and_then(|s| s.available_models.clone()), - ); + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(models) = content.language_models.as_ref() else { + return; + }; - // DeepSeek - let deepseek = value.deepseek.clone(); + if let Some(anthropic) = models.anthropic.as_ref() { + self.anthropic + .available_models + .merge_from(&anthropic.available_models); + self.anthropic.api_url.merge_from(&anthropic.api_url); + } - merge( - &mut settings.deepseek.api_url, - value.deepseek.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.deepseek.available_models, - deepseek.as_ref().and_then(|s| s.available_models.clone()), - ); + if let Some(bedrock) = models.bedrock.clone() { + self.bedrock + .available_models + .merge_from(&bedrock.available_models); - // OpenAI - let openai = value.openai.clone(); - merge( - &mut settings.openai.api_url, - openai.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.openai.available_models, - openai.as_ref().and_then(|s| s.available_models.clone()), - ); + if let Some(endpoint_url) = bedrock.endpoint_url { + self.bedrock.endpoint = Some(endpoint_url) + } - // OpenAI Compatible - if let Some(openai_compatible) = value.openai_compatible.clone() { - for (id, openai_compatible_settings) in openai_compatible { - settings.openai_compatible.insert( - id, - OpenAiCompatibleSettings { - api_url: openai_compatible_settings.api_url, - available_models: openai_compatible_settings.available_models, - }, - ); - } + if let Some(region) = bedrock.region { + self.bedrock.region = Some(region) } - // Vercel - let vercel = value.vercel.clone(); - merge( - &mut settings.vercel.api_url, - vercel.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.vercel.available_models, - vercel.as_ref().and_then(|s| s.available_models.clone()), - ); + if let Some(profile_name) = bedrock.profile { + self.bedrock.profile_name = Some(profile_name); + } - // XAI - let x_ai = value.x_ai.clone(); - merge( - &mut settings.x_ai.api_url, - x_ai.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.x_ai.available_models, - x_ai.as_ref().and_then(|s| s.available_models.clone()), - ); + if let Some(auth_method) = bedrock.authentication_method { + self.bedrock.authentication_method = Some(auth_method.into()); + } + } - // ZedDotDev - merge( - &mut settings.zed_dot_dev.available_models, - value - .zed_dot_dev - .as_ref() - .and_then(|s| s.available_models.clone()), - ); - merge( - &mut settings.google.api_url, - value.google.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.google.available_models, - value - .google - .as_ref() - .and_then(|s| s.available_models.clone()), - ); + if let Some(deepseek) = models.deepseek.as_ref() { + self.deepseek + .available_models + .merge_from(&deepseek.available_models); + self.deepseek.api_url.merge_from(&deepseek.api_url); + } - // Mistral - let mistral = value.mistral.clone(); - merge( - &mut settings.mistral.api_url, - mistral.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.mistral.available_models, - mistral.as_ref().and_then(|s| s.available_models.clone()), - ); + if let Some(google) = models.google.as_ref() { + self.google + .available_models + .merge_from(&google.available_models); + self.google.api_url.merge_from(&google.api_url); + } - // OpenRouter - let open_router = value.open_router.clone(); - merge( - &mut settings.open_router.api_url, - open_router.as_ref().and_then(|s| s.api_url.clone()), - ); - merge( - &mut settings.open_router.available_models, - open_router - .as_ref() - .and_then(|s| s.available_models.clone()), - ); + if let Some(lmstudio) = models.lmstudio.as_ref() { + self.lmstudio + .available_models + .merge_from(&lmstudio.available_models); + self.lmstudio.api_url.merge_from(&lmstudio.api_url); } - Ok(settings) + if let Some(mistral) = models.mistral.as_ref() { + self.mistral + .available_models + .merge_from(&mistral.available_models); + self.mistral.api_url.merge_from(&mistral.api_url); + } + if let Some(ollama) = models.ollama.as_ref() { + self.ollama + .available_models + .merge_from(&ollama.available_models); + self.ollama.api_url.merge_from(&ollama.api_url); + } + if let Some(open_router) = models.open_router.as_ref() { + self.open_router + .available_models + .merge_from(&open_router.available_models); + self.open_router.api_url.merge_from(&open_router.api_url); + } + if let Some(openai) = models.openai.as_ref() { + self.openai + .available_models + .merge_from(&openai.available_models); + self.openai.api_url.merge_from(&openai.api_url); + } + if let Some(openai_compatible) = models.openai_compatible.clone() { + for (name, value) in openai_compatible { + self.openai_compatible.insert( + name, + OpenAiCompatibleSettings { + api_url: value.api_url, + available_models: value.available_models, + }, + ); + } + } + if let Some(vercel) = models.vercel.as_ref() { + self.vercel + .available_models + .merge_from(&vercel.available_models); + self.vercel.api_url.merge_from(&vercel.api_url); + } + if let Some(x_ai) = models.x_ai.as_ref() { + self.x_ai + .available_models + .merge_from(&x_ai.available_models); + self.x_ai.api_url.merge_from(&x_ai.api_url); + } + if let Some(zed_dot_dev) = models.zed_dot_dev.as_ref() { + self.zed_dot_dev + .available_models + .merge_from(&zed_dot_dev.available_models); + } } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/ollama/Cargo.toml b/crates/ollama/Cargo.toml index 2765f234009c99ba4c9feb6e4a9fac804da7e5a1..0cf1a5505d8035808b8c3f2a0407557535b15008 100644 --- a/crates/ollama/Cargo.toml +++ b/crates/ollama/Cargo.toml @@ -22,4 +22,5 @@ http_client.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true +settings.workspace = true workspace-hack.workspace = true diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index c61108d8bd59375256b7eb8b511527e8a0a119c2..18cd5811b29dd73fed01c1e4d30c5e0ae802b76a 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -3,33 +3,11 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; use serde::{Deserialize, Serialize}; use serde_json::Value; +pub use settings::KeepAlive; use std::time::Duration; pub const OLLAMA_API_URL: &str = "http://localhost:11434"; -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum KeepAlive { - /// Keep model alive for N seconds - Seconds(isize), - /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc. - Duration(String), -} - -impl KeepAlive { - /// Keep model alive until a new model is loaded or until Ollama shuts down - fn indefinite() -> Self { - Self::Seconds(-1) - } -} - -impl Default for KeepAlive { - fn default() -> Self { - Self::indefinite() - } -} - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Model { diff --git a/crates/open_ai/Cargo.toml b/crates/open_ai/Cargo.toml index bae00f0a8e888390cc8be4909e752567b94d1a27..776e308c490bf464c641800399ffaf8a1f301702 100644 --- a/crates/open_ai/Cargo.toml +++ b/crates/open_ai/Cargo.toml @@ -23,5 +23,6 @@ schemars = { workspace = true, optional = true } log.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true strum.workspace = true workspace-hack.workspace = true diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index fda0544be1748f3bf958cd159bc55edccdbb5c14..1cada03a60c54668d2675c2e076345a9507fcb43 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -3,6 +3,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; use serde::{Deserialize, Serialize}; use serde_json::Value; +pub use settings::OpenAiReasoningEffort as ReasoningEffort; use std::{convert::TryFrom, future::Future}; use strum::EnumIter; @@ -278,16 +279,6 @@ pub enum ToolChoice { Other(ToolDefinition), } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[serde(rename_all = "lowercase")] -pub enum ReasoningEffort { - Minimal, - Low, - Medium, - High, -} - #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ToolDefinition { diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml index 54348ea5a598d5c92a30ddd60ea68d3fa7b6046f..ec52ead3cba96f9026bc4df1a04d3af94eb44ed6 100644 --- a/crates/open_router/Cargo.toml +++ b/crates/open_router/Cargo.toml @@ -22,7 +22,8 @@ http_client.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true -thiserror.workspace = true +settings.workspace = true strum.workspace = true +thiserror.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 2e5f33b161b5947eceb64c9b2c2c4ae81116b813..0081c877756dab46433481ac58f2180877e7667f 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -3,10 +3,13 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; use serde::{Deserialize, Serialize}; use serde_json::Value; +pub use settings::DataCollection; +pub use settings::ModelMode; +pub use settings::OpenRouterAvailableModel as AvailableModel; +pub use settings::OpenRouterProvider as Provider; use std::{convert::TryFrom, io, time::Duration}; use strum::EnumString; use thiserror::Error; -use util::serde::default_true; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; @@ -65,41 +68,6 @@ impl From for String { } } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum DataCollection { - Allow, - Disallow, -} - -impl Default for DataCollection { - fn default() -> Self { - Self::Allow - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Provider { - #[serde(skip_serializing_if = "Option::is_none")] - order: Option>, - #[serde(default = "default_true")] - allow_fallbacks: bool, - #[serde(default)] - require_parameters: bool, - #[serde(default)] - data_collection: DataCollection, - #[serde(skip_serializing_if = "Option::is_none")] - only: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - ignore: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - quantizations: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - sort: Option, -} - #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Model { @@ -113,16 +81,6 @@ pub struct Model { pub provider: Option, } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub enum ModelMode { - #[default] - Default, - Thinking { - budget_tokens: Option, - }, -} - impl Model { pub fn default_fast() -> Self { Self::new( diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 652506e5937688ac0d1be8be5ddf1044db7140c9..cde160886c1590c8042492f87034786a38246914 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1,13 +1,16 @@ mod agent; mod editor; mod language; +mod language_model; mod project; mod terminal; mod theme; mod workspace; + pub use agent::*; pub use editor::*; pub use language::*; +pub use language_model::*; pub use project::*; pub use terminal::*; pub use theme::*; @@ -97,6 +100,8 @@ pub struct SettingsContent { pub line_indicator_format: Option, + pub language_models: Option, + pub outline_panel: Option, /// Configuration for the Message Editor diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs new file mode 100644 index 0000000000000000000000000000000000000000..0dcd95888a50cc1c3fd7cc985e362823ad6a732d --- /dev/null +++ b/crates/settings/src/settings_content/language_model.rs @@ -0,0 +1,393 @@ +use collections::HashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use std::sync::Arc; + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct AllLanguageModelSettingsContent { + pub anthropic: Option, + pub bedrock: Option, + pub deepseek: Option, + pub google: Option, + pub lmstudio: Option, + pub mistral: Option, + pub ollama: Option, + pub open_router: Option, + pub openai: Option, + pub openai_compatible: Option, OpenAiCompatibleSettingsContent>>, + pub vercel: Option, + pub x_ai: Option, + #[serde(rename = "zed.dev")] + pub zed_dot_dev: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct AnthropicSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct AnthropicAvailableModel { + /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc + pub name: String, + /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. + pub display_name: Option, + /// The model's context window size. + pub max_tokens: u64, + /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling. + pub tool_override: Option, + /// Configuration of Anthropic's caching API. + pub cache_configuration: Option, + pub max_output_tokens: Option, + pub default_temperature: Option, + #[serde(default)] + pub extra_beta_headers: Vec, + /// The model's mode (e.g. thinking) + pub mode: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct AmazonBedrockSettingsContent { + pub available_models: Option>, + pub endpoint_url: Option, + pub region: Option, + pub profile: Option, + pub authentication_method: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct BedrockAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub cache_configuration: Option, + pub max_output_tokens: Option, + pub default_temperature: Option, + pub mode: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum BedrockAuthMethodContent { + #[serde(rename = "named_profile")] + NamedProfile, + #[serde(rename = "sso")] + SingleSignOn, + /// IMDSv2, PodIdentity, env vars, etc. + #[serde(rename = "default")] + Automatic, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct OllamaSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OllamaAvailableModel { + /// The model name in the Ollama API (e.g. "llama3.2:latest") + pub name: String, + /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. + pub display_name: Option, + /// The Context Length parameter to the model (aka num_ctx or n_ctx) + pub max_tokens: u64, + /// The number of seconds to keep the connection open after the last request + pub keep_alive: Option, + /// Whether the model supports tools + pub supports_tools: Option, + /// Whether the model supports vision + pub supports_images: Option, + /// Whether to enable think mode + pub supports_thinking: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema)] +#[serde(untagged)] +pub enum KeepAlive { + /// Keep model alive for N seconds + Seconds(isize), + /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc. + Duration(String), +} + +impl KeepAlive { + /// Keep model alive until a new model is loaded or until Ollama shuts down + pub fn indefinite() -> Self { + Self::Seconds(-1) + } +} + +impl Default for KeepAlive { + fn default() -> Self { + Self::indefinite() + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct LmStudioSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct LmStudioAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub supports_tool_calls: bool, + pub supports_images: bool, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct DeepseekSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct DeepseekAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct MistralSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct MistralAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, + pub supports_tools: Option, + pub supports_images: Option, + pub supports_thinking: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct OpenAiSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OpenAiAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, + pub reasoning_effort: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum OpenAiReasoningEffort { + Minimal, + Low, + Medium, + High, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct OpenAiCompatibleSettingsContent { + pub api_url: String, + pub available_models: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OpenAiCompatibleAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, + #[serde(default)] + pub capabilities: OpenAiCompatibleModelCapabilities, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OpenAiCompatibleModelCapabilities { + pub tools: bool, + pub images: bool, + pub parallel_tool_calls: bool, + pub prompt_cache_key: bool, +} + +impl Default for OpenAiCompatibleModelCapabilities { + fn default() -> Self { + Self { + tools: true, + images: false, + parallel_tool_calls: false, + prompt_cache_key: false, + } + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct VercelSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct VercelAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct GoogleSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct GoogleAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub mode: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct XAiSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct XaiAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct ZedDotDevSettingsContent { + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ZedDotDevAvailableModel { + /// The provider of the language model. + pub provider: ZedDotDevAvailableProvider, + /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620 + pub name: String, + /// The name displayed in the UI, such as in the assistant panel model dropdown menu. + pub display_name: Option, + /// The size of the context window, indicating the maximum number of tokens the model can process. + pub max_tokens: usize, + /// The maximum number of output tokens allowed by the model. + pub max_output_tokens: Option, + /// The maximum number of completion tokens allowed by the model (o1-* only) + pub max_completion_tokens: Option, + /// Override this model with a different Anthropic model for tool calls. + pub tool_override: Option, + /// Indicates whether this custom model supports caching. + pub cache_configuration: Option, + /// The default temperature to use for this model. + pub default_temperature: Option, + /// Any extra beta headers to provide when using the model. + #[serde(default)] + pub extra_beta_headers: Vec, + /// The model's mode (e.g. thinking) + pub mode: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ZedDotDevAvailableProvider { + Anthropic, + OpenAi, + Google, +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct OpenRouterSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OpenRouterAvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, + pub supports_tools: Option, + pub supports_images: Option, + pub mode: Option, + pub provider: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct OpenRouterProvider { + #[serde(skip_serializing_if = "Option::is_none")] + order: Option>, + #[serde(default = "default_true")] + allow_fallbacks: bool, + #[serde(default)] + require_parameters: bool, + #[serde(default)] + data_collection: DataCollection, + #[serde(skip_serializing_if = "Option::is_none")] + only: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + ignore: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + quantizations: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + sort: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum DataCollection { + Allow, + Disallow, +} + +impl Default for DataCollection { + fn default() -> Self { + Self::Allow + } +} + +fn default_true() -> bool { + true +} + +/// Configuration for caching language model messages. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct LanguageModelCacheConfiguration { + pub max_cache_anchors: usize, + pub should_speculate: bool, + pub min_total_token: u64, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ModelMode { + #[default] + Default, + Thinking { + /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. + budget_tokens: Option, + }, +} From 72fb69f4642c403dafad9d4dec77d93a4b4f9260 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 11:32:55 -0600 Subject: [PATCH 069/117] Agent servers --- crates/agent_servers/src/claude.rs | 9 +++++++-- crates/agent_servers/src/custom.rs | 10 ++++++++-- crates/assistant_tools/src/edit_file_tool.rs | 1 + crates/language_models/src/provider/ollama.rs | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index c75c9539abe5fdd03293d98719d4a905b368c4a4..489839d82244fe76f6e9d1e9ea025a7b7c4a3bf7 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -45,8 +45,13 @@ impl AgentServer for ClaudeCode { } fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - update_settings_file::(fs, cx, |settings, _| { - settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string()) + update_settings_file(fs, cx, |settings, _| { + settings + .agent_servers + .get_or_insert_default() + .claude + .get_or_insert_default() + .default_mode = mode_id.map(|m| m.to_string()) }); } diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index f035952a7939201e4b7d990b97e1fc695105d505..aa2bbc0868dc64c5b415c445d19a357eb4b2ea85 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -49,8 +49,14 @@ impl crate::AgentServer for CustomAgentServer { fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { let name = self.name(); - update_settings_file::(fs, cx, move |settings, _| { - settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string()) + update_settings_file(fs, cx, move |settings, _| { + settings + .agent_servers + .get_or_insert_default() + .custom + .get_mut(&name) + .unwrap() + .default_mode = mode_id.map(|m| m.to_string()) }); } diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index d13f9891c3af1933ee49428c223d3e6737871047..43dcb4a3c4b07839f414a5df241da1fa25697979 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1540,6 +1540,7 @@ mod tests { store.update_user_settings::( cx, |settings| { + se settings.defaults.format_on_save = Some(FormatOnSave::On); settings.defaults.formatter = Some(language::language_settings::SelectedFormatter::Auto); diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 86226ea9199702ee329fc6d39d227314574fe8e9..2e377071789b6008965e40a45b37795f0c649acf 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -11,7 +11,7 @@ use language_model::{ LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, }; use ollama::{ - ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall, + ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, OllamaFunctionCall, OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion, }; pub use settings::OllamaAvailableModel as AvailableModel; From 7697cc2f379e7b8d3b141723a209061a3b020c90 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 11:39:13 -0600 Subject: [PATCH 070/117] assistant tools --- crates/agent2/src/agent.rs | 11 ++-- crates/agent2/src/thread.rs | 7 ++- crates/agent2/src/tools/edit_file_tool.rs | 49 ++++++++--------- crates/agent2/src/tools/grep_tool.rs | 13 +++-- .../agent2/src/tools/list_directory_tool.rs | 6 +-- crates/assistant_tools/src/edit_file_tool.rs | 52 +++++++++---------- crates/assistant_tools/src/grep_tool.rs | 15 +++--- .../src/list_directory_tool.rs | 14 ++--- crates/assistant_tools/src/read_file_tool.rs | 15 +++--- 9 files changed, 85 insertions(+), 97 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 44d4075b89306b6c6dd81d6de503888c036e6fbf..0b7680c7afa8dfa2c963a6ff0423ebca2dcfb846 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -6,7 +6,6 @@ use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, Token use acp_thread::{AcpThread, AgentModelSelector}; use action_log::ActionLog; use agent_client_protocol as acp; -use agent_settings::AgentSettings; use anyhow::{Context as _, Result, anyhow}; use collections::{HashSet, IndexMap}; use fs::Fs; @@ -873,13 +872,9 @@ impl AgentModelSelector for NativeAgentConnection { thread.set_model(model.clone(), cx); }); - update_settings_file::( - self.0.read(cx).fs.clone(), - cx, - move |settings, _cx| { - settings.set_model(model); - }, - ); + update_settings_file(self.0.read(cx).fs.clone(), cx, move |settings, _cx| { + settings.agent.get_or_insert_default().set_model(model); + }); Task::ready(Ok(())) } diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index b19d34adafe4a7b6567be9a1864b88ea2504bf12..18f993cbe33ca8bffc2235906baf76c627da0030 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -2477,8 +2477,11 @@ impl ToolCallEventStream { "always_allow" => { if let Some(fs) = fs.clone() { cx.update(|cx| { - update_settings_file::(fs, cx, |settings, _| { - settings.set_always_allow_tool_actions(true); + update_settings_file(fs, cx, |settings, _| { + settings + .agent + .get_or_insert_default() + .set_always_allow_tool_actions(true); }); })?; } diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 1e725b8f59d1219a0761334c5364940ee0e8bf6a..81f340b0b5c83648b1ec92210986b475b71c5bcf 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -791,14 +791,11 @@ mod tests { // First, test with format_on_save enabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::On); - settings.defaults.formatter = - Some(language::language_settings::SelectedFormatter::Auto); - }, - ); + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On); + settings.project.all_languages.defaults.formatter = + Some(language::language_settings::SelectedFormatter::Auto); + }); }); }); @@ -853,12 +850,10 @@ mod tests { // Next, test with format_on_save disabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::Off); - }, - ); + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.format_on_save = + Some(FormatOnSave::Off); + }); }); }); @@ -935,12 +930,13 @@ mod tests { // First, test with remove_trailing_whitespace_on_save enabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(true); - }, - ); + store.update_user_settings(cx, |settings| { + settings + .project + .all_languages + .defaults + .remove_trailing_whitespace_on_save = Some(true); + }); }); }); @@ -991,12 +987,13 @@ mod tests { // Next, test with remove_trailing_whitespace_on_save disabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(false); - }, - ); + store.update_user_settings(cx, |settings| { + settings + .project + .all_languages + .defaults + .remove_trailing_whitespace_on_save = Some(false); + }); }); }); diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 2bcfbd711bd7507be235481197e16cf76b391b6b..8ede37e66b904cfafd91b357a6bfdb201757eb0a 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -827,15 +827,14 @@ mod tests { cx.update(|cx| { use gpui::UpdateGlobal; - use project::WorktreeSettings; use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), @@ -1062,10 +1061,10 @@ mod tests { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 0fbe23fe205e6a9bd5a77e737460c17b997f9175..80cffa1fecc805920ecbc99a0afdfc47e05ee654 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -421,13 +421,13 @@ mod tests { // Configure settings explicitly cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), "**/.hidden_subdir".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 43dcb4a3c4b07839f414a5df241da1fa25697979..1fcd7bbf14fb2e37646902102d51392bc8a470f8 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -1445,8 +1445,8 @@ mod tests { fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { cx.update(|cx| { - // Set custom data directory (config will be under data_dir/config) paths::set_custom_data_dir(data_dir.to_str().unwrap()); + // Set custom data directory (config will be under data_dir/config) let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); @@ -1537,15 +1537,11 @@ mod tests { // First, test with format_on_save enabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - se - settings.defaults.format_on_save = Some(FormatOnSave::On); - settings.defaults.formatter = - Some(language::language_settings::SelectedFormatter::Auto); - }, - ); + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On); + settings.project.all_languages.defaults.formatter = + Some(language::language_settings::SelectedFormatter::Auto); + }); }); }); @@ -1604,12 +1600,10 @@ mod tests { // Next, test with format_on_save disabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::Off); - }, - ); + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.format_on_save = + Some(FormatOnSave::Off); + }); }); }); @@ -1680,12 +1674,13 @@ mod tests { // First, test with remove_trailing_whitespace_on_save enabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(true); - }, - ); + store.update_user_settings(cx, |settings| { + settings + .project + .all_languages + .defaults + .remove_trailing_whitespace_on_save = Some(true); + }); }); }); @@ -1742,12 +1737,13 @@ mod tests { // Next, test with remove_trailing_whitespace_on_save disabled cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(false); - }, - ); + store.update_user_settings(cx, |settings| { + settings + .project + .all_languages + .defaults + .remove_trailing_whitespace_on_save = Some(false); + }); }); }); diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index e43a54661ca146902a49fa1d975e44d486e18587..9f536df995cdfc5860cc3377dce65871386d50e0 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -314,7 +314,7 @@ mod tests { use gpui::{AppContext, TestAppContext, UpdateGlobal}; use language::{Language, LanguageConfig, LanguageMatcher}; use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project, WorktreeSettings}; + use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use unindent::Unindent; @@ -849,15 +849,14 @@ mod tests { cx.update(|cx| { use gpui::UpdateGlobal; - use project::WorktreeSettings; use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), @@ -1158,10 +1157,10 @@ mod tests { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index 5471d8923b557ac26d06a16c90fdeffb152049d1..9b4b501bfa55c3037bf6658686d92668cac966a6 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -230,7 +230,7 @@ mod tests { use gpui::{AppContext, TestAppContext, UpdateGlobal}; use indoc::indoc; use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project, WorktreeSettings}; + use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use util::path; @@ -507,13 +507,13 @@ mod tests { // Configure settings explicitly cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), "**/.hidden_subdir".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), @@ -698,10 +698,10 @@ mod tests { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 7222f061c7caba54ee2e3294378c4a7d957914f5..4ac2ec14ab9dde97b0ff89a40db356fef42d3741 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -299,7 +299,7 @@ mod test { use gpui::{AppContext, TestAppContext, UpdateGlobal}; use language::{Language, LanguageConfig, LanguageMatcher}; use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project, WorktreeSettings}; + use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use util::path; @@ -677,15 +677,14 @@ mod test { cx.update(|cx| { use gpui::UpdateGlobal; - use project::WorktreeSettings; use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), @@ -968,10 +967,10 @@ mod test { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); From 8b62dcb891b9989cb387510c5fe1e9b37c57fe9d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 11:41:45 -0600 Subject: [PATCH 071/117] agent2 --- crates/agent2/src/agent.rs | 12 ++++++++++-- crates/agent2/src/tools/grep_tool.rs | 2 +- crates/agent2/src/tools/list_directory_tool.rs | 8 ++++---- crates/agent2/src/tools/read_file_tool.rs | 13 ++++++------- .../agent_configuration/add_llm_provider_modal.rs | 1 + 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 0b7680c7afa8dfa2c963a6ff0423ebca2dcfb846..86fb50242c64917248df5c620782af066e639b54 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -20,7 +20,7 @@ use project::{Project, ProjectItem, ProjectPath, Worktree}; use prompt_store::{ ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, }; -use settings::update_settings_file; +use settings::{LanguageModelSelection, update_settings_file}; use std::any::Any; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -873,7 +873,15 @@ impl AgentModelSelector for NativeAgentConnection { }); update_settings_file(self.0.read(cx).fs.clone(), cx, move |settings, _cx| { - settings.agent.get_or_insert_default().set_model(model); + let provider = model.provider_id().0.to_string(); + let model = model.id().0.to_string(); + settings + .agent + .get_or_insert_default() + .set_model(LanguageModelSelection { + provider: provider.into(), + model, + }); }); Task::ready(Ok(())) diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs index 8ede37e66b904cfafd91b357a6bfdb201757eb0a..a1cd088c858683429103670c604ed3e08d179483 100644 --- a/crates/agent2/src/tools/grep_tool.rs +++ b/crates/agent2/src/tools/grep_tool.rs @@ -308,7 +308,7 @@ mod tests { use super::*; use gpui::{TestAppContext, UpdateGlobal}; use language::{Language, LanguageConfig, LanguageMatcher}; - use project::{FakeFs, Project, WorktreeSettings}; + use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use unindent::Unindent; diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs index 80cffa1fecc805920ecbc99a0afdfc47e05ee654..41198269d69c17e028cca250069c2e1000ac8dfe 100644 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ b/crates/agent2/src/tools/list_directory_tool.rs @@ -214,7 +214,7 @@ mod tests { use super::*; use gpui::{TestAppContext, UpdateGlobal}; use indoc::indoc; - use project::{FakeFs, Project, WorktreeSettings}; + use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use util::path; @@ -565,10 +565,10 @@ mod tests { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 87163e769c26b0cee053fcf149d047fc451c470f..0d1913572cb663fc11cde2342edfc74be5b92ae0 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -587,15 +587,14 @@ mod test { cx.update(|cx| { use gpui::UpdateGlobal; - use project::WorktreeSettings; use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), ]); - settings.private_files = Some(vec![ + settings.project.worktree.private_files = Some(vec![ "**/.mysecrets".to_string(), "**/*.privatekey".to_string(), "**/*.mysensitive".to_string(), @@ -803,10 +802,10 @@ mod test { // Set global settings cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); + settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]); }); }); }); diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 182831f488870997d175cce0ad7e1c94e392f1ea..080a32ca128a4563020eb35e4543c1a14e0df920 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -239,6 +239,7 @@ fn save_provider_to_settings( .map_err(|_| "Failed to write API key to keychain")?; cx.update(|cx| { update_settings_file::(fs, cx, |settings, _cx| { + settings.language_models.getO settings.openai_compatible.get_or_insert_default().insert( provider_name, OpenAiCompatibleSettingsContent { From 3968b9cd09f1c9341c6f220e2e779d68842be253 Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 17 Sep 2025 19:55:30 +0200 Subject: [PATCH 072/117] Add open WSL shortcut (#38342) Adds a shortcut to add a WSL distro for better wsl feature discoverability. - [x] Open wsl from open remote - [x] Open local folder in wsl action - [x] Open wsl shortcut (shortcuts to open remote) Release Notes: - N/A --- crates/recent_projects/src/recent_projects.rs | 12 ++++++ crates/recent_projects/src/remote_servers.rs | 38 ++++++++++++++++++- crates/zed_actions/src/lib.rs | 9 +++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 35ef024743475fc2036600724224f9f3c06bca4a..71a27737bdd1bfc82923a45459b7e100adbbf22e 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -90,6 +90,18 @@ pub fn init(cx: &mut App) { }); }); + #[cfg(target_os = "windows")] + cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenWsl, cx| { + let create_new_window = open_wsl.create_new_window; + with_active_or_new_workspace(cx, move |workspace, window, cx| { + let handle = cx.entity().downgrade(); + let fs = workspace.project().read(cx).fs().clone(); + workspace.toggle_modal(window, cx, |window, cx| { + RemoteServerProjects::wsl(create_new_window, fs, window, handle, cx) + }); + }); + }); + cx.on_action(|open_recent: &OpenRecent, cx| { let create_new_window = open_recent.create_new_window; with_active_or_new_workspace(cx, move |workspace, window, cx| { diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f7b9001444dea0371009a2ca878d12ab808a8823..d8dd37485e140d603e219ff6bdf8c9780da51528 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -522,12 +522,48 @@ impl Mode { } impl RemoteServerProjects { + #[cfg(target_os = "windows")] + pub fn wsl( + create_new_window: bool, + fs: Arc, + window: &mut Window, + workspace: WeakEntity, + cx: &mut Context, + ) -> Self { + Self::new_inner( + Mode::AddWslDistro(AddWslDistro::new(window, cx)), + create_new_window, + fs, + window, + workspace, + cx, + ) + } + pub fn new( create_new_window: bool, fs: Arc, window: &mut Window, workspace: WeakEntity, cx: &mut Context, + ) -> Self { + Self::new_inner( + Mode::default_mode(&BTreeSet::new(), cx), + create_new_window, + fs, + window, + workspace, + cx, + ) + } + + fn new_inner( + mode: Mode, + create_new_window: bool, + fs: Arc, + window: &mut Window, + workspace: WeakEntity, + cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config; @@ -558,7 +594,7 @@ impl RemoteServerProjects { }); Self { - mode: Mode::default_mode(&BTreeSet::new(), cx), + mode, focus_handle, workspace, retained_connections: Vec::new(), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 2015f6db3b6c5754fe6b7e433866f05f43de440f..81cca94f067de206a52241d202acf517dc80c614 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -512,4 +512,13 @@ pub mod wsl_actions { #[serde(default)] pub create_new_window: bool, } + + /// Open a wsl distro. + #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] + #[action(namespace = projects)] + #[serde(deny_unknown_fields)] + pub struct OpenWsl { + #[serde(default)] + pub create_new_window: bool, + } } From f05323745437b28c4f749d7d0098743b4bac65e8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 12:02:51 -0600 Subject: [PATCH 073/117] Onboarding --- crates/agent_settings/src/agent_settings.rs | 6 +- crates/agent_ui/src/acp/thread_view.rs | 4 +- .../add_llm_provider_modal.rs | 29 ++-- .../configure_context_server_modal.rs | 21 ++- crates/agent_ui/src/agent_panel.rs | 9 +- .../src/context_server_configuration.rs | 3 +- crates/onboarding/Cargo.toml | 3 + crates/onboarding/src/ai_setup_page.rs | 4 +- crates/onboarding/src/base_keymap_picker.rs | 4 +- crates/onboarding/src/basics_page.rs | 24 +-- crates/onboarding/src/editing_page.rs | 56 +++---- crates/project/src/project_settings.rs | 12 ++ crates/settings/src/base_keymap_setting.rs | 14 ++ crates/settings/src/settings_content/agent.rs | 15 +- crates/theme/src/settings.rs | 144 +++++++++--------- 15 files changed, 188 insertions(+), 160 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 546594e663f642559bc9fbdba794ec574bcfd495..cf92c58df2a5a7d582036e1ed21a658223d55863 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -8,8 +8,8 @@ use language_model::LanguageModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ - AgentDockPosition, DefaultAgentView, LanguageModelParameters, LanguageModelSelection, - NotifyWhenAgentWaiting, Settings, SettingsContent, + DefaultAgentView, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, + Settings, SettingsContent, }; use util::MergeFrom; @@ -28,7 +28,7 @@ pub fn init(cx: &mut App) { pub struct AgentSettings { pub enabled: bool, pub button: bool, - pub dock: AgentDockPosition, + pub dock: DockPosition, pub default_width: Pixels, pub default_height: Pixels, pub default_model: Option, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d3986155d741393a01bce829a886aba4ee497948..fd848d2c42ab6ae4bea9255876793dba22022760 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -7,7 +7,7 @@ use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, AgentServerDelegate}; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; use anyhow::{Context as _, Result, anyhow, bail}; use arrayvec::ArrayVec; @@ -35,7 +35,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use project::{Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; use rope::Point; -use settings::{Settings as _, SettingsStore}; +use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; use std::cell::RefCell; use std::path::Path; use std::sync::Arc; diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 080a32ca128a4563020eb35e4543c1a14e0df920..373756b2c45ceeb65afebaf1f2d82b1fc16c017d 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -5,11 +5,8 @@ use collections::HashSet; use fs::Fs; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task}; use language_model::LanguageModelRegistry; -use language_models::{ - AllLanguageModelSettings, OpenAiCompatibleSettingsContent, - provider::open_ai_compatible::{AvailableModel, ModelCapabilities}, -}; -use settings::update_settings_file; +use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities}; +use settings::{OpenAiCompatibleSettingsContent, update_settings_file}; use ui::{ Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, }; @@ -238,15 +235,19 @@ fn save_provider_to_settings( task.await .map_err(|_| "Failed to write API key to keychain")?; cx.update(|cx| { - update_settings_file::(fs, cx, |settings, _cx| { - settings.language_models.getO - settings.openai_compatible.get_or_insert_default().insert( - provider_name, - OpenAiCompatibleSettingsContent { - api_url, - available_models: models, - }, - ); + update_settings_file(fs, cx, |settings, _cx| { + settings + .language_models + .get_or_insert_default() + .openai_compatible + .get_or_insert_default() + .insert( + provider_name, + OpenAiCompatibleSettingsContent { + api_url, + available_models: models, + }, + ); }); }) .ok(); diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 4d338840143fbcf007f7d5c66e2406ef4bb9fc88..ce8e167dab3ed2e4d84c4afd747cb266740f1d42 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -422,18 +422,17 @@ impl ConfigureContextServerModal { workspace.update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); let original_server_id = self.original_server_id.clone(); - update_settings_file::( - fs.clone(), - cx, - move |project_settings, _| { - if let Some(original_id) = original_server_id { - if original_id != id { - project_settings.context_servers.remove(&original_id.0); - } + update_settings_file(fs.clone(), cx, move |current, _| { + if let Some(original_id) = original_server_id { + if original_id != id { + current.project.context_servers.remove(&original_id.0); } - project_settings.context_servers.insert(id.0, settings); - }, - ); + } + current + .project + .context_servers + .insert(id.0, settings.into()); + }); }); } else if let Some(existing_server) = existing_server { self.context_server_store diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7dd72c53631fa3c818fa1f4573e65c41bb5f0944..bc4b345d81d8a20d9c1d7f27bbce55dca2d700d7 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,6 +10,7 @@ use project::agent_server_store::{ AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME, }; use serde::{Deserialize, Serialize}; +use settings::DefaultAgentView as DefaultView; use zed_actions::OpenBrowser; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; @@ -33,7 +34,7 @@ use agent::{ history_store::{HistoryEntryId, HistoryStore}, thread_store::{TextThreadStore, ThreadStore}, }; -use agent_settings::{AgentDockPosition, AgentSettings, DefaultAgentView}; +use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; @@ -1424,11 +1425,7 @@ impl Focusable for AgentPanel { } fn agent_panel_dock_position(cx: &App) -> DockPosition { - match AgentSettings::get_global(cx).dock { - AgentDockPosition::Left => DockPosition::Left, - AgentDockPosition::Bottom => DockPosition::Bottom, - AgentDockPosition::Right => DockPosition::Right, - } + AgentSettings::get_global(cx).dock } impl EventEmitter for AgentPanel {} diff --git a/crates/agent_ui/src/context_server_configuration.rs b/crates/agent_ui/src/context_server_configuration.rs index fe15e8606d4026054ef0d04f1aab08375a440cf7..d6994e46b0ae39a50c22033f0479cd18747e47e3 100644 --- a/crates/agent_ui/src/context_server_configuration.rs +++ b/crates/agent_ui/src/context_server_configuration.rs @@ -69,8 +69,9 @@ fn remove_context_server_settings( fs: Arc, cx: &mut App, ) { - update_settings_file::(fs, cx, move |settings, _| { + update_settings_file(fs, cx, move |settings, _| { settings + .project .context_servers .retain(|server_id, _| !context_server_ids.contains(server_id)); }); diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 4157be31723fe66eb8f12ddf581c292e8320be78..3354b985449e0e389f6c02f748ddf26630ff193c 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -45,3 +45,6 @@ workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true zlog.workspace = true + +[dev-dependencies] +db = {workspace = true, features = ["test-support"]} diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 3631ad00dfb8662d5d4142a4cbd11186c1b1b137..6ed5e5e240201752938ea7e589c9f98b674b919c 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -264,8 +264,8 @@ pub(crate) fn render_ai_setup_page( ); let fs = ::global(cx); - update_settings_file::(fs, cx, move |ai_settings, _| { - ai_settings.disable_ai = Some(enabled); + update_settings_file(fs, cx, move |settings, _| { + settings.disable_ai = Some(enabled); }); }, ) diff --git a/crates/onboarding/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs index 950ed1d3133549a566808bd7cee002437708a2ad..63a2894a93504bd658dbf8199534efddf4746f1d 100644 --- a/crates/onboarding/src/base_keymap_picker.rs +++ b/crates/onboarding/src/base_keymap_picker.rs @@ -186,8 +186,8 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { value = base_keymap.to_string() ); - update_settings_file::(self.fs.clone(), cx, move |setting, _| { - setting.base_keymap = Some(base_keymap) + update_settings_file(self.fs.clone(), cx, move |setting, _| { + setting.base_keymap = Some(base_keymap.into()) }); } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index aef9dcca86ce49a70f1a508c0a43614737a653c7..09ca95b1a2a44be8bdcfec36a3b032de67b91222 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -194,27 +194,27 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement fn write_mode_change(mode: ThemeMode, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, _cx| { - settings.set_mode(mode); + update_settings_file(fs, cx, move |settings, _cx| { + theme::set_mode(settings, mode); }); } fn write_theme_change(theme: impl Into>, theme_mode: ThemeMode, cx: &mut App) { let fs = ::global(cx); let theme = theme.into(); - update_settings_file::(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, cx| { if theme_mode == ThemeMode::System { let (light_theme, dark_theme) = get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); - settings.theme = Some(ThemeSelection::Dynamic { + settings.theme.theme = Some(settings::ThemeSelection::Dynamic { mode: ThemeMode::System, light: ThemeName(light_theme.into()), dark: ThemeName(dark_theme.into()), }); } else { let appearance = *SystemAppearance::global(cx); - settings.set_theme(theme, appearance); + theme::set_theme(settings, theme, appearance); } }); } @@ -247,10 +247,10 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement ToggleState::Indeterminate => { return; }, }; - update_settings_file::( + update_settings_file( fs.clone(), cx, - move |setting, _| setting.metrics = Some(enabled), + move |setting, _| setting.telemetry.get_or_insert_default().metrics = Some(enabled), ); // This telemetry event shouldn't fire when it's off. If it does we'll be alerted @@ -286,10 +286,10 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement ToggleState::Indeterminate => { return; }, }; - update_settings_file::( + update_settings_file( fs.clone(), cx, - move |setting, _| setting.diagnostics = Some(enabled), + move |setting, _| setting.telemetry.get_or_insert_default().diagnostics = Some(enabled), ); // This telemetry event shouldn't fire when it's off. If it does we'll be alerted @@ -358,8 +358,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |setting, _| { - setting.base_keymap = Some(keymap_base); + update_settings_file(fs, cx, move |setting, _| { + setting.base_keymap = Some(keymap_base.into()); }); telemetry::event!("Welcome Keymap Changed", keymap = keymap_base); @@ -387,7 +387,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme return; } }; - update_settings_file::(fs.clone(), cx, move |setting, _| { + update_settings_file(fs.clone(), cx, move |setting, _| { setting.vim_mode = Some(vim_mode); }); diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 297016abd4a1499feb6f637d028056ca0b412d31..567444007bd75ae34709495103ac257a8c4701e0 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -34,13 +34,13 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) { curr_settings.minimap.show = show; EditorSettings::override_global(curr_settings, cx); - update_settings_file::(fs, cx, move |editor_settings, _| { + update_settings_file(fs, cx, move |settings, _| { telemetry::event!( "Welcome Minimap Clicked", - from = editor_settings.minimap.unwrap_or_default(), + from = settings.editor.minimap.clone().unwrap_or_default(), to = show ); - editor_settings.minimap.get_or_insert_default().show = Some(show); + settings.editor.minimap.get_or_insert_default().show = Some(show); }); } @@ -58,8 +58,10 @@ fn write_inlay_hints(enabled: bool, cx: &mut App) { curr_settings.defaults.inlay_hints.enabled = enabled; AllLanguageSettings::override_global(curr_settings, cx); - update_settings_file::(fs, cx, move |all_language_settings, cx| { - all_language_settings + update_settings_file(fs, cx, move |settings, cx| { + settings + .project + .all_languages .defaults .inlay_hints .get_or_insert_with(|| { @@ -80,64 +82,61 @@ fn write_git_blame(enabled: bool, cx: &mut App) { let fs = ::global(cx); let mut curr_settings = ProjectSettings::get_global(cx).clone(); - curr_settings - .git - .inline_blame - .get_or_insert_default() - .enabled = enabled; + curr_settings.git.inline_blame.enabled = enabled; ProjectSettings::override_global(curr_settings, cx); - update_settings_file::(fs, cx, move |project_settings, _| { - project_settings + update_settings_file(fs, cx, move |settings, _| { + settings .git + .get_or_insert_default() .inline_blame .get_or_insert_default() - .enabled = enabled; + .enabled = Some(enabled); }); } fn write_ui_font_family(font: SharedString, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |theme_settings, _| { + update_settings_file(fs, cx, move |settings, _| { telemetry::event!( "Welcome Font Changed", type = "ui font", - old = theme_settings.ui_font_family, + old = settings.theme.ui_font_family, new = font ); - theme_settings.ui_font_family = Some(FontFamilyName(font.into())); + settings.theme.ui_font_family = Some(FontFamilyName(font.into())); }); } fn write_ui_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |theme_settings, _| { - theme_settings.ui_font_size = Some(size.into()); + update_settings_file(fs, cx, move |settings, _| { + settings.theme.ui_font_size = Some(size.into()); }); } fn write_buffer_font_size(size: Pixels, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |theme_settings, _| { - theme_settings.buffer_font_size = Some(size.into()); + update_settings_file(fs, cx, move |settings, _| { + settings.theme.buffer_font_size = Some(size.into()); }); } fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |theme_settings, _| { + update_settings_file(fs, cx, move |settings, _| { telemetry::event!( "Welcome Font Changed", type = "editor font", - old = theme_settings.buffer_font_family, + old = settings.theme.buffer_font_family, new = font_family ); - theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); + settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into())); }); } @@ -153,8 +152,9 @@ fn write_font_ligatures(enabled: bool, cx: &mut App) { let fs = ::global(cx); let bit = if enabled { 1 } else { 0 }; - update_settings_file::(fs, cx, move |theme_settings, _| { - let mut features = theme_settings + update_settings_file(fs, cx, move |settings, _| { + let mut features = settings + .theme .buffer_font_features .as_mut() .map(|features| features.tag_value_list().to_vec()) @@ -166,7 +166,7 @@ fn write_font_ligatures(enabled: bool, cx: &mut App) { features.push(("calt".into(), bit)); } - theme_settings.buffer_font_features = Some(FontFeatures(Arc::new(features))); + settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features))); }); } @@ -180,8 +180,8 @@ fn read_format_on_save(cx: &App) -> bool { fn write_format_on_save(format_on_save: bool, cx: &mut App) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |language_settings, _| { - language_settings.defaults.format_on_save = Some(match format_on_save { + update_settings_file(fs, cx, move |settings, _| { + settings.project.all_languages.defaults.format_on_save = Some(match format_on_save { true => FormatOnSave::On, false => FormatOnSave::Off, }); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 3b88261e8122fabb9c56118b21482c78ebb63873..eb1dbe7534c6d16da6a23bb4d0ac0cf8167ac7f4 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -150,6 +150,18 @@ impl From for ContextServerSettings { } } } +impl Into for ContextServerSettings { + fn into(self) -> settings::ContextServerSettingsContent { + match self { + ContextServerSettings::Custom { enabled, command } => { + settings::ContextServerSettingsContent::Custom { enabled, command } + } + ContextServerSettings::Extension { enabled, settings } => { + settings::ContextServerSettingsContent::Extension { enabled, settings } + } + } + } +} impl ContextServerSettings { pub fn default_extension() -> Self { diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 4c212e60d3c7ac427cd71608640470da8f3cbe4b..2e0478a49a30f8e8b23e192b3215f41b3c1b5b65 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -42,6 +42,20 @@ impl From for BaseKeymap { } } } +impl Into for BaseKeymap { + fn into(self) -> BaseKeymapContent { + match self { + BaseKeymap::VSCode => BaseKeymapContent::VSCode, + BaseKeymap::JetBrains => BaseKeymapContent::JetBrains, + BaseKeymap::SublimeText => BaseKeymapContent::SublimeText, + BaseKeymap::Atom => BaseKeymapContent::Atom, + BaseKeymap::TextMate => BaseKeymapContent::TextMate, + BaseKeymap::Emacs => BaseKeymapContent::Emacs, + BaseKeymap::Cursor => BaseKeymapContent::Cursor, + BaseKeymap::None => BaseKeymapContent::None, + } + } +} impl Display for BaseKeymap { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 1c6296f67ec4e4dbfce3e16522f5a255324ba10c..ec50c3c2084473dee23be5304aa5bbcd4ea0a061 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -4,6 +4,8 @@ use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, path::PathBuf, sync::Arc}; +use crate::DockPosition; + #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct AgentSettingsContent { /// Whether the Agent is enabled. @@ -17,7 +19,7 @@ pub struct AgentSettingsContent { /// Where to dock the agent panel. /// /// Default: right - pub dock: Option, + pub dock: Option, /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 @@ -103,7 +105,7 @@ pub struct AgentSettingsContent { } impl AgentSettingsContent { - pub fn set_dock(&mut self, dock: AgentDockPosition) { + pub fn set_dock(&mut self, dock: DockPosition) { self.dock = Some(dock); } @@ -174,15 +176,6 @@ pub struct ContextServerPresetContent { pub tools: IndexMap, bool>, } -#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AgentDockPosition { - Left, - #[default] - Right, - Bottom, -} - #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DefaultAgentView { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 7328c4fcccda5130777c38cefad7ded10476d371..1204f647b1f26cef2c6481fe36e9bc05999e5b07 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -416,26 +416,32 @@ impl IconThemeSelection { } // impl ThemeSettingsContent { -// /// Sets the theme for the given appearance to the theme with the specified name. -// pub fn set_theme(&mut self, theme_name: impl Into>, appearance: Appearance) { -// if let Some(selection) = self.theme.as_mut() { -// let theme_to_update = match selection { -// ThemeSelection::Static(theme) => theme, -// ThemeSelection::Dynamic { mode, light, dark } => match mode { -// ThemeMode::Light => light, -// ThemeMode::Dark => dark, -// ThemeMode::System => match appearance { -// Appearance::Light => light, -// Appearance::Dark => dark, -// }, -// }, -// }; +/// Sets the theme for the given appearance to the theme with the specified name. +pub fn set_theme( + current: &mut SettingsContent, + theme_name: impl Into>, + appearance: Appearance, +) { + if let Some(selection) = current.theme.theme.as_mut() { + let theme_to_update = match selection { + settings::ThemeSelection::Static(theme) => theme, + settings::ThemeSelection::Dynamic { mode, light, dark } => match mode { + ThemeMode::Light => light, + ThemeMode::Dark => dark, + ThemeMode::System => match appearance { + Appearance::Light => light, + Appearance::Dark => dark, + }, + }, + }; -// *theme_to_update = ThemeName(theme_name.into()); -// } else { -// self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into()))); -// } -// } + *theme_to_update = ThemeName(theme_name.into()); + } else { + current.theme.theme = Some(settings::ThemeSelection::Static(ThemeName( + theme_name.into(), + ))); + } +} // /// Sets the icon theme for the given appearance to the icon theme with the specified name. // pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) { @@ -460,56 +466,58 @@ impl IconThemeSelection { // } // } -// /// Sets the mode for the theme. -// pub fn set_mode(&mut self, mode: ThemeMode) { -// if let Some(selection) = self.theme.as_mut() { -// match selection { -// ThemeSelection::Static(theme) => { -// // If the theme was previously set to a single static theme, -// // we don't know whether it was a light or dark theme, so we -// // just use it for both. -// self.theme = Some(ThemeSelection::Dynamic { -// mode, -// light: theme.clone(), -// dark: theme.clone(), -// }); -// } -// ThemeSelection::Dynamic { -// mode: mode_to_update, -// .. -// } => *mode_to_update = mode, -// } -// } else { -// self.theme = Some(ThemeSelection::Dynamic { -// mode, -// light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()), -// dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()), -// }); -// } +/// Sets the mode for the theme. +pub fn set_mode(content: &mut SettingsContent, mode: ThemeMode) { + let theme = content.theme.as_mut(); -// if let Some(selection) = self.icon_theme.as_mut() { -// match selection { -// IconThemeSelection::Static(icon_theme) => { -// // If the icon theme was previously set to a single static -// // theme, we don't know whether it was a light or dark -// // theme, so we just use it for both. -// self.icon_theme = Some(IconThemeSelection::Dynamic { -// mode, -// light: icon_theme.clone(), -// dark: icon_theme.clone(), -// }); -// } -// IconThemeSelection::Dynamic { -// mode: mode_to_update, -// .. -// } => *mode_to_update = mode, -// } -// } else { -// self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( -// DEFAULT_ICON_THEME_NAME.into(), -// ))); -// } -// } + if let Some(selection) = theme.theme.as_mut() { + match selection { + settings::ThemeSelection::Static(theme) => { + // If the theme was previously set to a single static theme, + // we don't know whether it was a light or dark theme, so we + // just use it for both. + *selection = settings::ThemeSelection::Dynamic { + mode, + light: theme.clone(), + dark: theme.clone(), + }; + } + settings::ThemeSelection::Dynamic { + mode: mode_to_update, + .. + } => *mode_to_update = mode, + } + } else { + theme.theme = Some(settings::ThemeSelection::Dynamic { + mode, + light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()), + dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()), + }); + } + + if let Some(selection) = theme.icon_theme.as_mut() { + match selection { + settings::IconThemeSelection::Static(icon_theme) => { + // If the icon theme was previously set to a single static + // theme, we don't know whether it was a light or dark + // theme, so we just use it for both. + *selection = settings::IconThemeSelection::Dynamic { + mode, + light: icon_theme.clone(), + dark: icon_theme.clone(), + }; + } + settings::IconThemeSelection::Dynamic { + mode: mode_to_update, + .. + } => *mode_to_update = mode, + } + } else { + theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName( + DEFAULT_ICON_THEME_NAME.into(), + ))); + } +} // } /// The buffer's line height. From 22449cd1e55bcbdfe9a4c9b7c091840c5dc9e3bf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 12:28:22 -0600 Subject: [PATCH 074/117] recent projects --- Cargo.lock | 1 + crates/agent_settings/src/agent_settings.rs | 6 +- crates/agent_ui/src/agent_configuration.rs | 35 +++++--- .../src/agent_configuration/tool_picker.rs | 19 ++-- crates/agent_ui/src/agent_panel.rs | 41 ++++----- crates/agent_ui/src/text_thread_editor.rs | 22 +++-- crates/collab/src/tests/editor_tests.rs | 20 ++--- crates/outline_panel/src/outline_panel.rs | 18 ++-- crates/recent_projects/src/recent_projects.rs | 9 +- .../recent_projects/src/remote_connections.rs | 86 ++++--------------- crates/recent_projects/src/remote_servers.rs | 39 ++++----- crates/remote/Cargo.toml | 1 + crates/remote/src/transport/ssh.rs | 24 +++--- crates/settings/src/settings_content.rs | 52 +++++++++++ .../settings/src/settings_content/project.rs | 2 +- crates/settings_ui/src/settings_ui.rs | 2 +- 16 files changed, 195 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f599385230bac594ad5759ae3f90b53217501979..c98a90bb09f0f8b4e9f43facb6bb67c5df2f8e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13507,6 +13507,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "settings", "shlex", "smol", "tempfile", diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index cf92c58df2a5a7d582036e1ed21a658223d55863..e5744458601b7fd208bd97c11da7d136cb329f05 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -8,8 +8,8 @@ use language_model::LanguageModel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ - DefaultAgentView, LanguageModelParameters, LanguageModelSelection, NotifyWhenAgentWaiting, - Settings, SettingsContent, + DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, + NotifyWhenAgentWaiting, Settings, SettingsContent, }; use util::MergeFrom; @@ -24,7 +24,7 @@ pub fn init(cx: &mut App) { AgentSettings::register(cx); } -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct AgentSettings { pub enabled: bool, pub button: bool, diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index faa6b524ff49fffbd5c8113e164b2c9ab158d71a..bbb84de4bd6b806c7fa095efe7301aac5ea3ab6b 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -413,8 +413,8 @@ impl AgentConfiguration { always_allow_tool_actions, move |state, _window, cx| { let allow = state == &ToggleState::Selected; - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.set_always_allow_tool_actions(allow); + update_settings_file(fs.clone(), cx, move |settings, _| { + settings.agent.get_or_insert_default.set_always_allow_tool_actions(allow); }); }, ) @@ -431,8 +431,11 @@ impl AgentConfiguration { single_file_review, move |state, _window, cx| { let allow = state == &ToggleState::Selected; - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.set_single_file_review(allow); + update_settings_file(fs.clone(), cx, move |settings, _| { + settings + .agent + .get_or_insert_default() + .set_single_file_review(allow); }); }, ) @@ -1279,7 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor( let settings = cx.global::(); let mut unique_server_name = None; - let edits = settings.edits_for_update::(&text, |file| { + let edits = settings.edits_for_update(&text, |settings| { let server_name: Option = (0..u8::MAX) .map(|i| { if i == 0 { @@ -1288,20 +1291,26 @@ async fn open_new_agent_servers_entry_in_settings_editor( format!("your_agent_{}", i).into() } }) - .find(|name| !file.custom.contains_key(name)); + .find(|name| { + !settings + .agent_servers + .is_some_and(|agent_servers| agent_servers.custom.contains_key(name)) + }); if let Some(server_name) = server_name { unique_server_name = Some(server_name.clone()); - file.custom.insert( - server_name, - CustomAgentServerSettings { - command: AgentServerCommand { + settings + .agent_servers + .get_or_insert_default() + .custom + .insert( + server_name, + settings::CustomAgentServerSettings { path: "path_to_executable".into(), args: vec![], env: Some(HashMap::default()), + default_mode: None, }, - default_mode: None, - }, - ); + ); } }); diff --git a/crates/agent_ui/src/agent_configuration/tool_picker.rs b/crates/agent_ui/src/agent_configuration/tool_picker.rs index 2ba92fa6b7993664d278cfd57d851dcfd9cb0922..c624948944c0624e75e385d1b4b15aa77fea9bcd 100644 --- a/crates/agent_ui/src/agent_configuration/tool_picker.rs +++ b/crates/agent_ui/src/agent_configuration/tool_picker.rs @@ -1,14 +1,11 @@ use std::{collections::BTreeMap, sync::Arc}; -use agent_settings::{ - AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent, - ContextServerPresetContent, -}; +use agent_settings::{AgentProfileId, AgentProfileSettings}; use assistant_tool::{ToolSource, ToolWorkingSet}; use fs::Fs; use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window}; use picker::{Picker, PickerDelegate}; -use settings::update_settings_file; +use settings::{AgentProfileContent, ContextServerPresetContent, update_settings_file}; use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt as _; @@ -266,15 +263,19 @@ impl PickerDelegate for ToolPickerDelegate { is_enabled }; - update_settings_file::(self.fs.clone(), cx, { + update_settings_file(self.fs.clone(), cx, { let profile_id = self.profile_id.clone(); let default_profile = self.profile_settings.clone(); let server_id = server_id.clone(); let tool_name = tool_name.clone(); - move |settings: &mut AgentSettingsContent, _cx| { - let profiles = settings.profiles.get_or_insert_default(); + move |settings, _cx| { + let profiles = settings + .agent + .get_or_insert_default() + .profiles + .get_or_insert_default(); let profile = profiles - .entry(profile_id) + .entry(profile_id.0) .or_insert_with(|| AgentProfileContent { name: default_profile.name.into(), tools: default_profile.tools, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index bc4b345d81d8a20d9c1d7f27bbce55dca2d700d7..576a523fb05b41db3eaf7e02d844aa8720db8a24 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1059,17 +1059,14 @@ impl AgentPanel { match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { if persist { - update_settings_file::( - self.fs.clone(), - cx, - move |settings, cx| { - let agent_font_size = - ThemeSettings::get_global(cx).agent_font_size(cx) + delta; - let _ = settings - .agent_font_size - .insert(Some(theme::clamp_font_size(agent_font_size).into())); - }, - ); + update_settings_file(self.fs.clone(), cx, move |settings, cx| { + let agent_font_size = + ThemeSettings::get_global(cx).agent_font_size(cx) + delta; + let _ = settings + .theme + .agent_font_size + .insert(Some(theme::clamp_font_size(agent_font_size).into())); + }); } else { theme::adjust_agent_font_size(cx, |size| size + delta); } @@ -1176,11 +1173,9 @@ impl AgentPanel { .is_none_or(|model| model.provider.id() != provider.id()) && let Some(model) = provider.default_model(cx) { - update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| settings.set_model(model), - ); + update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.agent.get_or_insert_default().set_model(model) + }); } self.new_thread(&NewThread::default(), window, cx); @@ -1425,7 +1420,7 @@ impl Focusable for AgentPanel { } fn agent_panel_dock_position(cx: &App) -> DockPosition { - AgentSettings::get_global(cx).dock + AgentSettings::get_global(cx).dock.into() } impl EventEmitter for AgentPanel {} @@ -1444,13 +1439,11 @@ impl Panel for AgentPanel { } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left => AgentDockPosition::Left, - DockPosition::Bottom => AgentDockPosition::Bottom, - DockPosition::Right => AgentDockPosition::Right, - }; - settings.set_dock(dock); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings + .agent + .get_or_insert_default() + .set_dock(position.into()); }); } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index d979db5e0468b696d32ed755aec1ef47e2fd3df3..b40b996ae74f93220d88c97e8ae7d99dd8576cf1 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -3,7 +3,7 @@ use crate::{ language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; -use agent_settings::{AgentSettings, CompletionMode}; +use agent_settings::CompletionMode; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases}; @@ -41,7 +41,10 @@ use project::{Project, Worktree}; use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate}; use rope::Point; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore, update_settings_file}; +use settings::{ + LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore, + update_settings_file, +}; use std::{ any::TypeId, cmp, @@ -294,11 +297,16 @@ impl TextThreadEditor { language_model_selector( |cx| LanguageModelRegistry::read_global(cx).default_model(), move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| settings.set_model(model.clone()), - ); + update_settings_file(fs.clone(), cx, move |settings, _| { + let provider = model.provider_id().0.to_string(); + let model = model.id().0.to_string(); + settings.agent.get_or_insert_default().set_model( + LanguageModelSelection { + provider: LanguageModelProviderSetting(provider), + model: model.clone(), + }, + ) + }); }, window, cx, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a3f63c527693a19bb7ac1cd87c104cee3d5cfa6e..a5255f419b38dfc14dac78177f87769b428fe31b 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1789,8 +1789,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, show_value_hints: true, edit_debounce_ms: 0, @@ -1806,8 +1806,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { show_value_hints: true, enabled: true, edit_debounce_ms: 0, @@ -2039,8 +2039,8 @@ async fn test_inlay_hint_refresh_is_forwarded( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { show_value_hints: true, enabled: false, edit_debounce_ms: 0, @@ -2056,8 +2056,8 @@ async fn test_inlay_hint_refresh_is_forwarded( }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { show_value_hints: true, enabled: true, edit_debounce_ms: 0, @@ -2242,14 +2242,14 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |settings| { - settings.lsp_document_colors = Some(DocumentColorsRenderMode::None); + settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None); }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings::(cx, |settings| { - settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay); + settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay); }); }); }); diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 8f501b2f970019c24c36e65fa94099b80454dfe2..c14e1a6be26332a776107d604e7ed2c7b4c04be5 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4846,17 +4846,13 @@ impl Panel for OutlinePanel { } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left, - DockPosition::Right => OutlinePanelDockPosition::Right, - }; - settings.dock = Some(dock); - }, - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left, + DockPosition::Right => OutlinePanelDockPosition::Right, + }; + settings.outline_panel.get_or_insert_default().dock = Some(dock); + }); } fn size(&self, _: &Window, cx: &App) -> Pixels { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index aa0ce7661b29123c25fdf20cbde5f53e6525d2d6..013c8f6724b55861add48ff94c8e79e4e5bb4756 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -626,7 +626,7 @@ mod tests { use dap::debugger_settings::DebuggerSettings; use editor::Editor; use gpui::{TestAppContext, UpdateGlobal, WindowHandle}; - use project::{Project, project_settings::ProjectSettings}; + use project::Project; use serde_json::json; use settings::SettingsStore; use util::path; @@ -640,8 +640,11 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.session.restore_unsaved_buffers = false + store.update_user_settings(cx, |settings| { + settings + .session + .get_or_insert_default() + .restore_unsaved_buffers = Some(false) }); }); }); diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index 3e6810239c80c72d74624bcc243157290fcd93fa..5fd52ada77e28ba3afb2b5b6337fe02f5a95a029 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result}; @@ -17,30 +16,26 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use release_channel::ReleaseChannel; use remote::{ ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform, - SshConnectionOptions, SshPortForwardOption, + SshConnectionOptions, }; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +pub use settings::SshConnection; use theme::ThemeSettings; use ui::{ ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled, Window, prelude::*, }; -use util::serde::default_true; +use util::MergeFrom; use workspace::{AppState, ModalView, Workspace}; -#[derive(Deserialize)] pub struct SshSettings { - pub ssh_connections: Option>, - /// Whether to read ~/.ssh/config for ssh connection sources. - #[serde(default = "default_true")] + pub ssh_connections: Vec, pub read_ssh_config: bool, } impl SshSettings { pub fn ssh_connections(&self) -> impl Iterator + use<> { - self.ssh_connections.clone().into_iter().flatten() + self.ssh_connections.clone().into_iter() } pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { @@ -75,67 +70,22 @@ impl SshSettings { } } -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)] -pub struct SshConnection { - pub host: SharedString, - #[serde(skip_serializing_if = "Option::is_none")] - pub username: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub port: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub projects: BTreeSet, - /// Name to use for this server in UI. - #[serde(skip_serializing_if = "Option::is_none")] - pub nickname: Option, - // By default Zed will download the binary to the host directly. - // If this is set to true, Zed will download the binary to your local machine, - // and then upload it over the SSH connection. Useful if your SSH server has - // limited outbound internet access. - #[serde(skip_serializing_if = "Option::is_none")] - pub upload_binary_over_ssh: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub port_forwards: Option>, -} - -impl From for SshConnectionOptions { - fn from(val: SshConnection) -> Self { - SshConnectionOptions { - host: val.host.into(), - username: val.username, - port: val.port, - password: None, - args: Some(val.args), - nickname: val.nickname, - upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(), - port_forwards: val.port_forwards, +impl Settings for SshSettings { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let remote = &content.remote; + Self { + ssh_connections: remote.ssh_connections.clone().unwrap_or_default(), + read_ssh_config: remote.read_ssh_config.unwrap(), } } -} - -#[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)] -pub struct SshProject { - pub paths: Vec, -} -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(None)] -pub struct RemoteSettingsContent { - pub ssh_connections: Option>, - pub read_ssh_config: Option, -} - -impl Settings for SshSettings { - type FileContent = RemoteSettingsContent; - - fn load(sources: SettingsSources, _: &mut App) -> Result { - sources.json_merge() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + if let Some(ssh_connections) = content.remote.ssh_connections.clone() { + self.ssh_connections.extend(ssh_connections) + } + self.read_ssh_config + .merge_from(&content.remote.read_ssh_config); } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } pub struct RemoteConnectionPrompt { diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 39032642b887350730c16a12d696253c256cfd72..d100c33905b89cff1b13bf6e1d0d1add4cf84605 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,7 +1,7 @@ use crate::{ remote_connections::{ - RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent, SshConnection, - SshConnectionHeader, SshProject, SshSettings, connect_over_ssh, open_remote_project, + RemoteConnectionModal, RemoteConnectionPrompt, SshConnection, SshConnectionHeader, + SshSettings, connect_over_ssh, open_remote_project, }, ssh_config::parse_ssh_config_hosts, }; @@ -20,7 +20,10 @@ use remote::{ RemoteClient, RemoteConnectionOptions, SshConnectionOptions, remote_client::ConnectionIdentifier, }; -use settings::{Settings, SettingsStore, update_settings_file, watch_config_file}; +use settings::{ + RemoteSettingsContent, Settings, SettingsStore, SshProject, update_settings_file, + watch_config_file, +}; use smol::stream::StreamExt as _; use std::{ borrow::Cow, @@ -173,13 +176,14 @@ impl ProjectPicker { cx.update(|_, cx| { let fs = app_state.fs.clone(); - update_settings_file::(fs, cx, { + update_settings_file(fs, cx, { let paths = paths .iter() .map(|path| path.to_string_lossy().to_string()) .collect(); move |setting, _| { if let Some(server) = setting + .remote .ssh_connections .as_mut() .and_then(|connections| connections.get_mut(ix)) @@ -987,7 +991,7 @@ impl RemoteServerProjects { else { return; }; - update_settings_file::(fs, cx, move |setting, cx| f(setting, cx)); + update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx)); } fn delete_ssh_server(&mut self, server: usize, cx: &mut Context) { @@ -1403,24 +1407,15 @@ impl RemoteServerProjects { cx: &mut Context, ) -> impl IntoElement { let ssh_settings = SshSettings::get_global(cx); - let mut should_rebuild = false; - - if ssh_settings - .ssh_connections - .as_ref() - .is_some_and(|connections| { - state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { connection, .. } => Some(connection), - RemoteEntry::SshConfig { .. } => None, - }) - .ne(connections.iter()) + + let mut should_rebuild = state + .servers + .iter() + .filter_map(|server| match server { + RemoteEntry::Project { connection, .. } => Some(connection), + RemoteEntry::SshConfig { .. } => None, }) - { - should_rebuild = true; - }; + .ne(&ssh_settings.ssh_connections); if !should_rebuild && ssh_settings.read_ssh_config { let current_ssh_hosts: BTreeSet = state diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 5985bcae827c42f4ae535b1dd859e436167e3fe5..3aea56c6411b1a084219734361cc2da9578a6b1e 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -35,6 +35,7 @@ rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true shlex.workspace = true smol.workspace = true tempfile.workspace = true diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 42c6da04b5c39b0b1133b2d13549585fa9433ef7..10946cd11c2a137f1c8951999bc47fa20a27fb67 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -15,8 +15,7 @@ use itertools::Itertools; use parking_lot::Mutex; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use rpc::proto::Envelope; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +pub use settings::SshPortForwardOption; use smol::{ fs, process::{self, Child, Stdio}, @@ -53,14 +52,19 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] -pub struct SshPortForwardOption { - #[serde(skip_serializing_if = "Option::is_none")] - pub local_host: Option, - pub local_port: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub remote_host: Option, - pub remote_port: u16, +impl From for SshConnectionOptions { + fn from(val: settings::SshConnection) -> Self { + SshConnectionOptions { + host: val.host.into(), + username: val.username, + port: val.port, + password: None, + args: Some(val.args), + nickname: val.nickname, + upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(), + port_forwards: val.port_forwards, + } + } } #[derive(Clone)] diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index cde160886c1590c8042492f87034786a38246914..eff8f9ff60f348efd09bf3be4195a29ebe6c76af 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -44,6 +44,9 @@ pub struct SettingsContent { #[serde(flatten)] pub editor: EditorSettingsContent, + #[serde(flatten)] + pub remote: RemoteSettingsContent, + /// Settings related to the file finder. pub file_finder: Option, @@ -712,3 +715,52 @@ pub enum ImageFileSizeUnit { /// Displays file size in decimal units (e.g., KB, MB). Decimal, } + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct RemoteSettingsContent { + pub ssh_connections: Option>, + pub read_ssh_config: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct SshConnection { + pub host: SharedString, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub projects: collections::BTreeSet, + /// Name to use for this server in UI. + #[serde(skip_serializing_if = "Option::is_none")] + pub nickname: Option, + // By default Zed will download the binary to the host directly. + // If this is set to true, Zed will download the binary to your local machine, + // and then upload it over the SSH connection. Useful if your SSH server has + // limited outbound internet access. + #[serde(skip_serializing_if = "Option::is_none")] + pub upload_binary_over_ssh: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub port_forwards: Option>, +} + +#[derive( + Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, +)] +pub struct SshProject { + pub paths: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] +pub struct SshPortForwardOption { + #[serde(skip_serializing_if = "Option::is_none")] + pub local_host: Option, + pub local_port: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_host: Option, + pub remote_port: u16, +} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 541b700950be45b83d54e93427f236b867cfb136..3628f70ac6a61da6cac46517eadb60581ca1bd11 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -139,7 +139,7 @@ pub struct DapSettings { pub args: Vec, } -#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettingsContent { /// Whether or not to restore unsaved buffers on restart. /// diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 790889cf93585b406c895e4754b814e952940f2c..4ba961773df2e605660a841f755be939f64faae0 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::Context as _; -use editor::{Editor, EditorSettingsControls}; +use editor::Editor; use feature_flags::{FeatureFlag, FeatureFlagAppExt}; use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; use settings::{ From f0b21508ec64b8293074d28d590321a868155c1a Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 17 Sep 2025 20:37:36 +0200 Subject: [PATCH 075/117] editor: Properly layout expand toggles with git blame enabled (#38349) Release Notes: - Fixed an issue where expand toggles were too large with the git blame deployed. --- crates/editor/src/element.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2212507e38b2e577eed1cf140eea362425152623..37ae90d70b91017016e46b409f67c7000c2b0f91 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3019,6 +3019,12 @@ impl EditorElement { .ilog10() + 1; + let git_gutter_width = Self::gutter_strip_width(line_height) + + gutter_dimensions + .git_blame_entries_width + .unwrap_or_default(); + let available_width = gutter_dimensions.left_padding - git_gutter_width; + buffer_rows .iter() .enumerate() @@ -3034,9 +3040,6 @@ impl EditorElement { ExpandExcerptDirection::UpAndDown => IconName::ExpandVertical, }; - let git_gutter_width = Self::gutter_strip_width(line_height); - let available_width = gutter_dimensions.left_padding - git_gutter_width; - let editor = self.editor.clone(); let is_wide = max_line_number_length >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32 From e9b643e99960f2af49ca2f44a3774ca356b41095 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 12:53:50 -0600 Subject: [PATCH 076/117] Agent UI --- assets/settings/default.json | 10 +- crates/agent_ui/src/agent_configuration.rs | 140 ++++++++---------- crates/agent_ui/src/agent_model_selector.rs | 15 +- crates/agent_ui/src/agent_panel.rs | 18 ++- crates/agent_ui/src/agent_ui.rs | 4 +- .../src/context_server_configuration.rs | 1 - crates/agent_ui/src/profile_selector.rs | 20 +-- crates/agent_ui/src/slash_command_settings.rs | 33 +++-- crates/collab/src/tests/editor_tests.rs | 4 +- .../src/edit_prediction_button.rs | 30 ++-- crates/language_models/src/provider/vercel.rs | 1 + crates/outline_panel/src/outline_panel.rs | 10 +- .../src/outline_panel_settings.rs | 10 +- .../settings/src/settings_content/project.rs | 16 ++ .../settings/src/settings_content/terminal.rs | 2 +- crates/theme/src/settings.rs | 48 +++--- .../theme_selector/src/icon_theme_selector.rs | 4 +- crates/theme_selector/src/theme_selector.rs | 4 +- crates/vim/src/vim.rs | 2 +- crates/zed/src/zed.rs | 20 ++- crates/zeta/src/init.rs | 17 +-- 21 files changed, 227 insertions(+), 182 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3a0c0754c9ee0b48275e76c12f1ad91f7945e53f..3c871937d0a10f2af143d61887f8a7e92df4accb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1790,6 +1790,7 @@ "anthropic": { "api_url": "https://api.anthropic.com" }, + "bedrock": {}, "google": { "api_url": "https://generativelanguage.googleapis.com" }, @@ -1811,7 +1812,14 @@ }, "mistral": { "api_url": "https://api.mistral.ai/v1" - } + }, + "vercel": { + "api_url": "https://api.v0.dev/v1" + }, + "x_ai": { + "api_url": "https://api.x.ai/v1" + }, + "zed.dev": {} }, "session": { /// Whether or not to restore unsaved buffers on restart. diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index bbb84de4bd6b806c7fa095efe7301aac5ea3ab6b..ea1ecb10e55241368bacc636b75d182b64210bc7 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -26,11 +26,9 @@ use language_model::{ use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ agent_server_store::{ - AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME, - CustomAgentServerSettings, GEMINI_NAME, + AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME, }, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, - project_settings::{ContextServerSettings, ProjectSettings}, }; use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ @@ -414,7 +412,7 @@ impl AgentConfiguration { move |state, _window, cx| { let allow = state == &ToggleState::Selected; update_settings_file(fs.clone(), cx, move |settings, _| { - settings.agent.get_or_insert_default.set_always_allow_tool_actions(allow); + settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow); }); }, ) @@ -454,8 +452,8 @@ impl AgentConfiguration { play_sound_when_agent_done, move |state, _window, cx| { let allow = state == &ToggleState::Selected; - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.set_play_sound_when_agent_done(allow); + update_settings_file(fs.clone(), cx, move |settings, _| { + settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow); }); }, ) @@ -474,8 +472,8 @@ impl AgentConfiguration { use_modifier_to_send, move |state, _window, cx| { let allow = state == &ToggleState::Selected; - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.set_use_modifier_to_send(allow); + update_settings_file(fs.clone(), cx, move |settings, _| { + settings.agent.get_or_insert_default().set_use_modifier_to_send(allow); }); }, ) @@ -777,14 +775,14 @@ impl AgentConfiguration { async move |cx| { uninstall_extension_task.await?; cx.update(|cx| { - update_settings_file::( + update_settings_file( fs.clone(), cx, { let context_server_id = context_server_id.clone(); move |settings, _| { - settings + settings.project .context_servers .remove(&context_server_id.0); } @@ -879,67 +877,53 @@ impl AgentConfiguration { .gap_1() .child(context_server_configuration_menu) .child( - Switch::new("context-server-switch", is_running.into()) - .color(SwitchColor::Accent) - .on_click({ - let context_server_manager = - self.context_server_store.clone(); - let fs = self.fs.clone(); - - move |state, _window, cx| { - let is_enabled = match state { - ToggleState::Unselected - | ToggleState::Indeterminate => { - context_server_manager.update( - cx, - |this, cx| { - this.stop_server( - &context_server_id, - cx, - ) - .log_err(); - }, - ); - false - } - ToggleState::Selected => { - context_server_manager.update( - cx, - |this, cx| { - if let Some(server) = - this.get_server(&context_server_id) - { - this.start_server(server, cx); - } - }, - ); - true - } - }; - update_settings_file::( - fs.clone(), - cx, - { - let context_server_id = - context_server_id.clone(); - - move |settings, _| { - settings - .context_servers - .entry(context_server_id.0) - .or_insert_with(|| { - ContextServerSettings::Extension { - enabled: is_enabled, - settings: serde_json::json!({}), - } - }) - .set_enabled(is_enabled); + Switch::new("context-server-switch", is_running.into()) + .color(SwitchColor::Accent) + .on_click({ + let context_server_manager = self.context_server_store.clone(); + let fs = self.fs.clone(); + + move |state, _window, cx| { + let is_enabled = match state { + ToggleState::Unselected + | ToggleState::Indeterminate => { + context_server_manager.update(cx, |this, cx| { + this.stop_server(&context_server_id, cx) + .log_err(); + }); + false + } + ToggleState::Selected => { + context_server_manager.update(cx, |this, cx| { + if let Some(server) = + this.get_server(&context_server_id) + { + this.start_server(server, cx); } - }, - ); - } - }), - ), + }); + true + } + }; + update_settings_file(fs.clone(), cx, { + let context_server_id = context_server_id.clone(); + + move |settings, _| { + settings + .project + .context_servers + .entry(context_server_id.0) + .or_insert_with(|| { + settings::ContextServerSettingsContent::Extension { + enabled: is_enabled, + settings: serde_json::json!({}), + } + }) + .set_enabled(is_enabled); + } + }); + } + }), + ), ), ) .map(|parent| { @@ -1236,15 +1220,12 @@ fn show_unable_to_uninstall_extension_with_context_server( let context_server_id = context_server_id.clone(); async move |_workspace_handle, cx| { cx.update(|cx| { - update_settings_file::( - fs, - cx, - move |settings, _| { - settings - .context_servers - .remove(&context_server_id.0); - }, - ); + update_settings_file(fs, cx, move |settings, _| { + settings + .project + .context_servers + .remove(&context_server_id.0); + }); })?; anyhow::Ok(()) } @@ -1294,6 +1275,7 @@ async fn open_new_agent_servers_entry_in_settings_editor( .find(|name| { !settings .agent_servers + .as_ref() .is_some_and(|agent_servers| agent_servers.custom.contains_key(name)) }); if let Some(server_name) = server_name { diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index 58b95d9b1f1b1e8abe7335a2299bee7545b7653e..fe25cadc3c1df785c89318882a246e2209cb42e6 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -2,7 +2,6 @@ use crate::{ ModelUsageContext, language_model_selector::{LanguageModelSelector, language_model_selector}, }; -use agent_settings::AgentSettings; use fs::Fs; use gpui::{Entity, FocusHandle, SharedString}; use picker::popover_menu::PickerPopoverMenu; @@ -39,14 +38,12 @@ impl AgentModelSelector { let model_id = model.id().0.to_string(); match &model_usage_context { ModelUsageContext::InlineAssistant => { - update_settings_file::( - fs.clone(), - cx, - move |settings, _cx| { - settings - .set_inline_assistant_model(provider.clone(), model_id); - }, - ); + update_settings_file(fs.clone(), cx, move |settings, _cx| { + settings + .agent + .get_or_insert_default() + .set_inline_assistant_model(provider.clone(), model_id); + }); } } }, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 576a523fb05b41db3eaf7e02d844aa8720db8a24..976dbb411990b4eb647bdec9af83f97c3a9bda84 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,7 +10,9 @@ use project::agent_server_store::{ AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME, }; use serde::{Deserialize, Serialize}; -use settings::DefaultAgentView as DefaultView; +use settings::{ + DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection, +}; use zed_actions::OpenBrowser; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; @@ -1087,8 +1089,8 @@ impl AgentPanel { cx: &mut Context, ) { if action.persist { - update_settings_file::(self.fs.clone(), cx, move |settings, _| { - settings.agent_font_size = None; + update_settings_file(self.fs.clone(), cx, move |settings, _| { + settings.theme.agent_font_size = None; }); } else { theme::reset_agent_font_size(cx); @@ -1174,7 +1176,15 @@ impl AgentPanel { && let Some(model) = provider.default_model(cx) { update_settings_file(self.fs.clone(), cx, move |settings, _| { - settings.agent.get_or_insert_default().set_model(model) + let provider = model.provider_id().0.to_string(); + let model = model.id().0.to_string(); + settings + .agent + .get_or_insert_default() + .set_model(LanguageModelSelection { + provider: LanguageModelProviderSetting(provider), + model, + }) }); } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 94efa767c5f1cd126695b8345230fba78583eb2f..f4c3fe1069eb42ad53c5771d4a511f88ff780664 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -24,7 +24,7 @@ use std::rc::Rc; use std::sync::Arc; use agent::ThreadId; -use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; +use agent_settings::{AgentProfileId, AgentSettings}; use assistant_slash_command::SlashCommandRegistry; use client::Client; use command_palette_hooks::CommandPaletteFilter; @@ -40,7 +40,7 @@ use project::agent_server_store::AgentServerCommand; use prompt_store::PromptBuilder; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings as _, SettingsStore}; +use settings::{LanguageModelSelection, Settings as _, SettingsStore}; use std::any::TypeId; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; diff --git a/crates/agent_ui/src/context_server_configuration.rs b/crates/agent_ui/src/context_server_configuration.rs index d6994e46b0ae39a50c22033f0479cd18747e47e3..3a1a8695172fcb68e52b6269b88b7b1576c2a5cb 100644 --- a/crates/agent_ui/src/context_server_configuration.rs +++ b/crates/agent_ui/src/context_server_configuration.rs @@ -5,7 +5,6 @@ use extension::ExtensionManifest; use fs::Fs; use gpui::WeakEntity; use language::LanguageRegistry; -use project::project_settings::ProjectSettings; use settings::update_settings_file; use ui::prelude::*; use util::ResultExt; diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index 85f74a0f7445df03a24bf083f31d56f97e5a07b2..af2354f7a854fd84483889f18e0f51e1c294d8a2 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -1,11 +1,10 @@ use crate::{ManageProfiles, ToggleProfileSelector}; use agent_settings::{ - AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, - builtin_profiles, + AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles, }; use fs::Fs; use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*}; -use settings::{Settings as _, SettingsStore, update_settings_file}; +use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file}; use std::sync::Arc; use ui::{ ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu, @@ -142,10 +141,13 @@ impl ProfileSelector { let fs = self.fs.clone(); let provider = self.provider.clone(); move |_window, cx| { - update_settings_file::(fs.clone(), cx, { + update_settings_file(fs.clone(), cx, { let profile_id = profile_id.clone(); move |settings, _cx| { - settings.set_profile(profile_id); + settings + .agent + .get_or_insert_default() + .set_profile(profile_id.0); } }); @@ -216,10 +218,10 @@ impl Render for ProfileSelector { } } -fn documentation_side(position: AgentDockPosition) -> DocumentationSide { +fn documentation_side(position: DockPosition) -> DocumentationSide { match position { - AgentDockPosition::Left => DocumentationSide::Right, - AgentDockPosition::Bottom => DocumentationSide::Left, - AgentDockPosition::Right => DocumentationSide::Left, + DockPosition::Left => DocumentationSide::Right, + DockPosition::Bottom => DocumentationSide::Left, + DockPosition::Right => DocumentationSide::Left, } } diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 939459a6129a85dc6b1d8297ad3659f3a0982067..1655c54da05fbec5f28c695d2668f10f86e4f1e4 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -1,8 +1,6 @@ -use anyhow::Result; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +use util::MergeFrom; /// Settings for slash commands. #[derive(Debug, Default, Clone)] @@ -18,18 +16,33 @@ pub struct CargoWorkspaceCommandSettings { pub enabled: bool, } +// todo!() I think this setting is bogus... default.json has "slash_commands": {"project"} impl Settings for SlashCommandSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { Self { cargo_workspace: CargoWorkspaceCommandSettings { - enabled: content.project.slash_commands.unwrap(), + enabled: content + .project + .slash_commands + .clone() + .unwrap() + .cargo_workspace + .unwrap() + .enabled + .unwrap(), }, } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { - todo!() + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(slash_command) = content.project.slash_commands.as_ref() else { + return; + }; + let Some(cargo_workspace) = slash_command.cargo_workspace.as_ref() else { + return; + }; + self.cargo_workspace + .enabled + .merge_from(&cargo_workspace.enabled); } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a5255f419b38dfc14dac78177f87769b428fe31b..f2b7a1028b31200fe001e4fa152521e3cf5e44be 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2241,14 +2241,14 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { + store.update_user_settings(cx, |settings| { settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None); }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { + store.update_user_settings(cx, |settings| { settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay); }); }); diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index d4c1763c405825f834b640dfb03b38bd3016288d..fb49b0c7dd3b14950361256eac53d0b3821a2cda 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -948,9 +948,12 @@ async fn open_disabled_globs_setting_in_editor( } fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredictionProvider) { - update_settings_file::(fs, cx, move |file, _| { - file.features - .get_or_insert(Default::default()) + update_settings_file(fs, cx, move |settings, _| { + settings + .project + .all_languages + .features + .get_or_insert_default() .edit_prediction_provider = Some(provider); }); } @@ -962,8 +965,11 @@ fn toggle_show_edit_predictions_for_language( ) { let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(Some(&language), cx); - update_settings_file::(fs, cx, move |file, _| { - file.languages + update_settings_file(fs, cx, move |settings, _| { + settings + .project + .all_languages + .languages .0 .entry(language.name()) .or_default() @@ -972,8 +978,11 @@ fn toggle_show_edit_predictions_for_language( } fn hide_copilot(fs: Arc, cx: &mut App) { - update_settings_file::(fs, cx, move |file, _| { - file.features + update_settings_file(fs, cx, move |settings, _| { + settings + .project + .all_languages + .features .get_or_insert(Default::default()) .edit_prediction_provider = Some(EditPredictionProvider::None); }); @@ -984,11 +993,12 @@ fn toggle_edit_prediction_mode(fs: Arc, mode: EditPredictionsMode, cx: & let current_mode = settings.edit_predictions_mode(); if current_mode != mode { - update_settings_file::(fs, cx, move |settings, _cx| { - if let Some(edit_predictions) = settings.edit_predictions.as_mut() { + update_settings_file(fs, cx, move |settings, _cx| { + if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut() + { edit_predictions.mode = mode; } else { - settings.edit_predictions = + settings.project.all_languages.edit_predictions = Some(language_settings::EditPredictionSettingsContent { mode, ..Default::default() diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index ba0bb78bc75eb69d79d17305a34b44f220348747..2daa74306c92d758c5d293dc5f49cd665de586e2 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -27,6 +27,7 @@ use crate::{AllLanguageModelSettings, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel"); const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel"); +// todo!() -> Remove default implementation #[derive(Default, Clone, Debug, PartialEq)] pub struct VercelSettings { pub api_url: String, diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index c14e1a6be26332a776107d604e7ed2c7b4c04be5..b4c0bb71ecb21f986d4a155fec045dd6b56987ea 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -38,7 +38,7 @@ use std::{ u32, }; -use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides}; +use outline_panel_settings::{LeftRightDockPosition, OutlinePanelSettings, ShowIndentGuides}; use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; use search::{BufferSearchBar, ProjectSearchView}; use serde::{Deserialize, Serialize}; @@ -4836,8 +4836,8 @@ impl Panel for OutlinePanel { fn position(&self, _: &Window, cx: &App) -> DockPosition { match OutlinePanelSettings::get_global(cx).dock { - OutlinePanelDockPosition::Left => DockPosition::Left, - OutlinePanelDockPosition::Right => DockPosition::Right, + LeftRightDockPosition::Left => DockPosition::Left, + LeftRightDockPosition::Right => DockPosition::Right, } } @@ -4848,8 +4848,8 @@ impl Panel for OutlinePanel { fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { let dock = match position { - DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left, - DockPosition::Right => OutlinePanelDockPosition::Right, + DockPosition::Left | DockPosition::Bottom => LeftRightDockPosition::Left, + DockPosition::Right => LeftRightDockPosition::Right, }; settings.outline_panel.get_or_insert_default().dock = Some(dock); }); diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 9cbac56e891c9d081c50cf558f00308ce777e96e..5505c6a49f714173991166467afd9459b9a5ae22 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -1,6 +1,6 @@ use editor::EditorSettings; use gpui::{App, Pixels}; -pub use settings::{OutlinePanelDockPosition, Settings, ShowIndentGuides}; +pub use settings::{LeftRightDockPosition, Settings, ShowIndentGuides}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; use util::MergeFrom; @@ -8,7 +8,7 @@ use util::MergeFrom; pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, - pub dock: OutlinePanelDockPosition, + pub dock: LeftRightDockPosition, pub file_icons: bool, pub folder_icons: bool, pub git_status: bool, @@ -48,7 +48,7 @@ impl ScrollbarVisibility for OutlinePanelSettings { } impl Settings for OutlinePanelSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { let panel = content.outline_panel.as_ref().unwrap(); Self { button: panel.button.unwrap(), @@ -64,7 +64,7 @@ impl Settings for OutlinePanelSettings { auto_reveal_entries: panel.auto_reveal_entries.unwrap(), auto_fold_dirs: panel.auto_fold_dirs.unwrap(), scrollbar: ScrollbarSettings { - show: panel.scrollbar.unwrap().show.unwrap(), + show: panel.scrollbar.unwrap().show.flatten().map(Into::into), }, expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(), } @@ -93,7 +93,7 @@ impl Settings for OutlinePanelSettings { self.auto_fold_dirs.merge_from(&panel.auto_fold_dirs); if let Some(scrollbar) = panel.scrollbar.as_ref() { - self.scrollbar.show.merge_from(&scrollbar.show); + self.scrollbar.show.merge_from(&scrollbar.show.flatten()); } } fn import_from_vscode( diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 3628f70ac6a61da6cac46517eadb60581ca1bd11..74a39daa1803465efefda2038eab0dcf132d2831 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -172,6 +172,22 @@ pub enum ContextServerSettingsContent { settings: serde_json::Value, }, } +impl ContextServerSettingsContent { + pub fn set_enabled(&mut self, enabled: bool) { + match self { + ContextServerSettingsContent::Custom { + enabled: custom_enabled, + command: _, + } => { + *custom_enabled = enabled; + } + ContextServerSettingsContent::Extension { + enabled: ext_enabled, + settings: _, + } => *ext_enabled = enabled, + } + } +} #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct ContextServerCommand { diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index 8a98265c564f17be39888a4b8ec8adbdd5e7eddb..f182d95c42a788ae6de962b734411efaeeab1b0b 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -160,7 +160,7 @@ pub enum WorkingDirectory { Always { directory: String }, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettingsContent { /// When to show the scrollbar in the terminal. /// diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 1204f647b1f26cef2c6481fe36e9bc05999e5b07..2efaa4f20c316952a300e669e8b93eee8e5bad39 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -443,28 +443,32 @@ pub fn set_theme( } } -// /// Sets the icon theme for the given appearance to the icon theme with the specified name. -// pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) { -// if let Some(selection) = self.icon_theme.as_mut() { -// let icon_theme_to_update = match selection { -// IconThemeSelection::Static(theme) => theme, -// IconThemeSelection::Dynamic { mode, light, dark } => match mode { -// ThemeMode::Light => light, -// ThemeMode::Dark => dark, -// ThemeMode::System => match appearance { -// Appearance::Light => light, -// Appearance::Dark => dark, -// }, -// }, -// }; - -// *icon_theme_to_update = IconThemeName(icon_theme_name.into()); -// } else { -// self.icon_theme = Some(IconThemeSelection::Static(IconThemeName( -// icon_theme_name.into(), -// ))); -// } -// } +/// Sets the icon theme for the given appearance to the icon theme with the specified name. +pub fn set_icon_theme( + current: &mut SettingsContent, + icon_theme_name: String, + appearance: Appearance, +) { + if let Some(selection) = current.theme.icon_theme.as_mut() { + let icon_theme_to_update = match selection { + settings::IconThemeSelection::Static(theme) => theme, + settings::IconThemeSelection::Dynamic { mode, light, dark } => match mode { + ThemeMode::Light => light, + ThemeMode::Dark => dark, + ThemeMode::System => match appearance { + Appearance::Light => light, + Appearance::Dark => dark, + }, + }, + }; + + *icon_theme_to_update = IconThemeName(icon_theme_name.into()); + } else { + current.theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName( + icon_theme_name.into(), + ))); + } +} /// Sets the mode for the theme. pub fn set_mode(content: &mut SettingsContent, mode: ThemeMode) { diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 5b26124079d9bf20edac38baa3dd49f2bb579456..5cd04aa8951dff890c68e8512e8082e5288f0f63 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -180,8 +180,8 @@ impl PickerDelegate for IconThemeSelectorDelegate { let appearance = Appearance::from(window.appearance()); - update_settings_file::(self.fs.clone(), cx, move |settings, _| { - settings.set_icon_theme(theme_name.to_string(), appearance); + update_settings_file(self.fs.clone(), cx, move |settings, _| { + theme::set_icon_theme(settings, theme_name.to_string(), appearance); }); self.selector diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 8cc450c97fb1fe0b8bdf7866156880471d0673d4..de41f3155f3c86cc5144c54c2b187ad0fd217b1c 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -238,8 +238,8 @@ impl PickerDelegate for ThemeSelectorDelegate { let appearance = Appearance::from(window.appearance()); - update_settings_file::(self.fs.clone(), cx, move |settings, _| { - settings.set_theme(theme_name.to_string(), appearance); + update_settings_file(self.fs.clone(), cx, move |settings, _| { + theme::set_theme(settings, theme_name.to_string(), appearance); }); self.selector diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8bffc45b7e1cdf30781e2d685404b8fb5526f2af..248e6acc58de7056f272d97153ed6342a18667c5 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -266,7 +266,7 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ToggleVimMode, _, cx| { let fs = workspace.app_state().fs.clone(); let currently_enabled = Vim::enabled(cx); - update_settings_file::(fs, cx, move |setting, _| { + update_settings_file(fs, cx, move |setting, _| { setting.vim_mode = Some(!currently_enabled) }) }); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 87664d20f0df08761ca6f5cc3a14ad59e978605f..5c1168713db687908c0d6a047f3585f920f32b80 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -727,9 +727,10 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, cx| { + update_settings_file(fs.clone(), cx, move |settings, cx| { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0); let _ = settings + .theme .ui_font_size .insert(theme::clamp_font_size(ui_font_size).0); }); @@ -742,9 +743,10 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, cx| { + update_settings_file(fs.clone(), cx, move |settings, cx| { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0); let _ = settings + .theme .ui_font_size .insert(theme::clamp_font_size(ui_font_size).0); }); @@ -757,8 +759,8 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::ResetUiFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.ui_font_size = None; + update_settings_file(fs.clone(), cx, move |settings, _| { + settings.theme.ui_font_size = None; }); } else { theme::reset_ui_font_size(cx); @@ -769,10 +771,11 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, cx| { + update_settings_file(fs.clone(), cx, move |settings, cx| { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0); let _ = settings + .theme .buffer_font_size .insert(theme::clamp_font_size(buffer_font_size).0); }); @@ -785,10 +788,11 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, cx| { + update_settings_file(fs.clone(), cx, move |settings, cx| { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0); let _ = settings + .theme .buffer_font_size .insert(theme::clamp_font_size(buffer_font_size).0); }); @@ -801,8 +805,8 @@ fn register_actions( let fs = app_state.fs.clone(); move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| { if action.persist { - update_settings_file::(fs.clone(), cx, move |settings, _| { - settings.buffer_font_size = None; + update_settings_file(fs.clone(), cx, move |settings, _| { + settings.theme.buffer_font_size = None; }); } else { theme::reset_buffer_font_size(cx); diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index f27667de6332bf4c3b8d2d705f281c9e3ba96a83..7c8d6fb9f0180b332187e7dbcd32f4b96925b231 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -44,15 +44,14 @@ pub fn init(cx: &mut App) { ); workspace.register_action(|workspace, _: &ResetOnboarding, _window, cx| { - update_settings_file::( - workspace.app_state().fs.clone(), - cx, - move |file, _| { - file.features - .get_or_insert(Default::default()) - .edit_prediction_provider = Some(EditPredictionProvider::None) - }, - ); + update_settings_file(workspace.app_state().fs.clone(), cx, move |settings, _| { + settings + .project + .all_languages + .features + .get_or_insert_default() + .edit_prediction_provider = Some(EditPredictionProvider::None) + }); }); }) .detach(); From 86a26499448209f465282dc06ca2a6bc91f7b460 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 17 Sep 2025 15:04:18 -0400 Subject: [PATCH 077/117] docs: Add whitespace_map (#38355) Adds docs for settings introduced in: - https://github.com/zed-industries/zed/pull/37704 Release Notes: - N/A --- docs/src/configuring-zed.md | 15 +++++++++++++++ docs/src/visual-customization.md | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 6c25d62e291f91f3faf4dc77e5a6dab3b8637ca8..58cde307662febbd99826b8b0954dddf4984cd9d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2626,6 +2626,7 @@ The following settings can be overridden for each specific language: - [`remove_trailing_whitespace_on_save`](#remove-trailing-whitespace-on-save) - [`show_edit_predictions`](#show-edit-predictions) - [`show_whitespaces`](#show-whitespaces) +- [`whitespace_map`](#whitespace-map) - [`soft_wrap`](#soft-wrap) - [`tab_size`](#tab-size) - [`use_autoclose`](#use-autoclose) @@ -3348,6 +3349,20 @@ Positive integer values 3. `none` 4. `boundary` +## Whitespace Map + +- Description: Specify the characters used to render whitespace when show_whitespaces is enabled. +- Setting: `whitespace_map` +- Default: + +```json +{ + "whitespace_map": { + "space": "•", + "tab": "→" + }, +``` + ## Soft Wrap - Description: Whether or not to automatically wrap lines of text to fit editor / preferred width. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 150b701168f49980844ea37c223efe00b6dc06cc..55f2dfe9b4d40d46a640520a99952964712c640e 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -185,6 +185,10 @@ TBD: Centered layout related settings // Visually show tabs and spaces (none, all, selection, boundary, trailing) "show_whitespaces": "selection", + "whitespace_map": { // Which characters to show when `show_whitespaces` enabled + "space": "•", + "tab": "→" + }, "unnecessary_code_fade": 0.3, // How much to fade out unused code. From f6d08fe59ce469a4a757739b34cfe1e3f0318d2c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Sep 2025 15:15:56 -0400 Subject: [PATCH 078/117] Remove `/cargo-workspace` slash command (#38354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the `/cargo-workspace` slash command. We never fully shipped this—with it requiring explicit opt-in via a setting—and it doesn't seem like the feature is needed in an agentic world. Release Notes: - Removed the `/cargo-workspace` slash command. --- Cargo.lock | 2 - assets/settings/default.json | 8 - crates/agent_ui/src/agent_ui.rs | 22 --- crates/agent_ui/src/slash_command_settings.rs | 37 ---- crates/assistant_slash_commands/Cargo.toml | 2 - .../src/assistant_slash_commands.rs | 2 - .../src/cargo_workspace_command.rs | 158 ------------------ 7 files changed, 231 deletions(-) delete mode 100644 crates/agent_ui/src/slash_command_settings.rs delete mode 100644 crates/assistant_slash_commands/src/cargo_workspace_command.rs diff --git a/Cargo.lock b/Cargo.lock index 6de762f43b31405d47c4ddcfc26a2be03eaadb80..13acb75ee146f412f704ed7d2c209ac59928a3cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,7 +901,6 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_slash_command", - "cargo_toml", "chrono", "collections", "context_server", @@ -924,7 +923,6 @@ dependencies = [ "settings", "smol", "text", - "toml 0.8.20", "ui", "util", "workspace", diff --git a/assets/settings/default.json b/assets/settings/default.json index daab56dc93976728667ed6995cb445f363ee2be8..6b521f9bcf1adc4fc2ebd85170f3aa187ca5b734 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -920,14 +920,6 @@ // Default: 4 "message_editor_min_lines": 4 }, - // The settings for slash commands. - "slash_commands": { - // Settings for the `/project` slash command. - "project": { - // Whether `/project` is enabled. - "enabled": false - } - }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 94efa767c5f1cd126695b8345230fba78583eb2f..a1de392158e80be11d69a9d98cbe292f2cadf540 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -14,7 +14,6 @@ mod message_editor; mod profile_selector; mod slash_command; mod slash_command_picker; -mod slash_command_settings; mod terminal_codegen; mod terminal_inline_assistant; mod text_thread_editor; @@ -46,7 +45,6 @@ use std::any::TypeId; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate}; pub use crate::inline_assistant::InlineAssistant; -use crate::slash_command_settings::SlashCommandSettings; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor}; use zed_actions; @@ -257,7 +255,6 @@ pub fn init( cx: &mut App, ) { AgentSettings::register(cx); - SlashCommandSettings::register(cx); assistant_context::init(client.clone(), cx); rules_library::init(cx); @@ -413,8 +410,6 @@ fn register_slash_commands(cx: &mut App) { slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true); - slash_command_registry - .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true); slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false); @@ -434,21 +429,4 @@ fn register_slash_commands(cx: &mut App) { } }) .detach(); - - update_slash_commands_from_settings(cx); - cx.observe_global::(update_slash_commands_from_settings) - .detach(); -} - -fn update_slash_commands_from_settings(cx: &mut App) { - let slash_command_registry = SlashCommandRegistry::global(cx); - let settings = SlashCommandSettings::get_global(cx); - - if settings.cargo_workspace.enabled { - slash_command_registry - .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true); - } else { - slash_command_registry - .unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand); - } } diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs deleted file mode 100644 index 9580ffef0f317fbe726c57041fad4f0fa438e143..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/slash_command_settings.rs +++ /dev/null @@ -1,37 +0,0 @@ -use anyhow::Result; -use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; - -/// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "slash_commands")] -pub struct SlashCommandSettings { - /// Settings for the `/cargo-workspace` slash command. - #[serde(default)] - pub cargo_workspace: CargoWorkspaceCommandSettings, -} - -/// Settings for the `/cargo-workspace` slash command. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] -pub struct CargoWorkspaceCommandSettings { - /// Whether `/cargo-workspace` is enabled. - #[serde(default)] - pub enabled: bool, -} - -impl Settings for SlashCommandSettings { - type FileContent = Self; - - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - SettingsSources::::json_merge_with( - [sources.default] - .into_iter() - .chain(sources.user) - .chain(sources.server), - ) - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index c054c3ced84825bcd131bdd76644c00595c4c4a9..f151515d4235b7ecb150539aceb1c5478960517b 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -14,7 +14,6 @@ path = "src/assistant_slash_commands.rs" [dependencies] anyhow.workspace = true assistant_slash_command.workspace = true -cargo_toml.workspace = true chrono.workspace = true collections.workspace = true context_server.workspace = true @@ -35,7 +34,6 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true text.workspace = true -toml.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs index fb00a912197e07942a67ad92418b85c4920ad66b..2bf2573e99d7a5a0140c1972967ec68523b0b56a 100644 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs @@ -1,4 +1,3 @@ -mod cargo_workspace_command; mod context_server_command; mod default_command; mod delta_command; @@ -12,7 +11,6 @@ mod streaming_example_command; mod symbols_command; mod tab_command; -pub use crate::cargo_workspace_command::*; pub use crate::context_server_command::*; pub use crate::default_command::*; pub use crate::delta_command::*; diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs deleted file mode 100644 index d58b2edc4c3dffd799dd9eb1c104686dc6488687..0000000000000000000000000000000000000000 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ /dev/null @@ -1,158 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use fs::Fs; -use gpui::{App, Entity, Task, WeakEntity}; -use language::{BufferSnapshot, LspAdapterDelegate}; -use project::{Project, ProjectPath}; -use std::{ - fmt::Write, - path::Path, - sync::{Arc, atomic::AtomicBool}, -}; -use ui::prelude::*; -use workspace::Workspace; - -pub struct CargoWorkspaceSlashCommand; - -impl CargoWorkspaceSlashCommand { - async fn build_message(fs: Arc, path_to_cargo_toml: &Path) -> Result { - let buffer = fs.load(path_to_cargo_toml).await?; - let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?; - - let mut message = String::new(); - writeln!(message, "You are in a Rust project.")?; - - if let Some(workspace) = cargo_toml.workspace { - writeln!( - message, - "The project is a Cargo workspace with the following members:" - )?; - for member in workspace.members { - writeln!(message, "- {member}")?; - } - - if !workspace.default_members.is_empty() { - writeln!(message, "The default members are:")?; - for member in workspace.default_members { - writeln!(message, "- {member}")?; - } - } - - if !workspace.dependencies.is_empty() { - writeln!( - message, - "The following workspace dependencies are installed:" - )?; - for dependency in workspace.dependencies.keys() { - writeln!(message, "- {dependency}")?; - } - } - } else if let Some(package) = cargo_toml.package { - writeln!( - message, - "The project name is \"{name}\".", - name = package.name - )?; - - let description = package - .description - .as_ref() - .and_then(|description| description.get().ok().cloned()); - if let Some(description) = description.as_ref() { - writeln!(message, "It describes itself as \"{description}\".")?; - } - - if !cargo_toml.dependencies.is_empty() { - writeln!(message, "The following dependencies are installed:")?; - for dependency in cargo_toml.dependencies.keys() { - writeln!(message, "- {dependency}")?; - } - } - } - - Ok(message) - } - - fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> { - let worktree = project.read(cx).worktrees(cx).next()?; - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path("Cargo.toml")?; - let path = ProjectPath { - worktree_id: worktree.id(), - path: entry.path.clone(), - }; - Some(Arc::from( - project.read(cx).absolute_path(&path, cx)?.as_path(), - )) - } -} - -impl SlashCommand for CargoWorkspaceSlashCommand { - fn name(&self) -> String { - "cargo-workspace".into() - } - - fn description(&self) -> String { - "insert project workspace metadata".into() - } - - fn menu_text(&self) -> String { - "Insert Project Workspace Metadata".into() - } - - fn complete_argument( - self: Arc, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) - } - - fn requires_argument(&self) -> bool { - false - } - - fn run( - self: Arc, - _arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _window: &mut Window, - cx: &mut App, - ) -> Task { - let output = workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - let fs = workspace.project().read(cx).fs().clone(); - let path = Self::path_to_cargo_toml(project, cx); - let output = cx.background_spawn(async move { - let path = path.with_context(|| "Cargo.toml not found")?; - Self::build_message(fs, &path).await - }); - - cx.foreground_executor().spawn(async move { - let text = output.await?; - let range = 0..text.len(); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - icon: IconName::FileTree, - label: "Project".into(), - metadata: None, - }], - run_commands_in_text: false, - } - .into_event_stream()) - }) - }); - output.unwrap_or_else(|error| Task::ready(Err(error))) - } -} From e0ac8f7a9dafdf212840b11fe9e00b4f28c8044f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 13:32:09 -0600 Subject: [PATCH 079/117] project panel --- crates/project_panel/src/project_panel.rs | 45 ++-- .../src/project_panel_settings.rs | 252 ++++++++---------- .../project_panel/src/project_panel_tests.rs | 81 +++--- crates/settings/src/settings_content.rs | 6 +- .../src/settings_content/workspace.rs | 92 ++++++- 5 files changed, 270 insertions(+), 206 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b53dfe6fdf8bdf81761299c075eee302a06a8956..90c02be50d7cbe723c1ea16a8fd19bef21238ba6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -36,12 +36,13 @@ use project::{ project_settings::GoToDiagnosticSeverityFilter, relativize_path, }; -use project_panel_settings::{ - ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides, -}; +use project_panel_settings::ProjectPanelSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore, update_settings_file}; +use settings::{ + DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides, + update_settings_file, +}; use smallvec::SmallVec; use std::{any::TypeId, time::Instant}; use std::{ @@ -343,8 +344,14 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| { let fs = workspace.app_state().fs.clone(); - update_settings_file::(fs, cx, move |setting, _| { - setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false)); + update_settings_file(fs, cx, move |setting, _| { + setting.project_panel.get_or_insert_default().hide_gitignore = Some( + !setting + .project_panel + .get_or_insert_default() + .hide_gitignore + .unwrap_or(false), + ); }) }); @@ -4457,8 +4464,8 @@ impl ProjectPanel { .indent_level(depth) .indent_step_size(px(settings.indent_size)) .spacing(match settings.entry_spacing { - project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense, - project_panel_settings::EntrySpacing::Standard => { + ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense, + ProjectPanelEntrySpacing::Standard => { ListItemSpacing::ExtraDense } }) @@ -5733,8 +5740,8 @@ impl EventEmitter for ProjectPanel {} impl Panel for ProjectPanel { fn position(&self, _: &Window, cx: &App) -> DockPosition { match ProjectPanelSettings::get_global(cx).dock { - ProjectPanelDockPosition::Left => DockPosition::Left, - ProjectPanelDockPosition::Right => DockPosition::Right, + DockSide::Left => DockPosition::Left, + DockSide::Right => DockPosition::Right, } } @@ -5743,17 +5750,13 @@ impl Panel for ProjectPanel { } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, - DockPosition::Right => ProjectPanelDockPosition::Right, - }; - settings.dock = Some(dock); - }, - ); + settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => DockSide::Left, + DockPosition::Right => DockSide::Right, + }; + settings.project_panel.get_or_insert_default().dock = Some(dock); + }); } fn size(&self, _: &Window, cx: &App) -> Pixels { diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 2a01d10a02449d47cad8a1c89bb9269f25db2725..b2646477697118e14abbbe6647a284d5a19a8866 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -2,40 +2,23 @@ use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; -use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelDockPosition { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowIndentGuides { - Always, - Never, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum EntrySpacing { - /// Comfortable spacing of entries. - #[default] - Comfortable, - /// The standard spacing of entries. - Standard, -} +use settings::{ + DockSide, ProjectPanelEntrySpacing, Settings, SettingsContent, ShowDiagnostics, + ShowIndentGuides, +}; +use ui::{ + px, + scrollbars::{ScrollbarVisibility, ShowScrollbar}, +}; +use util::MergeFrom; #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct ProjectPanelSettings { pub button: bool, pub hide_gitignore: bool, pub default_width: Pixels, - pub dock: ProjectPanelDockPosition, - pub entry_spacing: EntrySpacing, + pub dock: DockSide, + pub entry_spacing: ProjectPanelEntrySpacing, pub file_icons: bool, pub folder_icons: bool, pub git_status: bool, @@ -56,12 +39,6 @@ pub struct IndentGuidesSettings { pub show: ShowIndentGuides, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct IndentGuidesSettingsContent { - /// When to show the scrollbar in the project panel. - pub show: Option, -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettings { /// When to show the scrollbar in the project panel. @@ -70,105 +47,6 @@ pub struct ScrollbarSettings { pub show: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettingsContent { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option>, -} - -/// Whether to indicate diagnostic errors and/or warnings in project panel items. -/// -/// Default: all -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowDiagnostics { - /// Never mark the diagnostic errors/warnings in the project panel. - Off, - /// Mark files containing only diagnostic errors in the project panel. - Errors, - #[default] - /// Mark files containing diagnostic errors or warnings in the project panel. - All, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "project_panel")] -pub struct ProjectPanelSettingsContent { - /// Whether to show the project panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Whether to hide gitignore files in the project panel. - /// - /// Default: false - pub hide_gitignore: Option, - /// Customize default width (in pixels) taken by project panel - /// - /// Default: 240 - pub default_width: Option, - /// The position of project panel - /// - /// Default: left - pub dock: Option, - /// Spacing between worktree entries in the project panel. - /// - /// Default: comfortable - pub entry_spacing: Option, - /// Whether to show file icons in the project panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the project panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the project panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - pub indent_size: Option, - /// Whether to reveal it in the project panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Whether the project panel should open on startup. - /// - /// Default: true - pub starts_open: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Which files containing diagnostic errors/warnings to mark in the project panel. - /// - /// Default: all - pub show_diagnostics: Option, - /// Settings related to indent guides in the project panel. - pub indent_guides: Option, - /// Whether to hide the root entry when only one folder is open in the window. - /// - /// Default: false - pub hide_root: Option, - /// Whether to stick parent directories at top of the project panel. - /// - /// Default: true - pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, -} - impl ScrollbarVisibility for ProjectPanelSettings { fn visibility(&self, cx: &ui::App) -> ShowScrollbar { self.scrollbar @@ -178,32 +56,112 @@ impl ScrollbarVisibility for ProjectPanelSettings { } impl Settings for ProjectPanelSettings { - type FileContent = ProjectPanelSettingsContent; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { + let project_panel = content.project_panel.clone().unwrap(); + Self { + button: project_panel.button.unwrap(), + hide_gitignore: project_panel.hide_gitignore.unwrap(), + default_width: px(project_panel.default_width.unwrap()), + dock: project_panel.dock.unwrap(), + entry_spacing: project_panel.entry_spacing.unwrap(), + file_icons: project_panel.file_icons.unwrap(), + folder_icons: project_panel.folder_icons.unwrap(), + git_status: project_panel.git_status.unwrap(), + indent_size: project_panel.indent_size.unwrap(), + indent_guides: IndentGuidesSettings { + show: project_panel.indent_guides.unwrap().show.unwrap(), + }, + sticky_scroll: project_panel.sticky_scroll.unwrap(), + auto_reveal_entries: project_panel.auto_reveal_entries.unwrap(), + auto_fold_dirs: project_panel.auto_fold_dirs.unwrap(), + starts_open: project_panel.starts_open.unwrap(), + scrollbar: ScrollbarSettings { + show: project_panel + .scrollbar + .unwrap() + .show + .flatten() + .map(Into::into), + }, + show_diagnostics: project_panel.show_diagnostics.unwrap(), + hide_root: project_panel.hide_root.unwrap(), + drag_and_drop: project_panel.drag_and_drop.unwrap(), + } + } - fn load( - sources: SettingsSources, - _: &mut gpui::App, - ) -> anyhow::Result { - sources.json_merge() + fn refine(&mut self, content: &SettingsContent, _cx: &mut ui::App) { + let Some(project_panel) = content.project_panel.as_ref() else { + return; + }; + self.button.merge_from(&project_panel.button); + self.hide_gitignore + .merge_from(&project_panel.hide_gitignore); + self.default_width + .merge_from(&project_panel.default_width.map(px)); + self.dock.merge_from(&project_panel.dock); + self.entry_spacing.merge_from(&project_panel.entry_spacing); + self.file_icons.merge_from(&project_panel.file_icons); + self.folder_icons.merge_from(&project_panel.folder_icons); + self.git_status.merge_from(&project_panel.git_status); + self.indent_size.merge_from(&project_panel.indent_size); + self.sticky_scroll.merge_from(&project_panel.sticky_scroll); + self.auto_reveal_entries + .merge_from(&project_panel.auto_reveal_entries); + self.auto_fold_dirs + .merge_from(&project_panel.auto_fold_dirs); + self.starts_open.merge_from(&project_panel.starts_open); + self.show_diagnostics + .merge_from(&project_panel.show_diagnostics); + self.hide_root.merge_from(&project_panel.hide_root); + self.drag_and_drop.merge_from(&project_panel.drag_and_drop); + if let Some(show) = project_panel + .indent_guides + .as_ref() + .and_then(|indent| indent.show) + { + self.indent_guides.show = show; + } + if let Some(show) = project_panel + .scrollbar + .as_ref() + .and_then(|scrollbar| scrollbar.show) + { + self.scrollbar.show = show.map(Into::into) + } } - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { - vscode.bool_setting("explorer.excludeGitIgnore", &mut current.hide_gitignore); - vscode.bool_setting("explorer.autoReveal", &mut current.auto_reveal_entries); - vscode.bool_setting("explorer.compactFolders", &mut current.auto_fold_dirs); + fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { + if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") { + current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore); + } + if let Some(auto_reveal) = vscode.read_bool("explorer.autoReveal") { + current + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(auto_reveal); + } + if let Some(compact_folders) = vscode.read_bool("explorer.compactFolders") { + current.project_panel.get_or_insert_default().auto_fold_dirs = Some(compact_folders); + } if Some(false) == vscode.read_bool("git.decorations.enabled") { - current.git_status = Some(false); + current.project_panel.get_or_insert_default().git_status = Some(false); } if Some(false) == vscode.read_bool("problems.decorations.enabled") { - current.show_diagnostics = Some(ShowDiagnostics::Off); + current + .project_panel + .get_or_insert_default() + .show_diagnostics = Some(ShowDiagnostics::Off); } if let (Some(false), Some(false)) = ( vscode.read_bool("explorer.decorations.badges"), vscode.read_bool("explorer.decorations.colors"), ) { - current.git_status = Some(false); - current.show_diagnostics = Some(ShowDiagnostics::Off); + current.project_panel.get_or_insert_default().git_status = Some(false); + current + .project_panel + .get_or_insert_default() + .show_diagnostics = Some(ShowDiagnostics::Off); } } } diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index ad2a7d12ecce31cf1aa4458b3fd59e23f63ab08b..61684929a5e61e62c08d2f0e9d91def408448d8f 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -2,7 +2,7 @@ use super::*; use collections::HashSet; use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle}; use pretty_assertions::assert_eq; -use project::{FakeFs, WorktreeSettings}; +use project::FakeFs; use serde_json::json; use settings::SettingsStore; use std::path::{Path, PathBuf}; @@ -161,8 +161,8 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); }); }); @@ -3343,11 +3343,12 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(Vec::new()); - }); - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(false) + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(false); }); }) }); @@ -3465,8 +3466,11 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { cx.update(|_, cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(true) + store.update_user_settings(cx, |settings| { + settings + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(true) }); }) }); @@ -3579,13 +3583,14 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(Vec::new()); - worktree_settings.file_scan_inclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_inclusions = Some(vec!["always_included_but_ignored_dir/*".to_string()]); - }); - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(false) + settings + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(false) }); }) }); @@ -3654,8 +3659,11 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { cx.run_until_parked(); cx.update(|_, cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(true) + store.update_user_settings(cx, |settings| { + settings + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(true) }); }) }); @@ -3695,11 +3703,12 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(Vec::new()); - }); - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(false) + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings + .project_panel + .get_or_insert_default() + .auto_reveal_entries = Some(false) }); }) }); @@ -3896,8 +3905,8 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); }); }); @@ -6545,11 +6554,12 @@ fn init_test(cx: &mut TestAppContext) { Project::init_settings(cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_fold_dirs = Some(false); - }); - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(Vec::new()); + store.update_user_settings(cx, |settings| { + settings + .project_panel + .get_or_insert_default() + .auto_fold_dirs = Some(false); + settings.project.worktree.file_scan_exclusions = Some(Vec::new()); }); }); }); @@ -6567,11 +6577,12 @@ fn init_test_with_editor(cx: &mut TestAppContext) { Project::init_settings(cx); cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_fold_dirs = Some(false); - }); - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(Vec::new()); + store.update_user_settings(cx, |settings| { + settings + .project_panel + .get_or_insert_default() + .auto_fold_dirs = Some(false); + settings.project.worktree.file_scan_exclusions = Some(Vec::new()) }); }); }); diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index eff8f9ff60f348efd09bf3be4195a29ebe6c76af..b8a341fa05c5ed8bc964371737687dc83d04f9dc 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -107,6 +107,8 @@ pub struct SettingsContent { pub outline_panel: Option, + pub project_panel: Option, + /// Configuration for the Message Editor pub message_editor: Option, @@ -628,7 +630,7 @@ pub struct OutlinePanelSettingsContent { /// The position of outline panel /// /// Default: left - pub dock: Option, + pub dock: Option, /// Whether to show file icons in the outline panel. /// /// Default: true @@ -671,7 +673,7 @@ pub struct OutlinePanelSettingsContent { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] -pub enum OutlinePanelDockPosition { +pub enum DockSide { Left, Right, } diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index a5fe638df3c60380abc761b51f9976d6aa82d8a3..09fa136ea3574bacbd5d3dc10c2651ca3ba41c38 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -4,7 +4,7 @@ use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::DockPosition; +use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides}; #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceSettingsContent { @@ -328,3 +328,93 @@ impl OnLastWindowClosed { } } } + +#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ProjectPanelSettingsContent { + /// Whether to show the project panel button in the status bar. + /// + /// Default: true + pub button: Option, + /// Whether to hide gitignore files in the project panel. + /// + /// Default: false + pub hide_gitignore: Option, + /// Customize default width (in pixels) taken by project panel + /// + /// Default: 240 + pub default_width: Option, + /// The position of project panel + /// + /// Default: left + pub dock: Option, + /// Spacing between worktree entries in the project panel. + /// + /// Default: comfortable + pub entry_spacing: Option, + /// Whether to show file icons in the project panel. + /// + /// Default: true + pub file_icons: Option, + /// Whether to show folder icons or chevrons for directories in the project panel. + /// + /// Default: true + pub folder_icons: Option, + /// Whether to show the git status in the project panel. + /// + /// Default: true + pub git_status: Option, + /// Amount of indentation (in pixels) for nested items. + /// + /// Default: 20 + pub indent_size: Option, + /// Whether to reveal it in the project panel automatically, + /// when a corresponding project entry becomes active. + /// Gitignored entries are never auto revealed. + /// + /// Default: true + pub auto_reveal_entries: Option, + /// Whether to fold directories automatically + /// when directory has only one directory inside. + /// + /// Default: true + pub auto_fold_dirs: Option, + /// Whether the project panel should open on startup. + /// + /// Default: true + pub starts_open: Option, + /// Scrollbar-related settings + pub scrollbar: Option, + /// Which files containing diagnostic errors/warnings to mark in the project panel. + /// + /// Default: all + pub show_diagnostics: Option, + /// Settings related to indent guides in the project panel. + pub indent_guides: Option, + /// Whether to hide the root entry when only one folder is open in the window. + /// + /// Default: false + pub hide_root: Option, + /// Whether to stick parent directories at top of the project panel. + /// + /// Default: true + pub sticky_scroll: Option, + /// Whether to enable drag-and-drop operations in the project panel. + /// + /// Default: true + pub drag_and_drop: Option, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ProjectPanelEntrySpacing { + /// Comfortable spacing of entries. + #[default] + Comfortable, + /// The standard spacing of entries. + Standard, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ProjectPanelIndentGuidesSettings { + pub show: Option, +} From 96111c6ef3c138a2fa186a522da7633ffa998948 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Wed, 17 Sep 2025 21:36:06 +0200 Subject: [PATCH 080/117] extension_host: Sanitize cwd path for ResolvedTask (#38357) Ensures build task's CWD paths use POSIX-friendly path separator on Windows host so that `std::path::Path` ops work as expected within the Wasm guest. Release Notes: - N/A --- crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 84794d5386eda1517808d181eb259a3264f7b82d..e879ed0cb01f70f24a9b2b52438e1ff7d405f2d6 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -309,7 +309,14 @@ impl TryFrom for ResolvedTask { command: value.command.context("missing command")?, args: value.args, env: value.env.into_iter().collect(), - cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()), + cwd: value.cwd.map(|s| { + let s = s.to_string_lossy(); + if cfg!(target_os = "windows") { + s.replace('\\', "/") + } else { + s.into_owned() + } + }), }) } } From b66b5850ec9464c8ecdb3259a722ec256e57af46 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 13:39:38 -0600 Subject: [PATCH 081/117] WIP --- crates/collab/src/tests/editor_tests.rs | 32 ++++++++--------- crates/collab/src/tests/following_tests.rs | 6 ++-- crates/collab/src/tests/integration_tests.rs | 36 +++++++++---------- .../remote_editing_collaboration_tests.rs | 33 +++++++++-------- .../src/edit_prediction_button.rs | 10 +++--- crates/eval/src/eval.rs | 2 +- .../src/outline_panel_settings.rs | 2 +- crates/search/src/buffer_search.rs | 4 +-- crates/vim/src/digraph.rs | 4 +-- 9 files changed, 62 insertions(+), 67 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index f2b7a1028b31200fe001e4fa152521e3cf5e44be..3fe2e797bf95b6ebf449f388ad48e53f215ca37b 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -4,7 +4,7 @@ use crate::{ }; use call::ActiveCall; use editor::{ - DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects, + DocumentColorsRenderMode, Editor, RowInfo, SelectionEffects, actions::{ ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo, @@ -18,20 +18,16 @@ use fs::Fs; use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; -use language::{ - FakeLspAdapter, - language_settings::{AllLanguageSettings, InlayHintSettings}, -}; +use language::FakeLspAdapter; use lsp::LSP_REQUEST_TIMEOUT; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, - project_settings::{InlineBlameSettings, ProjectSettings}, }; use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; use serde_json::json; -use settings::SettingsStore; +use settings::{InlayHintSettings, InlineBlameSettings, SettingsStore}; use std::{ collections::BTreeSet, ops::{Deref as _, Range}, @@ -2422,8 +2418,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo cx_b.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background); + store.update_user_settings(cx, |settings| { + settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background); }); }); }); @@ -2450,8 +2446,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo cx_b.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay); + store.update_user_settings(cx, |settings| { + settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay); }); }); }); @@ -2478,8 +2474,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo cx_a.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border); + store.update_user_settings(cx, |settings| { + settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border); }); }); }); @@ -3306,20 +3302,20 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA cx_b.update(editor::init); // Turn inline-blame-off by default so no state is transferred without us explicitly doing so let inline_blame_off_settings = Some(InlineBlameSettings { - enabled: false, + enabled: Some(false), ..Default::default() }); cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.git.inline_blame = inline_blame_off_settings; + store.update_user_settings(cx, |settings| { + settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings; }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.git.inline_blame = inline_blame_off_settings; + store.update_user_settings(cx, |settings| { + settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings; }); }); }); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 0a9a69bfca9cdda3fc446ac48e9c63da5e75fe28..e6bab12934d9c262cbba378e0d2a94143e0a7602 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -12,7 +12,6 @@ use gpui::{ VisualContext, VisualTestContext, point, }; use language::Capability; -use project::WorktreeSettings; use rpc::proto::PeerId; use serde_json::json; use settings::SettingsStore; @@ -1710,8 +1709,9 @@ async fn test_following_into_excluded_file( for cx in [&mut cx_a, &mut cx_b] { cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); + store.update_user_settings(cx, |settings| { + settings.project.worktree.file_scan_exclusions = + Some(vec!["**/.git".to_string()]); }); }); }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 646dbfbd1575756e6955c0d60ae5af64a2760328..d2146ec433cce59ed1d0c32d49c28c7efcaea5aa 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -22,9 +22,7 @@ use gpui::{ use language::{ Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope, - language_settings::{ - AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter, - }, + language_settings::{Formatter, FormatterList, SelectedFormatter}, tree_sitter_rust, tree_sitter_typescript, }; use lsp::{LanguageServerId, OneOf}; @@ -38,7 +36,7 @@ use project::{ use prompt_store::PromptBuilder; use rand::prelude::*; use serde_json::json; -use settings::SettingsStore; +use settings::{PrettierSettingsContent, SettingsStore}; use std::{ cell::{Cell, RefCell}, env, future, mem, @@ -4598,15 +4596,15 @@ async fn test_formatting_buffer( // host's configuration is honored as opposed to using the guest's settings. cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single( - Formatter::External { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List( + FormatterList::Single(Formatter::External { command: "awk".into(), arguments: Some( vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(), ), - }, - ))); + }), + )); }); }); }); @@ -4694,24 +4692,24 @@ async fn test_prettier_formatting_buffer( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::Auto); - file.defaults.prettier = Some(PrettierSettings { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); + file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { allowed: true, - ..PrettierSettings::default() + ..Default::default() }); }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single( - Formatter::LanguageServer { name: None }, - ))); - file.defaults.prettier = Some(PrettierSettings { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List( + FormatterList::Single(Formatter::LanguageServer { name: None }), + )); + file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { allowed: true, - ..PrettierSettings::default() + ..Default::default() }); }); }); diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 6b46459a59b16717d965b42c4e19820f6d1dc062..96e9a0fc03d11a218baac323a8f699ba408cc705 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -15,8 +15,7 @@ use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, language_settings::{ - AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter, - language_settings, + Formatter, FormatterList, PrettierSettings, SelectedFormatter, language_settings, }, tree_sitter_typescript, }; @@ -30,7 +29,7 @@ use remote::RemoteClient; use remote_server::{HeadlessAppState, HeadlessProject}; use rpc::proto; use serde_json::json; -use settings::SettingsStore; +use settings::{PrettierSettingsContent, SettingsStore}; use std::{ path::Path, sync::{Arc, atomic::AtomicUsize}, @@ -499,24 +498,24 @@ async fn test_ssh_collaboration_formatting_with_prettier( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::Auto); - file.defaults.prettier = Some(PrettierSettings { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); + file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { allowed: true, - ..PrettierSettings::default() + ..Default::default() }); }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single( - Formatter::LanguageServer { name: None }, - ))); - file.defaults.prettier = Some(PrettierSettings { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List( + FormatterList::Single(Formatter::LanguageServer { name: None }), + )); + file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { allowed: true, - ..PrettierSettings::default() + ..Default::default() }); }); }); @@ -556,11 +555,11 @@ async fn test_ssh_collaboration_formatting_with_prettier( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |file| { - file.defaults.formatter = Some(SelectedFormatter::Auto); - file.defaults.prettier = Some(PrettierSettings { + store.update_user_settings(cx, |file| { + file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); + file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { allowed: true, - ..PrettierSettings::default() + ..Default::default() }); }); }); diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index fb49b0c7dd3b14950361256eac53d0b3821a2cda..55e0612ef2f6f68e69124641aebcc685493a84be 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -910,8 +910,10 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); // Ensure that we always have "edit_predictions { "disabled_globs": [] }" - let edits = settings.edits_for_update::(&text, |file| { - file.edit_predictions + let edits = settings.edits_for_update(&text, |file| { + file.project + .all_languages + .edit_predictions .get_or_insert_with(Default::default) .disabled_globs .get_or_insert_with(Vec::new); @@ -971,7 +973,7 @@ fn toggle_show_edit_predictions_for_language( .all_languages .languages .0 - .entry(language.name()) + .entry(language.name().0) .or_default() .show_edit_predictions = Some(!show_edit_predictions); }); @@ -999,7 +1001,7 @@ fn toggle_edit_prediction_mode(fs: Arc, mode: EditPredictionsMode, cx: & edit_predictions.mode = mode; } else { settings.project.all_languages.edit_predictions = - Some(language_settings::EditPredictionSettingsContent { + Some(settings::EditPredictionSettingsContent { mode, ..Default::default() }); diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 17b9486a9f5cbbc8f0cafe86f55a6ac00d1c3023..40d8c14f4f7ddc441f31581951ee4d6c26376a04 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -340,7 +340,7 @@ pub fn init(cx: &mut App) -> Arc { release_channel::init(app_version, cx); gpui_tokio::init(cx); - let mut settings_store = SettingsStore::new(cx, &settings::default_settings()); + let settings_store = SettingsStore::new(cx, &settings::default_settings()); cx.set_global(settings_store); client::init_settings(cx); diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 5505c6a49f714173991166467afd9459b9a5ae22..638bf76fcaf258caef149b9283ec49e28032cd69 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -1,6 +1,6 @@ use editor::EditorSettings; use gpui::{App, Pixels}; -pub use settings::{LeftRightDockPosition, Settings, ShowIndentGuides}; +pub use settings::{DockSide, Settings, ShowIndentGuides}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; use util::MergeFrom; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 86e5ab67c47cae32db3bc22a06c6fd360c4b0141..2f046808788a5ccc3af6eed5b341884530ecbc89 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2866,8 +2866,8 @@ mod tests { fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.search = Some(search_settings); + store.update_user_settings(cx, |settings| { + settings.editor.search = Some(search_settings); }); }); }); diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 796dad94c0329ed56c2e1c39f1cc2e2fc102fe4d..484b9f915e79f726ff346e8f216af420eb6cde45 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -294,11 +294,11 @@ mod test { let mut cx: VimTestContext = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { + store.update_user_settings(cx, |s| { let mut custom_digraphs = HashMap::default(); custom_digraphs.insert("|-".into(), "⊢".into()); custom_digraphs.insert(":)".into(), "👨‍💻".into()); - s.custom_digraphs = Some(custom_digraphs); + s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs); }); }); From 0968cc8256c2631633f6e61097e275be84865e61 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 13:41:46 -0600 Subject: [PATCH 082/117] outline panel --- crates/outline_panel/src/outline_panel.rs | 10 +++---- .../src/outline_panel_settings.rs | 16 ++++------ crates/vim/src/normal/paste.rs | 29 ++++++++++--------- crates/vim/src/normal/scroll.rs | 13 +++------ 4 files changed, 30 insertions(+), 38 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index b4c0bb71ecb21f986d4a155fec045dd6b56987ea..76ee4beec2315ed1db55bd3002af1d49d83095e0 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -38,7 +38,7 @@ use std::{ u32, }; -use outline_panel_settings::{LeftRightDockPosition, OutlinePanelSettings, ShowIndentGuides}; +use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides}; use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; use search::{BufferSearchBar, ProjectSearchView}; use serde::{Deserialize, Serialize}; @@ -4836,8 +4836,8 @@ impl Panel for OutlinePanel { fn position(&self, _: &Window, cx: &App) -> DockPosition { match OutlinePanelSettings::get_global(cx).dock { - LeftRightDockPosition::Left => DockPosition::Left, - LeftRightDockPosition::Right => DockPosition::Right, + DockSide::Left => DockPosition::Left, + DockSide::Right => DockPosition::Right, } } @@ -4848,8 +4848,8 @@ impl Panel for OutlinePanel { fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { let dock = match position { - DockPosition::Left | DockPosition::Bottom => LeftRightDockPosition::Left, - DockPosition::Right => LeftRightDockPosition::Right, + DockPosition::Left | DockPosition::Bottom => DockSide::Left, + DockPosition::Right => DockSide::Right, }; settings.outline_panel.get_or_insert_default().dock = Some(dock); }); diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 638bf76fcaf258caef149b9283ec49e28032cd69..b9b0518a7cf3eac107abc04c39ac1d6d65a61bb9 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -8,7 +8,7 @@ use util::MergeFrom; pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, - pub dock: LeftRightDockPosition, + pub dock: DockSide, pub file_icons: bool, pub folder_icons: bool, pub git_status: bool, @@ -33,12 +33,6 @@ pub struct IndentGuidesSettings { pub show: ShowIndentGuides, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct IndentGuidesSettingsContent { - /// When to show the scrollbar in the outline panel. - pub show: Option, -} - impl ScrollbarVisibility for OutlinePanelSettings { fn visibility(&self, cx: &App) -> ShowScrollbar { self.scrollbar @@ -70,7 +64,7 @@ impl Settings for OutlinePanelSettings { } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let Some(panel) = content.outline_panel.as_ref() else { return; }; @@ -92,8 +86,10 @@ impl Settings for OutlinePanelSettings { .merge_from(&panel.auto_reveal_entries); self.auto_fold_dirs.merge_from(&panel.auto_fold_dirs); - if let Some(scrollbar) = panel.scrollbar.as_ref() { - self.scrollbar.show.merge_from(&scrollbar.show.flatten()); + if let Some(scrollbar) = panel.scrollbar.as_ref() + && let Some(show) = scrollbar.show.flatten() + { + self.scrollbar.show = Some(show.into()) } } fn import_from_vscode( diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 933b119d3794540681cc096b0fe22b7f70da00cb..04761d3270b9f4ffef8c4e3880ee4e9fb21fd2ad 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -408,8 +408,8 @@ mod test { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::Never) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) }); }); @@ -444,8 +444,9 @@ mod test { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::OnYank) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = + Some(UseSystemClipboard::OnYank) }); }); @@ -709,8 +710,8 @@ mod test { Mode::Normal, ); cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |settings| { - settings.languages.0.insert( + store.update_user_settings(cx, |settings| { + settings.project.all_languages.languages.0.insert( LanguageName::new("Rust"), LanguageSettingsContent { auto_indent_on_paste: Some(false), @@ -772,8 +773,8 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::Never) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) }); }); @@ -818,8 +819,8 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::Never) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) }); }); @@ -847,8 +848,8 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::Never) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) }); }); @@ -906,8 +907,8 @@ mod test { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_system_clipboard = Some(UseSystemClipboard::Never) + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) }); }); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index eeb98692bc30c5c8c39c0be23ba17b3276b708df..647f955544ef876647af94f788484a3bc5fbfdfe 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -427,9 +427,7 @@ mod test { // First test without vertical scroll margin cx.neovim.set_option(&format!("scrolloff={}", 0)).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.vertical_scroll_margin = Some(0.0) - }); + store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(0.0)); }); let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); @@ -455,9 +453,7 @@ mod test { cx.neovim.set_option(&format!("scrolloff={}", 3)).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.vertical_scroll_margin = Some(3.0) - }); + store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(3.0)); }); // scroll down: ctrl-f @@ -485,9 +481,8 @@ mod test { cx.set_shared_state(&content).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off); - // s.vertical_scroll_margin = Some(0.); + store.update_user_settings(cx, |s| { + s.editor.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off); }); }); From 16974e64aa5979adc121bec52a0f8da111834231 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 13:49:48 -0600 Subject: [PATCH 083/117] repl --- assets/settings/default.json | 3 +- crates/repl/src/jupyter_settings.rs | 53 +++++-------------- crates/repl/src/repl_settings.rs | 47 ++++++---------- crates/settings/src/settings_content.rs | 17 ++++++ .../settings/src/settings_content/editor.rs | 6 +++ crates/vim/src/normal/search.rs | 4 +- 6 files changed, 55 insertions(+), 75 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3c871937d0a10f2af143d61887f8a7e92df4accb..2a6f49c3736a49bba64d2ddb1174969c9e792807 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1879,7 +1879,8 @@ }, // Jupyter settings "jupyter": { - "enabled": true + "enabled": true, + "kernel_selections": {} // Specify the language name as the key and the kernel name as the value. // "kernel_selections": { // "python": "conda-base" diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index c89736a03dc6d77dd89bb33c4990b25149189a41..8a468e9ec65feb8431222bd6b9a33c8bbd9efaf3 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -1,10 +1,8 @@ -use std::collections::HashMap; +use collections::HashMap; use editor::EditorSettings; use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; #[derive(Debug, Default)] pub struct JupyterSettings { @@ -20,45 +18,20 @@ impl JupyterSettings { } } -#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)] -#[settings_key(key = "jupyter")] -pub struct JupyterSettingsContent { - /// Default kernels to select for each language. - /// - /// Default: `{}` - pub kernel_selections: Option>, -} - -impl Default for JupyterSettingsContent { - fn default() -> Self { - JupyterSettingsContent { - kernel_selections: Some(HashMap::new()), +impl Settings for JupyterSettings { + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let jupyter = content.editor.jupyter.clone().unwrap(); + Self { + kernel_selections: jupyter.kernel_selections.unwrap_or_default(), } } -} -impl Settings for JupyterSettings { - type FileContent = JupyterSettingsContent; - - fn load( - sources: SettingsSources, - _cx: &mut gpui::App, - ) -> anyhow::Result - where - Self: Sized, - { - let mut settings = JupyterSettings::default(); - - for value in sources.defaults_and_customizations() { - if let Some(source) = &value.kernel_selections { - for (k, v) in source { - settings.kernel_selections.insert(k.clone(), v.clone()); - } - } + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(jupyter) = content.editor.jupyter.as_ref() else { + return; + }; + if let Some(kernel_selections) = jupyter.kernel_selections.clone() { + self.kernel_selections.extend(kernel_selections) } - - Ok(settings) } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} } diff --git a/crates/repl/src/repl_settings.rs b/crates/repl/src/repl_settings.rs index 2fbf4b63731009a30691f04067bf96fbb8250880..1c9ceeecfc19bec40243193e9d4ea21a2e266e45 100644 --- a/crates/repl/src/repl_settings.rs +++ b/crates/repl/src/repl_settings.rs @@ -1,55 +1,38 @@ use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsKey, SettingsSources, SettingsUi}; +use settings::Settings; +use util::MergeFrom; /// Settings for configuring REPL display and behavior. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)] -#[settings_key(key = "repl")] +#[derive(Clone, Debug)] pub struct ReplSettings { /// Maximum number of lines to keep in REPL's scrollback buffer. /// Clamped with [4, 256] range. /// /// Default: 32 - #[serde(default = "default_max_lines")] pub max_lines: usize, /// Maximum number of columns to keep in REPL's scrollback buffer. /// Clamped with [20, 512] range. /// /// Default: 128 - #[serde(default = "default_max_columns")] pub max_columns: usize, } impl Settings for ReplSettings { - type FileContent = Self; + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let repl = content.repl.as_ref().unwrap(); - fn load(sources: SettingsSources, _cx: &mut App) -> anyhow::Result { - let mut settings: ReplSettings = sources.json_merge()?; - settings.max_columns = settings.max_columns.clamp(20, 512); - settings.max_lines = settings.max_lines.clamp(4, 256); - Ok(settings) + Self { + max_lines: repl.max_lines.unwrap(), + max_columns: repl.max_columns.unwrap(), + } } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} - -const DEFAULT_NUM_LINES: usize = 32; -const DEFAULT_NUM_COLUMNS: usize = 128; - -fn default_max_lines() -> usize { - DEFAULT_NUM_LINES -} - -fn default_max_columns() -> usize { - DEFAULT_NUM_COLUMNS -} + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { + let Some(repl) = content.repl.as_ref() else { + return; + }; -impl Default for ReplSettings { - fn default() -> Self { - ReplSettings { - max_lines: DEFAULT_NUM_LINES, - max_columns: DEFAULT_NUM_COLUMNS, - } + self.max_columns.merge_from(&repl.max_columns); + self.max_lines.merge_from(&repl.max_lines); } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index b8a341fa05c5ed8bc964371737687dc83d04f9dc..55ec782e83369a571b958bacb2a3c4d223ce8567 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -88,6 +88,8 @@ pub struct SettingsContent { /// The settings for the image viewer. pub image_viewer: Option, + pub repl: Option, + /// Whether or not to enable Helix mode. /// /// Default: false @@ -766,3 +768,18 @@ pub struct SshPortForwardOption { pub remote_host: Option, pub remote_port: u16, } + +/// Settings for configuring REPL display and behavior. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ReplSettingsContent { + /// Maximum number of lines to keep in REPL's scrollback buffer. + /// Clamped with [4, 256] range. + /// + /// Default: 32 + pub max_lines: Option, + /// Maximum number of columns to keep in REPL's scrollback buffer. + /// Clamped with [20, 512] range. + /// + /// Default: 128 + pub max_columns: Option, +} diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index 5220c029697e9714dc80f31fec23a4406e8d1855..e8631d356dd1c664615d0a0933b479ac8f88bbcd 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -1,5 +1,6 @@ use std::num; +use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -541,6 +542,11 @@ pub struct JupyterContent { /// /// Default: true pub enabled: Option, + + /// Default kernels to select for each language. + /// + /// Default: `{}` + pub kernel_selections: Option>, } /// Whether to allow drag and drop text selection in buffer. diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index a92ec7f6d3a05b26d31feca567e7a2e08f7f2b20..6ebf8e6c02e94906d973cc1ebcd07726b4ef1ac7 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -694,7 +694,7 @@ mod test { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| s.search_wrap = Some(false)); + store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); }); cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); @@ -815,7 +815,7 @@ mod test { // check that searching with unable search wrap cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| s.search_wrap = Some(false)); + store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); }); cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); cx.simulate_keystrokes("/ c c enter"); From 0ade84e5e6ef6019cb03b7c435331a4beeba47ac Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 13:53:14 -0600 Subject: [PATCH 084/117] Search --- .../remote_editing_collaboration_tests.rs | 4 +- crates/editor/src/editor_settings.rs | 1 + crates/search/src/buffer_search.rs | 10 +++- crates/settings_ui/src/settings_ui.rs | 53 +------------------ crates/vim/src/normal.rs | 12 +++-- 5 files changed, 20 insertions(+), 60 deletions(-) diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 96e9a0fc03d11a218baac323a8f699ba408cc705..930a8ac972c924a494ec428a0bd2286036c36950 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -14,9 +14,7 @@ use gpui::{ use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, - language_settings::{ - Formatter, FormatterList, PrettierSettings, SelectedFormatter, language_settings, - }, + language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings}, tree_sitter_typescript, }; use node_runtime::NodeRuntime; diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f184aed90b85071418a006a36293d235d9a580a4..a6893a10791b1e5ce19d4ad484327d6f16539a74 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -176,6 +176,7 @@ pub struct SearchSettings { pub include_ignored: bool, pub regex: bool, } + impl EditorSettings { pub fn jupyter_enabled(cx: &App) -> bool { EditorSettings::get_global(cx).jupyter.enabled diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 2f046808788a5ccc3af6eed5b341884530ecbc89..dd096cde851b21983be80c5ce64b78338f54d78e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1480,7 +1480,7 @@ mod tests { use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext}; use language::{Buffer, Point}; use project::Project; - use settings::SettingsStore; + use settings::{SearchSettingsContent, SettingsStore}; use smol::stream::StreamExt as _; use unindent::Unindent as _; @@ -2867,7 +2867,13 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.editor.search = Some(search_settings); + settings.editor.search = Some(SearchSettingsContent { + button: Some(search_settings.button), + whole_word: Some(search_settings.whole_word), + case_sensitive: Some(search_settings.case_sensitive), + include_ignored: Some(search_settings.include_ignored), + regex: Some(search_settings.regex), + }); }); }); }); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 4ba961773df2e605660a841f755be939f64faae0..c5c633e73f90cf4f3c4e5243009ec6f34e905e6e 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -295,57 +295,8 @@ impl SettingsUiTree { // so that we can keep none/skip and still test in CI that all settings have #[cfg(feature = "test-support")] pub fn all_paths(&self, cx: &App) -> Vec> { - fn all_paths_rec( - tree: &[UiEntry], - paths: &mut Vec>, - current_path: &mut Vec, - idx: usize, - cx: &App, - ) { - let child = &tree[idx]; - let mut pushed_path = false; - if let Some(path) = child.path.as_ref() { - current_path.push(path.clone()); - paths.push(current_path.clone()); - pushed_path = true; - } - // todo(settings_ui): handle dynamic nodes here - let selected_descendant_index = child - .dynamic_render - .as_ref() - .map(|dynamic_render| { - read_settings_value_from_path( - SettingsStore::global(cx).raw_default_settings(), - ¤t_path, - ) - .map(|value| (dynamic_render.determine_option)(value, cx)) - }) - .and_then(|selected_descendant_index| { - selected_descendant_index.map(|index| child.nth_descendant_index(tree, index)) - }); - - if let Some(selected_descendant_index) = selected_descendant_index { - // just silently fail if we didn't find a setting value for the path - if let Some(descendant_index) = selected_descendant_index { - all_paths_rec(tree, paths, current_path, descendant_index, cx); - } - } else if let Some(desc_idx) = child.first_descendant_index() { - let mut desc_idx = Some(desc_idx); - while let Some(descendant_index) = desc_idx { - all_paths_rec(&tree, paths, current_path, descendant_index, cx); - desc_idx = tree[descendant_index].next_sibling; - } - } - if pushed_path { - current_path.pop(); - } - } - - let mut paths = Vec::new(); - for &index in &self.root_entry_indices { - all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx); - } - paths + // todo(settings_ui) this needs to be implemented not in terms of JSON anymore + Vec::default() } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index b8d1325a8b19aaa2dcbc2611b2ff66df721c17f3..8d3fec1d64e96bf675e16bed036e0862a664d840 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1724,8 +1724,8 @@ mod test { async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { - s.use_smartcase_find = Some(true); + store.update_user_settings(cx, |s| { + s.vim.get_or_insert_default().use_smartcase_find = Some(true); }); }); @@ -1891,8 +1891,12 @@ mod test { cx.update(|_, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.defaults.preferred_line_length = Some(5); + settings.update_user_settings(cx, |settings| { + settings + .project + .all_languages + .defaults + .preferred_line_length = Some(5); }); }) }); From 93968f256333a9c89ba99e9215e404c1b8b2c273 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 14:10:33 -0600 Subject: [PATCH 085/117] Vim --- Cargo.lock | 22 - Cargo.toml | 1 - crates/settings/src/settings_content.rs | 6 +- crates/settings_ui/Cargo.toml | 42 - crates/settings_ui/LICENSE-GPL | 1 - crates/settings_ui/src/settings_ui.rs | 940 ------------------ crates/vim/src/digraph.rs | 1 - crates/vim/src/normal.rs | 3 +- crates/vim/src/normal/paste.rs | 10 +- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/normal/search.rs | 1 - crates/vim/src/test.rs | 5 +- .../src/test/neovim_backed_test_context.rs | 13 +- crates/vim/src/test/vim_test_context.rs | 10 +- crates/vim/src/vim.rs | 57 +- crates/zed/Cargo.toml | 2 - crates/zed/src/main.rs | 1 - crates/zed/src/zed.rs | 48 +- crates/zeta/src/init.rs | 2 +- crates/zeta_cli/src/headless.rs | 2 +- 20 files changed, 76 insertions(+), 1093 deletions(-) delete mode 100644 crates/settings_ui/Cargo.toml delete mode 120000 crates/settings_ui/LICENSE-GPL delete mode 100644 crates/settings_ui/src/settings_ui.rs diff --git a/Cargo.lock b/Cargo.lock index c98a90bb09f0f8b4e9f43facb6bb67c5df2f8e40..7d6f16a3f3e5149c4dd403f3721040a39b00f85a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14925,27 +14925,6 @@ dependencies = [ "zed_actions", ] -[[package]] -name = "settings_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette_hooks", - "debugger_ui", - "editor", - "feature_flags", - "gpui", - "menu", - "serde", - "serde_json", - "settings", - "smallvec", - "theme", - "ui", - "workspace", - "workspace-hack", -] - [[package]] name = "settings_ui_macros" version = "0.1.0" @@ -20625,7 +20604,6 @@ dependencies = [ "session", "settings", "settings_profile_selector", - "settings_ui", "shellexpand 2.1.2", "smol", "snippet_provider", diff --git a/Cargo.toml b/Cargo.toml index de1d216561b9d3074eecff5c36d6405a29c4f7d3..3c1f5dd97692e191977b77b5a1254eda9f9b4e7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,7 +148,6 @@ members = [ "crates/session", "crates/settings", "crates/settings_profile_selector", - "crates/settings_ui", "crates/settings_ui_macros", "crates/snippet", "crates/snippet_provider", diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 55ec782e83369a571b958bacb2a3c4d223ce8567..5321201384edf12e5ad662be9e54d70499713c99 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -552,16 +552,12 @@ pub struct VimSettingsContent { pub cursor_shape: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] +#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] #[serde(rename_all = "snake_case")] pub enum ModeContent { #[default] Normal, Insert, - Replace, - Visual, - VisualLine, - VisualBlock, HelixNormal, } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml deleted file mode 100644 index 1d8e5e11226103e2ef7778816f5c1b41cd934b05..0000000000000000000000000000000000000000 --- a/crates/settings_ui/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "settings_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_ui.rs" - -[features] -default = [] -test-support = [] - -[dependencies] -anyhow.workspace = true -command_palette_hooks.workspace = true -editor.workspace = true -feature_flags.workspace = true -gpui.workspace = true -menu.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true -workspace-hack.workspace = true - - -[dev-dependencies] -debugger_ui.workspace = true - -# Uncomment other workspace dependencies as needed -# assistant.workspace = true -# client.workspace = true -# project.workspace = true -# settings.workspace = true diff --git a/crates/settings_ui/LICENSE-GPL b/crates/settings_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/settings_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs deleted file mode 100644 index c5c633e73f90cf4f3c4e5243009ec6f34e905e6e..0000000000000000000000000000000000000000 --- a/crates/settings_ui/src/settings_ui.rs +++ /dev/null @@ -1,940 +0,0 @@ -use std::{ - num::NonZeroU32, - ops::{Not, Range}, - rc::Rc, -}; - -use anyhow::Context as _; -use editor::Editor; -use feature_flags::{FeatureFlag, FeatureFlagAppExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; -use settings::{ - NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem, - SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion, - SettingsValue, -}; -use smallvec::SmallVec; -use ui::{ - ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, - prelude::*, -}; -use workspace::{ - Workspace, - item::{Item, ItemEvent}, -}; - -pub struct SettingsUiFeatureFlag; - -impl FeatureFlag for SettingsUiFeatureFlag { - const NAME: &'static str = "settings-ui"; -} - -actions!( - zed, - [ - /// Opens settings UI. - OpenSettingsUi - ] -); - -pub fn open_settings_editor( - workspace: &mut Workspace, - _: &OpenSettingsUi, - window: &mut Window, - cx: &mut Context, -) { - // todo(settings_ui) open in a local workspace if this is remote. - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = SettingsPage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx) - } -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action_renderer(|div, _, _, cx| { - let settings_ui_actions = [std::any::TypeId::of::()]; - let has_flag = cx.has_flag::(); - command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| { - if has_flag { - filter.show_action_types(&settings_ui_actions); - } else { - filter.hide_action_types(&settings_ui_actions); - } - }); - if has_flag { - div.on_action(cx.listener(open_settings_editor)) - } else { - div - } - }); - }) - .detach(); -} - -pub struct SettingsPage { - focus_handle: FocusHandle, - settings_tree: SettingsUiTree, -} - -impl SettingsPage { - pub fn new(_workspace: &Workspace, cx: &mut Context) -> Entity { - cx.new(|cx| Self { - focus_handle: cx.focus_handle(), - settings_tree: SettingsUiTree::new(cx), - }) - } -} - -impl EventEmitter for SettingsPage {} - -impl Focusable for SettingsPage { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for SettingsPage { - type Event = ItemEvent; - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::Settings)) - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Settings".into() - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } -} - -// We want to iterate over the side bar with root groups -// - this is a loop over top level groups, and if any are expanded, recursively displaying their items -// - Should be able to get all items from a group (flatten a group) -// - Should be able to toggle/untoggle groups in UI (at least in sidebar) -// - Search should be available -// - there should be an index of text -> item mappings, for using fuzzy::match -// - Do we want to show the parent groups when a item is matched? - -struct UiEntry { - title: SharedString, - path: Option, - documentation: Option, - _depth: usize, - // a - // b < a descendant range < a total descendant range - // f | | - // g | | - // c < | - // d | - // e < - descendant_range: Range, - total_descendant_range: Range, - next_sibling: Option, - // expanded: bool, - render: Option, - dynamic_render: Option, - generate_items: Option<( - SettingsUiItem, - fn(&serde_json::Value, &App) -> Vec, - SmallVec<[SharedString; 1]>, - )>, -} - -impl UiEntry { - fn first_descendant_index(&self) -> Option { - return self - .descendant_range - .is_empty() - .not() - .then_some(self.descendant_range.start); - } - - fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option { - let first_descendant_index = self.first_descendant_index()?; - let mut current_index = 0; - let mut current_descendant_index = Some(first_descendant_index); - while let Some(descendant_index) = current_descendant_index - && current_index < n - { - current_index += 1; - current_descendant_index = tree[descendant_index].next_sibling; - } - current_descendant_index - } -} - -pub struct SettingsUiTree { - root_entry_indices: Vec, - entries: Vec, - active_entry_index: usize, -} - -fn build_tree_item( - tree: &mut Vec, - entry: SettingsUiEntry, - depth: usize, - prev_index: Option, -) { - // let tree: HashMap; - let index = tree.len(); - tree.push(UiEntry { - title: entry.title.into(), - path: entry.path.map(SharedString::new_static), - documentation: entry.documentation.map(SharedString::new_static), - _depth: depth, - descendant_range: index + 1..index + 1, - total_descendant_range: index + 1..index + 1, - render: None, - next_sibling: None, - dynamic_render: None, - generate_items: None, - }); - if let Some(prev_index) = prev_index { - tree[prev_index].next_sibling = Some(index); - } - match entry.item { - SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => { - for group_item in group_items { - let prev_index = tree[index] - .descendant_range - .is_empty() - .not() - .then_some(tree[index].descendant_range.end - 1); - tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, group_item, depth + 1, prev_index); - tree[index].total_descendant_range.end = tree.len(); - } - } - SettingsUiItem::Single(item) => { - tree[index].render = Some(item); - } - SettingsUiItem::Union(dynamic_render) => { - // todo(settings_ui) take from item and store other fields instead of clone - // will also require replacing usage in render_recursive so it can know - // which options were actually rendered - let options = dynamic_render.options.clone(); - tree[index].dynamic_render = Some(dynamic_render); - for option in options { - let Some(option) = option else { continue }; - let prev_index = tree[index] - .descendant_range - .is_empty() - .not() - .then_some(tree[index].descendant_range.end - 1); - tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, option, depth + 1, prev_index); - tree[index].total_descendant_range.end = tree.len(); - } - } - SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap { - item: generate_settings_ui_item, - determine_items, - defaults_path, - }) => { - tree[index].generate_items = Some(( - generate_settings_ui_item(), - determine_items, - defaults_path - .into_iter() - .copied() - .map(SharedString::new_static) - .collect(), - )); - } - SettingsUiItem::None => { - return; - } - } -} - -impl SettingsUiTree { - pub fn new(cx: &App) -> Self { - let settings_store = SettingsStore::global(cx); - let mut tree = vec![]; - let mut root_entry_indices = vec![]; - for item in settings_store.settings_ui_items() { - if matches!(item.item, SettingsUiItem::None) - // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to - // link them to other groups - || matches!(item.item, SettingsUiItem::Single(_)) - { - continue; - } - - let prev_root_entry_index = root_entry_indices.last().copied(); - root_entry_indices.push(tree.len()); - build_tree_item(&mut tree, item, 0, prev_root_entry_index); - } - - root_entry_indices.sort_by_key(|i| &tree[*i].title); - - let active_entry_index = root_entry_indices[0]; - Self { - entries: tree, - root_entry_indices, - active_entry_index, - } - } - - // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree, - // so that we can keep none/skip and still test in CI that all settings have - #[cfg(feature = "test-support")] - pub fn all_paths(&self, cx: &App) -> Vec> { - // todo(settings_ui) this needs to be implemented not in terms of JSON anymore - Vec::default() - } -} - -fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context) -> Div { - let mut nav = v_flex().p_4().gap_2(); - for &index in &tree.root_entry_indices { - nav = nav.child( - div() - .id(index) - .on_click(cx.listener(move |settings, _, _, _| { - settings.settings_tree.active_entry_index = index; - })) - .child( - Label::new(tree.entries[index].title.clone()) - .size(LabelSize::Large) - .when(tree.active_entry_index == index, |this| { - this.color(Color::Selected) - }), - ), - ); - } - nav -} - -fn render_content( - tree: &SettingsUiTree, - window: &mut Window, - cx: &mut Context, -) -> Div { - let content = v_flex().size_full().gap_4(); - - let mut path = smallvec::smallvec![]; - - return render_recursive( - &tree.entries, - tree.active_entry_index, - &mut path, - content, - &mut None, - true, - window, - cx, - ); -} - -fn render_recursive( - tree: &[UiEntry], - index: usize, - path: &mut SmallVec<[SharedString; 1]>, - mut element: Div, - fallback_path: &mut Option>, - render_next_title: bool, - window: &mut Window, - cx: &mut App, -) -> Div { - let Some(child) = tree.get(index) else { - return element - .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); - }; - - if render_next_title { - element = element.child(Label::new(child.title.clone()).size(LabelSize::Large)); - } - - // todo(settings_ui): subgroups? - let mut pushed_path = false; - if let Some(child_path) = child.path.as_ref() { - path.push(child_path.clone()); - if let Some(fallback_path) = fallback_path.as_mut() { - fallback_path.push(child_path.clone()); - } - pushed_path = true; - } - let settings_value = settings_value_from_settings_and_path( - path.clone(), - fallback_path.as_ref().map(|path| path.as_slice()), - child.title.clone(), - child.documentation.clone(), - // PERF: how to structure this better? There feels like there's a way to avoid the clone - // and every value lookup - SettingsStore::global(cx).raw_user_settings(), - SettingsStore::global(cx).raw_default_settings(), - ); - if let Some(dynamic_render) = child.dynamic_render.as_ref() { - let value = settings_value.read(); - let selected_index = (dynamic_render.determine_option)(value, cx); - element = element.child(div().child(render_toggle_button_group_inner( - settings_value.title.clone(), - dynamic_render.labels, - Some(selected_index), - { - let path = settings_value.path.clone(); - let defaults = dynamic_render.defaults.clone(); - move |idx, cx| { - if idx == selected_index { - return; - } - let default = defaults.get(idx).cloned().unwrap_or_default(); - SettingsValue::write_value(&path, default, cx); - } - }, - ))); - // we don't add descendants for unit options, so we adjust the selected index - // by the number of options we didn't add descendants for, to get the descendant index - let selected_descendant_index = selected_index - - dynamic_render.options[..selected_index] - .iter() - .filter(|option| option.is_none()) - .count(); - if dynamic_render.options[selected_index].is_some() - && let Some(descendant_index) = - child.nth_descendant_index(tree, selected_descendant_index) - { - element = render_recursive( - tree, - descendant_index, - path, - element, - fallback_path, - false, - window, - cx, - ); - } - } else if let Some((settings_ui_item, generate_items, defaults_path)) = - child.generate_items.as_ref() - { - let generated_items = generate_items(settings_value.read(), cx); - let mut ui_items = Vec::with_capacity(generated_items.len()); - for item in generated_items { - let settings_ui_entry = SettingsUiEntry { - path: None, - title: "", - documentation: None, - item: settings_ui_item.clone(), - }; - let prev_index = if ui_items.is_empty() { - None - } else { - Some(ui_items.len() - 1) - }; - let item_index = ui_items.len(); - build_tree_item( - &mut ui_items, - settings_ui_entry, - child._depth + 1, - prev_index, - ); - if item_index < ui_items.len() { - ui_items[item_index].path = None; - ui_items[item_index].title = item.title.clone(); - ui_items[item_index].documentation = item.documentation.clone(); - - // push path instead of setting path on ui item so that the path isn't pushed to default_path as well - // when we recurse - path.push(item.path.clone()); - element = render_recursive( - &ui_items, - item_index, - path, - element, - &mut Some(defaults_path.clone()), - true, - window, - cx, - ); - path.pop(); - } - } - } else if let Some(child_render) = child.render.as_ref() { - element = element.child(div().child(render_item_single( - settings_value, - child_render, - window, - cx, - ))); - } else if let Some(child_index) = child.first_descendant_index() { - let mut index = Some(child_index); - while let Some(sub_child_index) = index { - element = render_recursive( - tree, - sub_child_index, - path, - element, - fallback_path, - true, - window, - cx, - ); - index = tree[sub_child_index].next_sibling; - } - } else { - element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted))) - } - - if pushed_path { - path.pop(); - if let Some(fallback_path) = fallback_path.as_mut() { - fallback_path.pop(); - } - } - return element; -} - -impl Render for SettingsPage { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new()); - div() - .grid() - .grid_cols(16) - .p_4() - .bg(cx.theme().colors().editor_background) - .size_full() - .child( - div() - .id("settings-ui-nav") - .col_span(2) - .h_full() - .child(render_nav(&self.settings_tree, window, cx)), - ) - .child( - div().col_span(6).h_full().child( - render_content(&self.settings_tree, window, cx) - .id("settings-ui-content") - .track_scroll(scroll_handle.read(cx)) - .overflow_y_scroll(), - ), - ) - } -} - -fn element_id_from_path(path: &[SharedString]) -> ElementId { - if path.len() == 0 { - panic!("Path length must not be zero"); - } else if path.len() == 1 { - ElementId::Name(path[0].clone()) - } else { - ElementId::from(( - ElementId::from(path[path.len() - 2].clone()), - path[path.len() - 1].clone(), - )) - } -} - -fn render_item_single( - settings_value: SettingsValue, - item: &SettingsUiItemSingle, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - match item { - SettingsUiItemSingle::Custom(_) => div() - .child(format!("Item: {}", settings_value.path.join("."))) - .into_any_element(), - SettingsUiItemSingle::SwitchField => { - render_any_item(settings_value, render_switch_field, window, cx) - } - SettingsUiItemSingle::NumericStepper(num_type) => { - render_any_numeric_stepper(settings_value, *num_type, window, cx) - } - SettingsUiItemSingle::ToggleGroup { - variants: values, - labels: titles, - } => render_toggle_button_group(settings_value, values, titles, window, cx), - SettingsUiItemSingle::DropDown { variants, labels } => { - render_dropdown(settings_value, variants, labels, window, cx) - } - SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx), - } -} - -pub fn read_settings_value_from_path<'a>( - settings_contents: &'a serde_json::Value, - path: &[impl AsRef], -) -> Option<&'a serde_json::Value> { - // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested - let Some((key, remaining)) = path.split_first() else { - return Some(settings_contents); - }; - let Some(value) = settings_contents.get(key.as_ref()) else { - return None; - }; - - read_settings_value_from_path(value, remaining) -} - -fn downcast_any_item( - settings_value: SettingsValue, -) -> SettingsValue { - let value = settings_value.value.map(|value| { - serde_json::from_value::(value.clone()) - .with_context(|| format!("path: {:?}", settings_value.path.join("."))) - .with_context(|| format!("value is not a {}: {}", std::any::type_name::(), value)) - .unwrap() - }); - // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values - let default_value = serde_json::from_value::(settings_value.default_value) - .with_context(|| format!("path: {:?}", settings_value.path.join("."))) - .with_context(|| format!("value is not a {}", std::any::type_name::())) - .unwrap(); - let deserialized_setting_value = SettingsValue { - title: settings_value.title, - path: settings_value.path, - documentation: settings_value.documentation, - value, - default_value, - }; - deserialized_setting_value -} - -fn render_any_item( - settings_value: SettingsValue, - render_fn: impl Fn(SettingsValue, &mut Window, &mut App) -> AnyElement + 'static, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let deserialized_setting_value = downcast_any_item(settings_value); - render_fn(deserialized_setting_value, window, cx) -} - -fn render_any_numeric_stepper( - settings_value: SettingsValue, - num_type: NumType, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - match num_type { - NumType::U64 => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| u64::saturating_sub(n, 1), - |n| u64::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert u64 to serde_json::Number") - }, - window, - cx, - ), - NumType::U32 => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| u32::saturating_sub(n, 1), - |n| u32::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert u32 to serde_json::Number") - }, - window, - cx, - ), - NumType::F32 => render_numeric_stepper::( - downcast_any_item(settings_value), - |a| a - 1.0, - |a| a + 1.0, - |n| { - serde_json::Number::from_f64(n as f64) - .context("Failed to convert f32 to serde_json::Number") - }, - window, - cx, - ), - NumType::USIZE => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| usize::saturating_sub(n, 1), - |n| usize::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert usize to serde_json::Number") - }, - window, - cx, - ), - NumType::U32NONZERO => render_numeric_stepper::( - downcast_any_item(settings_value), - |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN), - |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX), - |n| { - serde_json::Number::try_from(n.get()) - .context("Failed to convert usize to serde_json::Number") - }, - window, - cx, - ), - } -} - -fn render_numeric_stepper( - value: SettingsValue, - saturating_sub_1: fn(T) -> T, - saturating_add_1: fn(T) -> T, - to_serde_number: fn(T) -> anyhow::Result, - _window: &mut Window, - _cx: &mut App, -) -> AnyElement { - let id = element_id_from_path(&value.path); - let path = value.path.clone(); - let num = *value.read(); - - NumericStepper::new( - id, - num.to_string(), - { - let path = value.path; - move |_, _, cx| { - let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else { - return; - }; - let new_value = serde_json::Value::Number(number); - SettingsValue::write_value(&path, new_value, cx); - } - }, - move |_, _, cx| { - let Some(number) = to_serde_number(saturating_add_1(num)).ok() else { - return; - }; - - let new_value = serde_json::Value::Number(number); - - SettingsValue::write_value(&path, new_value, cx); - }, - ) - .style(ui::NumericStepperStyle::Outlined) - .into_any_element() -} - -fn render_switch_field( - value: SettingsValue, - _window: &mut Window, - _cx: &mut App, -) -> AnyElement { - let id = element_id_from_path(&value.path); - let path = value.path.clone(); - SwitchField::new( - id, - value.title.clone(), - value.documentation.clone(), - match value.read() { - true => ToggleState::Selected, - false => ToggleState::Unselected, - }, - move |toggle_state, _, cx| { - let new_value = serde_json::Value::Bool(match toggle_state { - ToggleState::Indeterminate => { - return; - } - ToggleState::Selected => true, - ToggleState::Unselected => false, - }); - - SettingsValue::write_value(&path, new_value, cx); - }, - ) - .into_any_element() -} - -fn render_text_field( - value: SettingsValue, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let path = value.path.clone(); - let editor = window.use_state(cx, { - let path = path.clone(); - move |window, cx| { - let mut editor = Editor::single_line(window, cx); - - cx.observe_global_in::(window, move |editor, window, cx| { - let user_settings = SettingsStore::global(cx).raw_user_settings(); - if let Some(value) = read_settings_value_from_path(&user_settings, &path).cloned() - && let Some(value) = value.as_str() - { - editor.set_text(value, window, cx); - } - }) - .detach(); - - editor.set_text(value.read().clone(), window, cx); - editor - } - }); - - let weak_editor = editor.downgrade(); - let theme_colors = cx.theme().colors(); - - div() - .child(editor) - .bg(theme_colors.editor_background) - .border_1() - .rounded_lg() - .border_color(theme_colors.border) - .on_action::({ - move |_, _, cx| { - let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok(); - - if let Some(new_value) = new_value { - SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx); - } - } - }) - .into_any_element() -} - -fn render_toggle_button_group( - value: SettingsValue, - variants: &'static [&'static str], - labels: &'static [&'static str], - _: &mut Window, - _: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let active_value = value.read(); - let selected_idx = variants.iter().position(|v| v == &active_value); - - return render_toggle_button_group_inner(value.title, labels, selected_idx, { - let path = value.path.clone(); - move |variant_index, cx| { - SettingsValue::write_value( - &path, - serde_json::Value::String(variants[variant_index].to_string()), - cx, - ); - } - }); -} - -fn render_dropdown( - value: SettingsValue, - variants: &'static [&'static str], - labels: &'static [&'static str], - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let id = element_id_from_path(&value.path); - - let menu = window.use_keyed_state(id.clone(), cx, |window, cx| { - let path = value.path.clone(); - let handler = Rc::new(move |variant: &'static str, cx: &mut App| { - SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx); - }); - - ContextMenu::build(window, cx, |mut menu, _, _| { - for (label, variant) in labels.iter().zip(variants) { - menu = menu.entry(*label, None, { - let handler = handler.clone(); - move |_, cx| { - handler(variant, cx); - } - }); - } - - menu - }) - }); - - DropdownMenu::new(id, value.read(), menu.read(cx).clone()) - .style(ui::DropdownStyle::Outlined) - .into_any_element() -} - -fn render_toggle_button_group_inner( - title: SharedString, - labels: &'static [&'static str], - selected_idx: Option, - on_write: impl Fn(usize, &mut App) + 'static, -) -> AnyElement { - fn make_toggle_group( - title: SharedString, - selected_idx: Option, - on_write: Rc, - labels: &'static [&'static str], - ) -> AnyElement { - let labels_array: [&'static str; LEN] = { - let mut arr = ["unused"; LEN]; - arr.copy_from_slice(labels); - arr - }; - - let mut idx = 0; - ToggleButtonGroup::single_row( - title, - labels_array.map(|label| { - idx += 1; - let on_write = on_write.clone(); - ToggleButtonSimple::new(label, move |_, _, cx| { - on_write(idx - 1, cx); - }) - }), - ) - .when_some(selected_idx, |this, ix| this.selected_index(ix)) - .style(ui::ToggleButtonGroupStyle::Filled) - .into_any_element() - } - - let on_write = Rc::new(on_write); - - macro_rules! templ_toggl_with_const_param { - ($len:expr) => { - if labels.len() == $len { - return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels); - } - }; - } - templ_toggl_with_const_param!(1); - templ_toggl_with_const_param!(2); - templ_toggl_with_const_param!(3); - templ_toggl_with_const_param!(4); - templ_toggl_with_const_param!(5); - templ_toggl_with_const_param!(6); - unreachable!("Too many variants"); -} - -fn settings_value_from_settings_and_path( - path: SmallVec<[SharedString; 1]>, - fallback_path: Option<&[SharedString]>, - title: SharedString, - documentation: Option, - user_settings: &serde_json::Value, - default_settings: &serde_json::Value, -) -> SettingsValue { - let default_value = read_settings_value_from_path(default_settings, &path) - .or_else(|| { - fallback_path.and_then(|fallback_path| { - read_settings_value_from_path(default_settings, fallback_path) - }) - }) - .with_context(|| format!("No default value for item at path {:?}", path.join("."))) - .expect("Default value set for item") - .clone(); - - let value = read_settings_value_from_path(user_settings, &path).cloned(); - let settings_value = SettingsValue { - default_value, - value, - documentation, - path, - // todo(settings_ui) is title required inside SettingsValue? - title, - }; - return settings_value; -} diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 484b9f915e79f726ff346e8f216af420eb6cde45..7a2ae08cace066bd6fa73d9914aac847b0fb1136 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -224,7 +224,6 @@ mod test { use settings::SettingsStore; use crate::{ - VimSettings, state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 8d3fec1d64e96bf675e16bed036e0862a664d840..7dfdb973c7603e9ef28bf757a9a716e729b72170 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -989,11 +989,10 @@ impl Vim { mod test { use gpui::{KeyBinding, TestAppContext, UpdateGlobal}; use indoc::indoc; - use language::language_settings::AllLanguageSettings; use settings::SettingsStore; use crate::{ - VimSettings, motion, + motion, state::Mode::{self}, test::{NeovimBackedTestContext, VimTestContext}, }; diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 04761d3270b9f4ffef8c4e3880ee4e9fb21fd2ad..99e84fcaf1a81f7969baeb1630d36373c6852b53 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -311,17 +311,13 @@ impl Vim { #[cfg(test)] mod test { use crate::{ - UseSystemClipboard, VimSettings, state::{Mode, Register}, test::{NeovimBackedTestContext, VimTestContext}, }; use gpui::ClipboardItem; use indoc::indoc; - use language::{ - LanguageName, - language_settings::{AllLanguageSettings, LanguageSettingsContent}, - }; - use settings::SettingsStore; + use language::{LanguageName, language_settings::LanguageSettingsContent}; + use settings::{SettingsStore, UseSystemClipboard}; #[gpui::test] async fn test_paste(cx: &mut gpui::TestAppContext) { @@ -712,7 +708,7 @@ mod test { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings(cx, |settings| { settings.project.all_languages.languages.0.insert( - LanguageName::new("Rust"), + LanguageName::new("Rust").0, LanguageSettingsContent { auto_indent_on_paste: Some(false), ..Default::default() diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 647f955544ef876647af94f788484a3bc5fbfdfe..8f1a157013000ae4df2416ddca93743f2c926d29 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -271,7 +271,7 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::{EditorSettings, ScrollBeyondLastLine}; + use editor::ScrollBeyondLastLine; use gpui::{AppContext as _, point, px, size}; use indoc::indoc; use language::Point; diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 6ebf8e6c02e94906d973cc1ebcd07726b4ef1ac7..6da18164e42e42b3c1932b094fab5cecf048c155 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -645,7 +645,6 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::EditorSettings; use editor::{DisplayPoint, display_map::DisplayRow}; use indoc::indoc; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 84376719d141fa4862a3e7a1b0f6116dd809bfe5..2256c2577ecd282f690ee7b3afe9e2b21b6e8788 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -22,7 +22,6 @@ pub use vim_test_context::*; use indoc::indoc; use search::BufferSearchBar; -use workspace::WorkspaceSettings; use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode}; @@ -1562,10 +1561,10 @@ async fn test_plus_minus(cx: &mut gpui::TestAppContext) { async fn test_command_alias(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { + store.update_user_settings(cx, |s| { let mut aliases = HashMap::default(); aliases.insert("Q".to_string(), "upper".to_string()); - s.command_aliases = Some(aliases) + s.workspace.command_aliases = aliases }); }); diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 6c9df164e0fe880c81960a412519347aff5959bd..c36307221ed2d9486455f48effdbbc85e92d4ec0 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -6,7 +6,7 @@ use std::{ panic, thread, }; -use language::language_settings::{AllLanguageSettings, SoftWrap}; +use language::language_settings::SoftWrap; use util::test::marked_text_offsets; use super::{VimTestContext, neovim_connection::NeovimConnection}; @@ -245,9 +245,14 @@ impl NeovimBackedTestContext { self.update(|_, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); - settings.defaults.preferred_line_length = Some(columns); + settings.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.soft_wrap = + Some(SoftWrap::PreferredLineLength); + settings + .project + .all_languages + .defaults + .preferred_line_length = Some(columns); }); }) }) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 3e6c96ac72f1e6dee568dafbfda6956c1dd5970c..a2db0493d99190bc7355a5af5a0687befcd02f63 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -68,7 +68,7 @@ impl VimTestContext { pub fn init_keybindings(enabled: bool, cx: &mut App) { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(enabled)); + store.update_user_settings(cx, |s| s.vim_mode = Some(enabled)); }); let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( "keymaps/default-macos.json", @@ -137,7 +137,7 @@ impl VimTestContext { pub fn enable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(true)); + store.update_user_settings(cx, |s| s.vim_mode = Some(true)); }); }) } @@ -145,7 +145,7 @@ impl VimTestContext { pub fn disable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(false)); + store.update_user_settings(cx, |s| s.vim_mode = Some(false)); }); }) } @@ -153,9 +153,7 @@ impl VimTestContext { pub fn enable_helix(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| { - s.helix_mode = Some(true) - }); + store.update_user_settings(cx, |s| s.helix_mode = Some(true)); }); }) } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 248e6acc58de7056f272d97153ed6342a18667c5..0786745ae5c23832e670232d676d2b84b43c4eed 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -19,7 +19,6 @@ mod state; mod surrounds; mod visual; -use anyhow::Result; use collections::HashMap; use editor::{ Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects, @@ -38,15 +37,15 @@ use normal::search::SearchSubmit; use object::Object; use schemars::JsonSchema; use serde::Deserialize; -use serde::Serialize; -use settings::{ - Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi, update_settings_file, +pub use settings::{ + ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file, }; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use theme::ThemeSettings; use ui::{IntoElement, SharedString, px}; +use util::MergeFrom; use vim_mode_setting::HelixModeSetting; use vim_mode_setting::VimModeSetting; use workspace::{self, Pane, Workspace}; @@ -1793,34 +1792,61 @@ impl Vim { } } -#[derive(Deserialize)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, - pub use_system_clipboard: UseSystemClipboard, + pub use_system_clipboard: settings::UseSystemClipboard, pub use_smartcase_find: bool, pub custom_digraphs: HashMap>, pub highlight_on_yank_duration: u64, pub cursor_shape: CursorShapeSettings, } +/// The settings for cursor shape. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct CursorShapeSettings { + /// Cursor shape for the normal mode. + /// + /// Default: block + pub normal: Option, + /// Cursor shape for the replace mode. + /// + /// Default: underline + pub replace: Option, + /// Cursor shape for the visual mode. + /// + /// Default: block + pub visual: Option, + /// Cursor shape for the insert mode. + /// + /// The default value follows the primary cursor_shape. + pub insert: Option, +} + +impl From for CursorShapeSettings { + fn from(settings: settings::CursorShapeSettings) -> Self { + Self { + normal: settings.normal.map(Into::into), + replace: settings.replace.map(Into::into), + visual: settings.visual.map(Into::into), + insert: settings.insert.map(Into::into), + } + } +} + impl From for Mode { fn from(mode: ModeContent) -> Self { match mode { ModeContent::Normal => Self::Normal, ModeContent::Insert => Self::Insert, - ModeContent::Replace => Self::Replace, - ModeContent::Visual => Self::Visual, - ModeContent::VisualLine => Self::VisualLine, - ModeContent::VisualBlock => Self::VisualBlock, ModeContent::HelixNormal => Self::HelixNormal, } } } impl Settings for VimSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { - let vim = content.vim.as_ref().unwrap(); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let vim = content.vim.clone().unwrap(); Self { default_mode: vim.default_mode.unwrap().into(), toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(), @@ -1828,11 +1854,11 @@ impl Settings for VimSettings { use_smartcase_find: vim.use_smartcase_find.unwrap(), custom_digraphs: vim.custom_digraphs.unwrap(), highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), - cursor_shape: vim.cursor_shape.unwrap(), + cursor_shape: vim.cursor_shape.unwrap().into(), } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let Some(vim) = content.vim.as_ref() else { return; }; @@ -1846,7 +1872,8 @@ impl Settings for VimSettings { self.custom_digraphs.merge_from(&vim.custom_digraphs); self.highlight_on_yank_duration .merge_from(&vim.highlight_on_yank_duration); - self.cursor_shape.merge_from(&vim.cursor_shape); + self.cursor_shape + .merge_from(&vim.cursor_shape.map(Into::into)); } fn import_from_vscode( diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e53dedafd2f27699645662155f154f1220c6cd85..c3db2c0f9f8189501b4971318f0e7ff1972a89fa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,7 +126,6 @@ serde.workspace = true serde_json.workspace = true session.workspace = true settings.workspace = true -settings_ui.workspace = true keymap_editor.workspace = true shellexpand.workspace = true smol.workspace = true @@ -185,7 +184,6 @@ itertools.workspace = true language = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } -settings_ui = { workspace = true, features = ["test-support"] } terminal_view = { workspace = true, features = ["test-support"] } tree-sitter-md.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 63ee30043b61fd55af2edad7cd4a7a407af743bd..a2aefb47ab6de7296257c21b5cccf283beb30a79 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -624,7 +624,6 @@ pub fn main() { markdown_preview::init(cx); svg_preview::init(cx); onboarding::init(cx); - settings_ui::init(cx); keymap_editor::init(cx); extensions_ui::init(cx); zeta::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5c1168713db687908c0d6a047f3585f920f32b80..6029ce6530a729ccd0b299bdcab2acbd620bbbdd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1948,7 +1948,7 @@ mod tests { }; use language::{LanguageMatcher, LanguageRegistry}; use pretty_assertions::{assert_eq, assert_ne}; - use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings}; + use project::{Project, ProjectPath}; use serde_json::json; use settings::{SettingsStore, watch_config_file}; use std::{ @@ -2253,8 +2253,11 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.session.restore_unsaved_buffers = false + store.update_user_settings(cx, |settings| { + settings + .session + .get_or_insert_default() + .restore_unsaved_buffers = Some(false) }); }); }); @@ -2969,8 +2972,8 @@ mod tests { let app_state = init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = + store.update_user_settings(cx, |project_settings| { + project_settings.project.worktree.file_scan_exclusions = Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); }); }); @@ -4799,8 +4802,9 @@ mod tests { // 3. Add .zed to file scan exclusions in user settings cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]); + store.update_user_settings(cx, |worktree_settings| { + worktree_settings.project.worktree.file_scan_exclusions = + Some(vec![".zed".to_string()]); }); }); @@ -4860,34 +4864,4 @@ mod tests { "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" ); } - - #[gpui::test] - fn test_settings_defaults(cx: &mut TestAppContext) { - cx.update(|cx| { - settings::init(cx); - workspace::init_settings(cx); - title_bar::init(cx); - editor::init_settings(cx); - debugger_ui::init(cx); - }); - let default_json = - cx.read(|cx| cx.global::().raw_default_settings().clone()); - - let all_paths = cx.read(|cx| settings_ui::SettingsUiTree::new(cx).all_paths(cx)); - let mut failures = Vec::new(); - for path in all_paths { - if settings_ui::read_settings_value_from_path(&default_json, &path).is_none() { - failures.push(path); - } - } - if !failures.is_empty() { - panic!( - "No default value found for paths: {:#?}", - failures - .into_iter() - .map(|path| path.join(".")) - .collect::>() - ); - } - } } diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 7c8d6fb9f0180b332187e7dbcd32f4b96925b231..0167d878fa34976d7175a64269d9dfe29d18d8fe 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -3,7 +3,7 @@ use std::any::{Any, TypeId}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag}; use gpui::actions; -use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; +use language::language_settings::EditPredictionProvider; use project::DisableAiSettings; use settings::{Settings, SettingsStore, update_settings_file}; use ui::App; diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index 04d4e9279a5a6a82081078f338e215ca67dca3e8..bb4cb010cba6ea29a9bcd6d8cc0dc93475dbc2a0 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -31,7 +31,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { release_channel::init(app_version, cx); gpui_tokio::init(cx); - let mut settings_store = SettingsStore::new(cx, settings::default_settings()); + let settings_store = SettingsStore::new(cx, &settings::default_settings()); cx.set_global(settings_store); client::init_settings(cx); From 3c69144128241af6fc04640fb699526658c28a69 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 17 Sep 2025 16:22:50 -0400 Subject: [PATCH 086/117] Update release URLs in release actions (#38361) Release Notes: - N/A --- .github/workflows/community_release_actions.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/community_release_actions.yml b/.github/workflows/community_release_actions.yml index 37ade90574e76cd95755aad6b5601a43946a271c..0f7a73649e9e1180c78a66ddf54055bf66f243f9 100644 --- a/.github/workflows/community_release_actions.yml +++ b/.github/workflows/community_release_actions.yml @@ -1,3 +1,6 @@ +# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT +# TO BOTH STABLE AND PREVIEW CHANNELS + name: Release Actions on: @@ -13,9 +16,9 @@ jobs: id: get-release-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview/latest" + URL="https://zed.dev/releases/preview" else - URL="https://zed.dev/releases/stable/latest" + URL="https://zed.dev/releases/stable" fi echo "URL=$URL" >> "$GITHUB_OUTPUT" From 4912096599b42a97eaf10a14693892cd8a15aa3b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Sep 2025 16:31:03 -0400 Subject: [PATCH 087/117] collab: Remove unused feature flag queries (#38360) This PR removes the feature flag queries, as they were no longer used. Release Notes: - N/A --- crates/collab/src/db/queries/users.rs | 73 ------------------- crates/collab/src/db/tables.rs | 2 - crates/collab/src/db/tables/feature_flag.rs | 41 ----------- crates/collab/src/db/tables/user.rs | 23 ------ crates/collab/src/db/tables/user_feature.rs | 42 ----------- crates/collab/src/db/tests.rs | 1 - .../collab/src/db/tests/feature_flag_tests.rs | 66 ----------------- crates/collab/src/seed.rs | 58 +++------------ 8 files changed, 10 insertions(+), 296 deletions(-) delete mode 100644 crates/collab/src/db/tables/feature_flag.rs delete mode 100644 crates/collab/src/db/tables/user_feature.rs delete mode 100644 crates/collab/src/db/tests/feature_flag_tests.rs diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 4b0f66fcbe09d23af58b0a30ffebf68455651fd8..89211130b88c69d4bf524bba25ae116790321d3e 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -342,79 +342,6 @@ impl Database { result } - /// Returns all feature flags. - pub async fn list_feature_flags(&self) -> Result> { - self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) }) - .await - } - - /// Creates a new feature flag. - pub async fn create_user_flag(&self, flag: &str, enabled_for_all: bool) -> Result { - self.transaction(|tx| async move { - let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { - flag: ActiveValue::set(flag.to_string()), - enabled_for_all: ActiveValue::set(enabled_for_all), - ..Default::default() - }) - .exec(&*tx) - .await? - .last_insert_id; - - Ok(flag) - }) - .await - } - - /// Add the given user to the feature flag - pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { - self.transaction(|tx| async move { - user_feature::Entity::insert(user_feature::ActiveModel { - user_id: ActiveValue::set(user), - feature_id: ActiveValue::set(flag), - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - /// Returns the active flags for the user. - pub async fn get_user_flags(&self, user: UserId) -> Result> { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - Flag, - } - - let flags_enabled_for_all = feature_flag::Entity::find() - .filter(feature_flag::Column::EnabledForAll.eq(true)) - .select_only() - .column(feature_flag::Column::Flag) - .into_values::<_, QueryAs>() - .all(&*tx) - .await?; - - let flags_enabled_for_user = user::Model { - id: user, - ..Default::default() - } - .find_linked(user::UserFlags) - .select_only() - .column(feature_flag::Column::Flag) - .into_values::<_, QueryAs>() - .all(&*tx) - .await?; - - let mut all_flags = HashSet::from_iter(flags_enabled_for_all); - all_flags.extend(flags_enabled_for_user); - - Ok(all_flags.into_iter().collect()) - }) - .await - } - pub async fn get_users_missing_github_user_created_at(&self) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find() diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 0082a9fb030a27e4be13af725f08ea9c82217377..32c4570af5893b503f0fcfdaa1759616cf9be387 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -13,7 +13,6 @@ pub mod contributor; pub mod embedding; pub mod extension; pub mod extension_version; -pub mod feature_flag; pub mod follower; pub mod language_server; pub mod notification; @@ -29,7 +28,6 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; -pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs deleted file mode 100644 index 5bbfedd71e70b7f1cc58219475c49c28bc62ff3d..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/feature_flag.rs +++ /dev/null @@ -1,41 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::db::FlagId; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "feature_flags")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: FlagId, - pub flag: String, - pub enabled_for_all: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::user_feature::Entity")] - UserFeature, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserFeature.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -pub struct FlaggedUsers; - -impl Linked for FlaggedUsers { - type FromEntity = Entity; - - type ToEntity = super::user::Entity; - - fn link(&self) -> Vec { - vec![ - super::user_feature::Relation::Flag.def().rev(), - super::user_feature::Relation::User.def(), - ] - } -} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index af43fe300a6cc1224487541ca72af9d887a6fae3..8e8c03fafc92127f8754f473e04dfab39592ea14 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -35,8 +35,6 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, - #[sea_orm(has_many = "super::user_feature::Entity")] - UserFeatures, #[sea_orm(has_one = "super::contributor::Entity")] Contributor, } @@ -84,25 +82,4 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserFeatures.def() - } -} - impl ActiveModelBehavior for ActiveModel {} - -pub struct UserFlags; - -impl Linked for UserFlags { - type FromEntity = Entity; - - type ToEntity = super::feature_flag::Entity; - - fn link(&self) -> Vec { - vec![ - super::user_feature::Relation::User.def().rev(), - super::user_feature::Relation::Flag.def(), - ] - } -} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs deleted file mode 100644 index cc24b5e796342f7733f59933362d46a0df2be112..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tables/user_feature.rs +++ /dev/null @@ -1,42 +0,0 @@ -use sea_orm::entity::prelude::*; - -use crate::db::{FlagId, UserId}; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "user_features")] -pub struct Model { - #[sea_orm(primary_key)] - pub user_id: UserId, - #[sea_orm(primary_key)] - pub feature_id: FlagId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::feature_flag::Entity", - from = "Column::FeatureId", - to = "super::feature_flag::Column::Id" - )] - Flag, - #[sea_orm( - belongs_to = "super::user::Entity", - from = "Column::UserId", - to = "super::user::Column::Id" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Flag.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 25e03f1320a25455ede347b43477761d591fbd57..141262d5e94a4bf1d4d897e78f6281ab9ee3ccfc 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -6,7 +6,6 @@ mod db_tests; #[cfg(target_os = "macos")] mod embedding_tests; mod extension_tests; -mod feature_flag_tests; mod user_tests; use crate::migrations::run_database_migrations; diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs deleted file mode 100644 index 0e68dcc941cdb2488c3822548dada56746667bc2..0000000000000000000000000000000000000000 --- a/crates/collab/src/db/tests/feature_flag_tests.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::{ - db::{Database, NewUserParams}, - test_both_dbs, -}; -use pretty_assertions::assert_eq; -use std::sync::Arc; - -test_both_dbs!( - test_get_user_flags, - test_get_user_flags_postgres, - test_get_user_flags_sqlite -); - -async fn test_get_user_flags(db: &Arc) { - let user_1 = db - .create_user( - "user1@example.com", - None, - false, - NewUserParams { - github_login: "user1".to_string(), - github_user_id: 1, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - "user2@example.com", - None, - false, - NewUserParams { - github_login: "user2".to_string(), - github_user_id: 2, - }, - ) - .await - .unwrap() - .user_id; - - const FEATURE_FLAG_ONE: &str = "brand-new-ux"; - const FEATURE_FLAG_TWO: &str = "cool-feature"; - const FEATURE_FLAG_THREE: &str = "feature-enabled-for-everyone"; - - let feature_flag_one = db.create_user_flag(FEATURE_FLAG_ONE, false).await.unwrap(); - let feature_flag_two = db.create_user_flag(FEATURE_FLAG_TWO, false).await.unwrap(); - db.create_user_flag(FEATURE_FLAG_THREE, true).await.unwrap(); - - db.add_user_flag(user_1, feature_flag_one).await.unwrap(); - db.add_user_flag(user_1, feature_flag_two).await.unwrap(); - - db.add_user_flag(user_2, feature_flag_one).await.unwrap(); - - let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); - user_1_flags.sort(); - assert_eq!( - user_1_flags, - &[FEATURE_FLAG_ONE, FEATURE_FLAG_TWO, FEATURE_FLAG_THREE] - ); - - let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); - user_2_flags.sort(); - assert_eq!(user_2_flags, &[FEATURE_FLAG_ONE, FEATURE_FLAG_THREE]); -} diff --git a/crates/collab/src/seed.rs b/crates/collab/src/seed.rs index 2d070b30abada79dc177b2b600d9ecc40aa472e1..5f5779e1e4990d1a03461bb3ec2222e82d9f544e 100644 --- a/crates/collab/src/seed.rs +++ b/crates/collab/src/seed.rs @@ -46,27 +46,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result let mut first_user = None; let mut others = vec![]; - let flag_names = ["language-models"]; - let mut flags = Vec::new(); - - let existing_feature_flags = db.list_feature_flags().await?; - - for flag_name in flag_names { - if existing_feature_flags - .iter() - .any(|flag| flag.flag == flag_name) - { - log::info!("Flag {flag_name:?} already exists"); - continue; - } - - let flag = db - .create_user_flag(flag_name, false) - .await - .unwrap_or_else(|err| panic!("failed to create flag: '{flag_name}': {err}")); - flags.push(flag); - } - for admin_login in seed_config.admins { let user = fetch_github::( &client, @@ -90,15 +69,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result } else { others.push(user.user_id) } - - for flag in &flags { - db.add_user_flag(user.user_id, *flag) - .await - .context(format!( - "Unable to enable flag '{}' for user '{}'", - flag, user.user_id - ))?; - } } for channel in seed_config.channels { @@ -126,24 +96,16 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result for github_user in github_users { log::info!("Seeding {:?} from GitHub", github_user.login); - let user = db - .update_or_create_user_by_github_account( - &github_user.login, - github_user.id, - github_user.email.as_deref(), - github_user.name.as_deref(), - github_user.created_at, - None, - ) - .await - .expect("failed to insert user"); - - for flag in &flags { - db.add_user_flag(user.id, *flag).await.context(format!( - "Unable to enable flag '{}' for user '{}'", - flag, user.id - ))?; - } + db.update_or_create_user_by_github_account( + &github_user.login, + github_user.id, + github_user.email.as_deref(), + github_user.name.as_deref(), + github_user.created_at, + None, + ) + .await + .expect("failed to insert user"); } Ok(()) From 27b59c0dd130bac0b6dc26d46a9d04bdea8702a2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 14:38:29 -0600 Subject: [PATCH 088/117] TEMP --- assets/settings/default.json | 24 +++++----- crates/agent_ui/src/slash_command_settings.rs | 27 ++--------- .../src/edit_prediction_button.rs | 4 +- .../file_finder/src/file_finder_settings.rs | 2 +- crates/language/src/language_settings.rs | 45 +++++++++---------- crates/onboarding/src/basics_page.rs | 14 +++++- crates/settings/src/settings_content.rs | 2 +- .../settings/src/settings_content/language.rs | 43 +++--------------- crates/settings/src/settings_store.rs | 6 ++- 9 files changed, 65 insertions(+), 102 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2a6f49c3736a49bba64d2ddb1174969c9e792807..8dd3e46eec3f466a2647cf55c57dd4ef32c01776 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -937,14 +937,6 @@ /// Default: false "use_modifier_to_send": false }, - // The settings for slash commands. - "slash_commands": { - // Settings for the `/project` slash command. - "project": { - // Whether `/project` is enabled. - "enabled": false - } - }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. @@ -1287,7 +1279,13 @@ // }, // Whether edit predictions are enabled when editing text threads. // This setting has no effect if globally disabled. - "enabled_in_text_threads": true + "enabled_in_text_threads": true, + + "copilot": { + "enterprise_uri": null, + "proxy": null, + "proxy_no_verify": null + } }, // Settings specific to journaling "journal": { @@ -2011,5 +2009,11 @@ // } // } // } - "profiles": [] + "profiles": [], + + /// A map of log scopes to the desired log level. + /// Useful for filtering out noisy logs or enabling more verbose logging. + /// + /// Example: {"log": {"client": "warn"}} + "log": {} } diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 1655c54da05fbec5f28c695d2668f10f86e4f1e4..f0a04c6b49984ae94d629f1bbfa96c6de4e01606 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -1,6 +1,5 @@ use gpui::App; use settings::Settings; -use util::MergeFrom; /// Settings for slash commands. #[derive(Debug, Default, Clone)] @@ -18,31 +17,11 @@ pub struct CargoWorkspaceCommandSettings { // todo!() I think this setting is bogus... default.json has "slash_commands": {"project"} impl Settings for SlashCommandSettings { - fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + fn from_defaults(_content: &settings::SettingsContent, _cx: &mut App) -> Self { Self { - cargo_workspace: CargoWorkspaceCommandSettings { - enabled: content - .project - .slash_commands - .clone() - .unwrap() - .cargo_workspace - .unwrap() - .enabled - .unwrap(), - }, + cargo_workspace: CargoWorkspaceCommandSettings { enabled: false }, } } - fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { - let Some(slash_command) = content.project.slash_commands.as_ref() else { - return; - }; - let Some(cargo_workspace) = slash_command.cargo_workspace.as_ref() else { - return; - }; - self.cargo_workspace - .enabled - .merge_from(&cargo_workspace.enabled); - } + fn refine(&mut self, _content: &settings::SettingsContent, _cx: &mut App) {} } diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 55e0612ef2f6f68e69124641aebcc685493a84be..8822c05f6c4d1842fd74d45d876f64736eb62fbc 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -998,11 +998,11 @@ fn toggle_edit_prediction_mode(fs: Arc, mode: EditPredictionsMode, cx: & update_settings_file(fs, cx, move |settings, _cx| { if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut() { - edit_predictions.mode = mode; + edit_predictions.mode = Some(mode); } else { settings.project.all_languages.edit_predictions = Some(settings::EditPredictionSettingsContent { - mode, + mode: Some(mode), ..Default::default() }); } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 22d60160f535b674ef7ab7a3d9d967e924b43bbb..3f974b7412d740dd30e96fdd61487ac8774daa0e 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -19,7 +19,7 @@ impl Settings for FileFinderSettings { file_icons: file_finder.file_icons.unwrap(), modal_max_width: file_finder.modal_max_width.unwrap().into(), skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(), - include_ignored: file_finder.include_ignored.unwrap(), + include_ignored: file_finder.include_ignored.flatten(), } } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 72271dbd58c5def6a4017a94e811289f2f5d8b8a..22f1d5df982a1796c236f5813b273e5e6e78ae7b 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -430,33 +430,25 @@ impl settings::Settings for AllLanguageSettings { .features .as_ref() .and_then(|f| f.edit_prediction_provider); - let edit_predictions_mode = all_languages.edit_predictions.as_ref().unwrap().mode; - let disabled_globs: HashSet<&String> = all_languages - .edit_predictions - .as_ref() - .unwrap() + let edit_predictions = all_languages.edit_predictions.clone().unwrap(); + let edit_predictions_mode = edit_predictions.mode.unwrap(); + + let disabled_globs: HashSet<&String> = edit_predictions .disabled_globs .as_ref() .unwrap() .iter() .collect(); - let copilot_settings = all_languages - .edit_predictions - .as_ref() - .map(|settings| CopilotSettings { - proxy: settings.copilot.proxy.clone(), - proxy_no_verify: settings.copilot.proxy_no_verify, - enterprise_uri: settings.copilot.enterprise_uri.clone(), - }) - .unwrap_or_default(); + let copilot = edit_predictions.copilot.unwrap(); + let copilot_settings = CopilotSettings { + proxy: copilot.proxy, + proxy_no_verify: copilot.proxy_no_verify, + enterprise_uri: copilot.enterprise_uri, + }; - let enabled_in_text_threads = all_languages - .edit_predictions - .as_ref() - .map(|settings| settings.enabled_in_text_threads) - .unwrap_or(true); + let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap(); let mut file_types: FxHashMap, GlobSet> = FxHashMap::default(); let mut file_globs: FxHashMap, Vec> = FxHashMap::default(); @@ -511,9 +503,12 @@ impl settings::Settings for AllLanguageSettings { } if let Some(edit_predictions) = all_languages.edit_predictions.as_ref() { - self.edit_predictions.mode = edit_predictions.mode; - self.edit_predictions.enabled_in_text_threads = - edit_predictions.enabled_in_text_threads; + self.edit_predictions + .mode + .merge_from(&edit_predictions.mode); + self.edit_predictions + .enabled_in_text_threads + .merge_from(&edit_predictions.enabled_in_text_threads); if let Some(disabled_globs) = edit_predictions.disabled_globs.as_ref() { self.edit_predictions @@ -531,7 +526,7 @@ impl settings::Settings for AllLanguageSettings { if let Some(proxy) = all_languages .edit_predictions .as_ref() - .and_then(|settings| settings.copilot.proxy.clone()) + .and_then(|settings| settings.copilot.as_ref()?.proxy.clone()) { self.edit_predictions.copilot.proxy = Some(proxy); } @@ -539,7 +534,7 @@ impl settings::Settings for AllLanguageSettings { if let Some(proxy_no_verify) = all_languages .edit_predictions .as_ref() - .and_then(|settings| settings.copilot.proxy_no_verify) + .and_then(|settings| settings.copilot.as_ref()?.proxy_no_verify) { self.edit_predictions.copilot.proxy_no_verify = Some(proxy_no_verify); } @@ -547,7 +542,7 @@ impl settings::Settings for AllLanguageSettings { if let Some(enterprise_uri) = all_languages .edit_predictions .as_ref() - .and_then(|settings| settings.copilot.enterprise_uri.clone()) + .and_then(|settings| settings.copilot.as_ref()?.enterprise_uri.clone()) { self.edit_predictions.copilot.enterprise_uri = Some(enterprise_uri); } diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 09ca95b1a2a44be8bdcfec36a3b032de67b91222..d705d64bfe0f7cdd65e18196bebcd888d5a2fb5f 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -250,7 +250,12 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement update_settings_file( fs.clone(), cx, - move |setting, _| setting.telemetry.get_or_insert_default().metrics = Some(enabled), + move |setting, _| { + dbg!(&setting.telemetry); + setting.telemetry.get_or_insert_default().metrics = Some(enabled); + dbg!(&setting.telemetry); + } + , ); // This telemetry event shouldn't fire when it's off. If it does we'll be alerted @@ -289,7 +294,12 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement update_settings_file( fs.clone(), cx, - move |setting, _| setting.telemetry.get_or_insert_default().diagnostics = Some(enabled), + move |setting, _| { + dbg!(&setting.telemetry); + setting.telemetry.get_or_insert_default().diagnostics = Some(enabled); + dbg!(&setting.telemetry); + }, + ); // This telemetry event shouldn't fire when it's off. If it does we'll be alerted diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 5321201384edf12e5ad662be9e54d70499713c99..86a2ea9d286acf33e75559794cccfbe1bbf6a4e1 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -526,7 +526,7 @@ pub struct FileFinderSettingsContent { /// * `None`: Be smart and search for ignored when called from a gitignored worktree /// /// Default: None - /// todo!() -> Change this type to an enum + /// todo() -> Change this type to an enum pub include_ignored: Option>, } diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index e9588ac8de21181d2414fcf15f179b4b9f2966f7..7a679a2a06caf4468d5872dc1c06fb2462c88735 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -57,23 +57,15 @@ 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: EditPredictionsMode, + pub mode: Option, /// Settings specific to GitHub Copilot. - #[serde(default)] - pub copilot: CopilotSettingsContent, + pub copilot: Option, /// 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, -} - -fn default_true() -> bool { - true + pub enabled_in_text_threads: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] @@ -81,17 +73,14 @@ 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, } @@ -132,24 +121,20 @@ 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 @@ -157,52 +142,42 @@ pub struct LanguageSettingsContent { /// 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. /// @@ -212,7 +187,6 @@ pub struct LanguageSettingsContent { /// - `"..."` - 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. /// @@ -220,13 +194,11 @@ pub struct LanguageSettingsContent { /// 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. @@ -234,23 +206,18 @@ pub struct LanguageSettingsContent { /// 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. @@ -862,6 +829,10 @@ pub struct IndentGuideSettings { pub background_coloring: IndentGuideBackgroundColoring, } +fn default_true() -> bool { + true +} + fn line_width() -> u32 { 1 } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fa4cb8dd0db161e39de828043a2651a97d62942e..15d7ef280c2e81bd428860f3d0390519731b5201 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -424,6 +424,7 @@ impl SettingsStore { async move { let res = async move { let old_text = Self::load_settings(&fs).await?; + dbg!(&old_text); let new_text = update(old_text, cx)?; let settings_path = paths::settings_file().as_path(); if fs.is_file(settings_path).await { @@ -554,7 +555,10 @@ impl SettingsStore { text: &str, update: impl FnOnce(&mut SettingsContent), ) -> Vec<(Range, String)> { - let old_content: UserSettingsContent = serde_json::from_str(text).unwrap_or_default(); + dbg!(&text); + let old_content: UserSettingsContent = + parse_json_with_comments(text).log_err().unwrap_or_default(); + dbg!(&old_content); let mut new_content = old_content.clone(); update(&mut new_content.content); From ea473eea875dcdbca05a3e98d10117724cdad935 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 17 Sep 2025 16:45:47 -0400 Subject: [PATCH 089/117] acp: Fix agent servers sometimes not being registered when Zed starts (#38330) In local projects, initialize the list of agents in the agent server store immediately. Previously we were initializing the list only after a delay, in an attempt to avoid sending the `ExternalAgentsUpdated` message to the downstream client (if any) before its handlers were initialized. But we already have a separate codepath for that situation, in the `AgentServerStore::shared`, and we can insert the delay in that place instead. Release Notes: - acp: Fixed a bug where starting an external agent thread soon after Zed starts up would show a "not registered" error. --------- Co-authored-by: Michael Co-authored-by: Agus --- .../src/tree_sitter_index.rs | 14 ++---- crates/project/src/agent_server_store.rs | 43 ++++++++----------- crates/project/src/git_store/conflict_set.rs | 6 +-- crates/remote_server/src/headless_project.rs | 2 +- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/crates/edit_prediction_context/src/tree_sitter_index.rs b/crates/edit_prediction_context/src/tree_sitter_index.rs index 4dc00941fe1b8a7a095fffd5605b040001c02eb7..f905aa7a01f29d26083d219bc8d2bd600847036a 100644 --- a/crates/edit_prediction_context/src/tree_sitter_index.rs +++ b/crates/edit_prediction_context/src/tree_sitter_index.rs @@ -506,7 +506,6 @@ mod tests { use super::*; use std::{path::Path, sync::Arc}; - use futures::channel::oneshot; use gpui::TestAppContext; use indoc::indoc; use language::{Language, LanguageConfig, LanguageId, LanguageMatcher, tree_sitter_rust}; @@ -655,17 +654,10 @@ mod tests { expect_file_decl("a.rs", &decls[1], &project, cx); }); - // Drop the buffer and wait for release - let (release_tx, release_rx) = oneshot::channel(); - cx.update(|cx| { - cx.observe_release(&buffer, |_, _| { - release_tx.send(()).ok(); - }) - .detach(); + // Need to trigger flush_effects so that the observe_release handler will run. + cx.update(|_cx| { + drop(buffer); }); - drop(buffer); - cx.run_until_parked(); - release_rx.await.ok(); cx.run_until_parked(); index.read_with(cx, |index, cx| { diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index bdb2297624e4a404cb3c918f07eab15004944f97..abe27710d8969d6e365d9a2539855fd625645134 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -234,7 +234,7 @@ impl AgentServerStore { let subscription = cx.observe_global::(|this, cx| { this.agent_servers_settings_changed(cx); }); - let this = Self { + let mut this = Self { state: AgentServerStoreState::Local { node_runtime, fs, @@ -245,14 +245,7 @@ impl AgentServerStore { }, external_agents: Default::default(), }; - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(1)).await; - this.update(cx, |this, cx| { - this.agent_servers_settings_changed(cx); - }) - .ok(); - }) - .detach(); + this.agent_servers_settings_changed(cx); this } @@ -305,22 +298,29 @@ impl AgentServerStore { } } - pub fn shared(&mut self, project_id: u64, client: AnyProtoClient) { + pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { match &mut self.state { AgentServerStoreState::Local { downstream_client, .. } => { - client - .send(proto::ExternalAgentsUpdated { - project_id, - names: self - .external_agents + *downstream_client = Some((project_id, client.clone())); + // Send the current list of external agents downstream, but only after a delay, + // to avoid having the message arrive before the downstream project's agent server store + // sets up its handlers. + cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(1)).await; + let names = this.update(cx, |this, _| { + this.external_agents .keys() .map(|name| name.to_string()) - .collect(), - }) - .log_err(); - *downstream_client = Some((project_id, client)); + .collect() + })?; + client + .send(proto::ExternalAgentsUpdated { project_id, names }) + .log_err(); + anyhow::Ok(()) + }) + .detach(); } AgentServerStoreState::Remote { .. } => { debug_panic!( @@ -721,11 +721,6 @@ struct RemoteExternalAgentServer { new_version_available_tx: Option>>, } -// new method: status_updated -// does nothing in the all-local case -// for RemoteExternalAgentServer, sends on the stored tx -// etc. - impl ExternalAgentServer for RemoteExternalAgentServer { fn get_command( &mut self, diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 313a1e90adc2fde8a62dbe6aa60b4d3a366af22c..2bcfc75b32da3c5a4860cc72f3266bff38f022e3 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -257,7 +257,7 @@ impl EventEmitter for ConflictSet {} mod tests { use std::{path::Path, sync::mpsc}; - use crate::{Project, project_settings::ProjectSettings}; + use crate::Project; use super::*; use fs::FakeFs; @@ -484,7 +484,7 @@ mod tests { cx.update(|cx| { settings::init(cx); WorktreeSettings::register(cx); - ProjectSettings::register(cx); + Project::init_settings(cx); AllLanguageSettings::register(cx); }); let initial_text = " @@ -585,7 +585,7 @@ mod tests { cx.update(|cx| { settings::init(cx); WorktreeSettings::register(cx); - ProjectSettings::register(cx); + Project::init_settings(cx); AllLanguageSettings::register(cx); }); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index f107ff2c8f8860a621ad5c637e6fa34b54734a6d..504e6a4bfe852bad07c72a01a323e5de22d1a4c2 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -197,7 +197,7 @@ impl HeadlessProject { let agent_server_store = cx.new(|cx| { let mut agent_server_store = AgentServerStore::local(node_runtime.clone(), fs.clone(), environment, cx); - agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone()); + agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); agent_server_store }); From 448af397e695d0d056a054b28b644cf6192abb81 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 14:58:12 -0600 Subject: [PATCH 090/117] Fix inlay settings --- assets/settings/default.json | 1 + crates/collab/src/tests/editor_tests.rs | 94 +++--- crates/editor/src/editor.rs | 9 +- crates/editor/src/hover_links.rs | 20 +- crates/editor/src/hover_popover.rs | 20 +- crates/editor/src/inlay_hint_cache.rs | 315 +++++++++--------- crates/language/src/language_settings.rs | 123 ++++++- crates/onboarding/src/basics_page.rs | 4 - crates/onboarding/src/editing_page.rs | 11 +- .../settings/src/settings_content/language.rs | 64 +--- crates/settings/src/settings_store.rs | 3 - 11 files changed, 375 insertions(+), 289 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 8dd3e46eec3f466a2647cf55c57dd4ef32c01776..78fdc3d38d2e33febcb186e1edf68c3a40b01d66 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -603,6 +603,7 @@ // Toggle certain types of hints on and off, all switched on by default. "show_type_hints": true, "show_parameter_hints": true, + "show_value_hints": true, // Corresponds to null/None LSP hint type value. "show_other_hints": true, // Whether to show a background for inlay hints. diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 3fe2e797bf95b6ebf449f388ad48e53f215ca37b..c7c358a44b88d5e5d26115984aa6a731ed6502d4 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -27,7 +27,7 @@ use project::{ use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; use serde_json::json; -use settings::{InlayHintSettings, InlineBlameSettings, SettingsStore}; +use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore}; use std::{ collections::BTreeSet, ops::{Deref as _, Range}, @@ -1786,34 +1786,36 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_value_hints: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - show_background: false, - toggle_on_modifiers_press: None, - }) + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + enabled: true, + show_value_hints: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + show_background: false, + toggle_on_modifiers_press: None, + }) }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - show_background: false, - toggle_on_modifiers_press: None, - }) + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + show_value_hints: true, + enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + show_background: false, + toggle_on_modifiers_press: None, + }) }); }); }); @@ -2036,34 +2038,36 @@ async fn test_inlay_hint_refresh_is_forwarded( cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: false, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - show_background: false, - toggle_on_modifiers_press: None, - }) + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + show_value_hints: true, + enabled: false, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + show_background: false, + toggle_on_modifiers_press: None, + }) }); }); }); cx_b.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, - toggle_on_modifiers_press: None, - }) + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + show_value_hints: true, + enabled: true, + edit_debounce_ms: 0, + scroll_debounce_ms: 0, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + show_background: false, + toggle_on_modifiers_press: None, + }) }); }); }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 28dd71c94b341e42fe6a68de6efcf097c7536e6c..2bea1fc72b5586554219bb4967f44edc18424adf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -126,8 +126,8 @@ use language::{ Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, - language_settings, + self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, + all_language_settings, language_settings, }, point_from_lsp, point_to_lsp, text_diff_with_options, }; @@ -169,10 +169,7 @@ use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, }; use serde::{Deserialize, Serialize}; -use settings::{ - GitGutterSetting, InlayHintSettings, Settings, SettingsLocation, SettingsStore, - update_settings_file, -}; +use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; use std::{ diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index bf0367c159f5e576f9f2cf2a5c32f790f9c85ea5..d5a3f17822ff7f0f2324414aeaa9819b8605f53b 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -932,7 +932,7 @@ mod tests { use gpui::Modifiers; use indoc::indoc; use lsp::request::{GotoDefinition, GotoTypeDefinition}; - use settings::InlayHintSettings; + use settings::InlayHintSettingsContent; use util::{assert_set_eq, path}; use workspace::item::Item; @@ -1280,15 +1280,15 @@ mod tests { #[gpui::test] async fn test_inlay_hover_links(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - enabled: true, - show_value_hints: false, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + show_value_hints: Some(false), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 21b9c777e409ef2b2d8a3ecb990b5bd77d408ea5..1815029207cf485292277861cd8d18ed482d6077 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1005,7 +1005,7 @@ mod tests { use gpui::App; use indoc::indoc; use markdown::parser::MarkdownEvent; - use settings::InlayHintSettings; + use settings::InlayHintSettingsContent; use smol::stream::StreamExt; use std::sync::atomic; use std::sync::atomic::AtomicUsize; @@ -1551,15 +1551,15 @@ mod tests { #[gpui::test] async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 970c533710a09c84875ce0047060ede5e3a6aa56..59c52e4341a74e85236d99da4bf6ff1195266f68 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -20,12 +20,14 @@ use anyhow::Context as _; use clock::Global; use futures::future; use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window}; -use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind}; +use language::{ + Buffer, BufferSnapshot, + language_settings::{InlayHintKind, InlayHintSettings}, +}; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; use collections::{HashMap, HashSet, hash_map}; -use settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; use text::{BufferId, ToOffset, ToPoint}; @@ -1307,7 +1309,7 @@ pub mod tests { use parking_lot::Mutex; use project::{FakeFs, Project}; use serde_json::json; - use settings::{AllLanguageSettingsContent, InlayHintSettings, SettingsStore}; + use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use text::Point; use util::path; @@ -1318,15 +1320,17 @@ pub mod tests { async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))), + show_parameter_hints: Some( + allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + ), + show_other_hints: Some(allowed_hint_kinds.contains(&None)), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1428,15 +1432,15 @@ pub mod tests { #[gpui::test] async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1535,15 +1539,15 @@ pub mod tests { #[gpui::test] async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1765,15 +1769,17 @@ pub mod tests { async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))), + show_parameter_hints: Some( + allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + ), + show_other_hints: Some(allowed_hint_kinds.contains(&None)), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1926,16 +1932,19 @@ pub mod tests { ), ] { update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: new_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: new_allowed_hint_kinds.contains(&None), - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some( + new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + ), + show_parameter_hints: Some( + new_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + ), + show_other_hints: Some(new_allowed_hint_kinds.contains(&None)), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1970,16 +1979,19 @@ pub mod tests { let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: false, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: another_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: another_allowed_hint_kinds.contains(&None), - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(false), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some( + another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + ), + show_parameter_hints: Some( + another_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + ), + show_other_hints: Some(another_allowed_hint_kinds.contains(&None)), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2027,16 +2039,19 @@ pub mod tests { let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: final_allowed_hint_kinds - .contains(&Some(InlayHintKind::Parameter)), - show_other_hints: final_allowed_hint_kinds.contains(&None), - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some( + final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + ), + show_parameter_hints: Some( + final_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + ), + show_other_hints: Some(final_allowed_hint_kinds.contains(&None)), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2102,15 +2117,15 @@ pub mod tests { #[gpui::test] async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2239,15 +2254,15 @@ pub mod tests { #[gpui::test(iterations = 10)] async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2540,15 +2555,15 @@ pub mod tests { #[gpui::test] async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2864,15 +2879,15 @@ pub mod tests { #[gpui::test] async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(false), + show_parameter_hints: Some(false), + show_other_hints: Some(false), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -3041,15 +3056,15 @@ pub mod tests { .unwrap(); update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -3074,15 +3089,15 @@ pub mod tests { #[gpui::test] async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -3167,15 +3182,15 @@ pub mod tests { #[gpui::test] async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: false, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(false), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -3244,15 +3259,15 @@ pub mod tests { .unwrap(); update_test_language_settings(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -3305,15 +3320,15 @@ pub mod tests { #[gpui::test] async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { - settings.defaults.inlay_hints = Some(InlayHintSettings { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 22f1d5df982a1796c236f5813b273e5e6e78ae7b..f1daa0d3f8198fb36ef37bfe79a6ed01f7ea2add 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -7,7 +7,7 @@ use ec4rs::{ property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; -use gpui::App; +use gpui::{App, Modifiers}; use itertools::{Either, Itertools}; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; @@ -130,7 +130,7 @@ pub struct LanguageSettings { /// Whether to start a new line with a comment when a previous line is a comment as well. pub extend_comment_on_newline: bool, /// Inlay hint related settings. - pub inlay_hints: settings::InlayHintSettings, + pub inlay_hints: InlayHintSettings, /// Whether to automatically close brackets. pub use_autoclose: bool, /// Whether to automatically surround text with brackets. @@ -211,6 +211,73 @@ impl LanguageSettings { } } +// The settings for inlay hints. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct InlayHintSettings { + /// Global switch to toggle hints on and off. + /// + /// Default: false + pub enabled: bool, + /// Global switch to toggle inline values on and off when debugging. + /// + /// Default: true + pub show_value_hints: bool, + /// Whether type hints should be shown. + /// + /// Default: true + pub show_type_hints: bool, + /// Whether parameter hints should be shown. + /// + /// Default: true + pub show_parameter_hints: bool, + /// Whether other hints should be shown. + /// + /// 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 + pub show_background: bool, + /// Whether or not to debounce inlay hints updates after buffer edits. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 700 + pub edit_debounce_ms: u64, + /// Whether or not to debounce inlay hints updates after buffer scrolls. + /// + /// Set to 0 to disable debouncing. + /// + /// Default: 50 + 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 + pub toggle_on_modifiers_press: Option, +} + +impl InlayHintSettings { + /// Returns the kinds of inlay hints that are enabled based on the settings. + pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { + let mut kinds = HashSet::default(); + if self.show_type_hints { + kinds.insert(Some(InlayHintKind::Type)); + } + if self.show_parameter_hints { + kinds.insert(Some(InlayHintKind::Parameter)); + } + if self.show_other_hints { + kinds.insert(None); + } + kinds + } +} + /// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot) /// or [Supermaven](https://supermaven.com). #[derive(Clone, Debug, Default, SettingsUi)] @@ -377,6 +444,7 @@ impl settings::Settings for AllLanguageSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { let all_languages = &content.project.all_languages; let defaults = all_languages.defaults.clone(); + let inlay_hints = defaults.inlay_hints.unwrap(); let default_language_settings = LanguageSettings { tab_size: defaults.tab_size.unwrap(), hard_tabs: defaults.hard_tabs.unwrap(), @@ -401,7 +469,17 @@ impl settings::Settings for AllLanguageSettings { show_whitespaces: defaults.show_whitespaces.unwrap(), whitespace_map: defaults.whitespace_map.unwrap(), extend_comment_on_newline: defaults.extend_comment_on_newline.unwrap(), - inlay_hints: defaults.inlay_hints.unwrap(), + inlay_hints: InlayHintSettings { + enabled: inlay_hints.enabled.unwrap(), + show_value_hints: inlay_hints.show_value_hints.unwrap(), + show_type_hints: inlay_hints.show_type_hints.unwrap(), + show_parameter_hints: inlay_hints.show_parameter_hints.unwrap(), + show_other_hints: inlay_hints.show_other_hints.unwrap(), + show_background: inlay_hints.show_background.unwrap(), + edit_debounce_ms: inlay_hints.edit_debounce_ms.unwrap(), + scroll_debounce_ms: inlay_hints.scroll_debounce_ms.unwrap(), + toggle_on_modifiers_press: inlay_hints.toggle_on_modifiers_press, + }, use_autoclose: defaults.use_autoclose.unwrap(), use_auto_surround: defaults.use_auto_surround.unwrap(), use_on_type_format: defaults.use_on_type_format.unwrap(), @@ -805,7 +883,44 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent settings .extend_comment_on_newline .merge_from(&src.extend_comment_on_newline); - settings.inlay_hints.merge_from(&src.inlay_hints.clone()); + if let Some(inlay_hints) = &src.inlay_hints { + settings + .inlay_hints + .enabled + .merge_from(&inlay_hints.enabled); + settings + .inlay_hints + .show_value_hints + .merge_from(&inlay_hints.show_value_hints); + settings + .inlay_hints + .show_type_hints + .merge_from(&inlay_hints.show_type_hints); + settings + .inlay_hints + .show_parameter_hints + .merge_from(&inlay_hints.show_parameter_hints); + settings + .inlay_hints + .show_other_hints + .merge_from(&inlay_hints.show_other_hints); + settings + .inlay_hints + .show_background + .merge_from(&inlay_hints.show_background); + settings + .inlay_hints + .edit_debounce_ms + .merge_from(&inlay_hints.edit_debounce_ms); + settings + .inlay_hints + .scroll_debounce_ms + .merge_from(&inlay_hints.scroll_debounce_ms); + if let Some(toggle_on_modifiers_press) = &inlay_hints.toggle_on_modifiers_press { + settings.inlay_hints.toggle_on_modifiers_press = + Some(toggle_on_modifiers_press.clone()); + } + } settings .show_completions_on_input .merge_from(&src.show_completions_on_input); diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index d705d64bfe0f7cdd65e18196bebcd888d5a2fb5f..b3b2dfc9af85cd1e3d8b8b2892ad4245fc047ca4 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -251,9 +251,7 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement fs.clone(), cx, move |setting, _| { - dbg!(&setting.telemetry); setting.telemetry.get_or_insert_default().metrics = Some(enabled); - dbg!(&setting.telemetry); } , ); @@ -295,9 +293,7 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement fs.clone(), cx, move |setting, _| { - dbg!(&setting.telemetry); setting.telemetry.get_or_insert_default().diagnostics = Some(enabled); - dbg!(&setting.telemetry); }, ); diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 567444007bd75ae34709495103ac257a8c4701e0..404374dc06bd817f8084f320a107233b42232816 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -58,19 +58,14 @@ fn write_inlay_hints(enabled: bool, cx: &mut App) { curr_settings.defaults.inlay_hints.enabled = enabled; AllLanguageSettings::override_global(curr_settings, cx); - update_settings_file(fs, cx, move |settings, cx| { + update_settings_file(fs, cx, move |settings, _cx| { settings .project .all_languages .defaults .inlay_hints - .get_or_insert_with(|| { - AllLanguageSettings::get_global(cx) - .clone() - .defaults - .inlay_hints - }) - .enabled = enabled; + .get_or_insert_default() + .enabled = Some(enabled); }); } diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 7a679a2a06caf4468d5872dc1c06fb2462c88735..74f3838efe38bc05c04f0445e35d86182840eeb0 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -218,7 +218,7 @@ pub struct LanguageSettingsContent { /// Default: true pub extend_comment_on_newline: Option, /// Inlay hint related settings. - pub inlay_hints: Option, + 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. /// @@ -348,81 +348,55 @@ pub struct JsxTagAutoCloseSettings { /// The settings for inlay hints. /// todo!() the fields of this struct should likely be optional, /// and a similar struct exposed from the language crate. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct InlayHintSettings { +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct InlayHintSettingsContent { /// Global switch to toggle hints on and off. /// /// Default: false - #[serde(default)] - pub enabled: bool, + pub enabled: Option, /// Global switch to toggle inline values on and off when debugging. /// /// Default: true - #[serde(default = "default_true")] - pub show_value_hints: bool, + pub show_value_hints: Option, /// Whether type hints should be shown. /// /// Default: true - #[serde(default = "default_true")] - pub show_type_hints: bool, + pub show_type_hints: Option, /// Whether parameter hints should be shown. /// /// Default: true - #[serde(default = "default_true")] - pub show_parameter_hints: bool, + pub show_parameter_hints: Option, /// Whether other hints should be shown. /// /// Default: true - #[serde(default = "default_true")] - pub show_other_hints: bool, + pub show_other_hints: Option, /// 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, + pub show_background: Option, /// 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, + pub edit_debounce_ms: Option, /// 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, + pub scroll_debounce_ms: Option, /// 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`. + /// If no modifiers are specified, this is equivalent to `null`. /// - /// Default: None - #[serde(default)] + /// Default: null pub toggle_on_modifiers_press: Option, } -impl InlayHintSettings { - /// Returns the kinds of inlay hints that are enabled based on the settings. - pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { - let mut kinds = HashSet::default(); - if self.show_type_hints { - kinds.insert(Some(InlayHintKind::Type)); - } - if self.show_parameter_hints { - kinds.insert(Some(InlayHintKind::Parameter)); - } - if self.show_other_hints { - kinds.insert(None); - } - kinds - } -} - /// The kind of an inlay hint. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InlayHintKind { @@ -433,7 +407,7 @@ pub enum InlayHintKind { } impl InlayHintKind { - /// Returns the [`InlayHintKind`] from the given name. + /// Returns the [`InlayHintKind`]fromthe given name. /// /// Returns `None` if `name` does not match any of the expected /// string representations. @@ -454,15 +428,7 @@ impl InlayHintKind { } } -fn edit_debounce_ms() -> u64 { - 700 -} - -fn scroll_debounce_ms() -> u64 { - 50 -} - -/// Controls how completions are processed for this language. +/// Controls how completions are processedfor this anguage. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct CompletionSettings { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 15d7ef280c2e81bd428860f3d0390519731b5201..33158d44173ee9f360a21f9a0bcd5c7aa284708d 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -424,7 +424,6 @@ impl SettingsStore { async move { let res = async move { let old_text = Self::load_settings(&fs).await?; - dbg!(&old_text); let new_text = update(old_text, cx)?; let settings_path = paths::settings_file().as_path(); if fs.is_file(settings_path).await { @@ -555,10 +554,8 @@ impl SettingsStore { text: &str, update: impl FnOnce(&mut SettingsContent), ) -> Vec<(Range, String)> { - dbg!(&text); let old_content: UserSettingsContent = parse_json_with_comments(text).log_err().unwrap_or_default(); - dbg!(&old_content); let mut new_content = old_content.clone(); update(&mut new_content.content); From 047c873a6227fae5909ac9700f032e6a6a385dd7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 15:44:49 -0600 Subject: [PATCH 091/117] fix --- crates/collab/src/tests/editor_tests.rs | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index c7c358a44b88d5e5d26115984aa6a731ed6502d4..cc84ddc800d7c4c4979dc824d25fa705405a0aa7 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1788,14 +1788,14 @@ async fn test_mutual_editor_inlay_hint_cache_update( store.update_user_settings(cx, |settings| { settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettingsContent { - enabled: true, - show_value_hints: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - show_background: false, + enabled: Some(true), + show_value_hints: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(false), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -1806,14 +1806,14 @@ async fn test_mutual_editor_inlay_hint_cache_update( store.update_user_settings(cx, |settings| { settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettingsContent { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: false, - show_other_hints: true, - show_background: false, + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(false), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2040,14 +2040,14 @@ async fn test_inlay_hint_refresh_is_forwarded( store.update_user_settings(cx, |settings| { settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettingsContent { - show_value_hints: true, - enabled: false, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: false, - show_parameter_hints: false, - show_other_hints: false, - show_background: false, + show_value_hints: Some(true), + enabled: Some(false), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(false), + show_parameter_hints: Some(false), + show_other_hints: Some(false), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); @@ -2058,14 +2058,14 @@ async fn test_inlay_hint_refresh_is_forwarded( store.update_user_settings(cx, |settings| { settings.project.all_languages.defaults.inlay_hints = Some(InlayHintSettingsContent { - show_value_hints: true, - enabled: true, - edit_debounce_ms: 0, - scroll_debounce_ms: 0, - show_type_hints: true, - show_parameter_hints: true, - show_other_hints: true, - show_background: false, + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), toggle_on_modifiers_press: None, }) }); From d8176c9a14c90309f727c4eaeb33442a4fd6d36a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 15:56:33 -0600 Subject: [PATCH 092/117] skip_serializing_with --- Cargo.lock | 298 ++++++++++++------ Cargo.toml | 1 + crates/settings/Cargo.toml | 1 + crates/settings/src/base_keymap_setting.rs | 2 + crates/settings/src/settings_content.rs | 36 ++- crates/settings/src/settings_content/agent.rs | 10 + .../settings/src/settings_content/editor.rs | 11 + .../settings/src/settings_content/language.rs | 15 + .../src/settings_content/language_model.rs | 31 ++ .../settings/src/settings_content/project.rs | 18 ++ .../settings/src/settings_content/terminal.rs | 6 +- crates/settings/src/settings_content/theme.rs | 11 + .../src/settings_content/workspace.rs | 9 + crates/settings/src/settings_store.rs | 26 ++ 14 files changed, 367 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d6f16a3f3e5149c4dd403f3721040a39b00f85a..f7ef8f5ccc67f504da11872b7171817a72b6a116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ dependencies = [ "rand 0.9.1", "ref-cast", "rope", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -205,7 +205,7 @@ dependencies = [ "futures 0.3.31", "log", "parking_lot", - "schemars", + "schemars 1.0.1", "serde", "serde_json", ] @@ -257,7 +257,7 @@ dependencies = [ "prompt_store", "reqwest_client", "rust-embed", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -335,7 +335,7 @@ dependencies = [ "gpui", "language_model", "paths", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -410,7 +410,7 @@ dependencies = [ "release_channel", "rope", "rules_library", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -655,7 +655,7 @@ dependencies = [ "chrono", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -1006,7 +1006,7 @@ dependencies = [ "regex", "reqwest_client", "rust-embed", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -1409,7 +1409,7 @@ dependencies = [ "log", "parking_lot", "rodio", - "schemars", + "schemars 1.0.1", "serde", "settings", "smol", @@ -1442,7 +1442,7 @@ dependencies = [ "log", "paths", "release_channel", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -2144,7 +2144,7 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "futures 0.3.31", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "strum 0.27.1", @@ -2645,7 +2645,7 @@ dependencies = [ "log", "postage", "project", - "schemars", + "schemars 1.0.1", "serde", "settings", "telemetry", @@ -2821,7 +2821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "heck 0.4.1", - "indexmap", + "indexmap 2.9.0", "log", "proc-macro2", "quote", @@ -3099,7 +3099,7 @@ dependencies = [ "release_channel", "rpc", "rustls-pki-types", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_urlencoded", @@ -3410,7 +3410,7 @@ dependencies = [ "project", "release_channel", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -3432,7 +3432,7 @@ dependencies = [ name = "collections" version = "0.1.0" dependencies = [ - "indexmap", + "indexmap 2.9.0", "rustc-hash 2.1.1", "workspace-hack", ] @@ -3594,7 +3594,7 @@ dependencies = [ "net", "parking_lot", "postage", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -4395,7 +4395,7 @@ dependencies = [ "parking_lot", "paths", "proto", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -4415,7 +4415,7 @@ name = "dap-types" version = "0.0.1" source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8" dependencies = [ - "schemars", + "schemars 1.0.1", "serde", "serde_json", ] @@ -4445,6 +4445,41 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -4586,7 +4621,7 @@ dependencies = [ "pretty_assertions", "project", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -4627,7 +4662,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "workspace-hack", @@ -4825,7 +4860,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5087,7 +5122,7 @@ dependencies = [ "regex", "release_channel", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -5582,7 +5617,7 @@ dependencies = [ "release_channel", "remote", "reqwest_client", - "schemars", + "schemars 1.0.1", "semantic_version", "serde", "serde_json", @@ -5779,7 +5814,7 @@ dependencies = [ "picker", "pretty_assertions", "project", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -6389,7 +6424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" dependencies = [ "fallible-iterator", - "indexmap", + "indexmap 2.9.0", "stable_deref_trait", ] @@ -6412,7 +6447,7 @@ dependencies = [ "rand 0.9.1", "regex", "rope", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "smol", @@ -6454,7 +6489,7 @@ dependencies = [ "indoc", "pretty_assertions", "regex", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -6499,7 +6534,7 @@ dependencies = [ "postage", "pretty_assertions", "project", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -7334,7 +7369,7 @@ dependencies = [ "menu", "project", "rope", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -7366,7 +7401,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -7468,7 +7503,7 @@ dependencies = [ "reqwest_client", "resvg", "scap", - "schemars", + "schemars 1.0.1", "seahash", "semantic_version", "serde", @@ -7554,7 +7589,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -7573,7 +7608,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -8269,6 +8304,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -8351,7 +8392,7 @@ dependencies = [ "language", "log", "project", - "schemars", + "schemars 1.0.1", "serde", "settings", "theme", @@ -8382,6 +8423,17 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -8746,7 +8798,7 @@ dependencies = [ "hashbrown 0.15.3", "hex", "ignore", - "indexmap", + "indexmap 2.9.0", "interim", "itertools 0.14.0", "jj-lib-proc-macros", @@ -8843,7 +8895,7 @@ dependencies = [ "editor", "gpui", "log", - "schemars", + "schemars 1.0.1", "serde", "settings", "shellexpand 2.1.2", @@ -9078,7 +9130,7 @@ dependencies = [ "rand 0.9.1", "regex", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -9152,7 +9204,7 @@ dependencies = [ "open_router", "parking_lot", "proto", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -9201,7 +9253,7 @@ dependencies = [ "partial-json-fixer", "project", "release_channel", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -9315,7 +9367,7 @@ dependencies = [ "regex", "rope", "rust-embed", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -9729,7 +9781,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "workspace-hack", @@ -9835,7 +9887,7 @@ dependencies = [ "parking_lot", "postage", "release_channel", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "smol", @@ -10372,7 +10424,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "strum 0.27.1", @@ -10465,7 +10517,7 @@ dependencies = [ "half", "hashbrown 0.15.3", "hexf-parse", - "indexmap", + "indexmap 2.9.0", "log", "num-traits", "once_cell", @@ -11123,7 +11175,7 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "crc32fast", "hashbrown 0.15.3", - "indexmap", + "indexmap 2.9.0", "memchr", ] @@ -11134,7 +11186,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -11163,7 +11215,7 @@ dependencies = [ "notifications", "picker", "project", - "schemars", + "schemars 1.0.1", "serde", "settings", "telemetry", @@ -11243,7 +11295,7 @@ dependencies = [ "futures 0.3.31", "http_client", "log", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -11258,7 +11310,7 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -11438,7 +11490,7 @@ dependencies = [ "outline", "pretty_assertions", "project", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -12139,7 +12191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.9.0", ] [[package]] @@ -12213,7 +12265,7 @@ dependencies = [ "env_logger 0.11.8", "gpui", "menu", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "ui", @@ -12320,7 +12372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap", + "indexmap 2.9.0", "quick-xml 0.32.0", "serde", "time", @@ -12641,7 +12693,7 @@ dependencies = [ "gpui", "http_client", "image", - "indexmap", + "indexmap 2.9.0", "itertools 0.14.0", "language", "log", @@ -12659,7 +12711,7 @@ dependencies = [ "release_channel", "remote", "rpc", - "schemars", + "schemars 1.0.1", "semver", "serde", "serde_json", @@ -12701,12 +12753,12 @@ dependencies = [ "git", "git_ui", "gpui", - "indexmap", + "indexmap 2.9.0", "language", "menu", "pretty_assertions", "project", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -13331,7 +13383,7 @@ dependencies = [ "project", "release_channel", "remote", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -13504,7 +13556,7 @@ dependencies = [ "prost 0.9.0", "release_channel", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -13624,7 +13676,7 @@ dependencies = [ "picker", "project", "runtimelib", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -14417,13 +14469,25 @@ dependencies = [ "anyhow", "clap", "env_logger 0.11.8", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "theme", "workspace-hack", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.0.1" @@ -14431,7 +14495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ "dyn-clone", - "indexmap", + "indexmap 2.9.0", "ref-cast", "schemars_derive", "serde", @@ -14642,7 +14706,7 @@ dependencies = [ "language", "menu", "project", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -14786,7 +14850,7 @@ version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40" dependencies = [ - "indexmap", + "indexmap 2.9.0", "itoa", "memchr", "ryu", @@ -14799,7 +14863,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" dependencies = [ - "indexmap", + "indexmap 2.9.0", "itoa", "memchr", "ryu", @@ -14848,6 +14912,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars 0.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "serial2" version = "0.2.29" @@ -14888,12 +14983,13 @@ dependencies = [ "pretty_assertions", "release_channel", "rust-embed", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", "serde_path_to_error", "serde_repr", + "serde_with", "settings_ui_macros", "smallvec", "tree-sitter", @@ -15222,7 +15318,7 @@ dependencies = [ "indoc", "parking_lot", "paths", - "schemars", + "schemars 1.0.1", "serde", "serde_json_lenient", "snippet", @@ -15383,7 +15479,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.3", "hashlink 0.10.0", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "once_cell", @@ -16268,7 +16364,7 @@ dependencies = [ "menu", "picker", "project", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -16348,7 +16444,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "proto", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -16454,7 +16550,7 @@ dependencies = [ "rand 0.9.1", "regex", "release_channel", - "schemars", + "schemars 1.0.1", "serde", "settings", "smol", @@ -16500,7 +16596,7 @@ dependencies = [ "project", "rand 0.9.1", "regex", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -16550,13 +16646,13 @@ dependencies = [ "fs", "futures 0.3.31", "gpui", - "indexmap", + "indexmap 2.9.0", "inventory", "log", "palette", "parking_lot", "refineable", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -16589,7 +16685,7 @@ dependencies = [ "clap", "collections", "gpui", - "indexmap", + "indexmap 2.9.0", "log", "palette", "serde", @@ -16843,7 +16939,7 @@ dependencies = [ "project", "remote", "rpc", - "schemars", + "schemars 1.0.1", "serde", "settings", "smallvec", @@ -17042,7 +17138,7 @@ version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -17622,7 +17718,7 @@ dependencies = [ "icons", "itertools 0.14.0", "menu", - "schemars", + "schemars 1.0.1", "serde", "settings", "smallvec", @@ -17897,7 +17993,7 @@ dependencies = [ "rand 0.9.1", "regex", "rust-embed", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "serde_json_lenient", @@ -18007,7 +18103,7 @@ name = "vercel" version = "0.1.0" dependencies = [ "anyhow", - "schemars", + "schemars 1.0.1", "serde", "strum 0.27.1", "workspace-hack", @@ -18055,7 +18151,7 @@ dependencies = [ "project_panel", "regex", "release_channel", - "schemars", + "schemars 1.0.1", "search", "serde", "serde_json", @@ -18312,7 +18408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -18330,7 +18426,7 @@ dependencies = [ "anyhow", "auditable-serde", "flate2", - "indexmap", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -18360,7 +18456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ "bitflags 2.9.0", - "indexmap", + "indexmap 2.9.0", "semver", ] @@ -18372,7 +18468,7 @@ checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ "bitflags 2.9.0", "hashbrown 0.15.3", - "indexmap", + "indexmap 2.9.0", "semver", "serde", ] @@ -18385,7 +18481,7 @@ checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ "bitflags 2.9.0", "hashbrown 0.15.3", - "indexmap", + "indexmap 2.9.0", "semver", ] @@ -18414,7 +18510,7 @@ dependencies = [ "cfg-if", "encoding_rs", "hashbrown 0.14.5", - "indexmap", + "indexmap 2.9.0", "libc", "log", "mach2 0.4.2", @@ -18538,7 +18634,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli", - "indexmap", + "indexmap 2.9.0", "log", "object", "postcard", @@ -18663,7 +18759,7 @@ checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.9.0", "wit-parser 0.221.3", ] @@ -19739,7 +19835,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap", + "indexmap 2.9.0", "wasm-metadata 0.201.0", "wit-bindgen-core 0.22.0", "wit-component 0.201.0", @@ -19753,7 +19849,7 @@ checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.9.0", "prettyplease", "syn 2.0.101", "wasm-metadata 0.227.1", @@ -19798,7 +19894,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", "bitflags 2.9.0", - "indexmap", + "indexmap 2.9.0", "log", "serde", "serde_derive", @@ -19817,7 +19913,7 @@ checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" dependencies = [ "anyhow", "bitflags 2.9.0", - "indexmap", + "indexmap 2.9.0", "log", "serde", "serde_derive", @@ -19836,7 +19932,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.9.0", "log", "semver", "serde", @@ -19854,7 +19950,7 @@ checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.9.0", "log", "semver", "serde", @@ -19872,7 +19968,7 @@ checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.9.0", "log", "semver", "serde", @@ -19922,7 +20018,7 @@ dependencies = [ "pretty_assertions", "project", "remote", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "session", @@ -20019,7 +20115,7 @@ dependencies = [ "hyper 0.14.32", "hyper-rustls 0.27.5", "idna", - "indexmap", + "indexmap 2.9.0", "inout", "itertools 0.12.1", "itertools 0.13.0", @@ -20157,7 +20253,7 @@ dependencies = [ "pretty_assertions", "rand 0.9.1", "rpc", - "schemars", + "schemars 1.0.1", "serde", "serde_json", "settings", @@ -20235,7 +20331,7 @@ name = "x_ai" version = "0.1.0" dependencies = [ "anyhow", - "schemars", + "schemars 1.0.1", "serde", "strum 0.27.1", "workspace-hack", @@ -20654,7 +20750,7 @@ name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", - "schemars", + "schemars 1.0.1", "serde", "uuid", "workspace-hack", diff --git a/Cargo.toml b/Cargo.toml index 3c1f5dd97692e191977b77b5a1254eda9f9b4e7a..846b0e32ee61662efa2026c116b8beee87495bcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -625,6 +625,7 @@ serde_json_lenient = { version = "0.2", features = [ serde_path_to_error = "0.1.17" serde_repr = "0.1" serde_urlencoded = "0.7" +serde_with = "3.4.0" sha2 = "0.10" shellexpand = "2.1.0" shlex = "1.3.0" diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 4ff12f017f4a379efce7e2054c9c0217db082831..062af3d23926b5bfdc0e1f7239859b6bbfd7efdd 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -34,6 +34,7 @@ settings_ui_macros.workspace = true serde_json_lenient.workspace = true serde_repr.workspace = true serde_path_to_error.workspace = true +serde_with.workspace = true smallvec.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 2e0478a49a30f8e8b23e192b3215f41b3c1b5b65..858a3e758ff7d0aa6ec045197316f027913b49d6 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -7,6 +7,7 @@ use crate::{ use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use settings::{Settings, VsCodeSettings}; use settings_ui_macros::{SettingsKey, SettingsUi}; @@ -148,6 +149,7 @@ impl BaseKeymap { )] // extracted so that it can be an option, and still work with derive(SettingsUi) #[settings_key(None)] +#[skip_serializing_none] pub struct BaseKeymapSetting { pub base_keymap: Option, } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 86a2ea9d286acf33e75559794cccfbe1bbf6a4e1..b818e5e540a591e41d2469cdcd290e70026201e1 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -21,12 +21,14 @@ use gpui::{App, SharedString}; use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::env; use std::sync::Arc; pub use util::serde::default_true; use crate::ActiveSettingsProfileName; +#[skip_serializing_none] #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct SettingsContent { #[serde(flatten)] @@ -159,12 +161,14 @@ impl SettingsContent { } // todo!() what should this be? +#[skip_serializing_none] #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ServerSettingsContent { #[serde(flatten)] pub project: ProjectSettingsContent, } +#[skip_serializing_none] #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct UserSettingsContent { #[serde(flatten)] @@ -230,6 +234,7 @@ pub enum BaseKeymapContent { None, } +#[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct TitleBarSettingsContent { /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen". @@ -275,6 +280,7 @@ pub enum TitleBarVisibility { } /// Configuration of audio in Zed. +#[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AudioSettingsContent { /// Opt into the new audio system. @@ -296,6 +302,7 @@ pub struct AudioSettingsContent { } /// Control what info is collected by Zed. +#[skip_serializing_none] #[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug)] pub struct TelemetrySettingsContent { /// Send debug info like crash reports. @@ -308,6 +315,7 @@ pub struct TelemetrySettingsContent { pub metrics: Option, } +#[skip_serializing_none] #[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)] pub struct DebuggerSettingsContent { /// Determines the stepping granularity. @@ -363,6 +371,7 @@ pub enum DockPosition { } /// Settings for slash commands. +#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. @@ -370,6 +379,7 @@ pub struct SlashCommandSettings { } /// Settings for the `/cargo-workspace` slash command. +#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)] pub struct CargoWorkspaceCommandSettings { /// Whether `/cargo-workspace` is enabled. @@ -377,6 +387,7 @@ pub struct CargoWorkspaceCommandSettings { } /// Configuration of voice calls in Zed. +#[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct CallSettingsContent { /// Whether the microphone should be muted when joining a channel or a call. @@ -390,6 +401,7 @@ pub struct CallSettingsContent { pub share_on_join: Option, } +#[skip_serializing_none] #[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema)] pub struct ExtensionSettingsContent { /// The extensions that should be automatically installed by Zed. @@ -404,6 +416,7 @@ pub struct ExtensionSettingsContent { pub auto_update_extensions: HashMap, bool>, } +#[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct GitPanelSettingsContent { /// Whether to show the panel button in the status bar. @@ -453,11 +466,13 @@ pub enum StatusStyle { LabelColor, } +#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettings { pub show: Option, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct NotificationPanelSettingsContent { /// Whether to show the panel button in the status bar. @@ -474,6 +489,7 @@ pub struct NotificationPanelSettingsContent { pub default_width: Option, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct PanelSettingsContent { /// Whether to show the panel button in the status bar. @@ -490,6 +506,7 @@ pub struct PanelSettingsContent { pub default_width: Option, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. @@ -499,6 +516,7 @@ pub struct MessageEditorSettings { pub auto_replace_emoji_shortcode: Option, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct FileFinderSettingsContent { /// Whether to show file icons in the file finder. @@ -541,6 +559,7 @@ pub enum FileFinderWidthContent { Full, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema)] pub struct VimSettingsContent { pub default_mode: Option, @@ -574,6 +593,7 @@ pub enum UseSystemClipboard { } /// The settings for cursor shape. +#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct CursorShapeSettings { /// Cursor shape for the normal mode. @@ -595,6 +615,7 @@ pub struct CursorShapeSettings { } /// Settings specific to journaling +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct JournalSettingsContent { /// The path of the directory where journal entries are stored. @@ -615,6 +636,7 @@ pub enum HourFormat { Hour24, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct OutlinePanelSettingsContent { /// Whether to show the outline panel button in the status bar. @@ -683,6 +705,7 @@ pub enum ShowIndentGuides { Never, } +#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct IndentGuidesSettingsContent { /// When to show the scrollbar in the outline panel. @@ -698,6 +721,7 @@ pub enum LineIndicatorFormat { } /// The settings for the image viewer. +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)] pub struct ImageViewerSettingsContent { /// The unit to use for displaying image file sizes. @@ -706,6 +730,7 @@ pub struct ImageViewerSettingsContent { pub unit: Option, } +#[skip_serializing_none] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ImageFileSizeUnit { @@ -716,38 +741,35 @@ pub enum ImageFileSizeUnit { Decimal, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct RemoteSettingsContent { pub ssh_connections: Option>, pub read_ssh_config: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct SshConnection { pub host: SharedString, - #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub args: Vec, #[serde(default)] pub projects: collections::BTreeSet, /// Name to use for this server in UI. - #[serde(skip_serializing_if = "Option::is_none")] pub nickname: Option, // By default Zed will download the binary to the host directly. // If this is set to true, Zed will download the binary to your local machine, // and then upload it over the SSH connection. Useful if your SSH server has // limited outbound internet access. - #[serde(skip_serializing_if = "Option::is_none")] pub upload_binary_over_ssh: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub port_forwards: Option>, } +#[skip_serializing_none] #[derive( Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, )] @@ -755,6 +777,7 @@ pub struct SshProject { pub paths: Vec, } +#[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] pub struct SshPortForwardOption { #[serde(skip_serializing_if = "Option::is_none")] @@ -766,6 +789,7 @@ pub struct SshPortForwardOption { } /// Settings for configuring REPL display and behavior. +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ReplSettingsContent { /// Maximum number of lines to keep in REPL's scrollback buffer. diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index ec50c3c2084473dee23be5304aa5bbcd4ea0a061..6dd163972fc3d90abc8cece72a8d298647423b1b 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -2,10 +2,12 @@ use collections::{HashMap, IndexMap}; use gpui::SharedString; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::{borrow::Cow, path::PathBuf, sync::Arc}; use crate::DockPosition; +#[skip_serializing_none] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct AgentSettingsContent { /// Whether the Agent is enabled. @@ -160,6 +162,8 @@ impl AgentSettingsContent { self.default_profile = Some(profile_id); } } + +#[skip_serializing_none] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct AgentProfileContent { pub name: Arc, @@ -171,6 +175,7 @@ pub struct AgentProfileContent { pub context_servers: IndexMap, ContextServerPresetContent>, } +#[skip_serializing_none] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ContextServerPresetContent { pub tools: IndexMap, bool>, @@ -193,6 +198,7 @@ pub enum NotifyWhenAgentWaiting { Never, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct LanguageModelSelection { pub provider: LanguageModelProviderSetting, @@ -208,6 +214,7 @@ pub enum CompletionMode { Burn, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct LanguageModelParameters { pub provider: Option, @@ -256,6 +263,7 @@ impl From<&str> for LanguageModelProviderSetting { } } +#[skip_serializing_none] #[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AllAgentServersSettings { pub gemini: Option, @@ -266,6 +274,7 @@ pub struct AllAgentServersSettings { pub custom: HashMap, } +#[skip_serializing_none] #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] pub struct BuiltinAgentServerSettings { /// Absolute path to a binary to be used when launching this agent. @@ -297,6 +306,7 @@ pub struct BuiltinAgentServerSettings { pub default_mode: Option, } +#[skip_serializing_none] #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] pub struct CustomAgentServerSettings { #[serde(rename = "command")] diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index e8631d356dd1c664615d0a0933b479ac8f88bbcd..d5984d4213ea053589e51abc91b5ff9f1e7268f1 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -3,9 +3,11 @@ use std::num; use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use crate::{DiagnosticSeverityContent, ShowScrollbar}; +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. @@ -191,6 +193,7 @@ pub struct EditorSettingsContent { } // Status bar related settings +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct StatusBarContent { /// Whether to display the active language button in the status bar. @@ -204,6 +207,7 @@ pub struct StatusBarContent { } // Toolbar related settings +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { /// Whether to display breadcrumbs in the editor toolbar. @@ -230,6 +234,7 @@ pub struct ToolbarContent { } /// Scrollbar related settings +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct ScrollbarContent { /// When to show the scrollbar in the editor. @@ -265,6 +270,7 @@ pub struct ScrollbarContent { } /// Minimap related settings +#[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct MinimapContent { /// When to show the minimap in the editor. @@ -299,6 +305,7 @@ pub struct MinimapContent { } /// Forcefully enable or disable the scrollbar for each axis +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct ScrollbarAxesContent { /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. @@ -313,6 +320,7 @@ pub struct ScrollbarAxesContent { } /// Gutter related settings +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct GutterContent { /// Whether to show line numbers in the gutter. @@ -525,6 +533,7 @@ pub enum SnippetSortOrder { } /// Default options for buffer and project search items. +#[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct SearchSettingsContent { /// Whether to show the project search button in the status bar. @@ -535,6 +544,7 @@ pub struct SearchSettingsContent { pub regex: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct JupyterContent { @@ -550,6 +560,7 @@ pub struct JupyterContent { } /// Whether to allow drag and drop text selection in buffer. +#[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct DragAndDropSelectionContent { /// When true, enables drag and drop text selection in buffer. diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 74f3838efe38bc05c04f0445e35d86182840eeb0..4194e102838b188a232a82e920b43b2b0186a1c4 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -7,11 +7,13 @@ use serde::{ Deserialize, Deserializer, Serialize, de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor}, }; +use serde_with::skip_serializing_none; use std::sync::Arc; use util::schemars::replace_subschema; use crate::ParameterizedJsonSchema; +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. @@ -33,6 +35,7 @@ pub struct AllLanguageSettingsContent { } /// The settings for enabling/disabling features. +#[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { @@ -52,6 +55,7 @@ pub enum EditPredictionProvider { } /// The contents of the edit prediction settings. +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct EditPredictionSettingsContent { /// A list of globs representing files that edit predictions should be disabled for. @@ -68,6 +72,7 @@ pub struct EditPredictionSettingsContent { pub enabled_in_text_threads: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct CopilotSettingsContent { /// HTTP/HTTPS proxy to use for Copilot. @@ -116,6 +121,7 @@ pub enum SoftWrap { } /// The settings for a particular language. +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. @@ -303,6 +309,7 @@ pub enum ShowWhitespaceSetting { Trailing, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct WhitespaceMap { #[serde(default)] @@ -338,9 +345,11 @@ pub enum RewrapBehavior { Anywhere, } +#[skip_serializing_none] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct JsxTagAutoCloseSettings { /// Enables or disables auto-closing of JSX tags. + // todo! option #[serde(default)] pub enabled: bool, } @@ -348,6 +357,7 @@ pub struct JsxTagAutoCloseSettings { /// The settings for inlay hints. /// todo!() the fields of this struct should likely be optional, /// and a similar struct exposed from the language crate. +#[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettingsContent { /// Global switch to toggle hints on and off. @@ -429,6 +439,7 @@ impl InlayHintKind { } /// Controls how completions are processedfor this anguage. +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct CompletionSettings { @@ -506,6 +517,7 @@ fn default_3() -> usize { /// 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. +#[skip_serializing_none] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct PrettierSettingsContent { /// Enables or disables formatting with Prettier for a given language. @@ -766,6 +778,7 @@ pub enum Formatter { } /// The settings for indent guides. +#[skip_serializing_none] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct IndentGuideSettings { /// Whether to display indent guides in the editor. @@ -808,6 +821,7 @@ fn active_line_width() -> u32 { } /// The task settings for a particular language. +#[skip_serializing_none] #[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct LanguageTaskConfig { /// Extra task variables to set for a particular language. @@ -828,6 +842,7 @@ pub struct LanguageTaskConfig { /// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language /// names in the keys. +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageToSettingsMap(pub HashMap); diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs index 0dcd95888a50cc1c3fd7cc985e362823ad6a732d..1039b505a37df9469d144f3574037a12dcf5f9f4 100644 --- a/crates/settings/src/settings_content/language_model.rs +++ b/crates/settings/src/settings_content/language_model.rs @@ -1,9 +1,11 @@ use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::sync::Arc; +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct AllLanguageModelSettingsContent { pub anthropic: Option, @@ -22,12 +24,14 @@ pub struct AllLanguageModelSettingsContent { pub zed_dot_dev: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct AnthropicSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AnthropicAvailableModel { /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc @@ -48,6 +52,7 @@ pub struct AnthropicAvailableModel { pub mode: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct AmazonBedrockSettingsContent { pub available_models: Option>, @@ -57,6 +62,7 @@ pub struct AmazonBedrockSettingsContent { pub authentication_method: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct BedrockAvailableModel { pub name: String, @@ -79,12 +85,14 @@ pub enum BedrockAuthMethodContent { Automatic, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct OllamaSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OllamaAvailableModel { /// The model name in the Ollama API (e.g. "llama3.2:latest") @@ -125,12 +133,14 @@ impl Default for KeepAlive { } } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct LmStudioSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LmStudioAvailableModel { pub name: String, @@ -140,12 +150,14 @@ pub struct LmStudioAvailableModel { pub supports_images: bool, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct DeepseekSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct DeepseekAvailableModel { pub name: String, @@ -154,12 +166,14 @@ pub struct DeepseekAvailableModel { pub max_output_tokens: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct MistralSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct MistralAvailableModel { pub name: String, @@ -172,12 +186,14 @@ pub struct MistralAvailableModel { pub supports_thinking: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct OpenAiSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenAiAvailableModel { pub name: String, @@ -197,12 +213,14 @@ pub enum OpenAiReasoningEffort { High, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct OpenAiCompatibleSettingsContent { pub api_url: String, pub available_models: Vec, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenAiCompatibleAvailableModel { pub name: String, @@ -214,6 +232,7 @@ pub struct OpenAiCompatibleAvailableModel { pub capabilities: OpenAiCompatibleModelCapabilities, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenAiCompatibleModelCapabilities { pub tools: bool, @@ -233,12 +252,14 @@ impl Default for OpenAiCompatibleModelCapabilities { } } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct VercelSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct VercelAvailableModel { pub name: String, @@ -248,12 +269,14 @@ pub struct VercelAvailableModel { pub max_completion_tokens: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct GoogleSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct GoogleAvailableModel { pub name: String, @@ -262,12 +285,14 @@ pub struct GoogleAvailableModel { pub mode: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct XAiSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct XaiAvailableModel { pub name: String, @@ -277,11 +302,13 @@ pub struct XaiAvailableModel { pub max_completion_tokens: Option, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct ZedDotDevSettingsContent { pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ZedDotDevAvailableModel { /// The provider of the language model. @@ -317,12 +344,14 @@ pub enum ZedDotDevAvailableProvider { Google, } +#[skip_serializing_none] #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct OpenRouterSettingsContent { pub api_url: Option, pub available_models: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenRouterAvailableModel { pub name: String, @@ -336,6 +365,7 @@ pub struct OpenRouterAvailableModel { pub provider: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenRouterProvider { #[serde(skip_serializing_if = "Option::is_none")] @@ -374,6 +404,7 @@ fn default_true() -> bool { } /// Configuration for caching language model messages. +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageModelCacheConfiguration { pub max_cache_anchors: usize, diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 74a39daa1803465efefda2038eab0dcf132d2831..c76321d3a9ec662dde65d1cc33848217378bcb19 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -3,10 +3,12 @@ use std::{path::PathBuf, sync::Arc}; use collections::{BTreeMap, HashMap}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use util::serde::default_true; use crate::{AllLanguageSettingsContent, SlashCommandSettings}; +#[skip_serializing_none] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettingsContent { #[serde(flatten)] @@ -44,6 +46,7 @@ pub struct ProjectSettingsContent { pub git_hosting_providers: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct WorktreeSettingsContent { /// The displayed name of this project. If not set, the root directory name @@ -81,6 +84,7 @@ pub struct WorktreeSettingsContent { pub private_files: Option>, } +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub struct LspSettings { @@ -107,6 +111,7 @@ impl Default for LspSettings { } } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] pub struct BinarySettings { pub path: Option, @@ -115,6 +120,7 @@ pub struct BinarySettings { pub ignore_system_version: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] pub struct FetchSettings { // Whether to consider pre-releases for fetching @@ -122,6 +128,7 @@ pub struct FetchSettings { } /// Common language server settings. +#[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct GlobalLspSettingsContent { /// Whether to show the LSP servers button in the status bar. @@ -131,6 +138,7 @@ pub struct GlobalLspSettingsContent { } // todo! binary is actually just required, shouldn't be an option +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct DapSettings { @@ -139,6 +147,7 @@ pub struct DapSettings { pub args: Vec, } +#[skip_serializing_none] #[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettingsContent { /// Whether or not to restore unsaved buffers on restart. @@ -189,6 +198,7 @@ impl ContextServerSettingsContent { } } +#[skip_serializing_none] #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct ContextServerCommand { #[serde(rename = "command")] @@ -224,6 +234,7 @@ impl std::fmt::Debug for ContextServerCommand { } } +#[skip_serializing_none] #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { /// Whether or not to show the git gutter. @@ -259,6 +270,7 @@ pub enum GitGutterSetting { Hide, } +#[skip_serializing_none] #[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InlineBlameSettings { @@ -287,6 +299,7 @@ pub struct InlineBlameSettings { pub show_commit_summary: Option, } +#[skip_serializing_none] #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct BranchPickerSettingsContent { @@ -306,6 +319,7 @@ pub enum GitHunkStyleSetting { UnstagedHollow, } +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct DiagnosticsSettingsContent { /// Whether to show the project diagnostics button in the status bar. @@ -321,6 +335,7 @@ pub struct DiagnosticsSettingsContent { pub inline: Option, } +#[skip_serializing_none] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct LspPullDiagnosticsSettingsContent { /// Whether to pull for diagnostics or not. @@ -334,6 +349,7 @@ pub struct LspPullDiagnosticsSettingsContent { pub debounce_ms: Option, } +#[skip_serializing_none] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Eq)] pub struct InlineDiagnosticsSettingsContent { /// Whether or not to show inline diagnostics @@ -360,6 +376,7 @@ pub struct InlineDiagnosticsSettingsContent { pub max_severity: Option, } +#[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct NodeBinarySettings { /// The path to the Node binary. @@ -395,6 +412,7 @@ pub enum DiagnosticSeverityContent { } /// A custom Git hosting provider. +#[skip_serializing_none] #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct GitHostingProviderConfig { /// The type of the provider. diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index f182d95c42a788ae6de962b734411efaeeab1b0b..e4d76a049e03438dc351b2e1bcf628cfa0e5f5e1 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -4,9 +4,11 @@ use collections::HashMap; use gpui::{AbsoluteLength, FontFeatures, SharedString, px}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use crate::FontFamilyName; +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettingsContent { /// What shell to use when opening a terminal. @@ -160,6 +162,7 @@ pub enum WorkingDirectory { Always { directory: String }, } +#[skip_serializing_none] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarSettingsContent { /// When to show the scrollbar in the terminal. @@ -243,6 +246,7 @@ pub enum AlternateScroll { } // Toolbar related settings +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct TerminalToolbarContent { /// Whether to display the terminal title in breadcrumbs inside the terminal pane. @@ -269,7 +273,7 @@ pub enum VenvSettings { directories: Option>, }, } - +#[skip_serializing_none] pub struct VenvSettingsContent<'a> { pub activate_script: ActivateScript, pub venv_name: &'a str, diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index b0798e7c7b359b8c1d9890212d9e2b31982fe440..886448d696fa975749a1fcefe615694a903f8d29 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -6,7 +6,11 @@ use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::sync::Arc; +use serde_with::skip_serializing_none; + /// Settings for rendering text in UI and text buffers. + +#[skip_serializing_none] #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { /// The default font size for text in the UI. @@ -188,6 +192,7 @@ impl UiDensity { /// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at /// runtime. +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(transparent)] pub struct FontFamilyName(pub Arc); @@ -220,6 +225,7 @@ where } /// The content of a serialized theme. +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(default)] pub struct ThemeStyleContent { @@ -254,16 +260,19 @@ pub struct PlayerColorContent { } /// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime. +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(transparent)] pub struct ThemeName(pub Arc); /// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at /// runtime. +#[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(transparent)] pub struct IconThemeName(pub Arc); +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(default)] pub struct ThemeColorsContent { @@ -768,6 +777,7 @@ pub struct ThemeColorsContent { pub version_control_conflict_theirs_background: Option, } +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(default)] pub struct HighlightStyleContent { @@ -801,6 +811,7 @@ where Ok(T::deserialize(value).ok()) } +#[skip_serializing_none] #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(default)] pub struct StatusColorsContent { diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 09fa136ea3574bacbd5d3dc10c2651ca3ba41c38..ce0b43da931049d1d4f9cc056e46827107469644 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -3,9 +3,11 @@ use std::num::NonZeroUsize; use collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides}; +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceSettingsContent { /// Active pane styling settings. @@ -105,6 +107,7 @@ pub struct WorkspaceSettingsContent { pub zoomed_padding: Option, } +#[skip_serializing_none] #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ItemSettingsContent { /// Whether to show the Git file status on a tab item. @@ -134,6 +137,7 @@ pub struct ItemSettingsContent { pub show_close_button: Option, } +#[skip_serializing_none] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct PreviewTabsSettingsContent { /// Whether to show opened editors as preview tabs. @@ -186,6 +190,7 @@ pub enum ActivateOnClose { LeftNeighbour, } +#[skip_serializing_none] #[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct ActivePanelModifiers { @@ -252,6 +257,7 @@ pub enum RestoreOnStartupBehavior { LastSession, } +#[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] pub struct TabBarSettingsContent { /// Whether or not to show the tab bar in the editor. @@ -295,6 +301,7 @@ pub enum PaneSplitDirectionVertical { Right, } +#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub struct CenteredLayoutSettings { @@ -329,6 +336,7 @@ impl OnLastWindowClosed { } } +#[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { /// Whether to show the project panel button in the status bar. @@ -414,6 +422,7 @@ pub enum ProjectPanelEntrySpacing { Standard, } +#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ProjectPanelIndentGuidesSettings { pub show: Option, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 33158d44173ee9f360a21f9a0bcd5c7aa284708d..11b589504551f41e3472baa97a064d3fe2b34025 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1574,6 +1574,32 @@ mod tests { pretty_assertions::assert_eq!(new, expected); } + #[gpui::test] + fn test_update_git_settings(cx: &mut App) { + let store = SettingsStore::new(cx, &test_settings()); + + let actual = store.new_text_for_update("{}".to_string(), |current| { + current + .git + .get_or_insert_default() + .inline_blame + .get_or_insert_default() + .enabled = Some(true); + }); + assert_eq!( + actual, + r#"{ + "git": { + "inline_blame": { + "enabled": true + } + } + } + "# + .unindent() + ); + } + #[gpui::test] fn test_global_settings(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); From eaa1cb0ca32d978f2424c417fb10f4e622fb40cb Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 18 Sep 2025 00:02:44 -0400 Subject: [PATCH 093/117] acp: Add a basic test for ACP remoting (#38381) Tests that the downstream project can see custom agents configured in the remote server's settings, and that it constructs an appropriate `AgentServerCommand`. Release Notes: - N/A --- Cargo.lock | 2 + crates/remote_server/Cargo.toml | 2 + .../remote_server/src/remote_editing_tests.rs | 88 ++++++++++++++++++- crates/settings/src/settings_store.rs | 11 +++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 13acb75ee146f412f704ed7d2c209ac59928a3cc..294127b598926ddae6924f0af1dd273416cf1e1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13981,6 +13981,7 @@ dependencies = [ "clap", "client", "clock", + "collections", "crash-handler", "crashes", "dap", @@ -14009,6 +14010,7 @@ dependencies = [ "minidumper", "node_runtime", "paths", + "pretty_assertions", "project", "proto", "release_channel", diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 59b2af0f410c98385cf13a271833980aacd6c8bc..b1f12fb0a8133b95259b38bbec22dbd031937cd7 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -77,6 +77,7 @@ assistant_tool.workspace = true assistant_tools.workspace = true client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } +collections.workspace = true dap = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } @@ -85,6 +86,7 @@ gpui = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } node_runtime = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } remote = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index c0ccaf900d18ee176bab7193c2bfb65b8555318d..cb486732c0a0a63e7f6d5d5aed7fe0499ef98b80 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -6,6 +6,7 @@ use assistant_tool::{Tool as _, ToolResultContent}; use assistant_tools::{ReadFileTool, ReadFileToolInput}; use client::{Client, UserStore}; use clock::FakeSystemClock; +use collections::{HashMap, HashSet}; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; use extension::ExtensionHostProxy; @@ -20,6 +21,7 @@ use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, Language use node_runtime::NodeRuntime; use project::{ Project, ProjectPath, + agent_server_store::AgentServerCommand, search::{SearchQuery, SearchResult}, }; use remote::RemoteClient; @@ -27,7 +29,6 @@ use serde_json::json; use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; use smol::stream::StreamExt; use std::{ - collections::HashSet, path::{Path, PathBuf}, sync::Arc, }; @@ -1770,6 +1771,91 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu does_not_exist_result.output.await.unwrap_err(); } +#[gpui::test] +async fn test_remote_external_agent_server( + cx: &mut TestAppContext, + server_cx: &mut TestAppContext, +) { + let fs = FakeFs::new(server_cx.executor()); + fs.insert_tree(path!("/project"), json!({})).await; + + let (project, _headless_project) = init_test(&fs, cx, server_cx).await; + project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/project"), true, cx) + }) + .await + .unwrap(); + let names = project.update(cx, |project, cx| { + project + .agent_server_store() + .read(cx) + .external_agents() + .map(|name| name.to_string()) + .collect::>() + }); + pretty_assertions::assert_eq!(names, ["gemini", "claude"]); + server_cx.update_global::(|settings_store, cx| { + settings_store + .set_raw_server_settings( + Some(json!({ + "agent_servers": { + "foo": { + "command": "foo-cli", + "args": ["--flag"], + "env": { + "VAR": "val" + } + } + } + })), + cx, + ) + .unwrap(); + }); + server_cx.run_until_parked(); + cx.run_until_parked(); + let names = project.update(cx, |project, cx| { + project + .agent_server_store() + .read(cx) + .external_agents() + .map(|name| name.to_string()) + .collect::>() + }); + pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]); + let (command, root, login) = project + .update(cx, |project, cx| { + project.agent_server_store().update(cx, |store, cx| { + store + .get_external_agent(&"foo".into()) + .unwrap() + .get_command( + None, + HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]), + None, + None, + &mut cx.to_async(), + ) + }) + }) + .await + .unwrap(); + assert_eq!( + command, + AgentServerCommand { + path: "ssh".into(), + args: vec!["foo-cli".into(), "--flag".into()], + env: Some(HashMap::from_iter([ + ("VAR".into(), "val".into()), + ("OTHER_VAR".into(), "other-val".into()) + ])) + } + ); + assert_eq!(&PathBuf::from(root), paths::home_dir()); + assert!(login.is_none()); +} + pub async fn init_test( server_fs: &Arc, cx: &mut TestAppContext, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index abce84daf7cf2b0adc304894476d9c763e5e1a3d..e06e423e92aa3fe093c69d8436545d4a13d17a82 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -487,6 +487,17 @@ impl SettingsStore { Ok(()) } + /// Replaces current settings with the values from the given JSON. + pub fn set_raw_server_settings( + &mut self, + new_settings: Option, + cx: &mut App, + ) -> Result<()> { + self.raw_server_settings = 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 From a1f1522cfa98e948c4ff9c904454ebac9c469bff Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:33:07 -0700 Subject: [PATCH 094/117] Clippy --- Cargo.lock | 19 -------- crates/agent_settings/src/agent_profile.rs | 2 +- crates/agent_ui/src/text_thread_editor.rs | 2 +- crates/audio/Cargo.toml | 1 - crates/auto_update/Cargo.toml | 1 - crates/call/Cargo.toml | 1 - crates/collab_ui/Cargo.toml | 1 - crates/editor/src/editor.rs | 2 +- crates/extension_host/Cargo.toml | 1 - crates/git_hosting_providers/Cargo.toml | 1 - crates/go_to_line/Cargo.toml | 2 - crates/image_viewer/Cargo.toml | 1 - crates/journal/Cargo.toml | 1 - crates/journal/src/journal.rs | 4 +- crates/language/src/language_settings.rs | 3 +- .../language_models/src/provider/anthropic.rs | 2 +- crates/language_models/src/provider/google.rs | 2 +- .../src/provider/open_router.rs | 2 +- crates/open_router/Cargo.toml | 1 - crates/outline_panel/Cargo.toml | 1 - .../src/outline_panel_settings.rs | 2 +- crates/project/src/agent_server_store.rs | 2 +- crates/project/src/project_settings.rs | 4 +- crates/recent_projects/Cargo.toml | 1 - crates/remote/Cargo.toml | 1 - crates/repl/Cargo.toml | 1 - crates/theme/Cargo.toml | 1 - crates/vim_mode_setting/Cargo.toml | 1 - crates/workspace/src/workspace_settings.rs | 43 +++++++++---------- crates/worktree/Cargo.toml | 1 - crates/zlog_settings/Cargo.toml | 1 - 31 files changed, 33 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da94c746e7fc528e12dce31b547c13d248055d66..c0a44f86cab201c22dda7952e2807b360d858299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,7 +1410,6 @@ dependencies = [ "log", "parking_lot", "rodio", - "schemars 1.0.1", "serde", "settings", "smol", @@ -1443,7 +1442,6 @@ dependencies = [ "log", "paths", "release_channel", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -2646,7 +2644,6 @@ dependencies = [ "log", "postage", "project", - "schemars 1.0.1", "serde", "settings", "telemetry", @@ -3458,7 +3455,6 @@ dependencies = [ "project", "release_channel", "rpc", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -5745,7 +5741,6 @@ dependencies = [ "release_channel", "remote", "reqwest_client", - "schemars 1.0.1", "semantic_version", "serde", "serde_json", @@ -6866,7 +6861,6 @@ dependencies = [ "indoc", "pretty_assertions", "regex", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -7738,7 +7732,6 @@ dependencies = [ name = "go_to_line" version = "0.1.0" dependencies = [ - "anyhow", "editor", "gpui", "indoc", @@ -7746,7 +7739,6 @@ dependencies = [ "menu", "project", "rope", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -8772,7 +8764,6 @@ dependencies = [ "language", "log", "project", - "schemars 1.0.1", "serde", "settings", "theme", @@ -9275,7 +9266,6 @@ dependencies = [ "editor", "gpui", "log", - "schemars 1.0.1", "serde", "settings", "shellexpand 2.1.2", @@ -11695,7 +11685,6 @@ dependencies = [ "settings", "strum 0.27.1", "thiserror 2.0.12", - "util", "workspace-hack", ] @@ -11869,7 +11858,6 @@ dependencies = [ "outline", "pretty_assertions", "project", - "schemars 1.0.1", "search", "serde", "serde_json", @@ -13841,7 +13829,6 @@ dependencies = [ "project", "release_channel", "remote", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -14015,7 +14002,6 @@ dependencies = [ "prost 0.9.0", "release_channel", "rpc", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -14135,7 +14121,6 @@ dependencies = [ "picker", "project", "runtimelib", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -17179,7 +17164,6 @@ dependencies = [ "serde", "serde_json", "serde_json_lenient", - "serde_repr", "settings", "strum 0.27.1", "thiserror 2.0.12", @@ -18726,7 +18710,6 @@ dependencies = [ name = "vim_mode_setting" version = "0.1.0" dependencies = [ - "anyhow", "gpui", "settings", "workspace-hack", @@ -20849,7 +20832,6 @@ dependencies = [ "pretty_assertions", "rand 0.9.1", "rpc", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -21707,7 +21689,6 @@ dependencies = [ name = "zlog_settings" version = "0.1.0" dependencies = [ - "anyhow", "collections", "gpui", "settings", diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 2d39fe85adb92ce67006f4e0c40e8be817222f6a..999ddc8083a1a4b4c271ea9bde4c1e45307e9542 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -126,7 +126,7 @@ impl AgentProfileSettings { } profiles.insert( - profile_id.0.clone(), + profile_id.0, AgentProfileContent { name: self.name.clone().into(), tools: self.tools.clone(), diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index dadb6263c765a10fedc01b25ae5dd3dded19b877..3c09e47852ffae8f45a5315859a7bb3392b1680d 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -303,7 +303,7 @@ impl TextThreadEditor { settings.agent.get_or_insert_default().set_model( LanguageModelSelection { provider: LanguageModelProviderSetting(provider), - model: model.clone(), + model, }, ) }); diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 85274f651417f8df91e2f785056e5ee8da0220de..c083c9a659e50aef37acc2cdfc239696bd469c1e 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -21,7 +21,6 @@ gpui.workspace = true log.workspace = true parking_lot.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] } -schemars.workspace = true serde.workspace = true settings.workspace = true smol.workspace = true diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 35cef84f0366b16d9e31b2416e7a6a10173ff5ef..21df028a88f027b1ce3796ef3e04998ca205ce51 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -21,7 +21,6 @@ http_client.workspace = true log.workspace = true paths.workspace = true release_channel.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 9dfb4acbb424724ce976f918f3bf5124a450e372..1d5fbccb4644d9f168a2afd321a205f01c8f9cdc 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -35,7 +35,6 @@ language.workspace = true log.workspace = true postage.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true telemetry.workspace = true diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index b3b0f1b0e37207582293fcc6d8930cc004ad7405..24202445a79b5c906d4f6fe1f1a633422f24772a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -47,7 +47,6 @@ picker.workspace = true project.workspace = true release_channel.workspace = true rpc.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 38bbcecc16deadf8543f9aa280c607604882461b..8b4b81cd4ae3d42538f0cfd1226292614ec27232 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22916,7 +22916,7 @@ fn inlay_hint_settings( ) -> InlayHintSettings { let file = snapshot.file_at(location); let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints.clone() + language_settings(language, file, cx).inlay_hints } fn consume_contiguous_rows( diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index c933d253c65b525b29eb072ce6910514b15e5932..ba7f056866f41dbc61e7aea38dac8d8aca35979f 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -37,7 +37,6 @@ paths.workspace = true project.workspace = true remote.workspace = true release_channel.workspace = true -schemars.workspace = true semantic_version.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/git_hosting_providers/Cargo.toml b/crates/git_hosting_providers/Cargo.toml index cce7ea439ecfb7ea51cbf5bd89dae5e6c6a6f350..64c7e701a4dbdcec405a6fb8b37eba8663a4d8db 100644 --- a/crates/git_hosting_providers/Cargo.toml +++ b/crates/git_hosting_providers/Cargo.toml @@ -19,7 +19,6 @@ git.workspace = true gpui.workspace = true http_client.workspace = true regex.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 57438910fbef39f2b0308a3f4706d5e01df0f832..54a9b4d37c7a237cdedf20e0cc683895384caa03 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -13,12 +13,10 @@ path = "src/go_to_line.rs" doctest = false [dependencies] -anyhow.workspace = true editor.workspace = true gpui.workspace = true language.workspace = true menu.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true text.workspace = true diff --git a/crates/image_viewer/Cargo.toml b/crates/image_viewer/Cargo.toml index 254c916789df2cdfa3e3458ed30572e84153ad61..1afa2c5f9dd90956b93c2e9dfac3537c7253a610 100644 --- a/crates/image_viewer/Cargo.toml +++ b/crates/image_viewer/Cargo.toml @@ -24,7 +24,6 @@ gpui.workspace = true language.workspace = true log.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 089896cc1edafa760fbe908bcf7d8f689511edca..3c98e4a712a4e53358d2d31e7df981dccb86fa3c 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -18,7 +18,6 @@ chrono.workspace = true editor.workspace = true gpui.workspace = true log.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true shellexpand.workspace = true diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 4d0d1a41dafbe466abf61178c6e006b7b86161dc..24c3fecf78c2ea3ce1cd036e8b90f88a5d889ccd 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -39,7 +39,7 @@ impl settings::Settings for JournalSettings { Self { path: journal.path.unwrap(), - hour_format: journal.hour_format.unwrap().into(), + hour_format: journal.hour_format.unwrap(), } } @@ -195,7 +195,7 @@ fn heading_entry(now: NaiveTime, hour_format: &HourFormat) -> String { let hour = now.hour(); format!("# {}:{:02}", hour, now.minute()) } - HourFormat::Hour12 => { + HourFormat::Hour12 => { let (pm, hour) = now.hour12(); let am_or_pm = if pm { "PM" } else { "AM" }; format!("# {}:{:02} {}", hour, now.minute(), am_or_pm) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index f1daa0d3f8198fb36ef37bfe79a6ed01f7ea2add..28828f672316d6214ffc81e0c38b53449d178728 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -917,8 +917,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent .scroll_debounce_ms .merge_from(&inlay_hints.scroll_debounce_ms); if let Some(toggle_on_modifiers_press) = &inlay_hints.toggle_on_modifiers_press { - settings.inlay_hints.toggle_on_modifiers_press = - Some(toggle_on_modifiers_press.clone()); + settings.inlay_hints.toggle_on_modifiers_press = Some(*toggle_on_modifiers_press); } } settings diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index e1ca862a2fe12e7c9d0e1fffcef2960dac005b36..c9cd56f6328f3070cafe2b7d7ff58f8e5315a3da 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -188,7 +188,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider { max_output_tokens: model.max_output_tokens, default_temperature: model.default_temperature, extra_beta_headers: model.extra_beta_headers.clone(), - mode: model.mode.clone().unwrap_or_default().into(), + mode: model.mode.unwrap_or_default().into(), }, ); } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index fafb2258e606d712991a9e8a2febd07735fae997..70a7a27defdfba609765710902845f921a8333ac 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -209,7 +209,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { name: model.name.clone(), display_name: model.display_name.clone(), max_tokens: model.max_tokens, - mode: model.mode.unwrap_or_default().into(), + mode: model.mode.unwrap_or_default(), }, ); } diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index a69041737f2850e8209d923de617716ad258b8ad..bbc5c44cdc7835b4402543371d4677520bdcf645 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -211,7 +211,7 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { max_tokens: model.max_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, - mode: model.mode.clone().unwrap_or_default(), + mode: model.mode.unwrap_or_default(), provider: model.provider.clone(), }); } diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml index ec52ead3cba96f9026bc4df1a04d3af94eb44ed6..f9728673bd891488b90c062acf7cb507bb9e329f 100644 --- a/crates/open_router/Cargo.toml +++ b/crates/open_router/Cargo.toml @@ -25,5 +25,4 @@ serde_json.workspace = true settings.workspace = true strum.workspace = true thiserror.workspace = true -util.workspace = true workspace-hack.workspace = true diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml index 6950929304fb37e1f0d140adc2b461188cbeaaf1..b851ae672df0ba4a99b25e19c8c2ebaf49676346 100644 --- a/crates/outline_panel/Cargo.toml +++ b/crates/outline_panel/Cargo.toml @@ -26,7 +26,6 @@ log.workspace = true menu.workspace = true outline.workspace = true project.workspace = true -schemars.workspace = true search.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index b9b0518a7cf3eac107abc04c39ac1d6d65a61bb9..7652c633f1e1166f2515e0338cba3323d6017fb3 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -72,7 +72,7 @@ impl Settings for OutlinePanelSettings { self.button.merge_from(&panel.button); self.default_width .merge_from(&panel.default_width.map(Pixels::from)); - self.dock.merge_from(&panel.dock.map(Into::into)); + self.dock.merge_from(&panel.dock); self.file_icons.merge_from(&panel.file_icons); self.folder_icons.merge_from(&panel.folder_icons); self.git_status.merge_from(&panel.git_status); diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 9357bd773466645d6e7dad666b92dc5b5ed9ce4c..b7d1a75521480353433cf4953960b505201ab3c1 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -1071,7 +1071,7 @@ impl settings::Settings for AllAgentServersSettings { custom: agent_settings .custom .into_iter() - .map(|(k, v)| (k.clone(), v.into())) + .map(|(k, v)| (k, v.into())) .collect(), } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index eb1dbe7534c6d16da6a23bb4d0ac0cf8167ac7f4..836943952f76ce297b2b2bb16ae12e16f3b003aa 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -505,7 +505,7 @@ impl Settings for ProjectSettings { .lsp .clone() .into_iter() - .map(|(key, value)| (LanguageServerName(key.into()), value.into())) + .map(|(key, value)| (LanguageServerName(key.into()), value)) .collect(), global_lsp_settings: GlobalLspSettings { button: content @@ -559,7 +559,7 @@ impl Settings for ProjectSettings { .dap .clone() .into_iter() - .filter_map(|(key, value)| Some((DebugAdapterName(key.into()), value))), + .map(|(key, value)| (DebugAdapterName(key.into()), value)), ); if let Some(diagnostics) = content.diagnostics.as_ref() { if let Some(inline) = &diagnostics.inline { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 2ba6293ad2cf63c7ca664dba43f53d7facc70a57..1445b80fa6118bbc79fa83f113bc23bdf66c7dc4 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -31,7 +31,6 @@ picker.workspace = true project.workspace = true release_channel.workspace = true remote.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true smol.workspace = true diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 3aea56c6411b1a084219734361cc2da9578a6b1e..59d80206b0585d59df6c73e2fee44b9ba1fb70c1 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -32,7 +32,6 @@ paths.workspace = true prost.workspace = true release_channel.workspace = true rpc = { workspace = true, features = ["gpui"] } -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 5821bc6297266cffba19234d82efec3f6190c310..6386dc330af8fd1eb46380cb39c71f4adffea1e6 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -38,7 +38,6 @@ multi_buffer.workspace = true nbformat.workspace = true project.workspace = true runtimelib.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 00f8e1868e5ddf00fa1fdaadb042c23d6013f5d8..8c51b991ccc8580d2be3d38273a7c11118b37d52 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -33,7 +33,6 @@ schemars = { workspace = true, features = ["indexmap2"] } serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true -serde_repr.workspace = true settings.workspace = true strum.workspace = true thiserror.workspace = true diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml index fbb7f30b4c2a03aca48ad5db26283c33aedb885b..8371cca401fa77c63cba6748dc428645340f48b6 100644 --- a/crates/vim_mode_setting/Cargo.toml +++ b/crates/vim_mode_setting/Cargo.toml @@ -12,7 +12,6 @@ workspace = true path = "src/vim_mode_setting.rs" [dependencies] -anyhow.workspace = true gpui.workspace = true settings.workspace = true workspace-hack.workspace = true diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 165ec776febc939997d878fc69dfd6af4043d9ad..30bebb7c18b1f03e5bf59425026aea5a0f4bd552 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -82,25 +82,22 @@ impl Settings for WorkspaceSettings { .unwrap(), ), }, - bottom_dock_layout: workspace.bottom_dock_layout.clone().unwrap(), - pane_split_direction_horizontal: workspace - .pane_split_direction_horizontal - .clone() - .unwrap(), - pane_split_direction_vertical: workspace.pane_split_direction_vertical.clone().unwrap(), - centered_layout: workspace.centered_layout.clone().unwrap(), - confirm_quit: workspace.confirm_quit.clone().unwrap(), - show_call_status_icon: workspace.show_call_status_icon.clone().unwrap(), - autosave: workspace.autosave.clone().unwrap(), - restore_on_startup: workspace.restore_on_startup.clone().unwrap(), - restore_on_file_reopen: workspace.restore_on_file_reopen.clone().unwrap(), - drop_target_size: workspace.drop_target_size.clone().unwrap(), - use_system_path_prompts: workspace.use_system_path_prompts.clone().unwrap(), - use_system_prompts: workspace.use_system_prompts.clone().unwrap(), + bottom_dock_layout: workspace.bottom_dock_layout.unwrap(), + pane_split_direction_horizontal: workspace.pane_split_direction_horizontal.unwrap(), + pane_split_direction_vertical: workspace.pane_split_direction_vertical.unwrap(), + centered_layout: workspace.centered_layout.unwrap(), + confirm_quit: workspace.confirm_quit.unwrap(), + show_call_status_icon: workspace.show_call_status_icon.unwrap(), + autosave: workspace.autosave.unwrap(), + restore_on_startup: workspace.restore_on_startup.unwrap(), + restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(), + drop_target_size: workspace.drop_target_size.unwrap(), + use_system_path_prompts: workspace.use_system_path_prompts.unwrap(), + use_system_prompts: workspace.use_system_prompts.unwrap(), command_aliases: workspace.command_aliases.clone(), - max_tabs: workspace.max_tabs.clone(), - when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.clone().unwrap(), - on_last_window_closed: workspace.on_last_window_closed.clone().unwrap(), + max_tabs: workspace.max_tabs, + when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(), + on_last_window_closed: workspace.on_last_window_closed.unwrap(), resize_all_panels_in_dock: workspace .resize_all_panels_in_dock .clone() @@ -108,22 +105,22 @@ impl Settings for WorkspaceSettings { .into_iter() .map(Into::into) .collect(), - close_on_file_delete: workspace.close_on_file_delete.clone().unwrap(), - use_system_window_tabs: workspace.use_system_window_tabs.clone().unwrap(), - zoomed_padding: workspace.zoomed_padding.clone().unwrap(), + close_on_file_delete: workspace.close_on_file_delete.unwrap(), + use_system_window_tabs: workspace.use_system_window_tabs.unwrap(), + zoomed_padding: workspace.zoomed_padding.unwrap(), } } fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let workspace = &content.workspace; - if let Some(border_size) = *&workspace + if let Some(border_size) = workspace .active_pane_modifiers .and_then(|modifier| modifier.border_size) { self.active_pane_modifiers.border_size = Some(border_size); } - if let Some(inactive_opacity) = *&workspace + if let Some(inactive_opacity) = workspace .active_pane_modifiers .and_then(|modifier| modifier.inactive_opacity) { diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 507b95c00d52435411e307f8d68dafd718177598..fdeca37b7ac73759fe9851f722985349e0a183b7 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -38,7 +38,6 @@ parking_lot.workspace = true paths.workspace = true postage.workspace = true rpc = { workspace = true, features = ["gpui"] } -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/zlog_settings/Cargo.toml b/crates/zlog_settings/Cargo.toml index 00c81e0ef45863d36014d63ac5e07d690c507207..8ec63cefe447d944b4556f1e72e481d5461391f1 100644 --- a/crates/zlog_settings/Cargo.toml +++ b/crates/zlog_settings/Cargo.toml @@ -15,7 +15,6 @@ path = "src/zlog_settings.rs" default = [] [dependencies] -anyhow.workspace = true gpui.workspace = true collections.workspace = true settings.workspace = true From dfc5fdac231894c36c6521ef2116beb05786f2d0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Sep 2025 00:17:39 -0600 Subject: [PATCH 095/117] Remove some more sketchy defaults --- assets/settings/default.json | 3 +- crates/collab/src/tests/integration_tests.rs | 4 +- .../remote_editing_collaboration_tests.rs | 6 +- .../src/copilot_completion_provider.rs | 24 ++- crates/editor/src/editor_tests.rs | 107 ++++++------ crates/editor/src/jsx_tag_auto_close.rs | 8 +- crates/language/src/language_settings.rs | 153 ++++++++++++------ .../settings/src/settings_content/language.rs | 47 ++---- 8 files changed, 183 insertions(+), 169 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index e3e81b83b0ac809e7bc557796ee0879ee9d33cf8..091231521470ebec50cf1351a76063e9205a3d24 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -953,6 +953,7 @@ // // This is typically customized on a per-language basis. "language_servers": ["..."], + // When to automatically save edited buffers. This setting can // take four values. // @@ -1838,7 +1839,7 @@ // and configure default Prettier, used when no project-level Prettier installation is found. "prettier": { // // Whether to consider prettier formatter or not when attempting to format a file. - // "allowed": false, + "allowed": false // // // Use regular Prettier json configuration. // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d2146ec433cce59ed1d0c32d49c28c7efcaea5aa..e3925b7ecd9d5ec50722ff9b3f521243c264e6ec 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4695,7 +4695,7 @@ async fn test_prettier_formatting_buffer( store.update_user_settings(cx, |file| { file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { - allowed: true, + allowed: Some(true), ..Default::default() }); }); @@ -4708,7 +4708,7 @@ async fn test_prettier_formatting_buffer( FormatterList::Single(Formatter::LanguageServer { name: None }), )); file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { - allowed: true, + allowed: Some(true), ..Default::default() }); }); diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 930a8ac972c924a494ec428a0bd2286036c36950..e2e6d7b724386afafad27e72f867be70671263bc 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -499,7 +499,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( store.update_user_settings(cx, |file| { file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { - allowed: true, + allowed: Some(true), ..Default::default() }); }); @@ -512,7 +512,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( FormatterList::Single(Formatter::LanguageServer { name: None }), )); file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { - allowed: true, + allowed: Some(true), ..Default::default() }); }); @@ -556,7 +556,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( store.update_user_settings(cx, |file| { file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto); file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent { - allowed: true, + allowed: Some(true), ..Default::default() }); }); diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 9184c1b196e316f045cb9790e4d0cd61d3aba0bc..c122dccec069ff636d39c64f24ef0aca41145012 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -281,7 +281,7 @@ mod tests { use indoc::indoc; use language::{ Point, - language_settings::{CompletionSettings, LspInsertMode, WordsCompletionMode}, + language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode}, }; use project::Project; use serde_json::json; @@ -296,12 +296,11 @@ mod tests { async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) { // flaky init_test(cx, |settings| { - settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Disabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -529,12 +528,11 @@ mod tests { ) { // flaky init_test(cx, |settings| { - settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Disabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9b2ec8a4edeee2329b284cd80ab5059439691f12..e77f08e268d8c07364189e3e8ff076cc62008644 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25,7 +25,7 @@ use language::{ DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, language_settings::{ - CompletionSettings, FormatterList, LanguageSettingsContent, LspInsertMode, + CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode, SelectedFormatter, }, tree_sitter_python, @@ -11700,7 +11700,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { update_test_language_settings(cx, |settings| { // Enable Prettier formatting for the same buffer, and ensure // LSP is called instead of Prettier. - settings.defaults.prettier.get_or_insert_default().allowed = true; + settings.defaults.prettier.get_or_insert_default().allowed = Some(true); }); let mut fake_servers = language_registry.register_fake_lsp( "Rust", @@ -12086,7 +12086,7 @@ async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), ))); update_test_language_settings(cx, |settings| { - settings.defaults.prettier.get_or_insert_default().allowed = true; + settings.defaults.prettier.get_or_insert_default().allowed = Some(true); }); let mut fake_servers = language_registry.register_fake_lsp( "TypeScript", @@ -13341,12 +13341,11 @@ async fn test_completion_mode(cx: &mut TestAppContext) { ); update_test_language_settings(&mut cx, |settings| { - settings.defaults.completions = Some(CompletionSettings { - lsp_insert_mode, - words: WordsCompletionMode::Disabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, + settings.defaults.completions = Some(CompletionSettingsContent { + lsp_insert_mode: Some(lsp_insert_mode), + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), + ..Default::default() }); }); @@ -13401,13 +13400,12 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) let expected_with_replace_mode = "SubscriptionErrorˇ"; update_test_language_settings(&mut cx, |settings| { - settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Disabled, - words_min_length: 0, + settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), // set the opposite here to ensure that the action is overriding the default behavior - lsp_insert_mode: LspInsertMode::Insert, - lsp: true, - lsp_fetch_timeout_ms: 0, + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -13438,13 +13436,12 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) apply_additional_edits.await.unwrap(); update_test_language_settings(&mut cx, |settings| { - settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Disabled, - words_min_length: 0, + settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), // set the opposite here to ensure that the action is overriding the default behavior - lsp_insert_mode: LspInsertMode::Replace, - lsp: true, - lsp_fetch_timeout_ms: 0, + lsp_insert_mode: Some(LspInsertMode::Replace), + ..Default::default() }); }); @@ -14180,12 +14177,11 @@ async fn test_completion_reuse(cx: &mut TestAppContext) { async fn test_word_completion(cx: &mut TestAppContext) { let lsp_fetch_timeout_ms = 10; init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Fallback, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 10, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words_min_length: Some(0), + lsp_fetch_timeout_ms: Some(10), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -14277,12 +14273,11 @@ async fn test_word_completion(cx: &mut TestAppContext) { #[gpui::test] async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) { init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Enabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Enabled), + words_min_length: Some(0), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -14341,12 +14336,11 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext #[gpui::test] async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Disabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Disabled), + words_min_length: Some(0), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -14415,12 +14409,11 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { #[gpui::test] async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Fallback, - words_min_length: 0, - lsp: false, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words_min_length: Some(0), + lsp: Some(false), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -14478,12 +14471,11 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { #[gpui::test] async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Enabled, - words_min_length: 3, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Enabled), + words_min_length: Some(3), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -14548,12 +14540,11 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont #[gpui::test] async fn test_word_completions_disabled(cx: &mut TestAppContext) { init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Enabled, - words_min_length: 0, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, + language_settings.defaults.completions = Some(CompletionSettingsContent { + words: Some(WordsCompletionMode::Enabled), + words_min_length: Some(0), + lsp_insert_mode: Some(LspInsertMode::Insert), + ..Default::default() }); }); @@ -17986,7 +17977,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), ))); update_test_language_settings(cx, |settings| { - settings.defaults.prettier.get_or_insert_default().allowed = true; + settings.defaults.prettier.get_or_insert_default().allowed = Some(true); }); let test_plugin = "test_plugin"; diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index 9d78ac543db6fb1d14c98d6c675d4658db8f6f03..0e32bc686ad98a45b83712841c13fffc07421acb 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -328,7 +328,7 @@ pub(crate) fn refresh_enabled_in_any_buffer( snapshot.file(), cx, ); - if language_settings.jsx_tag_auto_close.enabled { + if language_settings.jsx_tag_auto_close { found_enabled = true; } } @@ -406,7 +406,7 @@ pub(crate) fn handle_from( }; let language_settings = snapshot.settings_at(edit.new.end, cx); - if !language_settings.jsx_tag_auto_close.enabled { + if !language_settings.jsx_tag_auto_close { continue; } @@ -630,7 +630,7 @@ mod jsx_tag_autoclose_tests { .defaults .jsx_tag_auto_close .get_or_insert_default() - .enabled = true; + .enabled = Some(true); }); let mut cx = EditorTestContext::new(cx).await; @@ -796,7 +796,7 @@ mod jsx_tag_autoclose_tests { .defaults .jsx_tag_auto_close .get_or_insert_default() - .enabled = true; + .enabled = Some(true); }); let buffer_a = cx.new(|cx| { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 28828f672316d6214ffc81e0c38b53449d178728..149492e7f188ce43894c1a07d34c49ce589dc59e 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -9,16 +9,17 @@ use ec4rs::{ use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; use itertools::{Either, Itertools}; -use schemars::{JsonSchema, json_schema}; -use serde::{Deserialize, Serialize}; +use schemars::json_schema; pub use settings::{ - CompletionSettings, EditPredictionProvider, EditPredictionsMode, FormatOnSave, Formatter, - FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, LspInsertMode, - RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, + CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave, + Formatter, FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, + LspInsertMode, RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, + WordsCompletionMode, }; use settings::{ - ParameterizedJsonSchema, Settings, SettingsContent, SettingsLocation, SettingsStore, SettingsUi, + ParameterizedJsonSchema, PrettierSettingsContent, Settings, SettingsContent, SettingsLocation, + SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; @@ -100,9 +101,9 @@ pub struct LanguageSettings { /// How to perform a buffer format. pub formatter: settings::SelectedFormatter, /// Zed's Prettier integration settings. - pub prettier: settings::PrettierSettingsContent, + pub prettier: PrettierSettings, /// Whether to automatically close JSX tags. - pub jsx_tag_auto_close: settings::JsxTagAutoCloseSettings, + pub jsx_tag_auto_close: bool, /// Whether to use language servers to provide code intelligence. pub enable_language_server: bool, /// The list of language servers to use (or disable) for this language. @@ -157,11 +158,80 @@ pub struct LanguageSettings { /// completions menu. pub show_completion_documentation: bool, /// Completion settings for this language. - pub completions: settings::CompletionSettings, + pub completions: CompletionSettings, /// Preferred debuggers for this language. pub debuggers: Vec, } +#[derive(Debug, Clone)] +pub struct CompletionSettings { + /// Controls how words are completed. + /// For large documents, not all words may be fetched for completion. + /// + /// Default: `fallback` + 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 + pub words_min_length: usize, + /// Whether to fetch LSP completions or not. + /// + /// 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 + pub lsp_fetch_timeout_ms: u64, + /// Controls how LSP completions are inserted. + /// + /// Default: "replace_suffix" + pub lsp_insert_mode: LspInsertMode, +} + +impl CompletionSettings { + pub fn merge_from(&mut self, src: &Option) { + let Some(src) = src else { return }; + self.words.merge_from(&src.words); + self.words_min_length.merge_from(&src.words_min_length); + self.lsp.merge_from(&src.lsp); + self.lsp_fetch_timeout_ms + .merge_from(&src.lsp_fetch_timeout_ms); + self.lsp_insert_mode.merge_from(&src.lsp_insert_mode); + } +} + +/// 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(Debug, Clone)] +pub struct PrettierSettings { + /// Enables or disables formatting with Prettier for a given language. + pub allowed: bool, + + /// Forces Prettier integration to use a specific parser name when formatting files with the language. + 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. + 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. + pub options: HashMap, +} + +impl PrettierSettings { + pub fn merge_from(&mut self, src: &Option) { + let Some(src) = src.clone() else { return }; + self.allowed.merge_from(&src.allowed); + self.parser = src.parser.clone(); + self.plugins.extend(src.plugins); + self.options.extend(src.options); + } +} + impl LanguageSettings { /// A token representing the rest of the available language servers. const REST_OF_LANGUAGE_SERVERS: &'static str = "..."; @@ -445,6 +515,9 @@ impl settings::Settings for AllLanguageSettings { let all_languages = &content.project.all_languages; let defaults = all_languages.defaults.clone(); let inlay_hints = defaults.inlay_hints.unwrap(); + let completions = defaults.completions.unwrap(); + let prettier = defaults.prettier.unwrap(); + let default_language_settings = LanguageSettings { tab_size: defaults.tab_size.unwrap(), hard_tabs: defaults.hard_tabs.unwrap(), @@ -459,8 +532,13 @@ impl settings::Settings for AllLanguageSettings { .unwrap(), ensure_final_newline_on_save: defaults.ensure_final_newline_on_save.unwrap(), formatter: defaults.formatter.unwrap(), - prettier: defaults.prettier.unwrap(), - jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap(), + prettier: PrettierSettings { + allowed: prettier.allowed.unwrap(), + parser: prettier.parser, + plugins: prettier.plugins, + options: prettier.options, + }, + jsx_tag_auto_close: defaults.jsx_tag_auto_close.unwrap().enabled.unwrap(), enable_language_server: defaults.enable_language_server.unwrap(), language_servers: defaults.language_servers.unwrap(), allow_rewrap: defaults.allow_rewrap.unwrap(), @@ -493,7 +571,13 @@ impl settings::Settings for AllLanguageSettings { tasks: defaults.tasks.unwrap(), show_completions_on_input: defaults.show_completions_on_input.unwrap(), show_completion_documentation: defaults.show_completion_documentation.unwrap(), - completions: defaults.completions.unwrap(), + completions: CompletionSettings { + words: completions.words.unwrap(), + words_min_length: completions.words_min_length.unwrap(), + lsp: completions.lsp.unwrap(), + lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(), + lsp_insert_mode: completions.lsp_insert_mode.unwrap(), + }, debuggers: defaults.debuggers.unwrap(), }; @@ -760,17 +844,7 @@ impl settings::Settings for AllLanguageSettings { } else { WordsCompletionMode::Disabled }; - if let Some(completion_settings) = d.completions.as_mut() { - completion_settings.words = mode; - } else { - d.completions = Some(CompletionSettings { - words: mode, - words_min_length: 3, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::ReplaceSuffix, - }); - } + d.completions.get_or_insert_default().words = Some(mode); } // TODO: pull ^ out into helper and reuse for per-language settings @@ -853,7 +927,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent settings.prettier.merge_from(&src.prettier.clone()); settings .jsx_tag_auto_close - .merge_from(&src.jsx_tag_auto_close.clone()); + .merge_from(&src.jsx_tag_auto_close.as_ref().and_then(|v| v.enabled)); settings .format_on_save .merge_from(&src.format_on_save.clone()); @@ -926,39 +1000,12 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent settings .show_completion_documentation .merge_from(&src.show_completion_documentation); - settings.completions.merge_from(&src.completions.clone()); -} - -/// 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, SettingsUi)] -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)] - #[settings_ui(skip)] - 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)] - #[settings_ui(skip)] - pub options: HashMap, + settings.completions.merge_from(&src.completions); } -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[derive(Default, Debug, Clone, PartialEq, Eq, SettingsUi)] pub struct JsxTagAutoCloseSettings { /// Enables or disables auto-closing of JSX tags. - #[serde(default)] pub enabled: bool, } diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 4194e102838b188a232a82e920b43b2b0186a1c4..03e6c9cffeadb15a9bc0aaa387eb472559e9ff57 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -180,7 +180,7 @@ pub struct LanguageSettingsContent { /// Default: off pub prettier: Option, /// Whether to automatically close JSX tags. - pub jsx_tag_auto_close: Option, + pub jsx_tag_auto_close: Option, /// Whether to use language servers to provide code intelligence. /// /// Default: true @@ -281,7 +281,7 @@ pub struct LanguageSettingsContent { /// Default: true pub show_completion_documentation: Option, /// Controls how completions are processed for this language. - pub completions: Option, + pub completions: Option, /// Preferred debuggers for this language. /// /// Default: [] @@ -312,9 +312,7 @@ pub enum ShowWhitespaceSetting { #[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct WhitespaceMap { - #[serde(default)] pub space: Option, - #[serde(default)] pub tab: Option, } @@ -347,11 +345,9 @@ pub enum RewrapBehavior { #[skip_serializing_none] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct JsxTagAutoCloseSettings { +pub struct JsxTagAutoCloseSettingsContent { /// Enables or disables auto-closing of JSX tags. - // todo! option - #[serde(default)] - pub enabled: bool, + pub enabled: Option, } /// The settings for inlay hints. @@ -440,37 +436,32 @@ impl InlayHintKind { /// Controls how completions are processedfor this anguage. #[skip_serializing_none] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] -pub struct CompletionSettings { +pub struct CompletionSettingsContent { /// 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, + pub words: Option, /// 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, + pub words_min_length: Option, /// Whether to fetch LSP completions or not. /// /// Default: true - #[serde(default = "default_true")] - pub lsp: bool, + pub lsp: Option, /// 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, + pub lsp_fetch_timeout_ms: Option, /// Controls how LSP completions are inserted. /// /// Default: "replace_suffix" - #[serde(default = "default_lsp_insert_mode")] - pub lsp_insert_mode: LspInsertMode, + pub lsp_insert_mode: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -502,18 +493,6 @@ pub enum WordsCompletionMode { 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. @@ -521,11 +500,9 @@ fn default_3() -> usize { #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct PrettierSettingsContent { /// Enables or disables formatting with Prettier for a given language. - #[serde(default)] - pub allowed: bool, + pub allowed: Option, /// 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. From 0a642126302aff860f9baee821b99c5513d6669d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Sep 2025 00:35:35 -0600 Subject: [PATCH 096/117] Remove remaining suspicious serde.defaults --- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/element.rs | 4 +- crates/language/src/language.rs | 2 +- crates/language/src/language_settings.rs | 99 ++++++++++++++++--- crates/multi_buffer/src/multi_buffer.rs | 10 +- .../settings/src/settings_content/language.rs | 41 +++----- .../src/settings_content/language_model.rs | 5 - 7 files changed, 105 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e77f08e268d8c07364189e3e8ff076cc62008644..80fda5297a501405ed894f670580d963abf9c3aa 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -30,7 +30,7 @@ use language::{ }, tree_sitter_python, }; -use language_settings::{Formatter, IndentGuideSettings}; +use language_settings::{Formatter, IndentGuideSettingsContent}; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -19961,7 +19961,7 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) - end_row: MultiBufferRow(end_row), depth, tab_size: 4, - settings: IndentGuideSettings { + settings: IndentGuideSettingsContent { enabled: true, line_width: 1, active_line_width: 1, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 30dfc7989a3640c73b55683e776c08b243e64d9c..7b6f26c85fb7619178c4ee364343763175f3c036 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -51,7 +51,7 @@ use gpui::{ transparent_black, }; use itertools::Itertools; -use language::language_settings::{IndentGuideSettings, ShowWhitespaceSetting}; +use language::language_settings::{IndentGuideSettingsContent, ShowWhitespaceSetting}; use markdown::Markdown; use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, @@ -10177,7 +10177,7 @@ pub struct IndentGuideLayout { single_indent_width: Pixels, depth: u32, active: bool, - settings: IndentGuideSettings, + settings: IndentGuideSettingsContent, } pub struct CursorLayout { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 3c951e50ff231a72e284da743bb3e5d409eb9c5e..291b5d3957cc7274bdd07b5260890c23b7a253d1 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -22,8 +22,8 @@ mod toolchain; #[cfg(test)] pub mod buffer_tests; -pub use crate::language_settings::EditPredictionsMode; use crate::language_settings::SoftWrap; +pub use crate::language_settings::{EditPredictionsMode, IndentGuideSettings}; use anyhow::{Context as _, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet, IndexSet}; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 149492e7f188ce43894c1a07d34c49ce589dc59e..a37e74b13f9cd789868e7bed5218e1e5efb0c7af 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -13,13 +13,13 @@ use schemars::json_schema; pub use settings::{ CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave, - Formatter, FormatterList, IndentGuideSettings, InlayHintKind, LanguageSettingsContent, + Formatter, FormatterList, IndentGuideSettingsContent, InlayHintKind, LanguageSettingsContent, LspInsertMode, RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{ - ParameterizedJsonSchema, PrettierSettingsContent, Settings, SettingsContent, SettingsLocation, - SettingsStore, SettingsUi, + LanguageTaskSettingsContent, ParameterizedJsonSchema, PrettierSettingsContent, Settings, + SettingsContent, SettingsLocation, SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; @@ -150,7 +150,7 @@ pub struct LanguageSettings { /// Whether to perform linked edits pub linked_edits: bool, /// Task configuration for this language. - pub tasks: settings::LanguageTaskConfig, + pub tasks: LanguageTaskSettings, /// Whether to pop the completions menu while typing in an editor without /// explicitly requesting it. pub show_completions_on_input: bool, @@ -202,6 +202,70 @@ impl CompletionSettings { } } +/// The settings for indent guides. +#[derive(Debug, Clone, PartialEq)] +pub struct IndentGuideSettings { + /// Whether to display indent guides in the editor. + /// + /// Default: true + pub enabled: bool, + /// The width of the indent guides in pixels, between 1 and 10. + /// + /// Default: 1 + pub line_width: u32, + /// The width of the active indent guide in pixels, between 1 and 10. + /// + /// Default: 1 + pub active_line_width: u32, + /// Determines how indent guides are colored. + /// + /// Default: Fixed + pub coloring: settings::IndentGuideColoring, + /// Determines how indent guide backgrounds are colored. + /// + /// Default: Disabled + pub background_coloring: settings::IndentGuideBackgroundColoring, +} + +impl IndentGuideSettings { + pub fn merge_from(&mut self, src: &Option) { + let Some(src) = src else { return }; + + self.enabled.merge_from(&src.enabled); + self.line_width.merge_from(&src.line_width); + self.active_line_width.merge_from(&src.active_line_width); + self.coloring.merge_from(&src.coloring); + self.background_coloring + .merge_from(&src.background_coloring); + } +} + +#[derive(Debug, Clone)] +pub struct LanguageTaskSettings { + /// Extra task variables to set for a particular language. + pub variables: HashMap, + 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) + pub prefer_lsp: bool, +} + +impl LanguageTaskSettings { + pub fn merge_from(&mut self, src: &Option) { + let Some(src) = src.clone() else { + return; + }; + self.variables.extend(src.variables); + self.enabled.merge_from(&src.enabled); + self.prefer_lsp.merge_from(&src.prefer_lsp); + } +} + /// 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. @@ -517,6 +581,8 @@ impl settings::Settings for AllLanguageSettings { let inlay_hints = defaults.inlay_hints.unwrap(); let completions = defaults.completions.unwrap(); let prettier = defaults.prettier.unwrap(); + let indent_guides = defaults.indent_guides.unwrap(); + let tasks = defaults.tasks.unwrap(); let default_language_settings = LanguageSettings { tab_size: defaults.tab_size.unwrap(), @@ -525,7 +591,13 @@ impl settings::Settings for AllLanguageSettings { preferred_line_length: defaults.preferred_line_length.unwrap(), show_wrap_guides: defaults.show_wrap_guides.unwrap(), wrap_guides: defaults.wrap_guides.unwrap(), - indent_guides: defaults.indent_guides.unwrap(), + indent_guides: IndentGuideSettings { + enabled: indent_guides.enabled.unwrap(), + line_width: indent_guides.line_width.unwrap(), + active_line_width: indent_guides.active_line_width.unwrap(), + coloring: indent_guides.coloring.unwrap(), + background_coloring: indent_guides.background_coloring.unwrap(), + }, format_on_save: defaults.format_on_save.unwrap(), remove_trailing_whitespace_on_save: defaults .remove_trailing_whitespace_on_save @@ -568,7 +640,11 @@ impl settings::Settings for AllLanguageSettings { .unwrap(), code_actions_on_format: defaults.code_actions_on_format.unwrap(), linked_edits: defaults.linked_edits.unwrap(), - tasks: defaults.tasks.unwrap(), + tasks: LanguageTaskSettings { + variables: tasks.variables, + enabled: tasks.enabled.unwrap(), + prefer_lsp: tasks.prefer_lsp.unwrap(), + }, show_completions_on_input: defaults.show_completions_on_input.unwrap(), show_completion_documentation: defaults.show_completion_documentation.unwrap(), completions: CompletionSettings { @@ -787,14 +863,7 @@ impl settings::Settings for AllLanguageSettings { d.wrap_guides = arr; } if let Some(b) = vscode.read_bool("editor.guides.indentation") { - if let Some(guide_settings) = d.indent_guides.as_mut() { - guide_settings.enabled = b; - } else { - d.indent_guides = Some(IndentGuideSettings { - enabled: b, - ..Default::default() - }); - } + d.indent_guides.get_or_insert_default().enabled = Some(b); } if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") { @@ -918,7 +987,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent .code_actions_on_format .merge_from(&src.code_actions_on_format.clone()); settings.linked_edits.merge_from(&src.linked_edits); - settings.tasks.merge_from(&src.tasks.clone()); + settings.tasks.merge_from(&src.tasks); settings .preferred_line_length diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 272caf28c8b8e6d1516292e49fb34817f1a1d062..2ceeffc89061aa429727142a1659a392d6374b09 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -17,11 +17,11 @@ use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task}; use itertools::Itertools; use language::{ AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, - CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentSize, Language, - LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, - TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, - Unclipped, - language_settings::{IndentGuideSettings, LanguageSettings, language_settings}, + CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuideSettings, + IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, + PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, + TreeSitterOptions, Unclipped, + language_settings::{LanguageSettings, language_settings}, }; use rope::DimensionPair; diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 03e6c9cffeadb15a9bc0aaa387eb472559e9ff57..f1333d8e405bf492a2e3a69184d56184f378642d 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -154,7 +154,7 @@ pub struct LanguageSettingsContent { /// Default: [] pub wrap_guides: Option>, /// Indent guide related settings. - pub indent_guides: Option, + pub indent_guides: Option, /// Whether or not to perform a buffer format before saving. /// /// Default: on @@ -269,7 +269,7 @@ pub struct LanguageSettingsContent { /// Task configuration for this language. /// /// Default: {} - pub tasks: Option, + pub tasks: Option, /// Whether to pop the completions menu while typing in an editor without /// explicitly requesting it. /// @@ -757,55 +757,37 @@ pub enum Formatter { /// The settings for indent guides. #[skip_serializing_none] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct IndentGuideSettings { +pub struct IndentGuideSettingsContent { /// Whether to display indent guides in the editor. /// /// Default: true - #[serde(default = "default_true")] - pub enabled: bool, + pub enabled: Option, /// The width of the indent guides in pixels, between 1 and 10. /// /// Default: 1 - #[serde(default = "line_width")] - pub line_width: u32, + pub line_width: Option, /// 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, + pub active_line_width: Option, /// Determines how indent guides are colored. /// /// Default: Fixed - #[serde(default)] - pub coloring: IndentGuideColoring, + pub coloring: Option, /// Determines how indent guide backgrounds are colored. /// /// Default: Disabled - #[serde(default)] - pub background_coloring: IndentGuideBackgroundColoring, -} - -fn default_true() -> bool { - true -} - -fn line_width() -> u32 { - 1 -} - -fn active_line_width() -> u32 { - line_width() + pub background_coloring: Option, } /// The task settings for a particular language. #[skip_serializing_none] #[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct LanguageTaskConfig { +pub struct LanguageTaskSettingsContent { /// Extra task variables to set for a particular language. #[serde(default)] pub variables: HashMap, - #[serde(default = "default_true")] - pub enabled: bool, + pub enabled: Option, /// 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. @@ -813,8 +795,7 @@ pub struct LanguageTaskConfig { /// 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, + pub prefer_lsp: Option, } /// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs index 1039b505a37df9469d144f3574037a12dcf5f9f4..cad06d5fff78d1fa4ad9cd8830ab24a438d3c9ea 100644 --- a/crates/settings/src/settings_content/language_model.rs +++ b/crates/settings/src/settings_content/language_model.rs @@ -368,7 +368,6 @@ pub struct OpenRouterAvailableModel { #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct OpenRouterProvider { - #[serde(skip_serializing_if = "Option::is_none")] order: Option>, #[serde(default = "default_true")] allow_fallbacks: bool, @@ -376,13 +375,9 @@ pub struct OpenRouterProvider { require_parameters: bool, #[serde(default)] data_collection: DataCollection, - #[serde(skip_serializing_if = "Option::is_none")] only: Option>, - #[serde(skip_serializing_if = "Option::is_none")] ignore: Option>, - #[serde(skip_serializing_if = "Option::is_none")] quantizations: Option>, - #[serde(skip_serializing_if = "Option::is_none")] sort: Option, } From 2dba9eed5680711054cb27d3b3d0aacecdc564df Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:46:28 -0700 Subject: [PATCH 097/117] remove auto_replace_emoji_shortcode setting (unused) --- crates/collab_ui/src/collab_ui.rs | 2 -- crates/collab_ui/src/panel_settings.rs | 33 -------------------------- 2 files changed, 35 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index f75dd663c838c84f167b3070b50a4e1f44e9aa2d..c43e865ef2dcbcd05a9b75cdde5e06bb0679de89 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -11,7 +11,6 @@ use gpui::{ App, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, point, }; -use panel_settings::MessageEditorSettings; pub use panel_settings::{CollaborationPanelSettings, NotificationPanelSettings}; use release_channel::ReleaseChannel; use settings::Settings; @@ -21,7 +20,6 @@ use workspace::AppState; pub fn init(app_state: &Arc, cx: &mut App) { CollaborationPanelSettings::register(cx); NotificationPanelSettings::register(cx); - MessageEditorSettings::register(cx); channel_view::init(cx); collab_panel::init(cx); diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 9e9a2e185bd9c24d62b075344e8fb0e61692cc46..f1cd24c820a631f86c215f810011cc5870ae1ff9 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -18,16 +18,6 @@ pub struct NotificationPanelSettings { pub default_width: Pixels, } -#[derive(Clone, Default, Debug)] -// todo! are these settings even relevant any more? -pub struct MessageEditorSettings { - /// Whether to automatically replace emoji shortcodes with emoji characters. - /// For example: typing `:wave:` gets replaced with `👋`. - /// - /// Default: false - pub auto_replace_emoji_shortcode: bool, -} - impl Settings for CollaborationPanelSettings { fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { let panel = content.collaboration_panel.as_ref().unwrap(); @@ -80,26 +70,3 @@ impl Settings for NotificationPanelSettings { ) { } } - -impl Settings for MessageEditorSettings { - fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self { - let messages = content.message_editor.as_ref().unwrap(); - Self { - auto_replace_emoji_shortcode: messages.auto_replace_emoji_shortcode.unwrap(), - } - } - - fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) { - let Some(messages) = content.message_editor.as_ref() else { - return; - }; - self.auto_replace_emoji_shortcode - .merge_from(&messages.auto_replace_emoji_shortcode); - } - - fn import_from_vscode( - _vscode: &settings::VsCodeSettings, - _current: &mut settings::SettingsContent, - ) { - } -} From 933c43ad1de50653410820eaa19292b4066672a0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:47:34 -0700 Subject: [PATCH 098/117] IndentGuideSettings can be the content type because it doesn't have any Options --- crates/language/src/language_settings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index a37e74b13f9cd789868e7bed5218e1e5efb0c7af..989079f0e600120c55f9136e5536324c8c5e959b 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -88,7 +88,6 @@ pub struct LanguageSettings { /// Character counts at which to show wrap guides (vertical rulers) in the editor. pub wrap_guides: Vec, /// Indent guide related settings. - /// todo!() shouldthis be not the content type? pub indent_guides: IndentGuideSettings, /// Whether or not to perform a buffer format before saving. pub format_on_save: FormatOnSave, From f6ce7578183406dbd309103437e6ca8f567a0996 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:53:28 -0700 Subject: [PATCH 099/117] Fix blame todos --- crates/editor/src/editor.rs | 4 +-- crates/editor/src/editor_settings_controls.rs | 2 +- crates/git_ui/src/blame_ui.rs | 3 +- crates/onboarding/src/editing_page.rs | 2 +- crates/project/src/project_settings.rs | 35 +++---------------- 5 files changed, 11 insertions(+), 35 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8b4b81cd4ae3d42538f0cfd1226292614ec27232..d8501f2104a00183be68dc461f9128a600227aa6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2187,7 +2187,7 @@ impl Editor { show_selection_menu: None, show_git_blame_inline_delay_task: None, git_blame_inline_enabled: full_mode - && ProjectSettings::get_global(cx).git.inline_blame_enabled(), + && ProjectSettings::get_global(cx).git.inline_blame.enabled, render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), serialize_dirty_buffers: !is_minimap && ProjectSettings::get_global(cx) @@ -20722,7 +20722,7 @@ impl Editor { if self.mode.is_full() { let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; - let inline_blame_enabled = project_settings.git.inline_blame_enabled(); + let inline_blame_enabled = project_settings.git.inline_blame.enabled; if self.show_inline_diagnostics != show_inline_diagnostics { self.show_inline_diagnostics = show_inline_diagnostics; self.refresh_inline_diagnostics(false, window, cx); diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index a26af2739924152a972e00a660b03f72e3eff8d6..20393f6cbb0ddca9d8561c7023fd0d9e5e863a9d 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -267,7 +267,7 @@ impl EditableSettingControl for InlineGitBlameControl { fn read(cx: &App) -> Self::Value { let settings = ProjectSettings::get_global(cx); - settings.git.inline_blame_enabled() + settings.git.inline_blame.enabled } fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) { diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index ad5823c1674353f2e0531e5f71e1420fe464bfe6..8ea5a0fb639c53c66085805c39b3d81f3eaa0e42 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -130,7 +130,8 @@ impl BlameRenderer for GitBlameRenderer { let author = blame_entry.author.as_deref().unwrap_or_default(); let summary_enabled = ProjectSettings::get_global(cx) .git - .show_inline_commit_summary(); + .inline_blame + .show_commit_summary; let text = match blame_entry.summary.as_ref() { Some(summary) if summary_enabled => { diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 404374dc06bd817f8084f320a107233b42232816..71b697ebb00ba44449621ee51bd4ac04b9e5c10d 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -70,7 +70,7 @@ fn write_inlay_hints(enabled: bool, cx: &mut App) { } fn read_git_blame(cx: &App) -> bool { - ProjectSettings::get_global(cx).git.inline_blame_enabled() + ProjectSettings::get_global(cx).git.inline_blame.enabled } fn write_git_blame(enabled: bool, cx: &mut App) { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 836943952f76ce297b2b2bb16ae12e16f3b003aa..db2b9d83010f8044fc4d3b3a8fb469c70da7a2e1 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -359,37 +359,12 @@ pub struct InlineBlameSettings { } impl GitSettings { - // todo! remove - pub fn inline_blame_enabled(&self) -> bool { - self.inline_blame.enabled - // #[allow(unknown_lints, clippy::manual_unwrap_or_default)] - // match self.inline_blame { - // Some(InlineBlameSettings { enabled, .. }) => enabled, - // _ => false, - // } - } - - // todo! remove pub fn inline_blame_delay(&self) -> Option { - Some(self.inline_blame.delay_ms) - // match self.inline_blame { - // Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => { - // Some(Duration::from_millis(delay_ms)) - // } - // _ => None, - // } - } - - // todo! remove - pub fn show_inline_commit_summary(&self) -> bool { - self.inline_blame.show_commit_summary - // match self.inline_blame { - // Some(InlineBlameSettings { - // show_commit_summary, - // .. - // }) => show_commit_summary, - // _ => false, - // } + if self.inline_blame.delay_ms.as_millis() > 0 { + Some(self.inline_blame.delay_ms) + } else { + None + } } } From d48f8f149bf5dbdfea1a7eee5039e0ceca913370 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:55:06 -0700 Subject: [PATCH 100/117] Resolve todo: get_or_insert_default is fine because this is the content type, and all of those defaults are None --- crates/project/src/project_settings.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index db2b9d83010f8044fc4d3b3a8fb469c70da7a2e1..80bf667b2cd204b9a0989fdbad87efa84cd5c819 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -647,10 +647,6 @@ impl Settings for ProjectSettings { } if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") { - // todo! get_or_insert_default is risky considering defaults probably don't correspond - // to default.json, - // probably need to pass in "defaults" for this type as additional arg, and - // use unwrap on those keys current .git .get_or_insert_default() From b0d16cdba9091349f7f10447a38516f981bdc32f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 22:57:02 -0700 Subject: [PATCH 101/117] Resolve todo that has been completed --- crates/settings/src/settings_content/language.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index f1333d8e405bf492a2e3a69184d56184f378642d..7e7e92ea88402f7654ad07acff9469736f103c5e 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -351,8 +351,6 @@ pub struct JsxTagAutoCloseSettingsContent { } /// The settings for inlay hints. -/// todo!() the fields of this struct should likely be optional, -/// and a similar struct exposed from the language crate. #[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettingsContent { From 3d2e5f43f4f3f54e9e0edcc0cb0e1365c445f228 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 23:00:46 -0700 Subject: [PATCH 102/117] Comment --- crates/settings/src/settings_content.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index d2c6069959c7476a70dc2a52431c287bc8ed063d..180d487d5afeda8adbf71c2642c8e932127c2c36 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -71,7 +71,11 @@ pub struct SettingsContent { /// Default: true pub auto_update: Option, - // todo!() comments?! + /// This base keymap settings adjusts the default keybindings in Zed to be similar + /// to other common code editors. By default, Zed's keymap closely follows VSCode's + /// keymap, with minor adjustments, this corresponds to the "VSCode" setting. + /// + /// Default: VSCode pub base_keymap: Option, /// Configuration for the collab panel visual settings. From 674b9451e9ad9bd9a889b1776c5a7eaf070a6001 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 23:01:52 -0700 Subject: [PATCH 103/117] Remove todo. We can answer this question in a follow up PR --- crates/settings/src/settings_content.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 180d487d5afeda8adbf71c2642c8e932127c2c36..542cc6c57b08389f85c71ab0ef8dfcdd1f78c189 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -165,7 +165,6 @@ impl SettingsContent { } } -// todo!() what should this be? #[skip_serializing_none] #[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ServerSettingsContent { From 9f3b251f779e2807148af025d90c78880e30f9a7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 23:03:19 -0700 Subject: [PATCH 104/117] no --- crates/settings/src/settings_json.rs | 9 +-------- crates/settings/src/settings_store.rs | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 614f5eed813a22d96eafe0ab0696161dc21b233f..762842d851b73a661baf93a7060f26fe5714c872 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -26,7 +26,6 @@ pub fn update_value_in_json_text<'a>( tab_size: usize, old_value: &'a Value, new_value: &'a Value, - preserved_keys: &[&str], edits: &mut Vec<(Range, String)>, ) { // If the old and new values are both objects, then compare them key by key, @@ -43,7 +42,6 @@ pub fn update_value_in_json_text<'a>( tab_size, old_sub_value, new_sub_value, - preserved_keys, edits, ); } else { @@ -64,17 +62,12 @@ pub fn update_value_in_json_text<'a>( tab_size, &Value::Null, new_sub_value, - preserved_keys, edits, ); } key_path.pop(); } - } else if key_path - .last() - .is_some_and(|key| preserved_keys.contains(key)) - || old_value != new_value - { + } else if old_value != new_value { let mut new_value = new_value.clone(); if let Some(new_object) = new_value.as_object_mut() { new_object.retain(|_, v| !v.is_null()); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 11b589504551f41e3472baa97a064d3fe2b34025..1dc8ad1fc9d459fa9db73a2cad61f809bce16b4e 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -572,7 +572,6 @@ impl SettingsStore { tab_size, &old_value, &new_value, - &[], // todo!() is this still needed? &mut edits, ); edits From d77cefd79a46b414e8d0c31cdffe31349adfb206 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 23:06:05 -0700 Subject: [PATCH 105/117] This second API seems like it's trying to avoid adding unescessary 'get_or_insert_default()' calls --- crates/settings/src/vscode_import.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index c0a055c3b6d33f741610852fab620fbb97f5c05b..85e9cfacf290dcbe4b63768e990a44203f9aa2a9 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -136,7 +136,6 @@ impl VsCodeSettings { } } - // todo! replace enum_setting pub fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { self.content.get(key).and_then(Value::as_str).and_then(f) } From 69a353a9fb660b445c6dcf4596634db176d179d4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Sep 2025 23:22:03 -0700 Subject: [PATCH 106/117] The original intent of the private files API was for it to be a merge, not an overwrite. --- crates/settings/src/settings_content/project.rs | 2 +- crates/worktree/src/worktree_settings.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index c76321d3a9ec662dde65d1cc33848217378bcb19..fd4f34ff4fc647c389ab977b1076003516cb7421 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -80,7 +80,7 @@ pub struct WorktreeSettingsContent { pub file_scan_inclusions: Option>, /// Treat the files matching these globs as `.env` files. - /// Default: [ "**/.env*" ] + /// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"] pub private_files: Option>, } diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 92f1d7220c3984bea98f439dfca51890c2d2e473..5093988ac2ac66682aba9447a6347ecfc54ecf76 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -66,11 +66,11 @@ impl Settings for WorktreeSettings { self.project_name = Some(project_name); } - // todo!() test this. Did it used to extend the arrays, or overwrite them? - - if let Some(private_files) = worktree.private_files.clone() { + if let Some(mut private_files) = worktree.private_files.clone() { + let sources = self.private_files.sources(); + private_files.extend_from_slice(sources); if let Some(matchers) = path_matchers(private_files, "private_files").log_err() { - self.private_files = matchers + self.private_files = matchers; } } From 018ee29e2d69e1e8b733a0c8fde6da2918d426d1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Sep 2025 00:55:19 -0600 Subject: [PATCH 107/117] Compiling again --- crates/editor/src/editor_tests.rs | 8 ++++---- crates/editor/src/element.rs | 4 ++-- crates/language/src/language_settings.rs | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 80fda5297a501405ed894f670580d963abf9c3aa..d0ae1e39cdda2fdc3d017f203bc1f38f2ca705a5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22,15 +22,15 @@ use indoc::indoc; use language::{ BracketPairConfig, Capability::ReadWrite, - DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, - LanguageName, Override, Point, + DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig, + LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point, language_settings::{ CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode, SelectedFormatter, }, tree_sitter_python, }; -use language_settings::{Formatter, IndentGuideSettingsContent}; +use language_settings::Formatter; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -19961,7 +19961,7 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) - end_row: MultiBufferRow(end_row), depth, tab_size: 4, - settings: IndentGuideSettingsContent { + settings: IndentGuideSettings { enabled: true, line_width: 1, active_line_width: 1, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7b6f26c85fb7619178c4ee364343763175f3c036..3bc05ccd537710c34c1c4e8e6d63c26440360f2e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -51,7 +51,7 @@ use gpui::{ transparent_black, }; use itertools::Itertools; -use language::language_settings::{IndentGuideSettingsContent, ShowWhitespaceSetting}; +use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting}; use markdown::Markdown; use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, @@ -10177,7 +10177,7 @@ pub struct IndentGuideLayout { single_indent_width: Pixels, depth: u32, active: bool, - settings: IndentGuideSettingsContent, + settings: IndentGuideSettings, } pub struct CursorLayout { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 989079f0e600120c55f9136e5536324c8c5e959b..0683b90e6d4c1f908cd51cd8b1e292f81453277f 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -13,13 +13,13 @@ use schemars::json_schema; pub use settings::{ CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave, - Formatter, FormatterList, IndentGuideSettingsContent, InlayHintKind, LanguageSettingsContent, - LspInsertMode, RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, - WordsCompletionMode, + Formatter, FormatterList, InlayHintKind, LspInsertMode, RewrapBehavior, SelectedFormatter, + ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{ - LanguageTaskSettingsContent, ParameterizedJsonSchema, PrettierSettingsContent, Settings, - SettingsContent, SettingsLocation, SettingsStore, SettingsUi, + IndentGuideSettingsContent, LanguageSettingsContent, LanguageTaskSettingsContent, + ParameterizedJsonSchema, PrettierSettingsContent, Settings, SettingsContent, SettingsLocation, + SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; From 4b1e78cd5c4ed69a168409620a272fc63fa2f433 Mon Sep 17 00:00:00 2001 From: Miao Date: Thu, 18 Sep 2025 16:15:52 +0800 Subject: [PATCH 108/117] terminal: Fix COLORTERM regression for true color support (#38379) Closes #38304 Release Notes: - Fixed true color detection regression by setting `COLORTERM=truecolor` --- Reason: The regression is possibly introduced in [pr#36576: Inject venv environment via the toolchain](https://github.com/zed-industries/zed/pull/36576/files#diff-6f30387876b79f1de44f8193401d6c8fb49a2156479c4f2e32bc922ec5d54d76), where `alacritty_terminal::tty::setup_env();` is removed. The `alacritty_terminal::tty::setup_env();` does 2 things, which sets `TERM` & `COLORTERM` envvar. ```rs /// Setup environment variables. pub fn setup_env() { // Default to 'alacritty' terminfo if it is available, otherwise // default to 'xterm-256color'. May be overridden by user's config // below. let terminfo = if terminfo_exists("alacritty") { "alacritty" } else { "xterm-256color" }; unsafe { env::set_var("TERM", terminfo) }; // Advertise 24-bit color support. unsafe { env::set_var("COLORTERM", "truecolor") }; } ``` --- crates/terminal/src/terminal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a07aef5f7b4da90373bcbf7c406dd8277cb09387..0fce02a97b04484b5a91d6d43b456ecfb1f75f15 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -364,6 +364,7 @@ impl TerminalBuilder { env.insert("ZED_TERM".to_string(), "true".to_string()); env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); env.insert("TERM".to_string(), "xterm-256color".to_string()); + env.insert("COLORTERM".to_string(), "truecolor".to_string()); env.insert( "TERM_PROGRAM_VERSION".to_string(), release_channel::AppVersion::global(cx).to_string(), From d85a6db6a3f1115d60cea103d33944894a2b2d4c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 10:22:26 +0200 Subject: [PATCH 109/117] git_ui: Use margin instead of padding for blame entries (#38397) This makes the hover background change keep a visible border element between the gutter and blame entries Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/git_ui/src/blame_ui.rs | 139 ++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index ad5823c1674353f2e0531e5f71e1420fe464bfe6..6229c80c739ee73b20fe2640f30f9f751a1b4411 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -47,75 +47,84 @@ impl BlameRenderer for GitBlameRenderer { let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED); Some( - h_flex() - .w_full() - .justify_between() - .font_family(style.font().family) - .line_height(style.line_height) - .id(("blame", ix)) - .text_color(cx.theme().status().hint) - .pr_2() - .gap_2() + div() + .mr_2() .child( h_flex() - .items_center() + .w_full() + .justify_between() + .font_family(style.font().family) + .line_height(style.line_height) + .id(("blame", ix)) + .text_color(cx.theme().status().hint) .gap_2() - .child(div().text_color(sha_color).child(short_commit_id)) - .child(name), - ) - .child(relative_timestamp) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .cursor_pointer() - .on_mouse_down(MouseButton::Right, { - let blame_entry = blame_entry.clone(); - let details = details.clone(); - move |event, window, cx| { - deploy_blame_entry_context_menu( - &blame_entry, - details.as_ref(), - editor.clone(), - event.position, - window, - cx, - ); - } - }) - .on_click({ - let blame_entry = blame_entry.clone(); - let repository = repository.clone(); - let workspace = workspace.clone(); - move |_, window, cx| { - CommitView::open( - CommitSummary { - sha: blame_entry.sha.to_string().into(), - subject: blame_entry.summary.clone().unwrap_or_default().into(), - commit_timestamp: blame_entry.committer_time.unwrap_or_default(), - author_name: blame_entry - .committer_name - .clone() - .unwrap_or_default() - .into(), - has_parent: true, - }, - repository.downgrade(), - workspace.clone(), - window, - cx, - ) - } - }) - .hoverable_tooltip(move |_window, cx| { - cx.new(|cx| { - CommitTooltip::blame_entry( - &blame_entry, - details.clone(), - repository.clone(), - workspace.clone(), - cx, + .child( + h_flex() + .items_center() + .gap_2() + .child(div().text_color(sha_color).child(short_commit_id)) + .child(name), ) - }) - .into() - }) + .child(relative_timestamp) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .cursor_pointer() + .on_mouse_down(MouseButton::Right, { + let blame_entry = blame_entry.clone(); + let details = details.clone(); + move |event, window, cx| { + deploy_blame_entry_context_menu( + &blame_entry, + details.as_ref(), + editor.clone(), + event.position, + window, + cx, + ); + } + }) + .on_click({ + let blame_entry = blame_entry.clone(); + let repository = repository.clone(); + let workspace = workspace.clone(); + move |_, window, cx| { + CommitView::open( + CommitSummary { + sha: blame_entry.sha.to_string().into(), + subject: blame_entry + .summary + .clone() + .unwrap_or_default() + .into(), + commit_timestamp: blame_entry + .committer_time + .unwrap_or_default(), + author_name: blame_entry + .committer_name + .clone() + .unwrap_or_default() + .into(), + has_parent: true, + }, + repository.downgrade(), + workspace.clone(), + window, + cx, + ) + } + }) + .hoverable_tooltip(move |_window, cx| { + cx.new(|cx| { + CommitTooltip::blame_entry( + &blame_entry, + details.clone(), + repository.clone(), + workspace.clone(), + cx, + ) + }) + .into() + }), + ) .into_any(), ) } From ed46e2ca775b69126f6916534d4f027e51cae452 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Thu, 18 Sep 2025 09:47:15 +0100 Subject: [PATCH 110/117] helix: Apply modification (e.g. switch case) on a single character only in helix mode (#38119) Closes #34192 Without selection, only current character would be affected. Also if #38117 is merged too, then transformations in SelectMode behave correctly too and selection is not collapsed. Release Notes: - helix: Implemented `~`, `` ` ``, `` Alt-` `` correctly in normal and select modes --------- Co-authored-by: Jakub Konka --- assets/keymaps/vim.json | 5 ++--- crates/vim/src/normal/convert.rs | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8464e03d251afc166ac45a349894ecf2f7247944..817198659657814dcc597926d689063ae2182c78 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -442,9 +442,8 @@ ">": "vim::Indent", "<": "vim::Outdent", "=": "vim::AutoIndent", - "g u": "vim::PushLowercase", - "g shift-u": "vim::PushUppercase", - "g ~": "vim::PushOppositeCase", + "`": "vim::ConvertToLowerCase", + "alt-`": "vim::ConvertToUpperCase", "g q": "vim::PushRewrap", "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index d5875fe963778756daf5cd78f452b3436b53642f..11d040850d341155bf428ebc337cc9e3f4cc42c3 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -214,11 +214,10 @@ impl Vim { Mode::HelixNormal | Mode::HelixSelect => { if selection.is_empty() { - // Handle empty selection by operating on the whole word - let (word_range, _) = snapshot.surrounding_word(selection.start, false); - let word_start = snapshot.offset_to_point(word_range.start); - let word_end = snapshot.offset_to_point(word_range.end); - ranges.push(word_start..word_end); + // Handle empty selection by operating on single character + let start = selection.start; + let end = snapshot.clip_point(start + Point::new(0, 1), Bias::Right); + ranges.push(start..end); cursor_positions.push(selection.start..selection.start); } else { ranges.push(selection.start..selection.end); @@ -445,15 +444,26 @@ mod test { cx.simulate_keystrokes("~"); cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal); - // Cursor-only (empty) selection + // Cursor-only (empty) selection - switch case cx.set_state("The ˇquick brown", Mode::HelixNormal); cx.simulate_keystrokes("~"); - cx.assert_state("The ˇQUICK brown", Mode::HelixNormal); + cx.assert_state("The ˇQuick brown", Mode::HelixNormal); + cx.simulate_keystrokes("~"); + cx.assert_state("The ˇquick brown", Mode::HelixNormal); + + // Cursor-only (empty) selection - switch to uppercase and lowercase explicitly + cx.set_state("The ˇquick brown", Mode::HelixNormal); + cx.simulate_keystrokes("alt-`"); + cx.assert_state("The ˇQuick brown", Mode::HelixNormal); + cx.simulate_keystrokes("`"); + cx.assert_state("The ˇquick brown", Mode::HelixNormal); // With `e` motion (which extends selection to end of word in Helix) cx.set_state("The ˇquick brown fox", Mode::HelixNormal); cx.simulate_keystrokes("e"); cx.simulate_keystrokes("~"); cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal); + + // Cursor-only } } From 32c868ff7d50f4163072c72bc2a09cf710e7b521 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 18 Sep 2025 11:38:59 +0200 Subject: [PATCH 111/117] acp: Fix behavior of read_text_file for ACP agents (#38401) We were incorrectly handling the line number as well as stripping out line breaks when returning portions of files. It also makes sure following is updated even when we load a snapshot from cache, which wasn't the case before. We also are able to load the text via a range in the snapshot, rather than allocating a string for the entire file and then another after iterating over lines in the file. Release Notes: - acp: Fix incorrect behavior when ACP agents requested to read portions of files. --- crates/acp_thread/src/acp_thread.rs | 140 +++++++++++++++++++++------- 1 file changed, 106 insertions(+), 34 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 68e5266f06aa8bddfaa252bdc1cf5b21891c7f10..f2327ca70b104de12f44d74aacd1a5a2bb1eca3b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1781,6 +1781,9 @@ impl AcpThread { reuse_shared_snapshot: bool, cx: &mut Context, ) -> Task> { + // Args are 1-based, move to 0-based + let line = line.unwrap_or_default().saturating_sub(1); + let limit = limit.unwrap_or(u32::MAX); let project = self.project.clone(); let action_log = self.action_log.clone(); cx.spawn(async move |this, cx| { @@ -1808,44 +1811,37 @@ impl AcpThread { action_log.update(cx, |action_log, cx| { action_log.buffer_read(buffer.clone(), cx); })?; - project.update(cx, |project, cx| { - let position = buffer - .read(cx) - .snapshot() - .anchor_before(Point::new(line.unwrap_or_default(), 0)); - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }), - cx, - ); - })?; - buffer.update(cx, |buffer, _| buffer.snapshot())? + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; + this.update(cx, |this, _| { + this.shared_buffers.insert(buffer.clone(), snapshot.clone()); + })?; + snapshot }; - this.update(cx, |this, _| { - let text = snapshot.text(); - this.shared_buffers.insert(buffer.clone(), snapshot); - if line.is_none() && limit.is_none() { - return Ok(text); - } - let limit = limit.unwrap_or(u32::MAX) as usize; - let Some(line) = line else { - return Ok(text.lines().take(limit).collect::()); - }; + let max_point = snapshot.max_point(); + if line >= max_point.row { + anyhow::bail!( + "Attempting to read beyond the end of the file, line {}:{}", + max_point.row + 1, + max_point.column + ); + } - let count = text.lines().count(); - if count < line as usize { - anyhow::bail!("There are only {} lines", count); - } - Ok(text - .lines() - .skip(line as usize + 1) - .take(limit) - .collect::()) - })? + let start = snapshot.anchor_before(Point::new(line, 0)); + let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0)); + + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: start, + }), + cx, + ); + })?; + + Ok(snapshot.text_for_range(start..end).collect::()) }) } @@ -2391,6 +2387,82 @@ mod tests { request.await.unwrap(); } + #[gpui::test] + async fn test_reading_from_line(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"})) + .await; + let project = Project::test(fs.clone(), [], cx).await; + project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/tmp/foo"), true, cx) + }) + .await + .unwrap(); + + let connection = Rc::new(FakeAgentConnection::new()); + + let thread = cx + .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) + .await + .unwrap(); + + // Whole file + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "one\ntwo\nthree\nfour\n"); + + // Only start line + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "three\nfour\n"); + + // Only limit + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "one\ntwo\n"); + + // Range + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx) + }) + .await + .unwrap(); + + assert_eq!(content, "two\nthree\n"); + + // Invalid + let err = thread + .update(cx, |thread, cx| { + thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx) + }) + .await + .unwrap_err(); + + assert_eq!( + err.to_string(), + "Attempting to read beyond the end of the file, line 5:0" + ); + } + #[gpui::test] async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) { init_test(cx); From 9f9e8063fccfaebd5a3c8939c87ef206e0bdca58 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:03:35 +0200 Subject: [PATCH 112/117] workspace: Pop a toast if manually spawning a task fails (#38405) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/workspace/src/tasks.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 71394c874ae988d7b8fef3e3a224d25e1c290640..5f52cb49e74d67619b9ba7c033a33fe8a7ad51c8 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -8,7 +8,7 @@ use remote::ConnectionState; use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; use ui::Window; -use crate::Workspace; +use crate::{Toast, Workspace, notifications::NotificationId}; impl Workspace { pub fn schedule_task( @@ -73,8 +73,10 @@ impl Workspace { if let Some(terminal_provider) = self.terminal_provider.as_ref() { let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); - let task = cx.background_spawn(async move { - match task_status.await { + + let task = cx.spawn(async |w, cx| { + let res = cx.background_spawn(task_status).await; + match res { Some(Ok(status)) => { if status.success() { log::debug!("Task spawn succeeded"); @@ -82,9 +84,15 @@ impl Workspace { log::debug!("Task spawn failed, code: {:?}", status.code()); } } - Some(Err(e)) => log::error!("Task spawn failed: {e:#}"), + Some(Err(e)) => { + log::error!("Task spawn failed: {e:#}"); + _ = w.update(cx, |w, cx| { + let id = NotificationId::unique::(); + w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx); + }) + } None => log::debug!("Task spawn got cancelled"), - } + }; }); self.scheduled_tasks.push(task); } From ca05ff89f434319f00c7d99a030bfd767d6edb39 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Thu, 18 Sep 2025 12:05:05 +0200 Subject: [PATCH 113/117] agent2: More efficent read file tool (#38407) Before we were always reading the entire file into memory as a string. Now we only read the range that is actually requested. Release Notes: - N/A --- crates/agent2/src/tools/read_file_tool.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 87163e769c26b0cee053fcf149d047fc451c470f..7be83d8bef174bf9b2799d67eb6240fcae4e5bb6 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -201,7 +201,6 @@ impl AgentTool for ReadFileTool { // Check if specific line ranges are provided let result = if input.start_line.is_some() || input.end_line.is_some() { let result = buffer.read_with(cx, |buffer, _cx| { - let text = buffer.text(); // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. let start = input.start_line.unwrap_or(1).max(1); let start_row = start - 1; @@ -210,13 +209,13 @@ impl AgentTool for ReadFileTool { anchor = Some(buffer.anchor_before(Point::new(start_row, column))); } - let lines = text.split('\n').skip(start_row as usize); - if let Some(end) = input.end_line { - let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line - itertools::intersperse(lines.take(count as usize), "\n").collect::() - } else { - itertools::intersperse(lines, "\n").collect::() + let mut end_row = input.end_line.unwrap_or(u32::MAX); + if end_row <= start_row { + end_row = start_row + 1; // read at least one lines } + let start = buffer.anchor_before(Point::new(start_row, 0)); + let end = buffer.anchor_before(Point::new(end_row, 0)); + buffer.text_for_range(start..end).collect::() })?; action_log.update(cx, |log, cx| { @@ -445,7 +444,7 @@ mod test { tool.run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into()); + assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4\n".into()); } #[gpui::test] @@ -475,7 +474,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1\nLine 2".into()); + assert_eq!(result.unwrap(), "Line 1\nLine 2\n".into()); // end_line of 0 should result in at least 1 line let result = cx @@ -488,7 +487,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 1".into()); + assert_eq!(result.unwrap(), "Line 1\n".into()); // when start_line > end_line, should still return at least 1 line let result = cx @@ -501,7 +500,7 @@ mod test { tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; - assert_eq!(result.unwrap(), "Line 3".into()); + assert_eq!(result.unwrap(), "Line 3\n".into()); } fn init_test(cx: &mut TestAppContext) { From 59a609c9fcebd3f641e64a498e15e3cf2042a648 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:06:43 +0200 Subject: [PATCH 114/117] Partially revert "project: Fix terminal activation scripts failing on Windows for new shells (#37986) (#38406) This partially reverts commit 4002602a8926b7fe799acf50fcee6bcffb36d376. Specifically the parts that closes https://github.com/zed-industries/zed/issues/38343 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/acp/thread_view.rs | 2 +- crates/terminal/src/terminal.rs | 8 ++--- crates/terminal_view/src/terminal_panel.rs | 38 ++++++++++++++++++++-- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8e8f908bc2eea651babb73749e26cb2d6474f74f..cf5284f643cfe3d58ff62a4fa549a84f0a62db69 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1591,7 +1591,7 @@ impl AcpThreadView { task.shell = shell; let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(login.clone(), window, cx) + terminal_panel.spawn_task(&login, window, cx) })?; let terminal = terminal.await?; diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0fce02a97b04484b5a91d6d43b456ecfb1f75f15..6bdeb9638a329c2384e538e27e13c21f02df7284 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -533,14 +533,10 @@ impl TerminalBuilder { child_exited: None, }; - if !activation_script.is_empty() && no_task { + if cfg!(not(target_os = "windows")) && !activation_script.is_empty() && no_task { for activation_script in activation_script { terminal.input(activation_script.into_bytes()); - terminal.write_to_pty(if cfg!(windows) { - &b"\r\n"[..] - } else { - &b"\n"[..] - }); + terminal.write_to_pty(b"\n"); } terminal.clear(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6c93644f992dcd5d3c0126a28c9aa8b8bab020d3..ef574728acdc06bb0db686a1cc9b4c8f8bc0bcce 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -19,7 +19,7 @@ use itertools::Itertools; use project::{Fs, Project, ProjectEntryId}; use search::{BufferSearchBar, buffer_search::DivRegistrar}; use settings::Settings; -use task::{RevealStrategy, RevealTarget, SpawnInTerminal, TaskId}; +use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId}; use terminal::{ Terminal, terminal_settings::{TerminalDockPosition, TerminalSettings}, @@ -521,10 +521,42 @@ impl TerminalPanel { pub fn spawn_task( &mut self, - task: SpawnInTerminal, + task: &SpawnInTerminal, window: &mut Window, cx: &mut Context, ) -> Task>> { + let remote_client = self + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + if project.is_via_collab() { + Err(anyhow!("cannot spawn tasks as a guest")) + } else { + Ok(project.remote_client()) + } + }) + .flatten(); + + let remote_client = match remote_client { + Ok(remote_client) => remote_client, + Err(e) => return Task::ready(Err(e)), + }; + + let remote_shell = remote_client + .as_ref() + .and_then(|remote_client| remote_client.read(cx).shell()); + + let builder = ShellBuilder::new(remote_shell.as_deref(), &task.shell); + let command_label = builder.command_label(&task.command_label); + let (command, args) = builder.build(task.command.clone(), &task.args); + + let task = SpawnInTerminal { + command_label, + command: Some(command), + args, + ..task.clone() + }; + if task.allow_concurrent_runs && task.use_new_terminal { return self.spawn_in_new_terminal(task, window, cx); } @@ -1558,7 +1590,7 @@ impl workspace::TerminalProvider for TerminalProvider { window.spawn(cx, async move |cx| { let terminal = terminal_panel .update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(task, window, cx) + terminal_panel.spawn_task(&task, window, cx) }) .ok()? .await; From b1aa2723e9b10350ec4270ab6685ffe57ffe4309 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 18 Sep 2025 12:58:10 +0200 Subject: [PATCH 115/117] editor: Reverse range of pending selection if required (#38410) cc https://github.com/zed-industries/zed/issues/38129 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/selections_collection.rs | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index e562be10e92344c1c892878ab674cba39beb74c2..4343443ff8c4cb4e388984c9014b13ddc8726523 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -469,13 +469,24 @@ impl<'a> MutableSelectionsCollection<'a> { } pub(crate) fn set_pending_anchor_range(&mut self, range: Range, mode: SelectMode) { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); self.collection.pending = Some(PendingSelection { - selection: Selection { - id: post_inc(&mut self.collection.next_selection_id), - start: range.start, - end: range.end, - reversed: false, - goal: SelectionGoal::None, + selection: { + let mut start = range.start; + let mut end = range.end; + let reversed = if start.cmp(&end, &buffer).is_gt() { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } }, mode, }); From 2b18032df54bf66096987a05e7f3dc1553de6381 Mon Sep 17 00:00:00 2001 From: Anthony Date: Thu, 18 Sep 2025 10:01:31 -0400 Subject: [PATCH 116/117] Create a DapSettingsContent type and remove optionals from DapSettings I did to fix the todo! for the DapSettings and make it more consistent with how the rest of settings are now defined --- crates/language_models/src/provider/vercel.rs | 2 +- crates/language_models/src/settings.rs | 1 - crates/project/src/debugger/dap_store.rs | 12 ++++--- crates/project/src/project_settings.rs | 33 ++++++++++++++++--- .../settings/src/settings_content/project.rs | 6 ++-- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 86f3dc6a1672e19716afefcdaf32ca71fc43ae88..5f6fa23c642afbf57acb88d6429c07721dd74acc 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -29,7 +29,7 @@ const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new( const API_KEY_ENV_VAR_NAME: &str = "VERCEL_API_KEY"; static API_KEY_ENV_VAR: LazyLock = env_var!(API_KEY_ENV_VAR_NAME); -#[derive(Default, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct VercelSettings { pub api_url: String, pub available_models: Vec, diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index 808a1e88d7c039ea2576aae9472d4ee73437accb..30b208a170794a0d4018e8ebce39f1020b4c9b29 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -18,7 +18,6 @@ pub fn init_settings(cx: &mut App) { AllLanguageModelSettings::register(cx); } -#[derive(Default)] pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index ab0d69b6d9f7da254e659a0d5e5d0cfee0322711..d96e5c220fcc98413a42dc46e0889004300d1e2f 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -5,8 +5,10 @@ use super::{ session::{self, Session, SessionStateEvent}, }; use crate::{ - InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks, - project_settings::ProjectSettings, worktree_store::WorktreeStore, + InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, + debugger::session::SessionQuirks, + project_settings::{DapBinary, ProjectSettings}, + worktree_store::WorktreeStore, }; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; @@ -209,8 +211,10 @@ impl DapStore { let dap_settings = ProjectSettings::get(Some(settings_location), cx) .dap .get(&adapter.name()); - let user_installed_path = - dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from)); + let user_installed_path = dap_settings.and_then(|s| match &s.binary { + DapBinary::Default => None, + DapBinary::Custom(binary) => Some(PathBuf::from(binary)), + }); let user_args = dap_settings.map(|s| s.args.clone()); let delegate = self.delegate(worktree, console, cx); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 80bf667b2cd204b9a0989fdbad87efa84cd5c819..8cba826ca95c48aa34064086fca8c267704f3923 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -20,8 +20,8 @@ use serde::{Deserialize, Serialize}; pub use settings::DirenvSettings; pub use settings::LspSettings; use settings::{ - InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsStore, SettingsUi, - parse_json_with_comments, watch_config_file, + DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, + SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, }; use std::{ path::{Path, PathBuf}, @@ -54,7 +54,7 @@ pub struct ProjectSettings { pub global_lsp_settings: GlobalLspSettings, /// Configuration for Debugger-related features - pub dap: HashMap, + pub dap: HashMap, /// Settings for context servers used for AI-related features. pub context_servers: HashMap, ContextServerSettings>, @@ -494,7 +494,7 @@ impl Settings for ProjectSettings { .dap .clone() .into_iter() - .map(|(key, value)| (DebugAdapterName(key.into()), value)) + .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value))) .collect(), diagnostics: DiagnosticsSettings { button: diagnostics.button.unwrap(), @@ -534,7 +534,7 @@ impl Settings for ProjectSettings { .dap .clone() .into_iter() - .map(|(key, value)| (DebugAdapterName(key.into()), value)), + .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value))), ); if let Some(diagnostics) = content.diagnostics.as_ref() { if let Some(inline) = &diagnostics.inline { @@ -1326,3 +1326,26 @@ pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSett LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug, } } + +#[derive(Debug, Clone)] +pub struct DapSettings { + pub binary: DapBinary, + pub args: Vec, +} + +impl From for DapSettings { + fn from(content: DapSettingsContent) -> Self { + DapSettings { + binary: content + .binary + .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)), + args: content.args.unwrap_or_default(), + } + } +} + +#[derive(Debug, Clone)] +pub enum DapBinary { + Default, + Custom(String), +} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index fd4f34ff4fc647c389ab977b1076003516cb7421..e857fdcb604da7f1b56e01507377667892a23bd0 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -30,7 +30,7 @@ pub struct ProjectSettingsContent { /// Configuration for Debugger-related features #[serde(default)] - pub dap: HashMap, DapSettings>, + pub dap: HashMap, DapSettingsContent>, /// Settings for context servers used for AI-related features. #[serde(default)] @@ -141,10 +141,10 @@ pub struct GlobalLspSettingsContent { #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct DapSettings { +pub struct DapSettingsContent { pub binary: Option, #[serde(default)] - pub args: Vec, + pub args: Option>, } #[skip_serializing_none] From 90b8fc8f1e32e9b104cb6b456c88a0fce6f596bc Mon Sep 17 00:00:00 2001 From: Anthony Date: Thu, 18 Sep 2025 10:03:17 -0400 Subject: [PATCH 117/117] Actually remove dap todo! --- crates/settings/src/settings_content/project.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index e857fdcb604da7f1b56e01507377667892a23bd0..2c154dd8ea337ae165cb1e76872640a8af334fe3 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -137,7 +137,6 @@ pub struct GlobalLspSettingsContent { pub button: Option, } -// todo! binary is actually just required, shouldn't be an option #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")]