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, binding.action()) {
66 Some(true)
67 } else {
68 Some(false)
69 }
70 } else {
71 None
72 }
73 } else if this.action_matches(&e.action, 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 mut name = binding.action().name();
84 if name == "zed::NoAction" {
85 name = "(null)"
86 }
87
88 (
89 name.to_owned().into(),
90 json!(predicate).to_string().into(),
91 match_state,
92 )
93 })
94 .collect();
95 cx.notify();
96 });
97 let sub2 = cx.observe_pending_input(window, |this, window, cx| {
98 this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
99 if this.pending_keystrokes.is_some() {
100 this.last_keystrokes.take();
101 }
102 cx.notify();
103 });
104
105 Self {
106 context_stack: Vec::new(),
107 pending_keystrokes: None,
108 last_keystrokes: None,
109 last_possibilities: Vec::new(),
110 focus_handle: cx.focus_handle(),
111 _subscriptions: [sub1, sub2],
112 }
113 }
114}
115
116impl EventEmitter<()> for KeyContextView {}
117
118impl Focusable for KeyContextView {
119 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
120 self.focus_handle.clone()
121 }
122}
123impl KeyContextView {
124 fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut Context<Self>) {
125 self.context_stack = stack;
126 cx.notify()
127 }
128
129 fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
130 predicate.depth_of(&self.context_stack).is_some()
131 }
132
133 fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
134 if let Some(last_action) = a {
135 last_action.partial_eq(b)
136 } else {
137 b.name() == "zed::NoAction"
138 }
139 }
140}
141
142impl Item for KeyContextView {
143 type Event = ();
144
145 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
146
147 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
148 "Keyboard Context".into()
149 }
150
151 fn telemetry_event_text(&self) -> Option<&'static str> {
152 None
153 }
154
155 fn clone_on_split(
156 &self,
157 _workspace_id: Option<workspace::WorkspaceId>,
158 window: &mut Window,
159 cx: &mut Context<Self>,
160 ) -> Option<Entity<Self>>
161 where
162 Self: Sized,
163 {
164 Some(cx.new(|cx| KeyContextView::new(window, cx)))
165 }
166}
167
168impl Render for KeyContextView {
169 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
170 use itertools::Itertools;
171
172 let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
173 v_flex()
174 .id("key-context-view")
175 .overflow_scroll()
176 .size_full()
177 .max_h_full()
178 .pt_4()
179 .pl_4()
180 .track_focus(&self.focus_handle)
181 .key_context("KeyContextView")
182 .on_mouse_up_out(
183 MouseButton::Left,
184 cx.listener(|this, _, window, cx| {
185 this.last_keystrokes.take();
186 this.set_context_stack(window.context_stack(), cx);
187 }),
188 )
189 .on_mouse_up_out(
190 MouseButton::Right,
191 cx.listener(|_, _, window, cx| {
192 cx.defer_in(window, |this, window, cx| {
193 this.last_keystrokes.take();
194 this.set_context_stack(window.context_stack(), cx);
195 });
196 }),
197 )
198 .child(Label::new("Keyboard Context").size(LabelSize::Large))
199 .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."))
200 .child(
201 h_flex()
202 .mt_4()
203 .gap_4()
204 .child(
205 Button::new("open_documentation", "Open Documentation")
206 .style(ButtonStyle::Filled)
207 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
208 )
209 .child(
210 Button::new("view_default_keymap", "View default keymap")
211 .style(ButtonStyle::Filled)
212 .key_binding(ui::KeyBinding::for_action(
213 &zed_actions::OpenDefaultKeymap,
214 window,
215 cx
216 ))
217 .on_click(|_, window, cx| {
218 window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
219 }),
220 )
221 .child(
222 Button::new("edit_your_keymap", "Edit your keymap")
223 .style(ButtonStyle::Filled)
224 .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window, cx))
225 .on_click(|_, window, cx| {
226 window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
227 }),
228 ),
229 )
230 .child(
231 Label::new("Current Context Stack")
232 .size(LabelSize::Large)
233 .mt_8(),
234 )
235 .children({
236 self.context_stack.iter().enumerate().map(|(i, context)| {
237 let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
238 let secondary = context
239 .secondary()
240 .map(|e| {
241 if let Some(value) = e.value.as_ref() {
242 format!("{}={}", e.key, value)
243 } else {
244 e.key.to_string()
245 }
246 })
247 .join(" ");
248 Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
249 })
250 })
251 .child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
252 .when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
253 el.child(
254 Label::new(format!(
255 "Waiting for more input: {}",
256 keystrokes.iter().map(|k| k.unparse()).join(" ")
257 ))
258 .ml(px(12.)),
259 )
260 })
261 .when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
262 el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
263 .children(
264 self.last_possibilities
265 .iter()
266 .map(|(name, predicate, state)| {
267 let (text, color) = match state {
268 Some(true) => ("(match)", ui::Color::Success),
269 Some(false) => ("(low precedence)", ui::Color::Hint),
270 None => ("(no match)", ui::Color::Error),
271 };
272 h_flex()
273 .gap_2()
274 .ml_8()
275 .child(div().min_w(px(200.)).child(Label::new(name.clone())))
276 .child(Label::new(predicate.clone()))
277 .child(Label::new(text).color(color))
278 }),
279 )
280 })
281 .when_some(key_equivalents, |el, key_equivalents| {
282 el.child(Label::new("Key Equivalents").mt_4().size(LabelSize::Large))
283 .child(Label::new("Shortcuts defined using some characters have been remapped so that shortcuts can be typed without holding option."))
284 .children(
285 key_equivalents
286 .iter()
287 .sorted()
288 .map(|(key, equivalent)| {
289 Label::new(format!("cmd-{} => cmd-{}", key, equivalent)).ml_8()
290 }),
291 )
292 })
293 }
294}