1use gpui::{
2 actions, Action, AppContext, EventEmitter, FocusHandle, FocusableView,
3 KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription,
4};
5use itertools::Itertools;
6use serde_json::json;
7use settings::get_key_equivalents;
8use ui::{
9 div, h_flex, px, v_flex, ButtonCommon, Clickable, FluentBuilder, InteractiveElement, Label,
10 LabelCommon, LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled,
11 ViewContext, VisualContext, WindowContext,
12};
13use ui::{Button, ButtonStyle};
14use workspace::Item;
15use workspace::Workspace;
16
17actions!(debug, [OpenKeyContextView]);
18
19pub fn init(cx: &mut AppContext) {
20 cx.observe_new_views(|workspace: &mut Workspace, _| {
21 workspace.register_action(|workspace, _: &OpenKeyContextView, cx| {
22 let key_context_view = cx.new_view(KeyContextView::new);
23 workspace.add_item_to_active_pane(Box::new(key_context_view), None, true, cx)
24 });
25 })
26 .detach();
27}
28
29struct KeyContextView {
30 pending_keystrokes: Option<Vec<Keystroke>>,
31 last_keystrokes: Option<SharedString>,
32 last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
33 context_stack: Vec<KeyContext>,
34 focus_handle: FocusHandle,
35 _subscriptions: [Subscription; 2],
36}
37
38impl KeyContextView {
39 pub fn new(cx: &mut ViewContext<Self>) -> Self {
40 let sub1 = cx.observe_keystrokes(|this, e, cx| {
41 let mut pending = this.pending_keystrokes.take().unwrap_or_default();
42 pending.push(e.keystroke.clone());
43 let mut possibilities = cx.all_bindings_for_input(&pending);
44 possibilities.reverse();
45 this.context_stack = cx.context_stack();
46 this.last_keystrokes = Some(
47 json!(pending.iter().map(|p| p.unparse()).join(" "))
48 .to_string()
49 .into(),
50 );
51 this.last_possibilities = possibilities
52 .into_iter()
53 .map(|binding| {
54 let match_state = if let Some(predicate) = binding.predicate() {
55 if this.matches(predicate) {
56 if this.action_matches(&e.action, binding.action()) {
57 Some(true)
58 } else {
59 Some(false)
60 }
61 } else {
62 None
63 }
64 } else {
65 if this.action_matches(&e.action, binding.action()) {
66 Some(true)
67 } else {
68 Some(false)
69 }
70 };
71 let predicate = if let Some(predicate) = binding.predicate() {
72 format!("{}", predicate)
73 } else {
74 "".to_string()
75 };
76 let mut name = binding.action().name();
77 if name == "zed::NoAction" {
78 name = "(null)"
79 }
80
81 (
82 name.to_owned().into(),
83 json!(predicate).to_string().into(),
84 match_state,
85 )
86 })
87 .collect();
88 });
89 let sub2 = cx.observe_pending_input(|this, cx| {
90 this.pending_keystrokes = cx
91 .pending_input_keystrokes()
92 .map(|k| k.iter().cloned().collect());
93 if this.pending_keystrokes.is_some() {
94 this.last_keystrokes.take();
95 }
96 cx.notify();
97 });
98
99 Self {
100 context_stack: Vec::new(),
101 pending_keystrokes: None,
102 last_keystrokes: None,
103 last_possibilities: Vec::new(),
104 focus_handle: cx.focus_handle(),
105 _subscriptions: [sub1, sub2],
106 }
107 }
108}
109
110impl EventEmitter<()> for KeyContextView {}
111
112impl FocusableView for KeyContextView {
113 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
114 self.focus_handle.clone()
115 }
116}
117impl KeyContextView {
118 fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut ViewContext<Self>) {
119 self.context_stack = stack;
120 cx.notify()
121 }
122
123 fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
124 let mut stack = self.context_stack.clone();
125 while !stack.is_empty() {
126 if predicate.eval(&stack) {
127 return true;
128 }
129 stack.pop();
130 }
131 false
132 }
133
134 fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
135 if let Some(last_action) = a {
136 last_action.partial_eq(b)
137 } else {
138 b.name() == "zed::NoAction"
139 }
140 }
141}
142
143impl Item for KeyContextView {
144 type Event = ();
145
146 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
147
148 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
149 Some("Keyboard Context".into())
150 }
151
152 fn telemetry_event_text(&self) -> Option<&'static str> {
153 None
154 }
155
156 fn clone_on_split(
157 &self,
158 _workspace_id: Option<workspace::WorkspaceId>,
159 cx: &mut ViewContext<Self>,
160 ) -> Option<gpui::View<Self>>
161 where
162 Self: Sized,
163 {
164 Some(cx.new_view(Self::new))
165 }
166}
167
168impl Render for KeyContextView {
169 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
170 use itertools::Itertools;
171 let key_equivalents = get_key_equivalents(cx.keyboard_layout());
172 v_flex()
173 .id("key-context-view")
174 .overflow_scroll()
175 .size_full()
176 .max_h_full()
177 .pt_4()
178 .pl_4()
179 .track_focus(&self.focus_handle)
180 .key_context("KeyContextView")
181 .on_mouse_up_out(
182 MouseButton::Left,
183 cx.listener(|this, _, cx| {
184 this.last_keystrokes.take();
185 this.set_context_stack(cx.context_stack(), cx);
186 }),
187 )
188 .on_mouse_up_out(
189 MouseButton::Right,
190 cx.listener(|_, _, cx| {
191 cx.defer(|this, cx| {
192 this.last_keystrokes.take();
193 this.set_context_stack(cx.context_stack(), cx);
194 });
195 }),
196 )
197 .child(Label::new("Keyboard Context").size(LabelSize::Large))
198 .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."))
199 .child(
200 h_flex()
201 .mt_4()
202 .gap_4()
203 .child(
204 Button::new("default", "Open Documentation")
205 .style(ButtonStyle::Filled)
206 .on_click(|_, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
207 )
208 .child(
209 Button::new("default", "View default keymap")
210 .style(ButtonStyle::Filled)
211 .key_binding(ui::KeyBinding::for_action(
212 &zed_actions::OpenDefaultKeymap,
213 cx,
214 ))
215 .on_click(|_, cx| {
216 cx.dispatch_action(workspace::SplitRight.boxed_clone());
217 cx.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone());
218 }),
219 )
220 .child(
221 Button::new("default", "Edit your keymap")
222 .style(ButtonStyle::Filled)
223 .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, cx))
224 .on_click(|_, cx| {
225 cx.dispatch_action(workspace::SplitRight.boxed_clone());
226 cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
227 }),
228 ),
229 )
230 .child(
231 Label::new("Current Context Stack")
232 .size(LabelSize::Large)
233 .mt_8(),
234 )
235 .children({
236 cx.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}