Files
oak-gpui/crates/language_tools/src/key_context_view.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

295 lines
11 KiB
Rust

use gpui::{
Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
actions,
};
use itertools::Itertools;
use serde_json::json;
use ui::{Button, ButtonStyle};
use ui::{
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div,
h_flex, px, v_flex,
};
use workspace::{Item, SplitDirection, Workspace};
actions!(
dev,
[
/// Opens the key context view for debugging keybindings.
OpenKeyContextView
]
);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &OpenKeyContextView, window, cx| {
let key_context_view = cx.new(|cx| KeyContextView::new(window, cx));
workspace.split_item(
SplitDirection::Right,
Box::new(key_context_view),
window,
cx,
)
});
})
.detach();
}
struct KeyContextView {
pending_keystrokes: Option<Vec<Keystroke>>,
last_keystrokes: Option<SharedString>,
last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
context_stack: Vec<KeyContext>,
focus_handle: FocusHandle,
_subscriptions: [Subscription; 2],
}
impl KeyContextView {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let sub1 = cx.observe_keystrokes(|this, e, _, cx| {
let mut pending = this.pending_keystrokes.take().unwrap_or_default();
pending.push(e.keystroke.clone());
let mut possibilities = cx.all_bindings_for_input(&pending);
possibilities.reverse();
this.last_keystrokes = Some(
json!(pending.iter().map(|p| p.unparse()).join(" "))
.to_string()
.into(),
);
this.context_stack = e.context_stack.clone();
this.last_possibilities = possibilities
.into_iter()
.map(|binding| {
let match_state = if let Some(predicate) = binding.predicate() {
if this.matches(&predicate) {
if this.action_matches(&e.action, binding.action()) {
Some(true)
} else {
Some(false)
}
} else {
None
}
} else if this.action_matches(&e.action, binding.action()) {
Some(true)
} else {
Some(false)
};
let predicate = if let Some(predicate) = binding.predicate() {
format!("{}", predicate)
} else {
"".to_string()
};
let mut name = binding.action().name();
if name == "zed::NoAction" {
name = "(null)"
}
(
name.to_owned().into(),
json!(predicate).to_string().into(),
match_state,
)
})
.collect();
cx.notify();
});
let sub2 = cx.observe_pending_input(window, |this, window, cx| {
this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
if this.pending_keystrokes.is_some() {
this.last_keystrokes.take();
}
cx.notify();
});
Self {
context_stack: Vec::new(),
pending_keystrokes: None,
last_keystrokes: None,
last_possibilities: Vec::new(),
focus_handle: cx.focus_handle(),
_subscriptions: [sub1, sub2],
}
}
}
impl EventEmitter<()> for KeyContextView {}
impl Focusable for KeyContextView {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl KeyContextView {
fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut Context<Self>) {
self.context_stack = stack;
cx.notify()
}
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
predicate.depth_of(&self.context_stack).is_some()
}
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
if let Some(last_action) = a {
last_action.partial_eq(b)
} else {
b.name() == "zed::NoAction"
}
}
}
impl Item for KeyContextView {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Keyboard Context".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<Entity<Self>>>
where
Self: Sized,
{
Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
}
}
impl Render for KeyContextView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
use itertools::Itertools;
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
v_flex()
.id("key-context-view")
.overflow_scroll()
.size_full()
.max_h_full()
.pt_4()
.pl_4()
.track_focus(&self.focus_handle)
.key_context("KeyContextView")
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
this.last_keystrokes.take();
this.set_context_stack(window.context_stack(), cx);
}),
)
.on_mouse_up_out(
MouseButton::Right,
cx.listener(|_, _, window, cx| {
cx.defer_in(window, |this, window, cx| {
this.last_keystrokes.take();
this.set_context_stack(window.context_stack(), cx);
});
}),
)
.child(Label::new("Keyboard Context").size(LabelSize::Large))
.child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
.child(
h_flex()
.mt_4()
.gap_4()
.child(
Button::new("open_documentation", "Open Documentation")
.style(ButtonStyle::Filled)
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
)
.child(
Button::new("view_default_keymap", "View Default Keymap")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(
&zed_actions::OpenDefaultKeymap,
cx
))
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
}),
)
.child(
Button::new("edit_your_keymap", "Edit Keymap File")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, cx))
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
}),
),
)
.child(
Label::new("Current Context Stack")
.size(LabelSize::Large)
.mt_8(),
)
.children({
self.context_stack.iter().enumerate().map(|(i, context)| {
let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
let secondary = context
.secondary()
.map(|e| {
if let Some(value) = e.value.as_ref() {
format!("{}={}", e.key, value)
} else {
e.key.to_string()
}
})
.join(" ");
Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
})
})
.child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
.when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
el.child(
Label::new(format!(
"Waiting for more input: {}",
keystrokes.iter().map(|k| k.unparse()).join(" ")
))
.ml(px(12.)),
)
})
.when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
.children(
self.last_possibilities
.iter()
.map(|(name, predicate, state)| {
let (text, color) = match state {
Some(true) => ("(match)", ui::Color::Success),
Some(false) => ("(low precedence)", ui::Color::Hint),
None => ("(no match)", ui::Color::Error),
};
h_flex()
.gap_2()
.ml_8()
.child(div().min_w(px(200.)).child(Label::new(name.clone())))
.child(Label::new(predicate.clone()))
.child(Label::new(text).color(color))
}),
)
})
.when_some(key_equivalents, |el, key_equivalents| {
el.child(Label::new("Key Equivalents").mt_4().size(LabelSize::Large))
.child(Label::new("Shortcuts defined using some characters have been remapped so that shortcuts can be typed without holding option."))
.children(
key_equivalents
.iter()
.sorted()
.map(|(key, equivalent)| {
Label::new(format!("cmd-{} => cmd-{}", key, equivalent)).ml_8()
}),
)
})
}
}