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 = dbg!(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 dbg!(&query);
119 let fuzzy_match = cx.background_spawn(async move {
120 fuzzy::match_strings(
121 &string_match_candidates,
122 &query,
123 true,
124 true,
125 keybind_count,
126 &Default::default(),
127 executor,
128 )
129 .await
130 });
131
132 cx.spawn(async move |this, cx| {
133 let matches = fuzzy_match.await;
134 dbg!(&matches);
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
208#[derive(Clone)]
209struct ProcessedKeybinding {
210 keystroke_text: SharedString,
211 action: SharedString,
212 context: SharedString,
213 source: Option<SharedString>,
214}
215
216impl Item for KeymapEditor {
217 type Event = ();
218
219 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
220 "Keymap Editor".into()
221 }
222}
223
224impl Render for KeymapEditor {
225 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
226 let row_count = self.matches.len();
227 let theme = cx.theme();
228
229 dbg!(&self.matches);
230
231 div()
232 .size_full()
233 .bg(theme.colors().background)
234 .id("keymap-editor")
235 .track_focus(&self.focus_handle)
236 .child(self.filter_editor.clone())
237 .child(
238 Table::new()
239 .interactable(&self.table_interaction_state)
240 .striped()
241 .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
242 .header(["Command", "Keystrokes", "Context", "Source"])
243 .uniform_list(
244 "keymap-editor-table",
245 row_count,
246 cx.processor(move |this, range: Range<usize>, _window, _cx| {
247 range
248 .filter_map(|index| {
249 dbg!(index);
250 let candidate_id = this.matches.get(index)?.candidate_id;
251 let binding = &this.keybindings[candidate_id];
252 Some(
253 [
254 binding.action.clone(),
255 binding.keystroke_text.clone(),
256 binding.context.clone(),
257 binding.source.clone().unwrap_or_default(),
258 ]
259 .map(IntoElement::into_any_element),
260 )
261 })
262 .collect()
263 }),
264 ),
265 )
266 }
267}
268
269impl SerializableItem for KeymapEditor {
270 fn serialized_item_kind() -> &'static str {
271 "KeymapEditor"
272 }
273
274 fn cleanup(
275 workspace_id: workspace::WorkspaceId,
276 alive_items: Vec<workspace::ItemId>,
277 _window: &mut Window,
278 cx: &mut App,
279 ) -> gpui::Task<gpui::Result<()>> {
280 workspace::delete_unloaded_items(
281 alive_items,
282 workspace_id,
283 "keybinding_editors",
284 &KEYBINDING_EDITORS,
285 cx,
286 )
287 }
288
289 fn deserialize(
290 _project: gpui::Entity<project::Project>,
291 _workspace: gpui::WeakEntity<Workspace>,
292 workspace_id: workspace::WorkspaceId,
293 item_id: workspace::ItemId,
294 window: &mut Window,
295 cx: &mut App,
296 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
297 window.spawn(cx, async move |cx| {
298 if KEYBINDING_EDITORS
299 .get_keybinding_editor(item_id, workspace_id)?
300 .is_some()
301 {
302 cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(window, cx)))
303 } else {
304 Err(anyhow!("No keybinding editor to deserialize"))
305 }
306 })
307 }
308
309 fn serialize(
310 &mut self,
311 workspace: &mut Workspace,
312 item_id: workspace::ItemId,
313 _closing: bool,
314 _window: &mut Window,
315 cx: &mut ui::Context<Self>,
316 ) -> Option<gpui::Task<gpui::Result<()>>> {
317 let workspace_id = workspace.database_id()?;
318 Some(cx.background_spawn(async move {
319 KEYBINDING_EDITORS
320 .save_keybinding_editor(item_id, workspace_id)
321 .await
322 }))
323 }
324
325 fn should_serialize(&self, _event: &Self::Event) -> bool {
326 false
327 }
328}
329
330mod persistence {
331 use db::{define_connection, query, sqlez_macros::sql};
332 use workspace::WorkspaceDb;
333
334 define_connection! {
335 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
336 &[sql!(
337 CREATE TABLE keybinding_editors (
338 workspace_id INTEGER,
339 item_id INTEGER UNIQUE,
340
341 PRIMARY KEY(workspace_id, item_id),
342 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
343 ON DELETE CASCADE
344 ) STRICT;
345 )];
346 }
347
348 impl KeybindingEditorDb {
349 query! {
350 pub async fn save_keybinding_editor(
351 item_id: workspace::ItemId,
352 workspace_id: workspace::WorkspaceId
353 ) -> Result<()> {
354 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
355 VALUES (?, ?)
356 }
357 }
358
359 query! {
360 pub fn get_keybinding_editor(
361 item_id: workspace::ItemId,
362 workspace_id: workspace::WorkspaceId
363 ) -> Result<Option<workspace::ItemId>> {
364 SELECT item_id
365 FROM keybinding_editors
366 WHERE item_id = ? AND workspace_id = ?
367 }
368 }
369 }
370}