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 context: context.into(),
179 source,
180 });
181 string_match_candidates.push(string_match_candidate);
182 }
183 (processed_bindings, string_match_candidates)
184 }
185
186 fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
187 let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
188 self.keybindings = key_bindings;
189 self.string_match_candidates = Arc::new(string_match_candidates);
190 self.matches = self
191 .string_match_candidates
192 .iter()
193 .enumerate()
194 .map(|(ix, candidate)| StringMatch {
195 candidate_id: ix,
196 score: 0.0,
197 positions: vec![],
198 string: candidate.string.clone(),
199 })
200 .collect();
201
202 self.update_matches(cx);
203 cx.notify();
204 }
205
206 fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
207 let mut dispatch_context = KeyContext::new_with_defaults();
208 dispatch_context.add("KeymapEditor");
209 dispatch_context.add("BufferSearchBar");
210 dispatch_context.add("menu");
211
212 // todo! track key context in keybind edit modal
213 // let identifier = if self.keymap_editor.focus_handle(cx).is_focused(window) {
214 // "editing"
215 // } else {
216 // "not_editing"
217 // };
218 // dispatch_context.add(identifier);
219
220 dispatch_context
221 }
222
223 fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
224 let index = usize::min(index, self.matches.len().saturating_sub(1));
225 self.table_interaction_state.update(cx, |this, _cx| {
226 this.scroll_handle.scroll_to_item(index, strategy);
227 });
228 }
229
230 fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
231 if let Some(selected) = self.selected_index {
232 let selected = selected + 1;
233 if selected >= self.matches.len() {
234 self.select_last(&Default::default(), window, cx);
235 } else {
236 self.selected_index = Some(selected);
237 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
238 cx.notify();
239 }
240 } else {
241 self.select_first(&Default::default(), window, cx);
242 }
243 }
244
245 fn select_previous(
246 &mut self,
247 _: &menu::SelectPrevious,
248 window: &mut Window,
249 cx: &mut Context<Self>,
250 ) {
251 if let Some(selected) = self.selected_index {
252 if selected == 0 {
253 return;
254 }
255
256 let selected = selected - 1;
257
258 if selected >= self.matches.len() {
259 self.select_last(&Default::default(), window, cx);
260 } else {
261 self.selected_index = Some(selected);
262 self.scroll_to_item(selected, ScrollStrategy::Center, cx);
263 cx.notify();
264 }
265 } else {
266 self.select_last(&Default::default(), window, cx);
267 }
268 }
269
270 fn select_first(
271 &mut self,
272 _: &menu::SelectFirst,
273 _window: &mut Window,
274 cx: &mut Context<Self>,
275 ) {
276 if self.matches.get(0).is_some() {
277 self.selected_index = Some(0);
278 self.scroll_to_item(0, ScrollStrategy::Center, cx);
279 cx.notify();
280 }
281 }
282
283 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
284 if self.matches.last().is_some() {
285 let index = self.matches.len() - 1;
286 self.selected_index = Some(index);
287 self.scroll_to_item(index, ScrollStrategy::Center, cx);
288 cx.notify();
289 }
290 }
291
292 fn focus_search(
293 &mut self,
294 _: &search::FocusSearch,
295 window: &mut Window,
296 cx: &mut Context<Self>,
297 ) {
298 if !self
299 .filter_editor
300 .focus_handle(cx)
301 .contains_focused(window, cx)
302 {
303 window.focus(&self.filter_editor.focus_handle(cx));
304 } else {
305 self.filter_editor.update(cx, |editor, cx| {
306 editor.select_all(&Default::default(), window, cx);
307 });
308 }
309 self.selected_index.take();
310 }
311}
312
313#[derive(Clone)]
314struct ProcessedKeybinding {
315 keystroke_text: SharedString,
316 action: SharedString,
317 context: SharedString,
318 source: Option<SharedString>,
319}
320
321impl Item for KeymapEditor {
322 type Event = ();
323
324 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
325 "Keymap Editor".into()
326 }
327}
328
329impl Render for KeymapEditor {
330 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
331 let row_count = self.matches.len();
332 let theme = cx.theme();
333
334 div()
335 .key_context(self.dispatch_context(window, cx))
336 .on_action(cx.listener(Self::select_next))
337 .on_action(cx.listener(Self::select_previous))
338 .on_action(cx.listener(Self::select_first))
339 .on_action(cx.listener(Self::select_last))
340 .on_action(cx.listener(Self::focus_search))
341 .size_full()
342 .bg(theme.colors().background)
343 .id("keymap-editor")
344 .track_focus(&self.focus_handle)
345 .px_4()
346 .v_flex()
347 .pb_4()
348 .child(
349 h_flex()
350 .w_full()
351 .h_12()
352 .px_4()
353 .my_4()
354 .border_2()
355 .border_color(theme.colors().border)
356 .child(self.filter_editor.clone()),
357 )
358 .child(
359 Table::new()
360 .interactable(&self.table_interaction_state)
361 .striped()
362 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
363 .header(["Command", "Keystrokes", "Context", "Source"])
364 .selected_item_index(self.selected_index.clone())
365 .on_click_row(cx.processor(|this, row_index, _window, _cx| {
366 this.selected_index = Some(row_index);
367 }))
368 .uniform_list(
369 "keymap-editor-table",
370 row_count,
371 cx.processor(move |this, range: Range<usize>, _window, _cx| {
372 range
373 .filter_map(|index| {
374 let candidate_id = this.matches.get(index)?.candidate_id;
375 let binding = &this.keybindings[candidate_id];
376 Some(
377 [
378 binding.action.clone(),
379 binding.keystroke_text.clone(),
380 binding.context.clone(),
381 binding.source.clone().unwrap_or_default(),
382 ]
383 .map(IntoElement::into_any_element),
384 )
385 })
386 .collect()
387 }),
388 ),
389 )
390 }
391}
392
393impl SerializableItem for KeymapEditor {
394 fn serialized_item_kind() -> &'static str {
395 "KeymapEditor"
396 }
397
398 fn cleanup(
399 workspace_id: workspace::WorkspaceId,
400 alive_items: Vec<workspace::ItemId>,
401 _window: &mut Window,
402 cx: &mut App,
403 ) -> gpui::Task<gpui::Result<()>> {
404 workspace::delete_unloaded_items(
405 alive_items,
406 workspace_id,
407 "keybinding_editors",
408 &KEYBINDING_EDITORS,
409 cx,
410 )
411 }
412
413 fn deserialize(
414 _project: gpui::Entity<project::Project>,
415 _workspace: gpui::WeakEntity<Workspace>,
416 workspace_id: workspace::WorkspaceId,
417 item_id: workspace::ItemId,
418 window: &mut Window,
419 cx: &mut App,
420 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
421 window.spawn(cx, async move |cx| {
422 if KEYBINDING_EDITORS
423 .get_keybinding_editor(item_id, workspace_id)?
424 .is_some()
425 {
426 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
427 } else {
428 Err(anyhow!("No keybinding editor to deserialize"))
429 }
430 })
431 }
432
433 fn serialize(
434 &mut self,
435 workspace: &mut Workspace,
436 item_id: workspace::ItemId,
437 _closing: bool,
438 _window: &mut Window,
439 cx: &mut ui::Context<Self>,
440 ) -> Option<gpui::Task<gpui::Result<()>>> {
441 let workspace_id = workspace.database_id()?;
442 Some(cx.background_spawn(async move {
443 KEYBINDING_EDITORS
444 .save_keybinding_editor(item_id, workspace_id)
445 .await
446 }))
447 }
448
449 fn should_serialize(&self, _event: &Self::Event) -> bool {
450 false
451 }
452}
453
454mod persistence {
455 use db::{define_connection, query, sqlez_macros::sql};
456 use workspace::WorkspaceDb;
457
458 define_connection! {
459 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
460 &[sql!(
461 CREATE TABLE keybinding_editors (
462 workspace_id INTEGER,
463 item_id INTEGER UNIQUE,
464
465 PRIMARY KEY(workspace_id, item_id),
466 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
467 ON DELETE CASCADE
468 ) STRICT;
469 )];
470 }
471
472 impl KeybindingEditorDb {
473 query! {
474 pub async fn save_keybinding_editor(
475 item_id: workspace::ItemId,
476 workspace_id: workspace::WorkspaceId
477 ) -> Result<()> {
478 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
479 VALUES (?, ?)
480 }
481 }
482
483 query! {
484 pub fn get_keybinding_editor(
485 item_id: workspace::ItemId,
486 workspace_id: workspace::WorkspaceId
487 ) -> Result<Option<workspace::ItemId>> {
488 SELECT item_id
489 FROM keybinding_editors
490 WHERE item_id = ? AND workspace_id = ?
491 }
492 }
493 }
494}