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.table_interaction_state.update(cx, |this, _cx| {
137 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
138 });
139 this.matches = matches;
140 cx.notify();
141 })
142 })
143 .detach();
144 }
145
146 fn process_bindings(
147 cx: &mut Context<Self>,
148 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
149 let key_bindings_ptr = cx.key_bindings();
150 let lock = key_bindings_ptr.borrow();
151 let key_bindings = lock.bindings();
152
153 let mut processed_bindings = Vec::new();
154 let mut string_match_candidates = Vec::new();
155
156 for key_binding in key_bindings {
157 let mut keystroke_text = String::new();
158 for keystroke in key_binding.keystrokes() {
159 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
160 }
161 let keystroke_text = keystroke_text.trim().to_string();
162
163 let context = key_binding
164 .predicate()
165 .map(|predicate| predicate.to_string())
166 .unwrap_or_else(|| "<global>".to_string());
167
168 let source = key_binding
169 .meta()
170 .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
171
172 let action_name = key_binding.action().name();
173
174 let index = processed_bindings.len();
175 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
176 processed_bindings.push(ProcessedKeybinding {
177 keystroke_text: keystroke_text.into(),
178 action: action_name.into(),
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 select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
225 if let Some(selected) = &mut self.selected_index {
226 *selected += 1;
227 if *selected >= self.matches.len() {
228 self.select_last(&Default::default(), window, cx);
229 } else {
230 cx.notify();
231 }
232 } else {
233 self.select_first(&Default::default(), window, cx);
234 }
235 }
236
237 fn select_previous(
238 &mut self,
239 _: &menu::SelectPrevious,
240 window: &mut Window,
241 cx: &mut Context<Self>,
242 ) {
243 if let Some(selected) = &mut self.selected_index {
244 *selected = selected.saturating_sub(1);
245 if *selected == 0 {
246 self.select_first(&Default::default(), window, cx);
247 } else if *selected >= self.matches.len() {
248 self.select_last(&Default::default(), window, cx);
249 } else {
250 cx.notify();
251 }
252 } else {
253 self.select_last(&Default::default(), window, cx);
254 }
255 }
256
257 fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
258 if self.matches.get(0).is_some() {
259 self.selected_index = Some(0);
260 cx.notify();
261 }
262 }
263
264 fn select_last(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
265 if self.matches.last().is_some() {
266 self.selected_index = Some(self.matches.len() - 1);
267 cx.notify();
268 }
269 }
270
271 fn focus_search(
272 &mut self,
273 _: &search::FocusSearch,
274 window: &mut Window,
275 cx: &mut Context<Self>,
276 ) {
277 if !self
278 .filter_editor
279 .focus_handle(cx)
280 .contains_focused(window, cx)
281 {
282 window.focus(&self.filter_editor.focus_handle(cx));
283 }
284 }
285}
286
287#[derive(Clone)]
288struct ProcessedKeybinding {
289 keystroke_text: SharedString,
290 action: SharedString,
291 context: SharedString,
292 source: Option<SharedString>,
293}
294
295impl Item for KeymapEditor {
296 type Event = ();
297
298 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
299 "Keymap Editor".into()
300 }
301}
302
303impl Render for KeymapEditor {
304 fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
305 let row_count = self.matches.len();
306 let theme = cx.theme();
307
308 div()
309 .key_context(self.dispatch_context(window, cx))
310 .on_action(cx.listener(Self::select_next))
311 .on_action(cx.listener(Self::select_previous))
312 .on_action(cx.listener(Self::select_first))
313 .on_action(cx.listener(Self::select_last))
314 .on_action(cx.listener(Self::focus_search))
315 .size_full()
316 .bg(theme.colors().background)
317 .id("keymap-editor")
318 .track_focus(&self.focus_handle)
319 .px_4()
320 .v_flex()
321 .pb_4()
322 .child(
323 h_flex()
324 .w_full()
325 .h_12()
326 .px_4()
327 .my_4()
328 .border_2()
329 .border_color(theme.colors().border)
330 .child(self.filter_editor.clone()),
331 )
332 .child(
333 Table::new()
334 .interactable(&self.table_interaction_state)
335 .striped()
336 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
337 .header(["Command", "Keystrokes", "Context", "Source"])
338 .selected_item_index(self.selected_index.clone())
339 .on_click_row(cx.processor(|this, row_index, _window, _cx| {
340 this.selected_index = Some(row_index);
341 }))
342 .uniform_list(
343 "keymap-editor-table",
344 row_count,
345 cx.processor(move |this, range: Range<usize>, _window, _cx| {
346 range
347 .filter_map(|index| {
348 let candidate_id = this.matches.get(index)?.candidate_id;
349 let binding = &this.keybindings[candidate_id];
350 Some(
351 [
352 binding.action.clone(),
353 binding.keystroke_text.clone(),
354 binding.context.clone(),
355 binding.source.clone().unwrap_or_default(),
356 ]
357 .map(IntoElement::into_any_element),
358 )
359 })
360 .collect()
361 }),
362 ),
363 )
364 }
365}
366
367impl SerializableItem for KeymapEditor {
368 fn serialized_item_kind() -> &'static str {
369 "KeymapEditor"
370 }
371
372 fn cleanup(
373 workspace_id: workspace::WorkspaceId,
374 alive_items: Vec<workspace::ItemId>,
375 _window: &mut Window,
376 cx: &mut App,
377 ) -> gpui::Task<gpui::Result<()>> {
378 workspace::delete_unloaded_items(
379 alive_items,
380 workspace_id,
381 "keybinding_editors",
382 &KEYBINDING_EDITORS,
383 cx,
384 )
385 }
386
387 fn deserialize(
388 _project: gpui::Entity<project::Project>,
389 _workspace: gpui::WeakEntity<Workspace>,
390 workspace_id: workspace::WorkspaceId,
391 item_id: workspace::ItemId,
392 window: &mut Window,
393 cx: &mut App,
394 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
395 window.spawn(cx, async move |cx| {
396 if KEYBINDING_EDITORS
397 .get_keybinding_editor(item_id, workspace_id)?
398 .is_some()
399 {
400 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
401 } else {
402 Err(anyhow!("No keybinding editor to deserialize"))
403 }
404 })
405 }
406
407 fn serialize(
408 &mut self,
409 workspace: &mut Workspace,
410 item_id: workspace::ItemId,
411 _closing: bool,
412 _window: &mut Window,
413 cx: &mut ui::Context<Self>,
414 ) -> Option<gpui::Task<gpui::Result<()>>> {
415 let workspace_id = workspace.database_id()?;
416 Some(cx.background_spawn(async move {
417 KEYBINDING_EDITORS
418 .save_keybinding_editor(item_id, workspace_id)
419 .await
420 }))
421 }
422
423 fn should_serialize(&self, _event: &Self::Event) -> bool {
424 false
425 }
426}
427
428mod persistence {
429 use db::{define_connection, query, sqlez_macros::sql};
430 use workspace::WorkspaceDb;
431
432 define_connection! {
433 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
434 &[sql!(
435 CREATE TABLE keybinding_editors (
436 workspace_id INTEGER,
437 item_id INTEGER UNIQUE,
438
439 PRIMARY KEY(workspace_id, item_id),
440 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
441 ON DELETE CASCADE
442 ) STRICT;
443 )];
444 }
445
446 impl KeybindingEditorDb {
447 query! {
448 pub async fn save_keybinding_editor(
449 item_id: workspace::ItemId,
450 workspace_id: workspace::WorkspaceId
451 ) -> Result<()> {
452 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
453 VALUES (?, ?)
454 }
455 }
456
457 query! {
458 pub fn get_keybinding_editor(
459 item_id: workspace::ItemId,
460 workspace_id: workspace::WorkspaceId
461 ) -> Result<Option<workspace::ItemId>> {
462 SELECT item_id
463 FROM keybinding_editors
464 WHERE item_id = ? AND workspace_id = ?
465 }
466 }
467 }
468}