1use std::fmt::Write as _;
2
3use db::anyhow::anyhow;
4use gpui::{
5 AnyElement, AppContext as _, Context, EventEmitter, FocusHandle, Focusable, FontWeight, Global,
6 IntoElement, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Subscription,
7 UniformListScrollHandle, actions, div, px, uniform_list,
8};
9
10use editor::ShowScrollbar;
11use ui::{
12 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, Scrollbar, ScrollbarState,
13 SharedString, Styled as _, Window, prelude::*,
14};
15use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
16
17use crate::keybindings::persistence::KEYBINDING_EDITORS;
18
19actions!(zed, [OpenKeymapEditor]);
20
21pub fn init(cx: &mut App) {
22 let keymap_event_channel = KeymapEventChannel::new();
23 cx.set_global(keymap_event_channel);
24
25 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
26 workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
27 let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx));
28 workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
29 });
30 })
31 .detach();
32
33 register_serializable_item::<KeymapEditor>(cx);
34}
35
36pub struct KeymapEventChannel {}
37
38impl Global for KeymapEventChannel {}
39
40impl KeymapEventChannel {
41 fn new() -> Self {
42 Self {}
43 }
44
45 pub fn trigger_keymap_changed(cx: &mut App) {
46 cx.update_global(|_event_channel: &mut Self, _| {
47 dbg!("updating global");
48 *_event_channel = Self::new();
49 });
50 }
51}
52
53struct KeymapEditor {
54 focus_handle: FocusHandle,
55 _keymap_subscription: Subscription,
56 processed_bindings: Vec<ProcessedKeybinding>,
57 scroll_handle: UniformListScrollHandle,
58 vertical_scrollbar_state: ScrollbarState,
59 show_vertical_scrollbar: bool,
60}
61
62impl EventEmitter<()> for KeymapEditor {}
63
64impl Focusable for KeymapEditor {
65 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
66 self.focus_handle.clone()
67 }
68}
69
70impl KeymapEditor {
71 fn new(cx: &mut gpui::Context<Self>) -> Self {
72 let focus_handle = cx.focus_handle();
73
74 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
75 let key_bindings = Self::process_bindings(cx);
76 this.processed_bindings = key_bindings;
77 _ = this.update_scrollbar_visibility(cx);
78 });
79 let scroll_handle = UniformListScrollHandle::new();
80 let vertical_scrollbar_state = ScrollbarState::new(scroll_handle.clone());
81 let mut this = Self {
82 focus_handle: focus_handle.clone(),
83 _keymap_subscription,
84 processed_bindings: vec![],
85 scroll_handle,
86 vertical_scrollbar_state,
87 show_vertical_scrollbar: false,
88 };
89
90 this.update_scrollbar_visibility(cx);
91 this
92 }
93
94 fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
95 let key_bindings_ptr = cx.key_bindings();
96 let lock = key_bindings_ptr.borrow();
97 let key_bindings = lock.bindings();
98
99 let mut processed_bindings = Vec::new();
100
101 for key_binding in key_bindings {
102 let mut keystroke_text = String::new();
103 for keystroke in key_binding.keystrokes() {
104 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
105 }
106 let keystroke_text = keystroke_text.trim().to_string();
107
108 let context = key_binding
109 .predicate()
110 .map(|predicate| predicate.to_string())
111 .unwrap_or_else(|| "<global>".to_string());
112
113 processed_bindings.push(ProcessedKeybinding {
114 keystroke_text: keystroke_text.into(),
115 action: key_binding.action().name().into(),
116 context: context.into(),
117 })
118 }
119 processed_bindings
120 }
121
122 fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
123 use editor::EditorSettings;
124 use settings::Settings;
125
126 let show_setting = EditorSettings::get_global(cx).scrollbar.show;
127
128 self.show_vertical_scrollbar = match show_setting {
129 ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
130 ShowScrollbar::Never => false,
131 };
132
133 cx.notify();
134 }
135
136 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
137 div()
138 .id("keymap-editor-vertical-scroll")
139 .occlude()
140 .flex_none()
141 .h_full()
142 .cursor_default()
143 .absolute()
144 .right_0()
145 .top_0()
146 .bottom_0()
147 .w(px(12.))
148 .on_mouse_move(cx.listener(|_, _, _, cx| {
149 cx.notify();
150 cx.stop_propagation()
151 }))
152 .on_hover(|_, _, cx| {
153 cx.stop_propagation();
154 })
155 .on_any_mouse_down(|_, _, cx| {
156 cx.stop_propagation();
157 })
158 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
159 cx.notify();
160 }))
161 .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone()))
162 }
163}
164
165struct ProcessedKeybinding {
166 keystroke_text: SharedString,
167 action: SharedString,
168 context: SharedString,
169}
170
171impl Item for KeymapEditor {
172 type Event = ();
173
174 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
175 "Keymap Editor".into()
176 }
177}
178
179impl Render for KeymapEditor {
180 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
181 dbg!("rendering");
182 if self.processed_bindings.is_empty() {
183 self.processed_bindings = Self::process_bindings(cx);
184 self.update_scrollbar_visibility(cx);
185 }
186
187 let table = Table::new(self.processed_bindings.len());
188
189 let theme = cx.theme();
190 let headers = ["Command", "Keystrokes", "Context"].map(Into::into);
191
192 div()
193 .size_full()
194 .bg(theme.colors().background)
195 .track_focus(&self.focus_handle)
196 .child(
197 div()
198 .relative()
199 .size_full()
200 .child(
201 table
202 .render()
203 .h_full()
204 .child(table.render_header(headers, cx))
205 .child(
206 uniform_list(
207 cx.entity(),
208 "keybindings",
209 table.row_count,
210 move |this, range, _, cx| {
211 return range
212 .map(|index| {
213 table.render_row(
214 index,
215 [
216 string_cell(
217 this.processed_bindings[index]
218 .action
219 .clone(),
220 ),
221 string_cell(
222 this.processed_bindings[index]
223 .keystroke_text
224 .clone(),
225 ),
226 string_cell(
227 this.processed_bindings[index]
228 .context
229 .clone(),
230 ),
231 // TODO: Add a source field
232 // string_cell(keybinding.source().to_string()),
233 ],
234 cx,
235 )
236 })
237 .collect();
238 },
239 )
240 .size_full()
241 .flex_grow()
242 .track_scroll(self.scroll_handle.clone())
243 .with_sizing_behavior(ListSizingBehavior::Auto)
244 .with_horizontal_sizing_behavior(
245 ListHorizontalSizingBehavior::Unconstrained,
246 ),
247 ),
248 )
249 .when(self.show_vertical_scrollbar, |this| {
250 this.child(self.render_vertical_scrollbar(cx))
251 }),
252 )
253 }
254}
255
256/// A table component
257#[derive(Clone, Copy)]
258pub struct Table<const COLS: usize> {
259 striped: bool,
260 width: Length,
261 row_count: usize,
262}
263
264impl<const COLS: usize> Table<COLS> {
265 /// Create a new table with a column count equal to the
266 /// number of headers provided.
267 pub fn new(row_count: usize) -> Self {
268 Table {
269 striped: false,
270 width: Length::Auto,
271 row_count,
272 }
273 }
274
275 /// Enables row striping.
276 pub fn striped(mut self) -> Self {
277 self.striped = true;
278 self
279 }
280
281 /// Sets the width of the table.
282 pub fn width(mut self, width: impl Into<Length>) -> Self {
283 self.width = width.into();
284 self
285 }
286
287 fn base_cell_style(cx: &App) -> Div {
288 div()
289 .px_1p5()
290 .flex_1()
291 .justify_start()
292 .text_ui(cx)
293 .whitespace_nowrap()
294 .text_ellipsis()
295 .overflow_hidden()
296 }
297
298 pub fn render_row(&self, row_index: usize, items: [TableCell; COLS], cx: &App) -> AnyElement {
299 let is_last = row_index == self.row_count - 1;
300 let bg = if row_index % 2 == 1 && self.striped {
301 Some(cx.theme().colors().text.opacity(0.05))
302 } else {
303 None
304 };
305 div()
306 .w_full()
307 .flex()
308 .flex_row()
309 .items_center()
310 .justify_between()
311 .px_1p5()
312 .py_1()
313 .when_some(bg, |row, bg| row.bg(bg))
314 .when(!is_last, |row| {
315 row.border_b_1().border_color(cx.theme().colors().border)
316 })
317 .children(items.into_iter().map(|cell| match cell {
318 TableCell::String(s) => Self::base_cell_style(cx).child(s),
319 TableCell::Element(e) => Self::base_cell_style(cx).child(e),
320 }))
321 .into_any_element()
322 }
323
324 fn render_header(&self, headers: [SharedString; COLS], cx: &mut App) -> impl IntoElement {
325 div()
326 .flex()
327 .flex_row()
328 .items_center()
329 .justify_between()
330 .w_full()
331 .p_2()
332 .border_b_1()
333 .border_color(cx.theme().colors().border)
334 .children(headers.into_iter().map(|h| {
335 Self::base_cell_style(cx)
336 .font_weight(FontWeight::SEMIBOLD)
337 .child(h.clone())
338 }))
339 }
340
341 fn render(&self) -> Div {
342 div().w(self.width).overflow_hidden()
343 }
344}
345
346/// Represents a cell in a table.
347pub enum TableCell {
348 /// A cell containing a string value.
349 String(SharedString),
350 /// A cell containing a UI element.
351 Element(AnyElement),
352}
353
354/// Creates a `TableCell` containing a string value.
355pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
356 TableCell::String(s.into())
357}
358
359/// Creates a `TableCell` containing an element.
360pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
361 TableCell::Element(e.into())
362}
363
364impl<E> From<E> for TableCell
365where
366 E: Into<SharedString>,
367{
368 fn from(e: E) -> Self {
369 TableCell::String(e.into())
370 }
371}
372
373impl SerializableItem for KeymapEditor {
374 fn serialized_item_kind() -> &'static str {
375 "KeymapEditor"
376 }
377
378 fn cleanup(
379 workspace_id: workspace::WorkspaceId,
380 alive_items: Vec<workspace::ItemId>,
381 _window: &mut Window,
382 cx: &mut App,
383 ) -> gpui::Task<gpui::Result<()>> {
384 workspace::delete_unloaded_items(
385 alive_items,
386 workspace_id,
387 "keybinding_editors",
388 &KEYBINDING_EDITORS,
389 cx,
390 )
391 }
392
393 fn deserialize(
394 _project: gpui::Entity<project::Project>,
395 _workspace: gpui::WeakEntity<Workspace>,
396 workspace_id: workspace::WorkspaceId,
397 item_id: workspace::ItemId,
398 _window: &mut Window,
399 cx: &mut App,
400 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
401 cx.spawn(async move |cx| {
402 if KEYBINDING_EDITORS
403 .get_keybinding_editor(item_id, workspace_id)?
404 .is_some()
405 {
406 cx.new(|cx| KeymapEditor::new(cx))
407 } else {
408 Err(anyhow!("No keybinding editor to deserialize"))
409 }
410 })
411 }
412
413 fn serialize(
414 &mut self,
415 workspace: &mut Workspace,
416 item_id: workspace::ItemId,
417 _closing: bool,
418 _window: &mut Window,
419 cx: &mut ui::Context<Self>,
420 ) -> Option<gpui::Task<gpui::Result<()>>> {
421 let Some(workspace_id) = workspace.database_id() else {
422 return None;
423 };
424 Some(cx.background_spawn(async move {
425 KEYBINDING_EDITORS
426 .save_keybinding_editor(item_id, workspace_id)
427 .await
428 }))
429 }
430
431 fn should_serialize(&self, _event: &Self::Event) -> bool {
432 false
433 }
434}
435
436mod persistence {
437 use db::{define_connection, query, sqlez_macros::sql};
438 use workspace::WorkspaceDb;
439
440 define_connection! {
441 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
442 &[sql!(
443 CREATE TABLE keybinding_editors (
444 workspace_id INTEGER,
445 item_id INTEGER UNIQUE,
446
447 PRIMARY KEY(workspace_id, item_id),
448 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
449 ON DELETE CASCADE
450 ) STRICT;
451 )];
452 }
453
454 impl KeybindingEditorDb {
455 query! {
456 pub async fn save_keybinding_editor(
457 item_id: workspace::ItemId,
458 workspace_id: workspace::WorkspaceId
459 ) -> Result<()> {
460 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
461 VALUES (?, ?)
462 }
463 }
464
465 query! {
466 pub fn get_keybinding_editor(
467 item_id: workspace::ItemId,
468 workspace_id: workspace::WorkspaceId
469 ) -> Result<Option<workspace::ItemId>> {
470 SELECT item_id
471 FROM keybinding_editors
472 WHERE item_id = ? AND workspace_id = ?
473 }
474 }
475 }
476}