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