gpui: Add keybind metadata API (#33316)
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 ...
This commit is contained in:
parent
13f134448d
commit
deb2564b31
6 changed files with 119 additions and 13 deletions
|
@ -10,6 +10,7 @@ pub struct KeyBinding {
|
||||||
pub(crate) action: Box<dyn Action>,
|
pub(crate) action: Box<dyn Action>,
|
||||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
|
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for KeyBinding {
|
impl Clone for KeyBinding {
|
||||||
|
@ -18,6 +19,7 @@ impl Clone for KeyBinding {
|
||||||
action: self.action.boxed_clone(),
|
action: self.action.boxed_clone(),
|
||||||
keystrokes: self.keystrokes.clone(),
|
keystrokes: self.keystrokes.clone(),
|
||||||
context_predicate: self.context_predicate.clone(),
|
context_predicate: self.context_predicate.clone(),
|
||||||
|
meta: self.meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,9 +61,21 @@ impl KeyBinding {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context_predicate,
|
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.
|
/// Check if the given keystrokes match this binding.
|
||||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
||||||
if self.keystrokes.len() < typed.len() {
|
if self.keystrokes.len() < typed.len() {
|
||||||
|
@ -91,6 +105,11 @@ impl KeyBinding {
|
||||||
pub fn predicate(&self) -> Option<Rc<KeyBindingContextPredicate>> {
|
pub fn predicate(&self) -> Option<Rc<KeyBindingContextPredicate>> {
|
||||||
self.context_predicate.as_ref().map(|rc| rc.clone())
|
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 {
|
impl std::fmt::Debug for KeyBinding {
|
||||||
|
@ -102,3 +121,9 @@ impl std::fmt::Debug for KeyBinding {
|
||||||
.finish()
|
.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);
|
||||||
|
|
|
@ -3,7 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||||
KeyBinding, KeyBindingContextPredicate, NoAction,
|
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
|
||||||
};
|
};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
|
@ -151,9 +151,21 @@ impl KeymapFile {
|
||||||
parse_json_with_comments::<Self>(content)
|
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) {
|
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, .. } => {
|
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
|
||||||
anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::KeymapFile;
|
use crate::KeymapFile;
|
||||||
|
|
|
@ -15,7 +15,8 @@ pub use editable_setting_control::*;
|
||||||
pub use json_schema::*;
|
pub use json_schema::*;
|
||||||
pub use key_equivalents::*;
|
pub use key_equivalents::*;
|
||||||
pub use keymap_file::{
|
pub use keymap_file::{
|
||||||
KeyBindingValidator, KeyBindingValidatorRegistration, KeymapFile, KeymapFileLoadResult,
|
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeymapFile,
|
||||||
|
KeymapFileLoadResult,
|
||||||
};
|
};
|
||||||
pub use settings_file::*;
|
pub use settings_file::*;
|
||||||
pub use settings_store::{
|
pub use settings_store::{
|
||||||
|
|
|
@ -146,7 +146,7 @@ fn load_embedded_fonts(cx: &App) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_storybook_keymap(cx: &mut App) {
|
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) {
|
pub fn init(cx: &mut App) {
|
||||||
|
|
|
@ -74,8 +74,12 @@ impl VimTestContext {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.bind_keys(default_key_bindings);
|
cx.bind_keys(default_key_bindings);
|
||||||
if enabled {
|
if enabled {
|
||||||
let vim_key_bindings =
|
let vim_key_bindings = settings::KeymapFile::load_asset(
|
||||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
"keymaps/vim.json",
|
||||||
|
Some(settings::KeybindSource::Vim),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
cx.bind_keys(vim_key_bindings);
|
cx.bind_keys(vim_key_bindings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,8 @@ use release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use search::project_search::ProjectSearchBar;
|
use search::project_search::ProjectSearchBar;
|
||||||
use settings::{
|
use settings::{
|
||||||
DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings,
|
DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult,
|
||||||
SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
|
Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content,
|
||||||
initial_project_settings_content, initial_tasks_content, update_settings_file,
|
initial_project_settings_content, initial_tasks_content, update_settings_file,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -1403,10 +1403,15 @@ fn show_markdown_app_notification<F>(
|
||||||
.detach();
|
.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();
|
cx.clear_key_bindings();
|
||||||
load_default_keymap(cx);
|
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.bind_keys(user_key_bindings);
|
||||||
|
|
||||||
cx.set_menus(app_menus());
|
cx.set_menus(app_menus());
|
||||||
// On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
|
// On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
@ -1422,14 +1427,18 @@ pub fn load_default_keymap(cx: &mut App) {
|
||||||
return;
|
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() {
|
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 {
|
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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue