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 .child(self.filter_editor.clone())
233 .child(
234 Table::new()
235 .interactable(&self.table_interaction_state)
236 .striped()
237 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
238 .header(["Command", "Keystrokes", "Context", "Source"])
239 .uniform_list(
240 "keymap-editor-table",
241 row_count,
242 cx.processor(move |this, range: Range<usize>, _window, _cx| {
243 range
244 .filter_map(|index| {
245 let candidate_id = this.matches.get(index)?.candidate_id;
246 let binding = &this.keybindings[candidate_id];
247 Some(
248 [
249 binding.action.clone(),
250 binding.keystroke_text.clone(),
251 binding.context.clone(),
252 binding.source.clone().unwrap_or_default(),
253 ]
254 .map(IntoElement::into_any_element),
255 )
256 })
257 .collect()
258 }),
259 ),
260 )
261 }
262}
263
264impl SerializableItem for KeymapEditor {
265 fn serialized_item_kind() -> &'static str {
266 "KeymapEditor"
267 }
268
269 fn cleanup(
270 workspace_id: workspace::WorkspaceId,
271 alive_items: Vec<workspace::ItemId>,
272 _window: &mut Window,
273 cx: &mut App,
274 ) -> gpui::Task<gpui::Result<()>> {
275 workspace::delete_unloaded_items(
276 alive_items,
277 workspace_id,
278 "keybinding_editors",
279 &KEYBINDING_EDITORS,
280 cx,
281 )
282 }
283
284 fn deserialize(
285 _project: gpui::Entity<project::Project>,
286 _workspace: gpui::WeakEntity<Workspace>,
287 workspace_id: workspace::WorkspaceId,
288 item_id: workspace::ItemId,
289 window: &mut Window,
290 cx: &mut App,
291 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
292 window.spawn(cx, async move |cx| {
293 if KEYBINDING_EDITORS
294 .get_keybinding_editor(item_id, workspace_id)?
295 .is_some()
296 {
297 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
298 } else {
299 Err(anyhow!("No keybinding editor to deserialize"))
300 }
301 })
302 }
303
304 fn serialize(
305 &mut self,
306 workspace: &mut Workspace,
307 item_id: workspace::ItemId,
308 _closing: bool,
309 _window: &mut Window,
310 cx: &mut ui::Context<Self>,
311 ) -> Option<gpui::Task<gpui::Result<()>>> {
312 let workspace_id = workspace.database_id()?;
313 Some(cx.background_spawn(async move {
314 KEYBINDING_EDITORS
315 .save_keybinding_editor(item_id, workspace_id)
316 .await
317 }))
318 }
319
320 fn should_serialize(&self, _event: &Self::Event) -> bool {
321 false
322 }
323}
324
325mod persistence {
326 use db::{define_connection, query, sqlez_macros::sql};
327 use workspace::WorkspaceDb;
328
329 define_connection! {
330 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
331 &[sql!(
332 CREATE TABLE keybinding_editors (
333 workspace_id INTEGER,
334 item_id INTEGER UNIQUE,
335
336 PRIMARY KEY(workspace_id, item_id),
337 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
338 ON DELETE CASCADE
339 ) STRICT;
340 )];
341 }
342
343 impl KeybindingEditorDb {
344 query! {
345 pub async fn save_keybinding_editor(
346 item_id: workspace::ItemId,
347 workspace_id: workspace::WorkspaceId
348 ) -> Result<()> {
349 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
350 VALUES (?, ?)
351 }
352 }
353
354 query! {
355 pub fn get_keybinding_editor(
356 item_id: workspace::ItemId,
357 workspace_id: workspace::WorkspaceId
358 ) -> Result<Option<workspace::ItemId>> {
359 SELECT item_id
360 FROM keybinding_editors
361 WHERE item_id = ? AND workspace_id = ?
362 }
363 }
364 }
365}