From f2c3f3b168bab7c808e3ce2c3392b0c692919f81 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:56:10 -0400 Subject: [PATCH] settings ui: Start work on creating the initial structure (#36904) ## Goal This PR creates the initial settings ui structure with the primary goal of making a settings UI that is - Comprehensive: All settings are available through the UI - Correct: Easy to understand the underlying JSON file from the UI - Intuitive - Easy to implement per setting so that UI is not a hindrance to future settings changes ### Structure The overall structure is settings layer -> data layer -> ui layer. The settings layer is the pre-existing settings definitions, that implement the `Settings` trait. The data layer is constructed from settings primarily through the `SettingsUi` trait, and it's associated derive macro. The data layer tracks the grouping of the settings, the json path of the settings, and a data representation of how to render the controls for the setting in the UI, that is either a marker value for the component to use (avoiding a dependency on the `ui` crate) or a custom render function. Abstracting the data layer from the ui layer allows crates depending on `settings` to implement their own UI without having to add additional UI dependencies, thus avoiding circular dependencies. In cases where custom UI is desired, and a creating a custom render function in the same crate is infeasible due to circular dependencies, the current solution is to implement a marker for the component in the `settings` crate, and then handle the rendering of that component in `settings_ui`. ### Foundation This PR creates a macro and a trait both called `SettingsUi`. The `SettingsUi` trait is added as a new trait bound on the `Settings` trait, this allows the type system to guarantee that all settings implement UI functionality. The macro is used to derived the trait for most types, and can be modified through attributes for unique cases as well. A derive-macro is used to generate the settings UI trait impl, allowing it the UI generation to be generated from the static information in our code base (`default.json`, Struct/Enum names, field names, `serde` attributes, etc). This allows the UI to be auto-generated for the most part, and ensures consistency across the UI. #### Immediate Follow ups - Add a new `SettingsPath` trait that will be a trait bound on `SettingsUi` and `Settings` - This trait will replace the `Settings::key` value to enable `SettingsUi` to infer the json path of it's derived type - Figure out how to render `Option where T: SettingsUi` correctly - Handle `serde` attributes in the `SettingsUi` proc macro to correctly get json path from a type's field and identity Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- Cargo.lock | 75 ++- Cargo.toml | 10 +- assets/settings/default.json | 3 + crates/agent_servers/src/settings.rs | 4 +- crates/agent_settings/src/agent_settings.rs | 4 +- crates/agent_ui/src/slash_command_settings.rs | 4 +- crates/audio/src/audio_settings.rs | 4 +- crates/auto_update/src/auto_update.rs | 3 +- crates/call/src/call_settings.rs | 4 +- crates/client/src/client.rs | 8 +- crates/collab_ui/src/panel_settings.rs | 10 +- crates/dap/src/debugger_settings.rs | 8 +- crates/editor/src/editor_settings.rs | 4 +- .../extension_host/src/extension_settings.rs | 4 +- .../file_finder/src/file_finder_settings.rs | 4 +- crates/git_hosting_providers/src/settings.rs | 4 +- crates/git_ui/src/git_panel_settings.rs | 4 +- crates/go_to_line/src/cursor_position.rs | 4 +- crates/gpui_macros/src/derive_action.rs | 7 + .../image_viewer/src/image_viewer_settings.rs | 4 +- crates/journal/src/journal.rs | 4 +- crates/keymap_editor/Cargo.toml | 53 ++ crates/keymap_editor/LICENSE-GPL | 1 + .../src/keymap_editor.rs} | 6 +- .../src/ui_components/keystroke_input.rs | 0 .../src/ui_components/mod.rs | 0 .../src/ui_components/table.rs | 0 crates/language/src/language_settings.rs | 4 +- crates/language_models/src/settings.rs | 4 +- .../src/outline_panel_settings.rs | 4 +- crates/project/src/project.rs | 2 +- crates/project/src/project_settings.rs | 4 +- .../src/project_panel_settings.rs | 4 +- crates/recent_projects/src/ssh_connections.rs | 4 +- crates/repl/src/jupyter_settings.rs | 4 +- crates/settings/Cargo.toml | 1 + crates/settings/src/base_keymap_setting.rs | 8 +- crates/settings/src/settings.rs | 4 + crates/settings/src/settings_json.rs | 12 +- crates/settings/src/settings_store.rs | 144 +++-- crates/settings/src/settings_ui.rs | 118 +++++ crates/settings/src/vscode_import.rs | 4 +- crates/settings_ui/Cargo.toml | 41 +- crates/settings_ui/src/settings_ui.rs | 500 +++++++++++++++++- crates/settings_ui_macros/Cargo.toml | 22 + crates/settings_ui_macros/LICENSE-GPL | 1 + .../src/settings_ui_macros.rs | 201 +++++++ crates/terminal/src/terminal_settings.rs | 4 +- crates/theme/src/settings.rs | 4 +- crates/title_bar/Cargo.toml | 2 +- crates/title_bar/src/title_bar.rs | 6 +- crates/title_bar/src/title_bar_settings.rs | 5 +- crates/vim/src/vim.rs | 4 +- .../vim_mode_setting/src/vim_mode_setting.rs | 4 +- crates/workspace/src/item.rs | 6 +- crates/workspace/src/workspace_settings.rs | 6 +- crates/worktree/src/worktree_settings.rs | 4 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 +- crates/zed/src/zed/app_menus.rs | 3 +- crates/zlog_settings/src/zlog_settings.rs | 4 +- 62 files changed, 1149 insertions(+), 229 deletions(-) create mode 100644 crates/keymap_editor/Cargo.toml create mode 120000 crates/keymap_editor/LICENSE-GPL rename crates/{settings_ui/src/keybindings.rs => keymap_editor/src/keymap_editor.rs} (99%) rename crates/{settings_ui => keymap_editor}/src/ui_components/keystroke_input.rs (100%) rename crates/{settings_ui => keymap_editor}/src/ui_components/mod.rs (100%) rename crates/{settings_ui => keymap_editor}/src/ui_components/table.rs (100%) create mode 100644 crates/settings/src/settings_ui.rs create mode 100644 crates/settings_ui_macros/Cargo.toml create mode 120000 crates/settings_ui_macros/LICENSE-GPL create mode 100644 crates/settings_ui_macros/src/settings_ui_macros.rs diff --git a/Cargo.lock b/Cargo.lock index e201b4af804b0be95f100c34f93652b6ecf6f8e6..4c68280de25b878187b3a5627362f6373808734b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8951,6 +8951,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "keymap_editor" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "command_palette", + "component", + "db", + "editor", + "fs", + "fuzzy", + "gpui", + "itertools 0.14.0", + "language", + "log", + "menu", + "notifications", + "paths", + "project", + "search", + "serde", + "serde_json", + "settings", + "telemetry", + "tempfile", + "theme", + "tree-sitter-json", + "tree-sitter-rust", + "ui", + "ui_input", + "util", + "vim", + "workspace", + "workspace-hack", + "zed_actions", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -14856,6 +14894,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_json_lenient", + "settings_ui_macros", "smallvec", "tree-sitter", "tree-sitter-json", @@ -14891,39 +14930,28 @@ name = "settings_ui" version = "0.1.0" dependencies = [ "anyhow", - "collections", - "command_palette", "command_palette_hooks", - "component", - "db", "editor", "feature_flags", - "fs", - "fuzzy", "gpui", - "itertools 0.14.0", - "language", - "log", - "menu", - "notifications", - "paths", - "project", - "search", "serde", "serde_json", "settings", - "telemetry", - "tempfile", + "smallvec", "theme", - "tree-sitter-json", - "tree-sitter-rust", "ui", - "ui_input", - "util", - "vim", "workspace", "workspace-hack", - "zed_actions", +] + +[[package]] +name = "settings_ui_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "workspace-hack", ] [[package]] @@ -16739,6 +16767,7 @@ dependencies = [ "db", "gpui", "http_client", + "keymap_editor", "notifications", "pretty_assertions", "project", @@ -16747,7 +16776,6 @@ dependencies = [ "schemars", "serde", "settings", - "settings_ui", "smallvec", "story", "telemetry", @@ -20458,6 +20486,7 @@ dependencies = [ "itertools 0.14.0", "jj_ui", "journal", + "keymap_editor", "language", "language_extension", "language_model", diff --git a/Cargo.toml b/Cargo.toml index d346043c0ef64b3cce0827c2553c5b3c254d66f7..b64113311adb2662562cc4ae488054f54d569c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,8 @@ members = [ "crates/deepseek", "crates/diagnostics", "crates/docs_preprocessor", + "crates/edit_prediction", + "crates/edit_prediction_button", "crates/editor", "crates/eval", "crates/explorer_command_injector", @@ -82,13 +84,12 @@ members = [ "crates/http_client_tls", "crates/icons", "crates/image_viewer", - "crates/edit_prediction", - "crates/edit_prediction_button", "crates/inspector_ui", "crates/install_cli", "crates/jj", "crates/jj_ui", "crates/journal", + "crates/keymap_editor", "crates/language", "crates/language_extension", "crates/language_model", @@ -146,6 +147,7 @@ members = [ "crates/settings", "crates/settings_profile_selector", "crates/settings_ui", + "crates/settings_ui_macros", "crates/snippet", "crates/snippet_provider", "crates/snippets_ui", @@ -156,9 +158,9 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", - "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", + "crates/system_specs", "crates/tab_switcher", "crates/task", "crates/tasks_ui", @@ -314,6 +316,7 @@ install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } jj_ui = { path = "crates/jj_ui" } journal = { path = "crates/journal" } +keymap_editor = { path = "crates/keymap_editor" } language = { path = "crates/language" } language_extension = { path = "crates/language_extension" } language_model = { path = "crates/language_model" } @@ -373,6 +376,7 @@ semantic_version = { path = "crates/semantic_version" } session = { path = "crates/session" } settings = { path = "crates/settings" } settings_ui = { path = "crates/settings_ui" } +settings_ui_macros = { path = "crates/settings_ui_macros" } snippet = { path = "crates/snippet" } snippet_provider = { path = "crates/snippet_provider" } snippets_ui = { path = "crates/snippets_ui" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 572193be4eecbeb63a19eab1811bff126638162b..b15eb6e5ce8de85bb088108f065a31494b9087a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1922,7 +1922,10 @@ "debugger": { "stepping_granularity": "line", "save_breakpoints": true, + "timeout": 2000, "dock": "bottom", + "log_dap_communications": true, + "format_dap_log_messages": true, "button": true }, // Configures any number of settings profiles that are temporarily applied on diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 81f80a7d7d9581b8c1862ae3393c4a5d5e6706b6..693d7d7b7014b3abbecfbe592bac67210b336872 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -6,13 +6,13 @@ use collections::HashMap; use gpui::{App, SharedString}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; pub fn init(cx: &mut App) { AllAgentServersSettings::register(cx); } -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)] pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index ed1ed2b89879c18eceaab22843390a766e4f6c77..3808cc510f7941107f6e4ab90c9a5f8a2c3d920a 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString}; use language_model::LanguageModel; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::borrow::Cow; pub use crate::agent_profile::*; @@ -48,7 +48,7 @@ pub enum NotifyWhenAgentWaiting { Never, } -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone, Debug, SettingsUi)] pub struct AgentSettings { pub enabled: bool, pub button: bool, diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 73e5622aa921ccf03a3813717446e830c21079b8..c54a10ed49a77d395c4968e551b1cd30ad1c6e07 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -2,10 +2,10 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// Settings for slash commands. -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. #[serde(default)] diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 807179881c7c3b27aad2e3142a84c730951eb709..e42918825cd3a25bb18d6f0b357801949520833f 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -2,9 +2,9 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct AudioSettings { /// Opt into the new audio system. #[serde(rename = "experimental.rodio_audio", default)] diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 2150873cadd0a84b4a2894ebbe373d9bd0e007f0..71dcf25aeea9d8ebd4feb01db9161dc177fcdd26 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, SettingsSources, SettingsStore}; +use settings::{Settings, SettingsSources, SettingsStore, SettingsUi}; use smol::{fs, io::AsyncReadExt}; use smol::{fs::File, process::Command}; use std::{ @@ -113,6 +113,7 @@ impl Drop for MacOsUnmounter { } } +#[derive(SettingsUi)] struct AutoUpdateSetting(bool); /// Whether or not to automatically check for updates. diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index c8f51e0c1a2019dd2c266210e469989946ed8a35..64d11d0df64eedbbc29f06b8205f0318d999ea30 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -2,9 +2,9 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct CallSettings { pub mute_on_join: bool, pub share_on_join: bool, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1e735b0025f1e8a15809b096c5a462361d4ed8f3..c5bb1af0d7605cfcfc28d86bc389189d653e28ae 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, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{ any::TypeId, convert::TryFrom, @@ -101,7 +101,7 @@ pub struct ClientSettingsContent { server_url: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct ClientSettings { pub server_url: String, } @@ -127,7 +127,7 @@ pub struct ProxySettingsContent { proxy: Option, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, SettingsUi)] pub struct ProxySettings { pub proxy: Option, } @@ -520,7 +520,7 @@ impl Drop for PendingEntitySubscription { } } -#[derive(Copy, Clone, Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] pub struct TelemetrySettings { pub diagnostics: bool, pub metrics: bool, diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 652d9eb67f6ce1f0ab583e20e4feab05cfb743e3..4e5c8ad8f005d00a8802ab0a1f79ff7fbb3d0861 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -1,10 +1,10 @@ use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct CollaborationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -20,7 +20,7 @@ pub enum ChatPanelButton { WhenInCall, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct ChatPanelSettings { pub button: ChatPanelButton, pub dock: DockPosition, @@ -43,7 +43,7 @@ pub struct ChatPanelSettingsContent { pub default_width: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, SettingsUi)] pub struct NotificationPanelSettings { pub button: bool, pub dock: DockPosition, @@ -66,7 +66,7 @@ pub struct PanelSettingsContent { pub default_width: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. diff --git a/crates/dap/src/debugger_settings.rs b/crates/dap/src/debugger_settings.rs index e1176633e5403116c2789161d654912337150e9a..6843f19e3811967084cc61a3874ec86451ab6faf 100644 --- a/crates/dap/src/debugger_settings.rs +++ b/crates/dap/src/debugger_settings.rs @@ -2,9 +2,9 @@ use dap_types::SteppingGranularity; use gpui::{App, Global}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum DebugPanelDockPosition { Left, @@ -12,12 +12,14 @@ pub enum DebugPanelDockPosition { Right, } -#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)] +#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)] #[serde(default)] +#[settings_ui(group = "Debugger", path = "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. /// diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 55c040428d7e73d9e6e9bf6cc66cc20d301038f2..c2baa9de024b1988f9acb77a529936f947103f56 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -6,12 +6,12 @@ use language::CursorShape; use project::project_settings::DiagnosticSeverity; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings}; use util::serde::default_true; /// Imports from the VSCode settings at /// https://code.visualstudio.com/docs/reference/default-settings -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, SettingsUi)] pub struct EditorSettings { pub cursor_blink: bool, pub cursor_shape: Option, diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs index cfa67990b09de9fda5bf0e26229a9b1b1410de46..6bd760795cec6d1c4208770f1355e8ac7a34eb95 100644 --- a/crates/extension_host/src/extension_settings.rs +++ b/crates/extension_host/src/extension_settings.rs @@ -3,10 +3,10 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::sync::Arc; -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)] pub struct ExtensionSettings { /// The extensions that should be automatically installed by Zed. /// diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 350e1de3b36c9073d137993ce4fbc50aa43bb36e..20057417a2ddbce7acd7fd5a8e09e54aab779638 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -1,9 +1,9 @@ use anyhow::Result; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct FileFinderSettings { pub file_icons: bool, pub modal_max_width: Option, diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 91179fea392bc38cfc2a513bfc391dd3eec6137d..34e3805a39ea8a13a6a2f79552a6a917c4597692 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, SettingsUi}; use url::Url; use util::ResultExt as _; @@ -78,7 +78,7 @@ pub struct GitHostingProviderConfig { pub name: String, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct GitHostingProviderSettings { /// The list of custom Git hosting providers. #[serde(default)] diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index b6891c7d256794b5b457669a20b17e6e41e4fd23..576949220405e408df1b23d189e661405c4c39e4 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::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use workspace::dock::DockPosition; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -77,7 +77,7 @@ pub struct GitPanelSettingsContent { pub collapse_untracked_diff: Option, } -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Debug, Clone, PartialEq, SettingsUi)] pub struct GitPanelSettings { pub button: bool, pub dock: DockPosition, diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index e60a3651aae3f062b16fdfa7aa01a28e5c845e85..345af8a867c6ff6c1790450d2b28cd275c04ebbb 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, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{fmt::Write, num::NonZeroU32, time::Duration}; use text::{Point, Selection}; use ui::{ @@ -293,7 +293,7 @@ impl StatusItemView for CursorPosition { } } -#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, PartialEq, JsonSchema, Deserialize, Serialize, SettingsUi)] #[serde(rename_all = "snake_case")] pub(crate) enum LineIndicatorFormat { Short, diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index 9c7f97371d86eecc29dc16902ba9e392d53b8660..4e6c6277e452189657b4725b4027780a54cfed1d 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -16,6 +16,13 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { let mut deprecated = None; let mut doc_str: Option = None; + /* + * + * #[action()] + * Struct Foo { + * bar: bool // is bar considered an attribute + } + */ for attr in &input.attrs { if attr.path().is_ident("action") { attr.parse_nested_meta(|meta| { diff --git a/crates/image_viewer/src/image_viewer_settings.rs b/crates/image_viewer/src/image_viewer_settings.rs index 1dcf99c0afcb3f69f48e2e1a82351852a4bf1c22..4949b266b4e03c7089d4bc25e2a223a0ce64a081 100644 --- a/crates/image_viewer/src/image_viewer_settings.rs +++ b/crates/image_viewer/src/image_viewer_settings.rs @@ -1,10 +1,10 @@ use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// The settings for the image viewer. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi)] pub struct ImageViewerSettings { /// The unit to use for displaying image file sizes. /// diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index c09ab6f764893589945f2c3cc00d71df84b8f77a..ffa24571c88a0f0e06252565261b1a6d285d098c 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -5,7 +5,7 @@ use editor::{Editor, SelectionEffects}; use gpui::{App, AppContext as _, Context, Window, actions}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use std::{ fs::OpenOptions, path::{Path, PathBuf}, @@ -22,7 +22,7 @@ actions!( ); /// Settings specific to journaling -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct JournalSettings { /// The path of the directory where journal entries are stored. /// diff --git a/crates/keymap_editor/Cargo.toml b/crates/keymap_editor/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ae3af21239f22a8d01ec9e792a3ab0daed6080bb --- /dev/null +++ b/crates/keymap_editor/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "keymap_editor" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/keymap_editor.rs" + +[dependencies] +anyhow.workspace = true +collections.workspace = true +command_palette.workspace = true +component.workspace = true +db.workspace = true +editor.workspace = true +fs.workspace = true +fuzzy.workspace = true +gpui.workspace = true +itertools.workspace = true +language.workspace = true +log.workspace = true +menu.workspace = true +notifications.workspace = true +paths.workspace = true +project.workspace = true +search.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +telemetry.workspace = true +tempfile.workspace = true +theme.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +ui.workspace = true +ui_input.workspace = true +util.workspace = true +vim.workspace = true +workspace-hack.workspace = true +workspace.workspace = true +zed_actions.workspace = true + +[dev-dependencies] +db = {"workspace"= true, "features" = ["test-support"]} +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/keymap_editor/LICENSE-GPL b/crates/keymap_editor/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/keymap_editor/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/keybindings.rs b/crates/keymap_editor/src/keymap_editor.rs similarity index 99% rename from crates/settings_ui/src/keybindings.rs rename to crates/keymap_editor/src/keymap_editor.rs index 161e1e768ddd8a111e001198d8aad352169d1cef..12149061124d2b3144a32b7f54a65ce5af70d492 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -5,6 +5,8 @@ use std::{ time::Duration, }; +mod ui_components; + use anyhow::{Context as _, anyhow}; use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; @@ -34,8 +36,10 @@ use workspace::{ register_serializable_item, }; +pub use ui_components::*; + use crate::{ - keybindings::persistence::KEYBINDING_EDITORS, + persistence::KEYBINDING_EDITORS, ui_components::{ keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording}, table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState}, diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/keymap_editor/src/ui_components/keystroke_input.rs similarity index 100% rename from crates/settings_ui/src/ui_components/keystroke_input.rs rename to crates/keymap_editor/src/ui_components/keystroke_input.rs diff --git a/crates/settings_ui/src/ui_components/mod.rs b/crates/keymap_editor/src/ui_components/mod.rs similarity index 100% rename from crates/settings_ui/src/ui_components/mod.rs rename to crates/keymap_editor/src/ui_components/mod.rs diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/keymap_editor/src/ui_components/table.rs similarity index 100% rename from crates/settings_ui/src/ui_components/table.rs rename to crates/keymap_editor/src/ui_components/table.rs diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0f82d3997f981286c81dc18c29f8763b0402ddd2..a44df4993af5f29cbfce337d2c90dd8f840d97a6 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -17,7 +17,7 @@ use serde::{ }; use settings::{ - ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, + ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, SettingsUi, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; @@ -55,7 +55,7 @@ pub fn all_language_settings<'a>( } /// The settings for all languages. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, SettingsUi)] pub struct AllLanguageSettings { /// The edit prediction settings. pub edit_predictions: EditPredictionSettings, diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index b163585aa7b745447381aa62f710e8c5dbdf469c..1d03ab48f7de3ab9a20c1a099803e6b759b8ea81 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -5,7 +5,7 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use crate::provider::{ self, @@ -29,7 +29,7 @@ pub fn init_settings(cx: &mut App) { AllLanguageModelSettings::register(cx); } -#[derive(Default)] +#[derive(Default, SettingsUi)] pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 133d28b748d2978e07a540b3c8c7517b03dc4767..c33125654f043022bfaa7a31200d43d1d6326607 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -18,7 +18,7 @@ pub enum ShowIndentGuides { Never, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct OutlinePanelSettings { pub button: bool, pub default_width: Pixels, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 74ad08570a996a2dc9fc07bfb616f0edc0085b9f..b32e95741f522650e5d20f80a6ba18c423805234 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -952,7 +952,7 @@ pub enum PulledDiagnostics { /// Whether to disable all AI features in Zed. /// /// Default: false -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, settings::SettingsUi)] pub struct DisableAiSettings { pub disable_ai: bool, } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4447c2512943257b27a91fb1ac051bccde6e3f7f..30a71c4caeb676509239151a4766beb590fdb47e 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, SettingsLocation, SettingsSources, - SettingsStore, parse_json_with_comments, watch_config_file, + SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file, }; use std::{ collections::BTreeMap, @@ -36,7 +36,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct ProjectSettings { /// Configuration for language servers. /// diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7b78e75a9e43a3e7bf0404624123685..9c7bd4fd66e9e5b884867bf13f88856c126974b6 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -2,7 +2,7 @@ use editor::ShowScrollbar; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -28,7 +28,7 @@ pub enum EntrySpacing { Standard, } -#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, SettingsUi)] pub struct ProjectPanelSettings { pub button: bool, pub hide_gitignore: bool, diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index e3fb249d1632a35d888996da2665d00ea98b2c26..29f6e75bbdebf72b36295b20295f0705b636214e 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -20,7 +20,7 @@ use remote::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use theme::ThemeSettings; use ui::{ ActiveTheme, Color, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, @@ -29,7 +29,7 @@ use ui::{ use util::serde::default_true; use workspace::{AppState, ModalView, Workspace}; -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct SshSettings { pub ssh_connections: Option>, /// Whether to read ~/.ssh/config for ssh connection sources. diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs index 8b00e0f75722e54766b3d7447894e73dfeb441f8..c3bfd2079dfae21c9b990b15faec4cf7d4ffaa68 100644 --- a/crates/repl/src/jupyter_settings.rs +++ b/crates/repl/src/jupyter_settings.rs @@ -4,9 +4,9 @@ use editor::EditorSettings; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Debug, Default)] +#[derive(Debug, Default, SettingsUi)] pub struct JupyterSettings { pub kernel_selections: HashMap, } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 892d4dea8b2daac7395bcbe273635fbb535a0e53..8768b4073602461a5031b8d70d3a1e930ad2a41e 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -31,6 +31,7 @@ schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true +settings_ui_macros.workspace = true serde_json_lenient.workspace = true smallvec.workspace = true tree-sitter-json.workspace = true diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index 91dda03d00ca282e5ccacde2c07f5359be1ebb16..087f25185a99cb927892e3ada22d92c1c319a390 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,13 +1,17 @@ use std::fmt::{Display, Formatter}; -use crate::{Settings, SettingsSources, VsCodeSettings}; +use crate as settings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources, VsCodeSettings}; +use settings_ui_macros::SettingsUi; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// /// Default: VSCode -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[derive( + Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUi, +)] pub enum BaseKeymap { #[default] VSCode, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index a0717333159e508ea42a1b95bd9f2226e6392871..983cd31dd31d6b9c2cd017568fffe0812f9ae4e5 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -4,6 +4,7 @@ mod keymap_file; mod settings_file; mod settings_json; mod settings_store; +mod settings_ui; mod vscode_import; use gpui::{App, Global}; @@ -23,6 +24,9 @@ pub use settings_store::{ InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, SettingsStore, }; +pub use settings_ui::*; +// Re-export the derive macro +pub use settings_ui_macros::SettingsUi; pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; #[derive(Clone, Debug, PartialEq)] diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index f112ec811d2828350d41eeab63161c8e345d4d77..b916df6e5c205c7fc2c0c920d0ac8343cb986a5c 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -87,9 +87,9 @@ pub fn update_value_in_json_text<'a>( } /// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`. -fn replace_value_in_json_text( +pub fn replace_value_in_json_text>( text: &str, - key_path: &[&str], + key_path: &[T], tab_size: usize, new_value: Option<&Value>, replace_key: Option<&str>, @@ -141,7 +141,7 @@ fn replace_value_in_json_text( let found_key = text .get(key_range.clone()) .map(|key_text| { - depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth]) + depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth].as_ref()) }) .unwrap_or(false); @@ -226,13 +226,13 @@ fn replace_value_in_json_text( } } else { // We have key paths, construct the sub objects - let new_key = key_path[depth]; + let new_key = key_path[depth].as_ref(); // We don't have the key, construct the nested objects let mut new_value = serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap(); for key in key_path[(depth + 1)..].iter().rev() { - new_value = serde_json::json!({ key.to_string(): new_value }); + new_value = serde_json::json!({ key.as_ref().to_string(): new_value }); } if let Some(first_key_start) = first_key_start { @@ -465,7 +465,7 @@ pub fn append_top_level_array_value_in_json_text( } let (mut replace_range, mut replace_value) = - replace_value_in_json_text("", &[], tab_size, Some(new_value), None); + replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None); replace_range.start = close_bracket_start; replace_range.end = close_bracket_start; diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index fbd0f75aefc2173a3affbb7423d4ccc718679919..09ac6f9766e32e7a0d8765b09919cd0f8c09866c 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -7,7 +7,7 @@ use futures::{ channel::{mpsc, oneshot}, future::LocalBoxFuture, }; -use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; +use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; @@ -31,14 +31,15 @@ use util::{ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, - WorktreeId, parse_json_with_comments, update_value_in_json_text, + ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, + VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, + settings_ui::SettingsUi, update_value_in_json_text, }; /// 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: SettingsUi + 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized /// from the root object. @@ -284,6 +285,7 @@ trait AnySettingValue: 'static + Send + Sync { text: &mut String, edits: &mut Vec<(Range, String)>, ); + fn settings_ui_item(&self) -> SettingsUiEntry; } struct DeserializedSetting(Box); @@ -480,6 +482,11 @@ impl SettingsStore { self.raw_global_settings.as_ref() } + /// Access the raw JSON value of the default settings. + pub fn raw_default_settings(&self) -> &Value { + &self.raw_default_settings + } + #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> Self { let mut this = Self::new(cx); @@ -532,49 +539,10 @@ impl SettingsStore { } } - pub fn update_settings_file( + fn update_settings_file_inner( &self, fs: Arc, - update: impl 'static + Send + FnOnce(&mut T::FileContent, &App), - ) { - self.setting_file_updates_tx - .unbounded_send(Box::new(move |cx: AsyncApp| { - async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = cx.read_global(|store: &SettingsStore, cx| { - store.new_text_for_update::(old_text, |content| update(content, cx)) - })?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; - - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; - } - - anyhow::Ok(()) - } - .boxed_local() - })) - .ok(); - } - - pub fn import_vscode_settings( - &self, - fs: Arc, - vscode_settings: VsCodeSettings, + update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx @@ -582,9 +550,7 @@ impl SettingsStore { async move { let res = async move { let old_text = Self::load_settings(&fs).await?; - let new_text = cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - })?; + let new_text = update(old_text, cx)?; let settings_path = paths::settings_file().as_path(); if fs.is_file(settings_path).await { let resolved_path = @@ -607,7 +573,6 @@ impl SettingsStore { format!("Failed to write settings to file {:?}", settings_path) })?; } - anyhow::Ok(()) } .await; @@ -622,9 +587,67 @@ impl SettingsStore { } .boxed_local() })) - .ok(); + .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err)) + .log_with_level(log::Level::Warn); + return rx; + } + + pub fn update_settings_file_at_path( + &self, + fs: Arc, + path: &[&str], + new_value: serde_json::Value, + ) -> oneshot::Receiver> { + let key_path = path + .into_iter() + .cloned() + .map(SharedString::new) + .collect::>(); + let update = move |mut old_text: String, cx: AsyncApp| { + cx.read_global(|store: &SettingsStore, _cx| { + // todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though... + let (range, replacement) = replace_value_in_json_text( + &old_text, + key_path.as_slice(), + store.json_tab_size(), + Some(&new_value), + None, + ); + old_text.replace_range(range, &replacement); + old_text + }) + }; + self.update_settings_file_inner(fs, update) + } - rx + pub fn update_settings_file( + &self, + fs: Arc, + update: impl 'static + Send + FnOnce(&mut T::FileContent, &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)) + }) + }); + } + + pub fn import_vscode_settings( + &self, + fs: Arc, + vscode_settings: VsCodeSettings, + ) -> oneshot::Receiver> { + self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { + cx.read_global(|store: &SettingsStore, _cx| { + store.get_vscode_edits(old_text, &vscode_settings) + }) + }) + } + + pub fn settings_ui_items(&self) -> impl IntoIterator { + self.setting_values + .values() + .map(|item| item.settings_ui_item()) } } @@ -1520,6 +1543,10 @@ impl AnySettingValue for SettingValue { edits, ); } + + fn settings_ui_item(&self) -> SettingsUiEntry { + ::settings_ui_entry() + } } #[cfg(test)] @@ -1527,7 +1554,10 @@ mod tests { use crate::VsCodeSettingsSource; use super::*; + // This is so the SettingsUi macro can still work properly + use crate as settings; use serde_derive::Deserialize; + use settings_ui_macros::SettingsUi; use unindent::Unindent; #[gpui::test] @@ -2070,14 +2100,14 @@ mod tests { pretty_assertions::assert_eq!(new, expected); } - #[derive(Debug, PartialEq, Deserialize)] + #[derive(Debug, PartialEq, Deserialize, SettingsUi)] struct UserSettings { name: String, age: u32, staff: bool, } - #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] + #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)] struct UserSettingsContent { name: Option, age: Option, @@ -2097,7 +2127,7 @@ mod tests { } } - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq, SettingsUi)] struct TurboSetting(bool); impl Settings for TurboSetting { @@ -2111,7 +2141,7 @@ mod tests { fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut Self::FileContent) {} } - #[derive(Clone, Debug, PartialEq, Deserialize)] + #[derive(Clone, Debug, PartialEq, Deserialize, SettingsUi)] struct MultiKeySettings { #[serde(default)] key1: String, @@ -2144,7 +2174,7 @@ mod tests { } } - #[derive(Debug, Deserialize)] + #[derive(Debug, Deserialize, SettingsUi)] struct JournalSettings { pub path: String, pub hour_format: HourFormat, @@ -2245,7 +2275,7 @@ mod tests { ); } - #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi)] struct LanguageSettings { #[serde(default)] languages: HashMap, diff --git a/crates/settings/src/settings_ui.rs b/crates/settings/src/settings_ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b30ebc9d5968943d3814f7569d1367d389e386a --- /dev/null +++ b/crates/settings/src/settings_ui.rs @@ -0,0 +1,118 @@ +use anyhow::Context as _; +use fs::Fs; +use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window}; +use smallvec::SmallVec; + +use crate::SettingsStore; + +pub trait SettingsUi { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::None + } + fn settings_ui_entry() -> SettingsUiEntry; +} + +pub struct SettingsUiEntry { + // todo(settings_ui): move this back here once there isn't a None variant + // pub path: &'static str, + // pub title: &'static str, + pub item: SettingsUiEntryVariant, +} + +pub enum SettingsUiEntryVariant { + Group { + path: &'static str, + title: &'static str, + items: Vec, + }, + Item { + path: &'static str, + item: SettingsUiItemSingle, + }, + // todo(settings_ui): remove + None, +} + +pub enum SettingsUiItemSingle { + SwitchField, + NumericStepper, + ToggleGroup(&'static [&'static str]), + /// This should be used when toggle group size > 6 + DropDown(&'static [&'static str]), + Custom(Box, &mut Window, &mut App) -> AnyElement>), +} + +pub struct SettingsValue { + pub title: &'static str, + pub path: SmallVec<[&'static str; 1]>, + pub value: Option, + pub default_value: T, +} + +impl SettingsValue { + pub fn read(&self) -> &T { + match &self.value { + Some(value) => value, + None => &self.default_value, + } + } +} + +impl SettingsValue { + pub fn write_value(path: &SmallVec<[&'static str; 1]>, value: serde_json::Value, cx: &mut App) { + let settings_store = SettingsStore::global(cx); + let fs = ::global(cx); + + let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value); + let path = path.clone(); + cx.background_spawn(async move { + rx.await? + .with_context(|| format!("Failed to update setting at path `{:?}`", path.join("."))) + }) + .detach_and_log_err(cx); + } +} + +impl SettingsValue { + pub fn write( + path: &SmallVec<[&'static str; 1]>, + value: T, + cx: &mut App, + ) -> Result<(), serde_json::Error> { + SettingsValue::write_value(path, serde_json::to_value(value)?, cx); + Ok(()) + } +} + +pub enum SettingsUiItem { + Group { + title: &'static str, + items: Vec, + }, + Single(SettingsUiItemSingle), + None, +} + +impl SettingsUi for bool { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::SwitchField) + } + + fn settings_ui_entry() -> SettingsUiEntry { + SettingsUiEntry { + item: SettingsUiEntryVariant::None, + } + } +} + +impl SettingsUi for u64 { + fn settings_ui_item() -> SettingsUiItem { + SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper) + } + + fn settings_ui_entry() -> SettingsUiEntry { + SettingsUiEntry { + item: SettingsUiEntryVariant::None, + } + } +} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 4a48c18f7c2cbd19538257d51e8342c15c69f587..53fbf797c3d9e56e49b1d96e7dabcac19ddde8e2 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -2,7 +2,7 @@ use anyhow::{Context as _, Result, anyhow}; use fs::Fs; use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; use serde_json::{Map, Value}; -use std::{path::Path, rc::Rc, sync::Arc}; +use std::{path::Path, sync::Arc}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum VsCodeSettingsSource { @@ -21,7 +21,7 @@ impl std::fmt::Display for VsCodeSettingsSource { pub struct VsCodeSettings { pub source: VsCodeSettingsSource, - pub path: Rc, + pub path: Arc, content: Map, } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 8a151359ec4bb246e23c4a09fdbe63c23c69a98a..7c2b81aee0ecf48afb7131adf5ddb19a165ca351 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -11,45 +11,26 @@ workspace = true [lib] path = "src/settings_ui.rs" +[features] +default = [] + [dependencies] anyhow.workspace = true -collections.workspace = true -command_palette.workspace = true command_palette_hooks.workspace = true -component.workspace = true -db.workspace = true editor.workspace = true feature_flags.workspace = true -fs.workspace = true -fuzzy.workspace = true gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -notifications.workspace = true -paths.workspace = true -project.workspace = true -search.workspace = true -serde.workspace = true serde_json.workspace = true +serde.workspace = true settings.workspace = true -telemetry.workspace = true -tempfile.workspace = true +smallvec.workspace = true theme.workspace = true -tree-sitter-json.workspace = true -tree-sitter-rust.workspace = true ui.workspace = true -ui_input.workspace = true -util.workspace = true -vim.workspace = true -workspace-hack.workspace = true workspace.workspace = true -zed_actions.workspace = true +workspace-hack.workspace = true -[dev-dependencies] -db = {"workspace"= true, "features" = ["test-support"]} -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } +# Uncomment other workspace dependencies as needed +# assistant.workspace = true +# client.workspace = true +# project.workspace = true +# settings.workspace = true diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 3022cc714268f641b7b6f30021b5e86d6072b7b6..ae03170a1a9a2cb3e53c67402c95c8e79e739ab9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,20 +1,24 @@ mod appearance_settings_controls; use std::any::TypeId; +use std::ops::{Not, Range}; +use anyhow::Context as _; use command_palette_hooks::CommandPaletteFilter; use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions}; -use ui::prelude::*; -use workspace::item::{Item, ItemEvent}; -use workspace::{Workspace, with_active_or_new_workspace}; +use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions}; +use settings::{SettingsStore, SettingsUiEntryVariant, SettingsUiItemSingle, SettingsValue}; +use smallvec::SmallVec; +use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*}; +use workspace::{ + Workspace, + item::{Item, ItemEvent}, + with_active_or_new_workspace, +}; use crate::appearance_settings_controls::AppearanceSettingsControls; -pub mod keybindings; -pub mod ui_components; - pub struct SettingsUiFeatureFlag; impl FeatureFlag for SettingsUiFeatureFlag { @@ -75,18 +79,18 @@ pub fn init(cx: &mut App) { .detach(); }) .detach(); - - keybindings::init(cx); } 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), }) } } @@ -119,26 +123,472 @@ impl Item for SettingsPage { } } +// 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: &'static str, + path: &'static str, + _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, +} + +struct SettingsUiTree { + root_entry_indices: Vec, + entries: Vec, + active_entry_index: usize, +} + +fn build_tree_item( + tree: &mut Vec, + group: SettingsUiEntryVariant, + depth: usize, + prev_index: Option, +) { + let index = tree.len(); + tree.push(UIEntry { + title: "", + path: "", + _depth: depth, + descendant_range: index + 1..index + 1, + total_descendant_range: index + 1..index + 1, + render: None, + next_sibling: None, + }); + if let Some(prev_index) = prev_index { + tree[prev_index].next_sibling = Some(index); + } + match group { + SettingsUiEntryVariant::Group { + path, + title, + items: group_items, + } => { + tree[index].path = path; + tree[index].title = title; + 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.item, depth + 1, prev_index); + tree[index].total_descendant_range.end = tree.len(); + } + } + SettingsUiEntryVariant::Item { path, item } => { + tree[index].path = path; + // todo(settings_ui) create title from path in macro, and use here + tree[index].title = path; + tree[index].render = Some(item); + } + SettingsUiEntryVariant::None => { + return; + } + } +} + +impl SettingsUiTree { + 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, SettingsUiEntryVariant::None) { + continue; + } + + assert!( + matches!(item.item, SettingsUiEntryVariant::Group { .. }), + "top level items must be groups: {:?}", + match item.item { + SettingsUiEntryVariant::Item { path, .. } => path, + _ => unreachable!(), + } + ); + let prev_root_entry_index = root_entry_indices.last().copied(); + root_entry_indices.push(tree.len()); + build_tree_item(&mut tree, item.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, + } + } +} + +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(SharedString::new_static(tree.entries[index].title)) + .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, +) -> impl IntoElement { + let Some(entry) = tree.entries.get(tree.active_entry_index) else { + return div() + .size_full() + .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); + }; + let mut content = v_flex().size_full().gap_4(); + + let mut child_index = entry + .descendant_range + .is_empty() + .not() + .then_some(entry.descendant_range.start); + let mut path = smallvec::smallvec![entry.path]; + + while let Some(index) = child_index { + let child = &tree.entries[index]; + child_index = child.next_sibling; + if child.render.is_none() { + // todo(settings_ui): subgroups? + continue; + } + path.push(child.path); + let settings_value = settings_value_from_settings_and_path( + path.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(), + ); + content = content.child( + div() + .child( + Label::new(SharedString::new_static(tree.entries[index].title)) + .size(LabelSize::Large) + .when(tree.active_entry_index == index, |this| { + this.color(Color::Selected) + }), + ) + .child(render_item_single( + settings_value, + child.render.as_ref().unwrap(), + window, + cx, + )), + ); + + path.pop(); + } + + return content; +} + impl Render for SettingsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .grid() + .grid_cols(16) .p_4() + .bg(cx.theme().colors().editor_background) .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()), - ), + div() + .col_span(2) + .h_full() + .child(render_nav(&self.settings_tree, window, cx)), ) + .child(div().col_span(4).h_full().child(render_content( + &self.settings_tree, + window, + cx, + ))) } } + +// 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: &[&'static str]) -> ElementId { + if path.len() == 0 { + panic!("Path length must not be zero"); + } else if path.len() == 1 { + ElementId::Name(SharedString::new_static(path[0])) + } else { + ElementId::from(( + ElementId::from(SharedString::new_static(path[path.len() - 2])), + SharedString::new_static(path[path.len() - 1]), + )) + } +} + +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 => { + render_any_item(settings_value, render_numeric_stepper, window, cx) + } + SettingsUiItemSingle::ToggleGroup(variants) => { + render_toggle_button_group(settings_value, variants, window, cx) + } + SettingsUiItemSingle::DropDown(_) => { + unimplemented!("This") + } + } +} + +fn read_settings_value_from_path<'a>( + settings_contents: &'a serde_json::Value, + path: &[&'static str], +) -> Option<&'a serde_json::Value> { + let Some((key, remaining)) = path.split_first() else { + return Some(settings_contents); + }; + let Some(value) = settings_contents.get(key) 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).expect("value is not a T")); + // 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) + .expect("default value is not an Option"); + let deserialized_setting_value = SettingsValue { + title: settings_value.title, + path: settings_value.path, + 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_numeric_stepper( + value: SettingsValue, + _window: &mut Window, + _cx: &mut App, +) -> AnyElement { + let id = element_id_from_path(&value.path); + let path = value.path.clone(); + let num = value.value.unwrap_or_else(|| value.default_value); + + NumericStepper::new( + id, + num.to_string(), + { + let path = value.path.clone(); + move |_, _, cx| { + let Some(number) = serde_json::Number::from_u128(num.saturating_sub(1) as u128) + else { + return; + }; + let new_value = serde_json::Value::Number(number); + SettingsValue::write_value(&path, new_value, cx); + } + }, + move |_, _, cx| { + let Some(number) = serde_json::Number::from_u128(num.saturating_add(1) as u128) 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, + SharedString::new_static(value.title), + None, + 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_toggle_button_group( + value: SettingsValue, + variants: &'static [&'static str], + _: &mut Window, + _: &mut App, +) -> AnyElement { + let value = downcast_any_item::(value); + + fn make_toggle_group( + group_name: &'static str, + value: SettingsValue, + variants: &'static [&'static str], + ) -> AnyElement { + let mut variants_array: [&'static str; LEN] = ["default"; LEN]; + variants_array.copy_from_slice(variants); + let active_value = value.read(); + + let selected_idx = variants_array + .iter() + .enumerate() + .find_map(|(idx, variant)| { + if variant == &active_value { + Some(idx) + } else { + None + } + }); + + ToggleButtonGroup::single_row( + group_name, + variants_array.map(|variant| { + let path = value.path.clone(); + ToggleButtonSimple::new(variant, move |_, _, cx| { + SettingsValue::write_value( + &path, + serde_json::Value::String(variant.to_string()), + cx, + ); + }) + }), + ) + .when_some(selected_idx, |this, ix| this.selected_index(ix)) + .style(ui::ToggleButtonGroupStyle::Filled) + .into_any_element() + } + + macro_rules! templ_toggl_with_const_param { + ($len:expr) => { + if variants.len() == $len { + return make_toggle_group::<$len>(value.title, value, variants); + } + }; + } + 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<[&'static str; 1]>, + user_settings: &serde_json::Value, + default_settings: &serde_json::Value, +) -> SettingsValue { + let default_value = read_settings_value_from_path(default_settings, &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, + path: path.clone(), + // todo(settings_ui) title for items + title: path.last().expect("path non empty"), + }; + return settings_value; +} diff --git a/crates/settings_ui_macros/Cargo.toml b/crates/settings_ui_macros/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e242e7546d1527632dba6eece9b17ccea27295f4 --- /dev/null +++ b/crates/settings_ui_macros/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "settings_ui_macros" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lib] +path = "src/settings_ui_macros.rs" +proc-macro = true + +[lints] +workspace = true + +[features] +default = [] + +[dependencies] +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true +workspace-hack.workspace = true diff --git a/crates/settings_ui_macros/LICENSE-GPL b/crates/settings_ui_macros/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/settings_ui_macros/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui_macros/src/settings_ui_macros.rs b/crates/settings_ui_macros/src/settings_ui_macros.rs new file mode 100644 index 0000000000000000000000000000000000000000..6e37745a7c24155de631e47ffc8c265209ee24e8 --- /dev/null +++ b/crates/settings_ui_macros/src/settings_ui_macros.rs @@ -0,0 +1,201 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{Data, DeriveInput, LitStr, Token, parse_macro_input}; + +/// Derive macro for the `SettingsUi` marker trait. +/// +/// This macro automatically implements the `SettingsUi` trait for the annotated type. +/// The `SettingsUi` trait is a marker trait used to indicate that a type can be +/// displayed in the settings UI. +/// +/// # Example +/// +/// ``` +/// use settings::SettingsUi; +/// use settings_ui_macros::SettingsUi; +/// +/// #[derive(SettingsUi)] +/// #[settings_ui(group = "Standard")] +/// struct MySettings { +/// enabled: bool, +/// count: usize, +/// } +/// ``` +#[proc_macro_derive(SettingsUi, attributes(settings_ui))] +pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Handle generic parameters if present + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let mut group_name = Option::::None; + let mut path_name = Option::::None; + + for attr in &input.attrs { + if attr.path().is_ident("settings_ui") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("group") { + if group_name.is_some() { + return Err(meta.error("Only one 'group' path can be specified")); + } + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + group_name = Some(lit.value()); + } else if meta.path.is_ident("path") { + // todo(settings_ui) try get KEY from Settings if possible, and once we do, + // if can get key from settings, throw error if path also passed + if path_name.is_some() { + return Err(meta.error("Only one 'path' can be specified")); + } + meta.input.parse::()?; + let lit: LitStr = meta.input.parse()?; + path_name = Some(lit.value()); + } + Ok(()) + }) + .unwrap_or_else(|e| panic!("in #[settings_ui] attribute: {}", e)); + } + } + + if path_name.is_none() && group_name.is_some() { + // todo(settings_ui) derive path from settings + panic!("path is required when group is specified"); + } + + let ui_render_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input); + + let settings_ui_item_fn_body = path_name + .as_ref() + .map(|path_name| map_ui_item_to_render(path_name, quote! { Self })) + .unwrap_or(quote! { + settings::SettingsUiEntry { + item: settings::SettingsUiEntryVariant::None + } + }); + + let expanded = quote! { + impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause { + fn settings_ui_item() -> settings::SettingsUiItem { + #ui_render_fn_body + } + + fn settings_ui_entry() -> settings::SettingsUiEntry { + #settings_ui_item_fn_body + } + } + }; + + proc_macro::TokenStream::from(expanded) +} + +fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream { + quote! { + settings::SettingsUiEntry { + item: match #ty::settings_ui_item() { + settings::SettingsUiItem::Group{title, items} => settings::SettingsUiEntryVariant::Group { + title, + path: #path, + items, + }, + settings::SettingsUiItem::Single(item) => settings::SettingsUiEntryVariant::Item { + path: #path, + item, + }, + settings::SettingsUiItem::None => settings::SettingsUiEntryVariant::None, + } + } + } +} + +fn generate_ui_item_body( + group_name: Option<&String>, + path_name: Option<&String>, + input: &syn::DeriveInput, +) -> TokenStream { + match (group_name, path_name, &input.data) { + (_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"), + (None, None, Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (Some(_), None, Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (None, Some(_), Data::Struct(_)) => quote! { + settings::SettingsUiItem::None + }, + (Some(group_name), _, Data::Struct(data_struct)) => { + let fields = data_struct + .fields + .iter() + .filter(|field| { + !field.attrs.iter().any(|attr| { + let mut has_skip = false; + if attr.path().is_ident("settings_ui") { + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("skip") { + has_skip = true; + } + Ok(()) + }); + } + + has_skip + }) + }) + .map(|field| { + ( + field.ident.clone().expect("tuple fields").to_string(), + field.ty.to_token_stream(), + ) + }) + .map(|(name, ty)| map_ui_item_to_render(&name, ty)); + + quote! { + settings::SettingsUiItem::Group{ title: #group_name, items: vec![#(#fields),*] } + } + } + (None, _, Data::Enum(data_enum)) => { + let mut lowercase = false; + for attr in &input.attrs { + if attr.path().is_ident("serde") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + meta.input.parse::()?; + let lit = meta.input.parse::()?.value(); + // todo(settings_ui) snake case + lowercase = lit == "lowercase" || lit == "snake_case"; + } + Ok(()) + }) + .ok(); + } + } + let length = data_enum.variants.len(); + + let variants = data_enum.variants.iter().map(|variant| { + let string = variant.ident.clone().to_string(); + + if lowercase { + string.to_lowercase() + } else { + string + } + }); + + if length > 6 { + quote! { + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown(&[#(#variants),*])) + } + } else { + quote! { + settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup(&[#(#variants),*])) + } + } + } + // todo(settings_ui) discriminated unions + (_, _, Data::Enum(_)) => quote! { + settings::SettingsUiItem::None + }, + } +} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 635e3e2ca5895562c7981d89169bf6f0632a223f..01f2d85f09e416b6c8ac40d7fa283d1f1e296cd5 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -6,7 +6,7 @@ use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::SettingsSources; +use settings::{SettingsSources, SettingsUi}; use std::path::PathBuf; use task::Shell; use theme::FontFamilyName; @@ -24,7 +24,7 @@ pub struct Toolbar { pub breadcrumbs: bool, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, SettingsUi)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index df147cfe92377962b135fed309ef0a7df68adcd8..61b41eba0642f10312a4c78df447ac7344f7e2dc 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -13,7 +13,7 @@ use gpui::{ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ParameterizedJsonSchema, Settings, SettingsSources}; +use settings::{ParameterizedJsonSchema, Settings, SettingsSources, SettingsUi}; use std::sync::Arc; use util::ResultExt as _; use util::schemars::replace_subschema; @@ -87,7 +87,7 @@ impl From for String { } /// Customizable settings for the UI and theme system. -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, SettingsUi)] pub struct ThemeSettings { /// The UI font size. Determines the size of text in the UI, /// as well as the size of a [gpui::Rems] unit. diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index cf178e2850397c5a2398033a02addb73ab615ec9..f60ac7c301359d0bb0d3d8ee1d4115c5d815cf69 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -42,7 +42,7 @@ rpc.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -settings_ui.workspace = true +keymap_editor.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } telemetry.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ac5e9201b3be083fef43e58c2e717cb59a0ba185..075b9fcd86276244d154be1aebe904fbfb4a7b6c 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -29,10 +29,10 @@ use gpui::{ IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, }; +use keymap_editor; use onboarding_banner::OnboardingBanner; use project::Project; use settings::Settings as _; -use settings_ui::keybindings; use std::sync::Arc; use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; @@ -684,7 +684,7 @@ impl TitleBar { "Settings Profiles", zed_actions::settings_profile_selector::Toggle.boxed_clone(), ) - .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) + .action("Key Bindings", Box::new(keymap_editor::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), @@ -732,7 +732,7 @@ impl TitleBar { "Settings Profiles", zed_actions::settings_profile_selector::Toggle.boxed_clone(), ) - .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) + .action("Key Bindings", Box::new(keymap_editor::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index a98e984d80e1dbf0c016b8d8e4c6dc609106c081..29d74c8590a63cd8aa75bdaa3655111d76fcf757 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,9 +1,10 @@ use db::anyhow; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Copy, Clone, Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)] +#[settings_ui(group = "Title Bar", path = "title_bar")] pub struct TitleBarSettings { pub show_branch_icon: bool, pub show_onboarding_banner: bool, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9da01e6f444d2284814282f9bf6eecfb0814953d..a5cd909d5b53079d1da49591a5eca21416ba415a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -39,7 +39,7 @@ use object::Object; use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; -use settings::{Settings, SettingsSources, SettingsStore, update_settings_file}; +use settings::{Settings, SettingsSources, SettingsStore, SettingsUi, update_settings_file}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; @@ -1774,7 +1774,7 @@ struct CursorShapeSettings { pub insert: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index 6f60d3f21fc707abd981c34d7617f4e9bb563477..7fb39ef4f6f10370f1a0fb2cf83dcb3a88b80d81 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -6,7 +6,7 @@ use anyhow::Result; use gpui::App; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; /// Initializes the `vim_mode_setting` crate. pub fn init(cx: &mut App) { @@ -17,6 +17,7 @@ pub fn init(cx: &mut App) { /// Whether or not to enable Vim mode. /// /// Default: false +#[derive(SettingsUi)] pub struct VimModeSetting(pub bool); impl Settings for VimModeSetting { @@ -43,6 +44,7 @@ impl Settings for VimModeSetting { /// Whether or not to enable Helix mode. /// /// Default: false +#[derive(SettingsUi)] pub struct HelixModeSetting(pub bool); impl Settings for HelixModeSetting { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b904b40d0eaf2466689156f03d3723f3..a513f8c9317645469e5d5ca54c3b5351383c1ca3 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -17,7 +17,7 @@ use gpui::{ use project::{Project, ProjectEntryId, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsSources}; +use settings::{Settings, SettingsLocation, SettingsSources, SettingsUi}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -49,7 +49,7 @@ impl Default for SaveOptions { } } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct ItemSettings { pub git_status: bool, pub close_position: ClosePosition, @@ -59,7 +59,7 @@ pub struct ItemSettings { pub show_close_button: ShowCloseButton, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct PreviewTabsSettings { pub enabled: bool, pub enable_preview_from_file_finder: bool, diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 0d7fb9bb9c1ae6f8ff4a6644132c4a347da4117d..419e33e54435779012207a024ea49e44a8acb1c2 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -6,9 +6,9 @@ use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct WorkspaceSettings { pub active_pane_modifiers: ActivePanelModifiers, pub bottom_dock_layout: BottomDockLayout, @@ -216,7 +216,7 @@ pub struct WorkspaceSettingsContent { pub zoomed_padding: Option, } -#[derive(Deserialize)] +#[derive(Deserialize, SettingsUi)] pub struct TabBarSettings { pub show: bool, pub show_nav_history_buttons: bool, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index b18d3509beb408c37beaf246a747248d2f17438a..df3a4d35570ad21b80f968539afbe681c58e2a06 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -4,10 +4,10 @@ use anyhow::Context as _; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; +use settings::{Settings, SettingsSources, SettingsUi}; use util::paths::PathMatcher; -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, SettingsUi)] pub struct WorktreeSettings { pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0ddfe3dde1b57de8f6fb5ae83d1bb3ccef8b12ff..bb46a5a4f65ac76a7cff2a5bc43525db30ed0930 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -131,6 +131,7 @@ serde_json.workspace = true session.workspace = true settings.workspace = true settings_ui.workspace = true +keymap_editor.workspace = true shellexpand.workspace = true smol.workspace = true snippet_provider.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5e7934c3094755b39535ef054f077dbc9fb180af..e4438792045617498e5c8cd3b52117b1d0b752ef 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -632,6 +632,7 @@ pub fn main() { svg_preview::init(cx); onboarding::init(cx); settings_ui::init(cx); + keymap_editor::init(cx); extensions_ui::init(cx); zeta::init(cx); inspector_ui::init(app_state.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5a180e4b42705332bd51dffe43943d131a42907f..5797070a39c8a60dc760ac3b82341842bc11d63e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1491,7 +1491,7 @@ fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec) { workspace::NewWindow, )]); // todo: nicer api here? - settings_ui::keybindings::KeymapEventChannel::trigger_keymap_changed(cx); + keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx); } pub fn load_default_keymap(cx: &mut App) { diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 6c7ab0b37403ae941660da853a83e6c147e5869f..342fd26cb77aa08dcbc346609b3185f3263f0f1d 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -1,6 +1,5 @@ use collab_ui::collab_panel; use gpui::{Menu, MenuItem, OsAction}; -use settings_ui::keybindings; use terminal_view::terminal_panel; pub fn app_menus() -> Vec { @@ -17,7 +16,7 @@ pub fn app_menus() -> Vec { name: "Settings".into(), items: vec![ MenuItem::action("Open Settings", super::OpenSettings), - MenuItem::action("Open Key Bindings", keybindings::OpenKeymapEditor), + MenuItem::action("Open Key Bindings", keymap_editor::OpenKeymapEditor), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action( "Open Default Key Bindings", diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index b58cbcc1433d01ce865580111d1f92c98987bbea..0cdc784489b47d89388edc9ed20aed6f3c2f9959 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -3,7 +3,7 @@ use anyhow::Result; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; +use settings::{Settings, SettingsStore, SettingsUi}; pub fn init(cx: &mut App) { ZlogSettings::register(cx); @@ -15,7 +15,7 @@ pub fn init(cx: &mut App) { .detach(); } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] pub struct ZlogSettings { #[serde(default, flatten)] pub scopes: std::collections::HashMap,