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,