gpui: Add keybind metadata API (#33316)

Ben Kunkle created

Closes #ISSUE

Adds a very simple API to track metadata about keybindings in GPUI,
namely the source of the binding. The motivation for this is displaying
the source of keybindings in the [keymap
UI](https://github.com/zed-industries/zed/pull/32436).

The API is designed to be as simple and flexible as possible, storing
only a `Option<u32>` on the bindings themselves to keep the struct
small. It is intended to be used as an index or key into a table/map
created and managed by the consumer of the API to map from indices to
arbitrary meta-data. I.e. the consumer is responsible for both
generating these indices and giving them meaning.

The current usage in Zed is stateless, just a mapping between constants
and User, Default, Base, and Vim keymap sources, however, this can be
extended in the future to also track _which_ base keymap is being used.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/gpui/src/keymap/binding.rs       | 25 +++++++++
crates/settings/src/keymap_file.rs      | 73 +++++++++++++++++++++++++-
crates/settings/src/settings.rs         |  3 
crates/storybook/src/storybook.rs       |  2 
crates/vim/src/test/vim_test_context.rs |  8 ++
crates/zed/src/zed.rs                   | 21 +++++--
6 files changed, 119 insertions(+), 13 deletions(-)

Detailed changes

crates/gpui/src/keymap/binding.rs 🔗

@@ -10,6 +10,7 @@ pub struct KeyBinding {
     pub(crate) action: Box<dyn Action>,
     pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
     pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
+    pub(crate) meta: Option<KeyBindingMetaIndex>,
 }
 
 impl Clone for KeyBinding {
@@ -18,6 +19,7 @@ impl Clone for KeyBinding {
             action: self.action.boxed_clone(),
             keystrokes: self.keystrokes.clone(),
             context_predicate: self.context_predicate.clone(),
+            meta: self.meta,
         }
     }
 }
@@ -59,9 +61,21 @@ impl KeyBinding {
             keystrokes,
             action,
             context_predicate,
+            meta: None,
         })
     }
 
+    /// Set the metadata for this binding.
+    pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self {
+        self.meta = Some(meta);
+        self
+    }
+
+    /// Set the metadata for this binding.
+    pub fn set_meta(&mut self, meta: KeyBindingMetaIndex) {
+        self.meta = Some(meta);
+    }
+
     /// Check if the given keystrokes match this binding.
     pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
         if self.keystrokes.len() < typed.len() {
@@ -91,6 +105,11 @@ impl KeyBinding {
     pub fn predicate(&self) -> Option<Rc<KeyBindingContextPredicate>> {
         self.context_predicate.as_ref().map(|rc| rc.clone())
     }
+
+    /// Get the metadata for this binding
+    pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
+        self.meta
+    }
 }
 
 impl std::fmt::Debug for KeyBinding {
@@ -102,3 +121,9 @@ impl std::fmt::Debug for KeyBinding {
             .finish()
     }
 }
+
+/// A unique identifier for retrieval of metadata associated with a key binding.
+/// Intended to be used as an index or key into a user-defined store of metadata
+/// associated with the binding, such as the source of the binding.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct KeyBindingMetaIndex(pub u32);

crates/settings/src/keymap_file.rs 🔗

@@ -3,7 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap};
 use fs::Fs;
 use gpui::{
     Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
-    KeyBinding, KeyBindingContextPredicate, NoAction,
+    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
 };
 use schemars::{
     JsonSchema,
@@ -151,9 +151,21 @@ impl KeymapFile {
         parse_json_with_comments::<Self>(content)
     }
 
-    pub fn load_asset(asset_path: &str, cx: &App) -> anyhow::Result<Vec<KeyBinding>> {
+    pub fn load_asset(
+        asset_path: &str,
+        source: Option<KeybindSource>,
+        cx: &App,
+    ) -> anyhow::Result<Vec<KeyBinding>> {
         match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
-            KeymapFileLoadResult::Success { key_bindings } => Ok(key_bindings),
+            KeymapFileLoadResult::Success { mut key_bindings } => match source {
+                Some(source) => Ok({
+                    for key_binding in &mut key_bindings {
+                        key_binding.set_meta(source.meta());
+                    }
+                    key_bindings
+                }),
+                None => Ok(key_bindings),
+            },
             KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
                 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
             }
@@ -619,6 +631,61 @@ impl KeymapFile {
     }
 }
 
+#[derive(Clone, Copy)]
+pub enum KeybindSource {
+    User,
+    Default,
+    Base,
+    Vim,
+}
+
+impl KeybindSource {
+    const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
+    const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(1);
+    const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
+    const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
+
+    pub fn name(&self) -> &'static str {
+        match self {
+            KeybindSource::User => "User",
+            KeybindSource::Default => "Default",
+            KeybindSource::Base => "Base",
+            KeybindSource::Vim => "Vim",
+        }
+    }
+
+    pub fn meta(&self) -> KeyBindingMetaIndex {
+        match self {
+            KeybindSource::User => Self::USER,
+            KeybindSource::Default => Self::DEFAULT,
+            KeybindSource::Base => Self::BASE,
+            KeybindSource::Vim => Self::VIM,
+        }
+    }
+
+    pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
+        match index {
+            _ if index == Self::USER => KeybindSource::User,
+            _ if index == Self::USER => KeybindSource::Base,
+            _ if index == Self::DEFAULT => KeybindSource::Default,
+            _ if index == Self::VIM => KeybindSource::Vim,
+            _ => unreachable!(),
+        }
+    }
+}
+
+impl From<KeyBindingMetaIndex> for KeybindSource {
+    fn from(index: KeyBindingMetaIndex) -> Self {
+        Self::from_meta(index)
+    }
+}
+
+impl From<KeybindSource> for KeyBindingMetaIndex {
+    fn from(source: KeybindSource) -> Self {
+        return source.meta();
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::KeymapFile;

crates/settings/src/settings.rs 🔗

@@ -15,7 +15,8 @@ pub use editable_setting_control::*;
 pub use json_schema::*;
 pub use key_equivalents::*;
 pub use keymap_file::{
-    KeyBindingValidator, KeyBindingValidatorRegistration, KeymapFile, KeymapFileLoadResult,
+    KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeymapFile,
+    KeymapFileLoadResult,
 };
 pub use settings_file::*;
 pub use settings_store::{

crates/storybook/src/storybook.rs 🔗

@@ -146,7 +146,7 @@ fn load_embedded_fonts(cx: &App) -> anyhow::Result<()> {
 }
 
 fn load_storybook_keymap(cx: &mut App) {
-    cx.bind_keys(KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap());
+    cx.bind_keys(KeymapFile::load_asset("keymaps/storybook.json", None, cx).unwrap());
 }
 
 pub fn init(cx: &mut App) {

crates/vim/src/test/vim_test_context.rs 🔗

@@ -74,8 +74,12 @@ impl VimTestContext {
         .unwrap();
         cx.bind_keys(default_key_bindings);
         if enabled {
-            let vim_key_bindings =
-                settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
+            let vim_key_bindings = settings::KeymapFile::load_asset(
+                "keymaps/vim.json",
+                Some(settings::KeybindSource::Vim),
+                cx,
+            )
+            .unwrap();
             cx.bind_keys(vim_key_bindings);
         }
     }

crates/zed/src/zed.rs 🔗

@@ -47,8 +47,8 @@ use release_channel::{AppCommitSha, ReleaseChannel};
 use rope::Rope;
 use search::project_search::ProjectSearchBar;
 use settings::{
-    DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings,
-    SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
+    DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult,
+    Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
     initial_project_settings_content, initial_tasks_content, update_settings_file,
 };
 use std::path::PathBuf;
@@ -1403,10 +1403,15 @@ fn show_markdown_app_notification<F>(
     .detach();
 }
 
-fn reload_keymaps(cx: &mut App, user_key_bindings: Vec<KeyBinding>) {
+fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
     cx.clear_key_bindings();
     load_default_keymap(cx);
+
+    for key_binding in &mut user_key_bindings {
+        key_binding.set_meta(KeybindSource::User.meta());
+    }
     cx.bind_keys(user_key_bindings);
+
     cx.set_menus(app_menus());
     // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
     #[cfg(not(target_os = "windows"))]
@@ -1422,14 +1427,18 @@ pub fn load_default_keymap(cx: &mut App) {
         return;
     }
 
-    cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
+    cx.bind_keys(
+        KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
+    );
 
     if let Some(asset_path) = base_keymap.asset_path() {
-        cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
+        cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
     }
 
     if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
-        cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
+        cx.bind_keys(
+            KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
+        );
     }
 }