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