Sketch in a table for the keybindings UI (#32436)

Adds the initial semblance of a keymap UI. It is currently gated behind the `settings-ui` feature flag. Follow up PRs will add polish and missing features.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Mikayla Maki 2025-06-30 16:25:11 -07:00 committed by GitHub
parent 32906bfa7c
commit 7609ca7a8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1967 additions and 494 deletions

View file

@ -3,7 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap};
use fs::Fs;
use gpui::{
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
};
use schemars::{JsonSchema, json_schema};
use serde::Deserialize;
@ -399,7 +399,13 @@ impl KeymapFile {
},
};
let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
let key_binding = match KeyBinding::load(
keystrokes,
action,
context,
key_equivalents,
action_input_string.map(SharedString::from),
) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(
@ -626,6 +632,13 @@ impl KeymapFile {
continue;
};
for (keystrokes, action) in bindings {
let Ok(keystrokes) = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<Vec<_>, _>>()
else {
continue;
};
if keystrokes != target.keystrokes {
continue;
}
@ -640,9 +653,9 @@ impl KeymapFile {
if let Some(index) = found_index {
let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
&keymap_contents,
&["bindings", target.keystrokes],
&["bindings", &target.keystrokes_unparsed()],
Some(&source_action_value),
Some(source.keystrokes),
Some(&source.keystrokes_unparsed()),
index,
tab_size,
)
@ -674,7 +687,7 @@ impl KeymapFile {
value.insert("bindings".to_string(), {
let mut bindings = serde_json::Map::new();
let action = keybinding.action_value()?;
bindings.insert(keybinding.keystrokes.into(), action);
bindings.insert(keybinding.keystrokes_unparsed(), action);
bindings.into()
});
@ -701,11 +714,11 @@ pub enum KeybindUpdateOperation<'a> {
}
pub struct KeybindUpdateTarget<'a> {
context: Option<&'a str>,
keystrokes: &'a str,
action_name: &'a str,
use_key_equivalents: bool,
input: Option<&'a str>,
pub context: Option<&'a str>,
pub keystrokes: &'a [Keystroke],
pub action_name: &'a str,
pub use_key_equivalents: bool,
pub input: Option<&'a str>,
}
impl<'a> KeybindUpdateTarget<'a> {
@ -721,6 +734,16 @@ impl<'a> KeybindUpdateTarget<'a> {
};
return Ok(value);
}
fn keystrokes_unparsed(&self) -> String {
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
for keystroke in self.keystrokes {
keystrokes.push_str(&keystroke.unparse());
keystrokes.push(' ');
}
keystrokes.pop();
keystrokes
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -804,6 +827,8 @@ mod tests {
#[test]
fn keymap_update() {
use gpui::Keystroke;
zlog::init_test();
#[track_caller]
fn check_keymap_update(
@ -816,10 +841,18 @@ mod tests {
pretty_assertions::assert_eq!(expected.to_string(), result);
}
#[track_caller]
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
return keystrokes
.split(' ')
.map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
.collect();
}
check_keymap_update(
"[]",
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-a",
keystrokes: &parse_keystrokes("ctrl-a"),
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
@ -845,7 +878,7 @@ mod tests {
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
@ -876,7 +909,7 @@ mod tests {
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
@ -912,7 +945,7 @@ mod tests {
]"#
.unindent(),
KeybindUpdateOperation::Add(KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: Some("Zed > Editor && some_condition = true"),
use_key_equivalents: true,
@ -951,14 +984,14 @@ mod tests {
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
keystrokes: &parse_keystrokes("ctrl-a"),
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
@ -997,14 +1030,14 @@ mod tests {
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
keystrokes: &parse_keystrokes("ctrl-a"),
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
@ -1038,14 +1071,14 @@ mod tests {
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
keystrokes: &parse_keystrokes("ctrl-a"),
action_name: "zed::SomeNonexistentAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,
@ -1081,14 +1114,14 @@ mod tests {
.unindent(),
KeybindUpdateOperation::Replace {
target: KeybindUpdateTarget {
keystrokes: "ctrl-a",
keystrokes: &parse_keystrokes("ctrl-a"),
action_name: "zed::SomeAction",
context: None,
use_key_equivalents: false,
input: None,
},
source: KeybindUpdateTarget {
keystrokes: "ctrl-b",
keystrokes: &parse_keystrokes("ctrl-b"),
action_name: "zed::SomeOtherAction",
context: None,
use_key_equivalents: false,

View file

@ -14,8 +14,8 @@ use util::asset_str;
pub use editable_setting_control::*;
pub use key_equivalents::*;
pub use keymap_file::{
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeymapFile,
KeymapFileLoadResult,
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
};
pub use settings_file::*;
pub use settings_json::*;

View file

@ -618,7 +618,7 @@ impl SettingsStore {
));
}
fn json_tab_size(&self) -> usize {
pub fn json_tab_size(&self) -> usize {
const DEFAULT_JSON_TAB_SIZE: usize = 2;
if let Some((setting_type_id, callback)) = &self.tab_size_callback {