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}