Files
oak-gpui/crates/gpui/src/keymap.rs
T
Finn Evers f393138711 Fix keybind hints flickering in certain scenarios (#40927)
Closes #39172

This refactors when we resolve UI keybindings in an effort to reduce
flickering whilst painting these: Previously, we would always resolve
these upon creating the binding. This could lead to cases where the
corresponding context was not yet available and no binding could be
resolved, even if the binding was then available on the next presented
frame. Following that, on the next rerender of whatever requested this
keybinding, the keybind for that context would then be found, we would
render that and then also win a layout shift in that process, as we went
from nothing rendered to something rendered between these frames.

With these changes, this now happens less often, because we only look
for the keybinding once the context can actually be resolved in the
window.

| Before | After | 
| --- | --- |
|
https://github.com/user-attachments/assets/adebf8ac-217d-4c7f-ae5a-bab3aa0b0ee8
|
https://github.com/user-attachments/assets/70a82b4b-488f-4a9f-94d7-b6d0a49aada9
|

Also reduced cloning in the keymap editor in this process, since that
requiered changing due to this anyway.

Release Notes:

- Fixed some cases where keybinds would appear with a slight delay,
causing a flicker in the process
2025-10-22 19:52:38 +00:00

716 lines
26 KiB
Rust

mod binding;
mod context;
pub use binding::*;
pub use context::*;
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
use collections::{HashMap, HashSet};
use smallvec::SmallVec;
use std::any::TypeId;
/// An opaque identifier of which version of the keymap is currently active.
/// The keymap's version is changed whenever bindings are added or removed.
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct KeymapVersion(usize);
/// A collection of key bindings for the user's application.
#[derive(Default)]
pub struct Keymap {
bindings: Vec<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
no_action_binding_indices: Vec<usize>,
version: KeymapVersion,
}
/// Index of a binding within a keymap.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct BindingIndex(usize);
impl Keymap {
/// Create a new keymap with the given bindings.
pub fn new(bindings: Vec<KeyBinding>) -> Self {
let mut this = Self::default();
this.add_bindings(bindings);
this
}
/// Get the current version of the keymap.
pub fn version(&self) -> KeymapVersion {
self.version
}
/// Add more bindings to the keymap.
pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
for binding in bindings {
let action_id = binding.action().as_any().type_id();
if is_no_action(&*binding.action) {
self.no_action_binding_indices.push(self.bindings.len());
} else {
self.binding_indices_by_action_id
.entry(action_id)
.or_default()
.push(self.bindings.len());
}
self.bindings.push(binding);
}
self.version.0 += 1;
}
/// Reset this keymap to its initial state.
pub fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_id.clear();
self.no_action_binding_indices.clear();
self.version.0 += 1;
}
/// Iterate over all bindings, in the order they were added.
pub fn bindings(&self) -> impl DoubleEndedIterator<Item = &KeyBinding> + ExactSizeIterator {
self.bindings.iter()
}
/// Iterate over all bindings for the given action, in the order they were added. For display,
/// the last binding should take precedence.
pub fn bindings_for_action<'a>(
&'a self,
action: &'a dyn Action,
) -> impl 'a + DoubleEndedIterator<Item = &'a KeyBinding> {
let action_id = action.type_id();
let binding_indices = self
.binding_indices_by_action_id
.get(&action_id)
.map_or(&[] as _, SmallVec::as_slice)
.iter();
binding_indices.filter_map(|ix| {
let binding = &self.bindings[*ix];
if !binding.action().partial_eq(action) {
return None;
}
for null_ix in &self.no_action_binding_indices {
if null_ix > ix {
let null_binding = &self.bindings[*null_ix];
if null_binding.keystrokes == binding.keystrokes {
let null_binding_matches =
match (&null_binding.context_predicate, &binding.context_predicate) {
(None, _) => true,
(Some(_), None) => false,
(Some(null_predicate), Some(predicate)) => {
null_predicate.is_superset(predicate)
}
};
if null_binding_matches {
return None;
}
}
}
}
Some(binding)
})
}
/// Returns all bindings that might match the input without checking context. The bindings
/// returned in precedence order (reverse of the order they were added to the keymap).
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
self.bindings()
.rev()
.filter(|binding| {
binding
.match_keystrokes(input)
.is_some_and(|pending| !pending)
})
.cloned()
.collect()
}
/// Returns a list of bindings that match the given input, and a boolean indicating whether or
/// not more bindings might match if the input was longer. Bindings are returned in precedence
/// order (higher precedence first, reverse of the order they were added to the keymap).
///
/// Precedence is defined by the depth in the tree (matches on the Editor take precedence over
/// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the
/// same as the deepest context.
///
/// In the case of multiple bindings at the same depth, the ones added to the keymap later take
/// precedence. User bindings are added after built-in bindings so that they take precedence.
///
/// If a user has disabled a binding with `"x": null` it will not be returned. Disabled bindings
/// are evaluated with the same precedence rules so you can disable a rule in a given context
/// only.
pub fn bindings_for_input(
&self,
input: &[impl AsKeystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new();
for (ix, binding) in self.bindings().enumerate().rev() {
let Some(depth) = self.binding_enabled(binding, context_stack) else {
continue;
};
let Some(pending) = binding.match_keystrokes(input) else {
continue;
};
if !pending {
matched_bindings.push((depth, BindingIndex(ix), binding));
} else {
pending_bindings.push((BindingIndex(ix), binding));
}
}
matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
});
let mut bindings: SmallVec<[_; 1]> = SmallVec::new();
let mut first_binding_index = None;
for (_, ix, binding) in matched_bindings {
if is_no_action(&*binding.action) {
// Only break if this is a user-defined NoAction binding
// This allows user keymaps to override base keymap NoAction bindings
if let Some(meta) = binding.meta {
if meta.0 == 0 {
break;
}
} else {
// If no meta is set, assume it's a user binding for safety
break;
}
// For non-user NoAction bindings, continue searching for user overrides
continue;
}
bindings.push(binding.clone());
first_binding_index.get_or_insert(ix);
}
let mut pending = HashSet::default();
for (ix, binding) in pending_bindings.into_iter().rev() {
if let Some(binding_ix) = first_binding_index
&& binding_ix > ix
{
continue;
}
if is_no_action(&*binding.action) {
pending.remove(&&binding.keystrokes);
continue;
}
pending.insert(&binding.keystrokes);
}
(bindings, !pending.is_empty())
}
/// Check if the given binding is enabled, given a certain key context.
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
if let Some(predicate) = &binding.context_predicate {
predicate.depth_of(contexts)
} else {
Some(contexts.len())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as gpui;
use gpui::NoAction;
actions!(
test_only,
[ActionAlpha, ActionBeta, ActionGamma, ActionDelta,]
);
#[test]
fn test_keymap() {
let bindings = [
KeyBinding::new("ctrl-a", ActionAlpha {}, None),
KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
// global bindings are enabled in all contexts
assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0));
assert_eq!(
keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]),
Some(1)
);
// contextual bindings are enabled in contexts that match their predicate
assert_eq!(
keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]),
None
);
assert_eq!(
keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]),
Some(1)
);
assert_eq!(
keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]),
None
);
assert_eq!(
keymap.binding_enabled(
&bindings[2],
&[KeyContext::parse("editor mode=full").unwrap()]
),
Some(1)
);
}
#[test]
fn test_depth_precedence() {
let bindings = [
KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")),
KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[
KeyContext::parse("pane").unwrap(),
KeyContext::parse("editor").unwrap(),
],
);
assert!(!pending);
assert_eq!(result.len(), 2);
assert!(result[0].action.partial_eq(&ActionGamma {}));
assert!(result[1].action.partial_eq(&ActionBeta {}));
}
#[test]
fn test_keymap_disabled() {
let bindings = [
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")),
KeyBinding::new("ctrl-b", NoAction {}, None),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// binding is only enabled in a specific context
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
.is_empty()
);
assert!(
!keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor").unwrap()],
)
.0
.is_empty()
);
// binding is disabled in a more specific context
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor mode=full").unwrap()],
)
.0
.is_empty()
);
// binding is globally disabled
assert!(
keymap
.bindings_for_input(
&[Keystroke::parse("ctrl-b").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
.is_empty()
);
}
#[test]
/// Tests for https://github.com/zed-industries/zed/issues/30259
fn test_multiple_keystroke_binding_disabled() {
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let space = || Keystroke::parse("space").unwrap();
let w = || Keystroke::parse("w").unwrap();
let space_w = [space(), w()];
let space_w_w = [space(), w(), w()];
let workspace_context = || [KeyContext::parse("workspace").unwrap()];
let editor_workspace_context = || {
[
KeyContext::parse("workspace").unwrap(),
KeyContext::parse("editor").unwrap(),
]
};
// Ensure `space` results in pending input on the workspace, but not editor
let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
assert!(space_workspace.0.is_empty());
assert!(space_workspace.1);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert!(!space_editor.1);
// Ensure `space w` results in pending input on the workspace, but not editor
let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
assert!(space_w_workspace.0.is_empty());
assert!(space_w_workspace.1);
let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
assert!(space_w_editor.0.is_empty());
assert!(!space_w_editor.1);
// Ensure `space w w` results in the binding in the workspace, but not in the editor
let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
assert!(!space_w_w_workspace.0.is_empty());
assert!(!space_w_w_workspace.1);
let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
assert!(space_w_w_editor.0.is_empty());
assert!(!space_w_w_editor.1);
// Now test what happens if we have another binding defined AFTER the NoAction
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert!(space_editor.1);
// Now test what happens if we have another binding defined BEFORE the NoAction
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert!(space_editor.1);
// Now test what happens if we have another binding defined at a higher context
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert!(space_editor.1);
}
#[test]
fn test_override_multikey() {
let bindings = [
KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-w").unwrap()],
&[KeyContext::parse("editor").unwrap()],
);
assert!(result.is_empty());
assert!(pending);
let bindings = [
KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-w").unwrap()],
&[KeyContext::parse("editor").unwrap()],
);
assert_eq!(result.len(), 1);
assert!(!pending);
}
#[test]
fn test_simple_disable() {
let bindings = [
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x").unwrap()],
&[KeyContext::parse("editor").unwrap()],
);
assert!(result.is_empty());
assert!(!pending);
}
#[test]
fn test_fail_to_disable() {
// disabled at the wrong level
let bindings = [
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x").unwrap()],
&[
KeyContext::parse("workspace").unwrap(),
KeyContext::parse("editor").unwrap(),
],
);
assert_eq!(result.len(), 1);
assert!(!pending);
}
#[test]
fn test_disable_deeper() {
let bindings = [
KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")),
KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x").unwrap()],
&[
KeyContext::parse("workspace").unwrap(),
KeyContext::parse("editor").unwrap(),
],
);
assert_eq!(result.len(), 0);
assert!(!pending);
}
#[test]
fn test_pending_match_enabled() {
let bindings = [
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
&[
KeyContext::parse("Workspace"),
KeyContext::parse("Pane"),
KeyContext::parse("Editor vim_mode=normal"),
]
.map(Result::unwrap),
);
assert_eq!(matched.0.len(), 1);
assert!(matched.0[0].action.partial_eq(&ActionBeta));
assert!(matched.1);
}
#[test]
fn test_pending_match_enabled_extended() {
let bindings = [
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
&[
KeyContext::parse("Workspace"),
KeyContext::parse("Pane"),
KeyContext::parse("Editor vim_mode=normal"),
]
.map(Result::unwrap),
);
assert_eq!(matched.0.len(), 1);
assert!(matched.0[0].action.partial_eq(&ActionBeta));
assert!(!matched.1);
let bindings = [
KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")),
KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
&[
KeyContext::parse("Workspace"),
KeyContext::parse("Pane"),
KeyContext::parse("Editor vim_mode=normal"),
]
.map(Result::unwrap),
);
assert_eq!(matched.0.len(), 1);
assert!(matched.0[0].action.partial_eq(&ActionBeta));
assert!(!matched.1);
}
#[test]
fn test_overriding_prefix() {
let bindings = [
KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
&[
KeyContext::parse("Workspace"),
KeyContext::parse("Pane"),
KeyContext::parse("Editor vim_mode=normal"),
]
.map(Result::unwrap),
);
assert_eq!(matched.0.len(), 1);
assert!(matched.0[0].action.partial_eq(&ActionBeta));
assert!(!matched.1);
}
#[test]
fn test_context_precedence_with_same_source() {
// Test case: User has both Workspace and Editor bindings for the same key
// Editor binding should take precedence over Workspace binding
let bindings = [
KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")),
KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
// Test with context stack: [Workspace, Editor] (Editor is deeper)
let (result, _) = keymap.bindings_for_input(
&[Keystroke::parse("cmd-r").unwrap()],
&[
KeyContext::parse("Workspace").unwrap(),
KeyContext::parse("Editor").unwrap(),
],
);
// Both bindings should be returned, but Editor binding should be first (highest precedence)
assert_eq!(result.len(), 2);
assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first
assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second
}
#[test]
fn test_bindings_for_action() {
let bindings = [
KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings);
assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
assert_bindings(&keymap, &ActionBeta {}, &[]);
assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]);
#[track_caller]
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
.map(|binding| binding.keystrokes[0].inner().unparse())
.collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action);
}
}
#[test]
fn test_source_precedence_sorting() {
// KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3)
// Test that user keymaps take precedence over default keymaps at the same context depth
let mut keymap = Keymap::default();
// Add a default keymap binding first
let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor"));
default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source
keymap.add_bindings([default_binding]);
// Add a user keymap binding
let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor"));
user_binding.set_meta(KeyBindingMetaIndex(0)); // User source
keymap.add_bindings([user_binding]);
// Test with Editor context stack
let (result, _) = keymap.bindings_for_input(
&[Keystroke::parse("cmd-r").unwrap()],
&[KeyContext::parse("Editor").unwrap()],
);
// User binding should take precedence over default binding
assert_eq!(result.len(), 2);
assert!(result[0].action.partial_eq(&ActionBeta {}));
assert!(result[1].action.partial_eq(&ActionAlpha {}));
}
}