From adca3a059a11f2f6bdeff499c01ff116d05d198a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 9 Jun 2025 23:14:25 -0700 Subject: [PATCH] Sketch in a table for the keybindings UI --- Cargo.lock | 4 + crates/gpui/src/app.rs | 5 + crates/settings_ui/Cargo.toml | 3 + crates/settings_ui/src/keybindings.rs | 196 ++++++++++++++++++++++++++ crates/settings_ui/src/settings_ui.rs | 4 + crates/ui/Cargo.toml | 1 + crates/ui/src/components/table.rs | 2 +- 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 crates/settings_ui/src/keybindings.rs diff --git a/Cargo.lock b/Cargo.lock index 70a05cf4aa2a47de3973dbf64d4b8c0430d06a2c..5c0184192121b354d51ab5b702485fa1521448a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14567,11 +14567,14 @@ name = "settings_ui" version = "0.1.0" dependencies = [ "command_palette_hooks", + "db", "editor", "feature_flags", "fs", "gpui", "log", + "paths", + "project", "schemars", "serde", "settings", @@ -17048,6 +17051,7 @@ dependencies = [ "gpui_macros", "icons", "itertools 0.14.0", + "log", "menu", "serde", "settings", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 109d5e7454c4a2b0bcb276243f7f5a6cc072efce..efa16a4960255649616451d399f13293af539666 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1334,6 +1334,11 @@ impl App { self.pending_effects.push_back(Effect::RefreshWindows); } + /// Get all key bindings in the app. + pub fn key_bindings(&self) -> Rc> { + self.keymap.clone() + } + /// Register a global listener for actions invoked via the keyboard. pub fn on_action(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { self.global_action_listeners diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 84d77e3fdcbfcad98e104a068af9bcafade3231f..904bea5b5ce2afc70520744ea0e1e4367b8bc5b4 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -13,11 +13,14 @@ path = "src/settings_ui.rs" [dependencies] command_palette_hooks.workspace = true +db.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true gpui.workspace = true log.workspace = true +paths.workspace = true +project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b87949ea15d64f986e922885166a0bc18f31867 --- /dev/null +++ b/crates/settings_ui/src/keybindings.rs @@ -0,0 +1,196 @@ +use std::fmt::Write as _; + +use db::anyhow::anyhow; +use gpui::{AppContext as _, EventEmitter, FocusHandle, Focusable, actions}; +use ui::{ + ActiveTheme as _, App, ParentElement as _, Render, Styled as _, Table, Window, div, string_cell, +}; +use workspace::{Item, SerializableItem, Workspace, register_serializable_item}; + +use crate::keybindings::persistence::KEYBINDING_EDITORS; + +actions!(zed, [OpenKeymapEditor]); + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _window, _cx| { + workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| { + let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx)); + workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx); + }); + }) + .detach(); + + register_serializable_item::(cx); +} + +struct KeymapEditor { + focus_handle: FocusHandle, +} + +impl EventEmitter<()> for KeymapEditor {} + +impl Focusable for KeymapEditor { + fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl KeymapEditor { + fn new(cx: &mut gpui::Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} + +impl SerializableItem for KeymapEditor { + fn serialized_item_kind() -> &'static str { + "KeymapEditor" + } + + fn cleanup( + workspace_id: workspace::WorkspaceId, + alive_items: Vec, + _window: &mut Window, + cx: &mut App, + ) -> gpui::Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "keybinding_editors", + &KEYBINDING_EDITORS, + cx, + ) + } + + fn deserialize( + _project: gpui::Entity, + _workspace: gpui::WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + _window: &mut Window, + cx: &mut App, + ) -> gpui::Task>> { + cx.spawn(async move |cx| { + if KEYBINDING_EDITORS + .get_keybinding_editor(item_id, workspace_id)? + .is_some() + { + cx.new(|cx| KeymapEditor::new(cx)) + } else { + Err(anyhow!("No keybinding editor to deserialize")) + } + }) + } + + fn serialize( + &mut self, + workspace: &mut Workspace, + item_id: workspace::ItemId, + _closing: bool, + _window: &mut Window, + cx: &mut ui::Context, + ) -> Option>> { + let Some(workspace_id) = workspace.database_id() else { + return None; + }; + Some(cx.background_spawn(async move { + KEYBINDING_EDITORS + .save_keybinding_editor(item_id, workspace_id) + .await + })) + } + + fn should_serialize(&self, _event: &Self::Event) -> bool { + false + } +} + +impl Item for KeymapEditor { + type Event = (); + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString { + "Keymap Editor".into() + } +} + +impl Render for KeymapEditor { + fn render(&mut self, _window: &mut Window, cx: &mut ui::Context) -> impl ui::IntoElement { + let key_bindings_ptr = cx.key_bindings(); + let lock = key_bindings_ptr.borrow(); + let key_bindings = lock.bindings(); + + let mut table = Table::new(vec!["Command", "Keystrokes", "Context"]); + for key_binding in key_bindings { + let mut keystroke_text = String::new(); + for keystroke in key_binding.keystrokes() { + write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok(); + } + let keystroke_text = keystroke_text.trim().to_string(); + + let context = key_binding + .predicate() + .map(|predicate| predicate.to_string()) + .unwrap_or_else(|| "".to_string()); + + // dbg!(key_binding.action().name(), &keystroke_text, &context); + + table = table.row(vec![ + string_cell(key_binding.action().name()), + string_cell(keystroke_text), + string_cell(context), + // TODO: Add a source field + // string_cell(keybinding.source().to_string()), + ]); + } + + let theme = cx.theme(); + + div() + .size_full() + .bg(theme.colors().background) + .child(table.striped()) + } +} + +mod persistence { + use db::{define_connection, query, sqlez_macros::sql}; + use workspace::WorkspaceDb; + + define_connection! { + pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = + &[sql!( + CREATE TABLE keybinding_editors ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + )]; + } + + impl KeybindingEditorDb { + query! { + pub async fn save_keybinding_editor( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId + ) -> Result<()> { + INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id) + VALUES (?, ?) + } + } + + query! { + pub fn get_keybinding_editor( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId + ) -> Result> { + SELECT item_id + FROM keybinding_editors + WHERE item_id = ? AND workspace_id = ? + } + } + } +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index dd6626a7160fac48ccc3be8bb1387a166aef4692..bb2d7ba63da1409a24b7b84104523a32bbde3cf9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -20,6 +20,8 @@ use workspace::{Workspace, with_active_or_new_workspace}; use crate::appearance_settings_controls::AppearanceSettingsControls; +mod keybindings; + pub struct SettingsUiFeatureFlag; impl FeatureFlag for SettingsUiFeatureFlag { @@ -121,6 +123,8 @@ pub fn init(cx: &mut App) { .detach(); }) .detach(); + + keybindings::init(cx); } async fn handle_import_vscode_settings( diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 625bdc62f5e899912929539e89c5357ea4e7e8f6..08d077a04aba3f87cfd719bb7332570a42ba2137 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -20,6 +20,7 @@ gpui.workspace = true gpui_macros.workspace = true icons.workspace = true itertools.workspace = true +log.workspace = true menu.workspace = true serde.workspace = true settings.workspace = true diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs index 3f1b73e441c4722330f8de57e9b317e734ff2ddf..931660ea872d513e980cf27994dd7935abe1935f 100644 --- a/crates/ui/src/components/table.rs +++ b/crates/ui/src/components/table.rs @@ -33,7 +33,7 @@ impl Table { if items.len() == self.column_count { self.rows.push(items.into_iter().map(Into::into).collect()); } else { - // TODO: Log error: Row length mismatch + log::error!("Row length mismatch"); } self }