1use std::{fmt::Write as _, ops::Range, sync::Arc};
2
3use db::anyhow::anyhow;
4use editor::{Editor, EditorEvent};
5use fuzzy::{StringMatch, StringMatchCandidate};
6use gpui::{
7 AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, KeyContext,
8 ScrollStrategy, Subscription, actions, div,
9};
10
11use ui::{
12 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
13 Window, prelude::*,
14};
15use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
16
17use crate::{
18 keybindings::persistence::KEYBINDING_EDITORS,
19 ui_components::table::{Table, TableInteractionState},
20};
21
22actions!(zed, [OpenKeymapEditor]);
23
24pub fn init(cx: &mut App) {
25 let keymap_event_channel = KeymapEventChannel::new();
26 cx.set_global(keymap_event_channel);
27
28 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
29 workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
30 let open_keymap_editor = cx.new(|cx| KeymapEditor::new(window, cx));
31 workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
32 });
33 })
34 .detach();
35
36 register_serializable_item::<KeymapEditor>(cx);
37}
38
39pub struct KeymapEventChannel {}
40
41impl Global for KeymapEventChannel {}
42
43impl KeymapEventChannel {
44 fn new() -> Self {
45 Self {}
46 }
47
48 pub fn trigger_keymap_changed(cx: &mut App) {
49 cx.update_global(|_event_channel: &mut Self, _| {
50 /* triggers observers in KeymapEditors */
51 });
52 }
53}
54
55struct KeymapEditor {
56 focus_handle: FocusHandle,
57 _keymap_subscription: Subscription,
58 keybindings: Vec<ProcessedKeybinding>,
59 // corresponds 1 to 1 with keybindings
60 string_match_candidates: Arc<Vec<StringMatchCandidate>>,
61 matches: Vec<StringMatch>,
62 table_interaction_state: Entity<TableInteractionState>,
63 filter_editor: Entity<Editor>,
64 selected_index: Option<usize>,
65}
66
67impl EventEmitter<()> for KeymapEditor {}
68
69impl Focusable for KeymapEditor {
70 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
71 return self.filter_editor.focus_handle(cx);
72 }
73}
74
75impl KeymapEditor {
76 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
77 let focus_handle = cx.focus_handle();
78
79 let _keymap_subscription =
80 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
81 let table_interaction_state = TableInteractionState::new(window, cx);
82
83 let filter_editor = cx.new(|cx| {
84 let mut editor = Editor::single_line(window, cx);
85 editor.set_placeholder_text("Filter action names...", cx);
86 editor
87 });
88
89 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
90 if !matches!(e, EditorEvent::BufferEdited) {
91 return;
92 }
93
94 this.update_matches(cx);
95 })
96 .detach();
97
98 let mut this = Self {
99 keybindings: vec![],
100 string_match_candidates: Arc::new(vec![]),
101 matches: vec![],
102 focus_handle: focus_handle.clone(),
103 _keymap_subscription,
104 table_interaction_state,
105 filter_editor,
106 selected_index: None,
107 };
108
109 this.update_keybindings(cx);
110
111 this
112 }
113
114 fn update_matches(&mut self, cx: &mut Context<Self>) {
115 let query = self.filter_editor.read(cx).text(cx);
116 let string_match_candidates = self.string_match_candidates.clone();
117 let executor = cx.background_executor().clone();
118 let keybind_count = self.keybindings.len();
119 let query = command_palette::normalize_action_query(&query);
120 let fuzzy_match = cx.background_spawn(async move {
121 fuzzy::match_strings(
122 &string_match_candidates,
123 &query,
124 true,
125 true,
126 keybind_count,
127 &Default::default(),
128 executor,
129 )
130 .await
131 });
132
133 cx.spawn(async move |this, cx| {
134 let matches = fuzzy_match.await;
135 this.update(cx, |this, cx| {
136 this.selected_index.take();
137 this.scroll_to_item(0, ScrollStrategy::Top, cx);
138 this.matches = matches;
139 cx.notify();
140 })
141 })
142 .detach();
143 }
144
145 fn process_bindings(
146 cx: &mut Context<Self>,
147 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
148 let key_bindings_ptr = cx.key_bindings();
149 let lock = key_bindings_ptr.borrow();
150 let key_bindings = lock.bindings();
151
152 let mut processed_bindings = Vec::new();
153 let mut string_match_candidates = Vec::new();
154
155 for key_binding in key_bindings {
156 let mut keystroke_text = String::new();
157 for keystroke in key_binding.keystrokes() {
158 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
159 }
160 let keystroke_text = keystroke_text.trim().to_string();
161
162 let context = key_binding
163 .predicate()
164 .map(|predicate| predicate.to_string())
165 .unwrap_or_else(|| "<global>".to_string());
166
167 let source = key_binding
168 .meta()
169 .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
170
171 let action_name = key_binding.action().name();
172
173 let index = processed_bindings.len();
174 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
175 processed_bindings.push(ProcessedKeybinding {
176 keystroke_text: keystroke_text.into(),
177 action: action_name.into(),
178 action_input: key_binding.action_input(),
179 context: context.into(),
180 source,
181 });
182 string_match_candidates.push(string_match_candidate);
183 }
184 (processed_bindings, string_match_candidates)
185 }
186
187 fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
188 let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
189 self.keybindings = key_bindings;
190 self.string_match_candidates = Arc::new(string_match_candidates);
191 self.matches = self
192 .string_match_candidates
193 .iter()
194 .enumerate()
195 .map(|(ix, candidate)| StringMatch {
196 candidate_id: ix,
197 score: 0.0,
198 positions: vec![],
199 string: candidate.string.clone(),
200 })
201 .collect();
202
203 self.update_matches(cx);
204 cx.notify();
205 }
206
207 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
208 let mut dispatch_context = KeyContext::new_with_defaults();
209 dispatch_context.add("KeymapEditor");
210 dispatch_context.add("BufferSearchBar");
211 dispatch_context.add("menu");
212
213 // todo! track key context in keybind edit modal
214 // let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
215 // "editing"
216 // } else {
217 // "not_editing"
218 // };
219 // dispatch_context.add(identifier);
220
221 dispatch_context
222 }
223
224 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
225 let index = usize::min(index, self.matches.len().saturating_sub(1));
226 self.table_interaction_state.update(cx, |this, _cx| {
227 this.scroll_handle.scroll_to_item(index, strategy);
228 });
229 }
230
231 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
232 if let Some(selected) = self.selected_index {
233 let selected = selected + 1;
234 if selected >= self.matches.len() {
235 self.select_last(&Default::default(), window, cx);
236 } else {
237 self.selected_index = Some(selected);
238 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
239 cx.notify();
240 }
241 } else {
242 self.select_first(&Default::default(), window, cx);
243 }
244 }
245
246 fn select_previous(
247 &mut self,
248 _: &menu::SelectPrevious,
249 window: &mut Window,
250 cx: &mut Context<Self>,
251 ) {
252 if let Some(selected) = self.selected_index {
253 if selected == 0 {
254 return;
255 }
256
257 let selected = selected - 1;
258
259 if selected >= self.matches.len() {
260 self.select_last(&Default::default(), window, cx);
261 } else {
262 self.selected_index = Some(selected);
263 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
264 cx.notify();
265 }
266 } else {
267 self.select_last(&Default::default(), window, cx);
268 }
269 }
270
271 fn select_first(
272 &mut self,
273 _: &menu::SelectFirst,
274 _window: &mut Window,
275 cx: &mut Context<Self>,
276 ) {
277 if self.matches.get(0).is_some() {
278 self.selected_index = Some(0);
279 self.scroll_to_item(0, ScrollStrategy::Center, cx);
280 cx.notify();
281 }
282 }
283
284 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
285 if self.matches.last().is_some() {
286 let index = self.matches.len() - 1;
287 self.selected_index = Some(index);
288 self.scroll_to_item(index, ScrollStrategy::Center, cx);
289 cx.notify();
290 }
291 }
292
293 fn focus_search(
294 &mut self,
295 _: &search::FocusSearch,
296 window: &mut Window,
297 cx: &mut Context<Self>,
298 ) {
299 if !self
300 .filter_editor
301 .focus_handle(cx)
302 .contains_focused(window, cx)
303 {
304 window.focus(&self.filter_editor.focus_handle(cx));
305 } else {
306 self.filter_editor.update(cx, |editor, cx| {
307 editor.select_all(&Default::default(), window, cx);
308 });
309 }
310 self.selected_index.take();
311 }
312}
313
314#[derive(Clone)]
315struct ProcessedKeybinding {
316 keystroke_text: SharedString,
317 action: SharedString,
318 action_input: Option<SharedString>,
319 context: SharedString,
320 source: Option<SharedString>,
321}
322
323impl Item for KeymapEditor {
324 type Event = ();
325
326 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
327 "Keymap Editor".into()
328 }
329}
330
331impl Render for KeymapEditor {
332 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
333 let row_count = self.matches.len();
334 let theme = cx.theme();
335
336 div()
337 .key_context(self.dispatch_context(window, cx))
338 .on_action(cx.listener(Self::select_next))
339 .on_action(cx.listener(Self::select_previous))
340 .on_action(cx.listener(Self::select_first))
341 .on_action(cx.listener(Self::select_last))
342 .on_action(cx.listener(Self::focus_search))
343 .size_full()
344 .bg(theme.colors().editor_background)
345 .id("keymap-editor")
346 .track_focus(&self.focus_handle)
347 .px_4()
348 .v_flex()
349 .pb_4()
350 .child(
351 h_flex()
352 .w_full()
353 .h_12()
354 .px_4()
355 .my_4()
356 .border_2()
357 .border_color(theme.colors().border)
358 .child(self.filter_editor.clone()),
359 )
360 .child(
361 Table::new()
362 .interactable(&self.table_interaction_state)
363 .striped()
364 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
365 .header(["Command", "Keystrokes", "Context", "Source"])
366 .selected_item_index(self.selected_index.clone())
367 .on_click_row(cx.processor(|this, row_index, _window, _cx| {
368 this.selected_index = Some(row_index);
369 }))
370 .uniform_list(
371 "keymap-editor-table",
372 row_count,
373 cx.processor(move |this, range: Range<usize>, _window, _cx| {
374 range
375 .filter_map(|index| {
376 let candidate_id = this.matches.get(index)?.candidate_id;
377 let binding = &this.keybindings[candidate_id];
378 let action = h_flex()
379 .items_start()
380 .gap_1()
381 .child(binding.action.clone())
382 .when_some(
383 binding.action_input.clone(),
384 |this, binding_input| this.child(binding_input),
385 );
386 let keystrokes = binding.keystroke_text.clone();
387 let context = binding.context.clone();
388 let source = binding.source.clone().unwrap_or_default();
389 Some([
390 action.into_any_element(),
391 keystrokes.into_any_element(),
392 context.into_any_element(),
393 source.into_any_element(),
394 ])
395 })
396 .collect()
397 }),
398 ),
399 )
400 }
401}
402
403impl SerializableItem for KeymapEditor {
404 fn serialized_item_kind() -> &'static str {
405 "KeymapEditor"
406 }
407
408 fn cleanup(
409 workspace_id: workspace::WorkspaceId,
410 alive_items: Vec<workspace::ItemId>,
411 _window: &mut Window,
412 cx: &mut App,
413 ) -> gpui::Task<gpui::Result<()>> {
414 workspace::delete_unloaded_items(
415 alive_items,
416 workspace_id,
417 "keybinding_editors",
418 &KEYBINDING_EDITORS,
419 cx,
420 )
421 }
422
423 fn deserialize(
424 _project: gpui::Entity<project::Project>,
425 _workspace: gpui::WeakEntity<Workspace>,
426 workspace_id: workspace::WorkspaceId,
427 item_id: workspace::ItemId,
428 window: &mut Window,
429 cx: &mut App,
430 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
431 window.spawn(cx, async move |cx| {
432 if KEYBINDING_EDITORS
433 .get_keybinding_editor(item_id, workspace_id)?
434 .is_some()
435 {
436 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
437 } else {
438 Err(anyhow!("No keybinding editor to deserialize"))
439 }
440 })
441 }
442
443 fn serialize(
444 &mut self,
445 workspace: &mut Workspace,
446 item_id: workspace::ItemId,
447 _closing: bool,
448 _window: &mut Window,
449 cx: &mut ui::Context<Self>,
450 ) -> Option<gpui::Task<gpui::Result<()>>> {
451 let workspace_id = workspace.database_id()?;
452 Some(cx.background_spawn(async move {
453 KEYBINDING_EDITORS
454 .save_keybinding_editor(item_id, workspace_id)
455 .await
456 }))
457 }
458
459 fn should_serialize(&self, _event: &Self::Event) -> bool {
460 false
461 }
462}
463
464mod persistence {
465 use db::{define_connection, query, sqlez_macros::sql};
466 use workspace::WorkspaceDb;
467
468 define_connection! {
469 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
470 &[sql!(
471 CREATE TABLE keybinding_editors (
472 workspace_id INTEGER,
473 item_id INTEGER UNIQUE,
474
475 PRIMARY KEY(workspace_id, item_id),
476 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
477 ON DELETE CASCADE
478 ) STRICT;
479 )];
480 }
481
482 impl KeybindingEditorDb {
483 query! {
484 pub async fn save_keybinding_editor(
485 item_id: workspace::ItemId,
486 workspace_id: workspace::WorkspaceId
487 ) -> Result<()> {
488 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
489 VALUES (?, ?)
490 }
491 }
492
493 query! {
494 pub fn get_keybinding_editor(
495 item_id: workspace::ItemId,
496 workspace_id: workspace::WorkspaceId
497 ) -> Result<Option<workspace::ItemId>> {
498 SELECT item_id
499 FROM keybinding_editors
500 WHERE item_id = ? AND workspace_id = ?
501 }
502 }
503 }
504}