1use gpui::{
2 Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
3 KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, actions,
4};
5use itertools::Itertools;
6use serde_json::json;
7use ui::{Button, ButtonStyle};
8use ui::{
9 ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
10 LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div,
11 h_flex, px, v_flex,
12};
13use workspace::{Item, SplitDirection, Workspace};
14
15actions!(
16 dev,
17 [
18 /// Opens the key context view for debugging keybindings.
19 OpenKeyContextView
20 ]
21);
22
23pub fn init(cx: &mut App) {
24 cx.observe_new(|workspace: &mut Workspace, _, _| {
25 workspace.register_action(|workspace, _: &OpenKeyContextView, window, cx| {
26 let key_context_view = cx.new(|cx| KeyContextView::new(window, cx));
27 workspace.split_item(
28 SplitDirection::Right,
29 Box::new(key_context_view),
30 window,
31 cx,
32 )
33 });
34 })
35 .detach();
36}
37
38struct KeyContextView {
39 pending_keystrokes: Option<Vec<Keystroke>>,
40 last_keystrokes: Option<SharedString>,
41 last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
42 context_stack: Vec<KeyContext>,
43 focus_handle: FocusHandle,
44 _subscriptions: [Subscription; 2],
45}
46
47impl KeyContextView {
48 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
49 let sub1 = cx.observe_keystrokes(|this, e, _, cx| {
50 let mut pending = this.pending_keystrokes.take().unwrap_or_default();
51 pending.push(e.keystroke.clone());
52 let mut possibilities = cx.all_bindings_for_input(&pending);
53 possibilities.reverse();
54 this.last_keystrokes = Some(
55 json!(pending.iter().map(|p| p.unparse()).join(" "))
56 .to_string()
57 .into(),
58 );
59 this.context_stack = e.context_stack.clone();
60 this.last_possibilities = possibilities
61 .into_iter()
62 .map(|binding| {
63 let match_state = if let Some(predicate) = binding.predicate() {
64 if this.matches(&predicate) {
65 if this.action_matches(e.action.as_deref(), binding.action()) {
66 Some(true)
67 } else {
68 Some(false)
69 }
70 } else {
71 None
72 }
73 } else if this.action_matches(e.action.as_deref(), binding.action()) {
74 Some(true)
75 } else {
76 Some(false)
77 };
78 let predicate = if let Some(predicate) = binding.predicate() {
79 format!("{}", predicate)
80 } else {
81 "".to_string()
82 };
83 let name = binding.action().map_or("(null)", |action| action.name());
84
85 (
86 name.to_owned().into(),
87 json!(predicate).to_string().into(),
88 match_state,
89 )
90 })
91 .collect();
92 cx.notify();
93 });
94 let sub2 = cx.observe_pending_input(window, |this, window, cx| {
95 this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
96 if this.pending_keystrokes.is_some() {
97 this.last_keystrokes.take();
98 }
99 cx.notify();
100 });
101
102 Self {
103 context_stack: Vec::new(),
104 pending_keystrokes: None,
105 last_keystrokes: None,
106 last_possibilities: Vec::new(),
107 focus_handle: cx.focus_handle(),
108 _subscriptions: [sub1, sub2],
109 }
110 }
111}
112
113impl EventEmitter<()> for KeyContextView {}
114
115impl Focusable for KeyContextView {
116 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
117 self.focus_handle.clone()
118 }
119}
120impl KeyContextView {
121 fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut Context<Self>) {
122 self.context_stack = stack;
123 cx.notify()
124 }
125
126 fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
127 predicate.depth_of(&self.context_stack).is_some()
128 }
129
130 fn action_matches(&self, a: Option<&dyn Action>, b: Option<&dyn Action>) -> bool {
131 a.zip(b).is_some_and(|(a, b)| a.partial_eq(b))
132 }
133}
134
135impl Item for KeyContextView {
136 type Event = ();
137
138 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
139
140 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
141 "Keyboard Context".into()
142 }
143
144 fn telemetry_event_text(&self) -> Option<&'static str> {
145 None
146 }
147
148 fn clone_on_split(
149 &self,
150 _workspace_id: Option<workspace::WorkspaceId>,
151 window: &mut Window,
152 cx: &mut Context<Self>,
153 ) -> Option<Entity<Self>>
154 where
155 Self: Sized,
156 {
157 Some(cx.new(|cx| KeyContextView::new(window, cx)))
158 }
159}
160
161impl Render for KeyContextView {
162 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
163 use itertools::Itertools;
164
165 let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
166 v_flex()
167 .id("key-context-view")
168 .overflow_scroll()
169 .size_full()
170 .max_h_full()
171 .pt_4()
172 .pl_4()
173 .track_focus(&self.focus_handle)
174 .key_context("KeyContextView")
175 .on_mouse_up_out(
176 MouseButton::Left,
177 cx.listener(|this, _, window, cx| {
178 this.last_keystrokes.take();
179 this.set_context_stack(window.context_stack(), cx);
180 }),
181 )
182 .on_mouse_up_out(
183 MouseButton::Right,
184 cx.listener(|_, _, window, cx| {
185 cx.defer_in(window, |this, window, cx| {
186 this.last_keystrokes.take();
187 this.set_context_stack(window.context_stack(), cx);
188 });
189 }),
190 )
191 .child(Label::new("Keyboard Context").size(LabelSize::Large))
192 .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."))
193 .child(
194 h_flex()
195 .mt_4()
196 .gap_4()
197 .child(
198 Button::new("open_documentation", "Open Documentation")
199 .style(ButtonStyle::Filled)
200 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
201 )
202 .child(
203 Button::new("view_default_keymap", "View default keymap")
204 .style(ButtonStyle::Filled)
205 .key_binding(ui::KeyBinding::for_action(
206 &zed_actions::OpenDefaultKeymap,
207 window,
208 cx
209 ))
210 .on_click(|_, window, cx| {
211 window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
212 }),
213 )
214 .child(
215 Button::new("edit_your_keymap", "Edit your keymap")
216 .style(ButtonStyle::Filled)
217 .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window, cx))
218 .on_click(|_, window, cx| {
219 window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
220 }),
221 ),
222 )
223 .child(
224 Label::new("Current Context Stack")
225 .size(LabelSize::Large)
226 .mt_8(),
227 )
228 .children({
229 self.context_stack.iter().enumerate().map(|(i, context)| {
230 let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
231 let secondary = context
232 .secondary()
233 .map(|e| {
234 if let Some(value) = e.value.as_ref() {
235 format!("{}={}", e.key, value)
236 } else {
237 e.key.to_string()
238 }
239 })
240 .join(" ");
241 Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
242 })
243 })
244 .child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
245 .when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
246 el.child(
247 Label::new(format!(
248 "Waiting for more input: {}",
249 keystrokes.iter().map(|k| k.unparse()).join(" ")
250 ))
251 .ml(px(12.)),
252 )
253 })
254 .when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
255 el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
256 .children(
257 self.last_possibilities
258 .iter()
259 .map(|(name, predicate, state)| {
260 let (text, color) = match state {
261 Some(true) => ("(match)", ui::Color::Success),
262 Some(false) => ("(low precedence)", ui::Color::Hint),
263 None => ("(no match)", ui::Color::Error),
264 };
265 h_flex()
266 .gap_2()
267 .ml_8()
268 .child(div().min_w(px(200.)).child(Label::new(name.clone())))
269 .child(Label::new(predicate.clone()))
270 .child(Label::new(text).color(color))
271 }),
272 )
273 })
274 .when_some(key_equivalents, |el, key_equivalents| {
275 el.child(Label::new("Key Equivalents").mt_4().size(LabelSize::Large))
276 .child(Label::new("Shortcuts defined using some characters have been remapped so that shortcuts can be typed without holding option."))
277 .children(
278 key_equivalents
279 .iter()
280 .sorted()
281 .map(|(key, equivalent)| {
282 Label::new(format!("cmd-{} => cmd-{}", key, equivalent)).ml_8()
283 }),
284 )
285 })
286 }
287}