key_context_view.rs

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