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, ScrollStrategy,
8 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}
65
66impl EventEmitter<()> for KeymapEditor {}
67
68impl Focusable for KeymapEditor {
69 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
70 self.focus_handle.clone()
71 }
72}
73
74impl KeymapEditor {
75 fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
76 let focus_handle = cx.focus_handle();
77
78 let _keymap_subscription =
79 cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
80 let table_interaction_state = TableInteractionState::new(window, cx);
81
82 let filter_editor = cx.new(|cx| {
83 let mut editor = Editor::single_line(window, cx);
84 editor.set_placeholder_text("Filter action names...", cx);
85 editor
86 });
87
88 cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
89 if !matches!(e, EditorEvent::BufferEdited) {
90 return;
91 }
92
93 this.update_matches(cx);
94 })
95 .detach();
96
97 let mut this = Self {
98 keybindings: vec![],
99 string_match_candidates: Arc::new(vec![]),
100 matches: vec![],
101 focus_handle: focus_handle.clone(),
102 _keymap_subscription,
103 table_interaction_state,
104 filter_editor,
105 };
106
107 this.update_keybindings(cx);
108
109 this
110 }
111
112 fn update_matches(&mut self, cx: &mut Context<Self>) {
113 let query = self.filter_editor.read(cx).text(cx);
114 let string_match_candidates = self.string_match_candidates.clone();
115 let executor = cx.background_executor().clone();
116 let keybind_count = self.keybindings.len();
117 let query = command_palette::normalize_action_query(&query);
118 let fuzzy_match = cx.background_spawn(async move {
119 fuzzy::match_strings(
120 &string_match_candidates,
121 &query,
122 true,
123 true,
124 keybind_count,
125 &Default::default(),
126 executor,
127 )
128 .await
129 });
130
131 cx.spawn(async move |this, cx| {
132 let matches = fuzzy_match.await;
133 this.update(cx, |this, cx| {
134 this.table_interaction_state.update(cx, |this, _cx| {
135 this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
136 });
137 this.matches = matches;
138 cx.notify();
139 })
140 })
141 .detach();
142 }
143
144 fn process_bindings(
145 cx: &mut Context<Self>,
146 ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
147 let key_bindings_ptr = cx.key_bindings();
148 let lock = key_bindings_ptr.borrow();
149 let key_bindings = lock.bindings();
150
151 let mut processed_bindings = Vec::new();
152 let mut string_match_candidates = Vec::new();
153
154 for key_binding in key_bindings {
155 let mut keystroke_text = String::new();
156 for keystroke in key_binding.keystrokes() {
157 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
158 }
159 let keystroke_text = keystroke_text.trim().to_string();
160
161 let context = key_binding
162 .predicate()
163 .map(|predicate| predicate.to_string())
164 .unwrap_or_else(|| "<global>".to_string());
165
166 let source = key_binding
167 .meta()
168 .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
169
170 let action_name = key_binding.action().name();
171
172 let index = processed_bindings.len();
173 let string_match_candidate = StringMatchCandidate::new(index, &action_name);
174 processed_bindings.push(ProcessedKeybinding {
175 keystroke_text: keystroke_text.into(),
176 action: action_name.into(),
177 context: context.into(),
178 source,
179 });
180 string_match_candidates.push(string_match_candidate);
181 }
182 (processed_bindings, string_match_candidates)
183 }
184
185 fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
186 let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
187 self.keybindings = key_bindings;
188 self.string_match_candidates = Arc::new(string_match_candidates);
189 self.matches = self
190 .string_match_candidates
191 .iter()
192 .enumerate()
193 .map(|(ix, candidate)| StringMatch {
194 candidate_id: ix,
195 score: 0.0,
196 positions: vec![],
197 string: candidate.string.clone(),
198 })
199 .collect();
200
201 self.update_matches(cx);
202 cx.notify();
203 }
204}
205
206#[derive(Clone)]
207struct ProcessedKeybinding {
208 keystroke_text: SharedString,
209 action: SharedString,
210 context: SharedString,
211 source: Option<SharedString>,
212}
213
214impl Item for KeymapEditor {
215 type Event = ();
216
217 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
218 "Keymap Editor".into()
219 }
220}
221
222impl Render for KeymapEditor {
223 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
224 let row_count = self.matches.len();
225 let theme = cx.theme();
226
227 div()
228 .size_full()
229 .bg(theme.colors().background)
230 .id("keymap-editor")
231 .track_focus(&self.focus_handle)
232 .px_4()
233 .v_flex()
234 .pb_4()
235 .child(
236 h_flex()
237 .w_full()
238 .h_12()
239 .px_4()
240 .my_4()
241 .border_2()
242 .border_color(theme.colors().border)
243 .child(self.filter_editor.clone()),
244 )
245 .child(
246 Table::new()
247 .interactable(&self.table_interaction_state)
248 .striped()
249 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
250 .header(["Command", "Keystrokes", "Context", "Source"])
251 .uniform_list(
252 "keymap-editor-table",
253 row_count,
254 cx.processor(move |this, range: Range<usize>, _window, _cx| {
255 range
256 .filter_map(|index| {
257 let candidate_id = this.matches.get(index)?.candidate_id;
258 let binding = &this.keybindings[candidate_id];
259 Some(
260 [
261 binding.action.clone(),
262 binding.keystroke_text.clone(),
263 binding.context.clone(),
264 binding.source.clone().unwrap_or_default(),
265 ]
266 .map(IntoElement::into_any_element),
267 )
268 })
269 .collect()
270 }),
271 ),
272 )
273 }
274}
275
276impl SerializableItem for KeymapEditor {
277 fn serialized_item_kind() -> &'static str {
278 "KeymapEditor"
279 }
280
281 fn cleanup(
282 workspace_id: workspace::WorkspaceId,
283 alive_items: Vec<workspace::ItemId>,
284 _window: &mut Window,
285 cx: &mut App,
286 ) -> gpui::Task<gpui::Result<()>> {
287 workspace::delete_unloaded_items(
288 alive_items,
289 workspace_id,
290 "keybinding_editors",
291 &KEYBINDING_EDITORS,
292 cx,
293 )
294 }
295
296 fn deserialize(
297 _project: gpui::Entity<project::Project>,
298 _workspace: gpui::WeakEntity<Workspace>,
299 workspace_id: workspace::WorkspaceId,
300 item_id: workspace::ItemId,
301 window: &mut Window,
302 cx: &mut App,
303 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
304 window.spawn(cx, async move |cx| {
305 if KEYBINDING_EDITORS
306 .get_keybinding_editor(item_id, workspace_id)?
307 .is_some()
308 {
309 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
310 } else {
311 Err(anyhow!("No keybinding editor to deserialize"))
312 }
313 })
314 }
315
316 fn serialize(
317 &mut self,
318 workspace: &mut Workspace,
319 item_id: workspace::ItemId,
320 _closing: bool,
321 _window: &mut Window,
322 cx: &mut ui::Context<Self>,
323 ) -> Option<gpui::Task<gpui::Result<()>>> {
324 let workspace_id = workspace.database_id()?;
325 Some(cx.background_spawn(async move {
326 KEYBINDING_EDITORS
327 .save_keybinding_editor(item_id, workspace_id)
328 .await
329 }))
330 }
331
332 fn should_serialize(&self, _event: &Self::Event) -> bool {
333 false
334 }
335}
336
337mod persistence {
338 use db::{define_connection, query, sqlez_macros::sql};
339 use workspace::WorkspaceDb;
340
341 define_connection! {
342 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
343 &[sql!(
344 CREATE TABLE keybinding_editors (
345 workspace_id INTEGER,
346 item_id INTEGER UNIQUE,
347
348 PRIMARY KEY(workspace_id, item_id),
349 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
350 ON DELETE CASCADE
351 ) STRICT;
352 )];
353 }
354
355 impl KeybindingEditorDb {
356 query! {
357 pub async fn save_keybinding_editor(
358 item_id: workspace::ItemId,
359 workspace_id: workspace::WorkspaceId
360 ) -> Result<()> {
361 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
362 VALUES (?, ?)
363 }
364 }
365
366 query! {
367 pub fn get_keybinding_editor(
368 item_id: workspace::ItemId,
369 workspace_id: workspace::WorkspaceId
370 ) -> Result<Option<workspace::ItemId>> {
371 SELECT item_id
372 FROM keybinding_editors
373 WHERE item_id = ? AND workspace_id = ?
374 }
375 }
376 }
377}