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