1use std::{fmt::Write as _, ops::Range};
2
3use db::anyhow::anyhow;
4use gpui::{
5 AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, actions, div,
6};
7
8use ui::{
9 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render,
10 SharedString, Styled as _, Window, prelude::*,
11};
12use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
13
14use crate::{
15 keybindings::persistence::KEYBINDING_EDITORS,
16 ui_components::table::{Table, TableInteractionState},
17};
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 = KeymapEditor::new(window, 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 table_interaction_state: Entity<TableInteractionState>,
57}
58
59impl EventEmitter<()> for KeymapEditor {}
60
61impl Focusable for KeymapEditor {
62 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
63 self.focus_handle.clone()
64 }
65}
66
67impl KeymapEditor {
68 fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
69 let this = cx.new(|cx| {
70 let focus_handle = cx.focus_handle();
71
72 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
73 let key_bindings = Self::process_bindings(cx);
74 this.processed_bindings = key_bindings;
75 });
76
77 let table_interaction_state = TableInteractionState::new(window, cx);
78
79 Self {
80 focus_handle: focus_handle.clone(),
81 _keymap_subscription,
82 processed_bindings: vec![],
83 table_interaction_state,
84 }
85 });
86 this
87 }
88
89 fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
90 let key_bindings_ptr = cx.key_bindings();
91 let lock = key_bindings_ptr.borrow();
92 let key_bindings = lock.bindings();
93
94 let mut processed_bindings = Vec::new();
95
96 for key_binding in key_bindings {
97 let mut keystroke_text = String::new();
98 for keystroke in key_binding.keystrokes() {
99 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
100 }
101 let keystroke_text = keystroke_text.trim().to_string();
102
103 let context = key_binding
104 .predicate()
105 .map(|predicate| predicate.to_string())
106 .unwrap_or_else(|| "<global>".to_string());
107
108 processed_bindings.push(ProcessedKeybinding {
109 keystroke_text: keystroke_text.into(),
110 action: key_binding.action().name().into(),
111 context: context.into(),
112 })
113 }
114 processed_bindings
115 }
116}
117
118struct ProcessedKeybinding {
119 keystroke_text: SharedString,
120 action: SharedString,
121 context: SharedString,
122}
123
124impl Item for KeymapEditor {
125 type Event = ();
126
127 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
128 "Keymap Editor".into()
129 }
130}
131
132impl Render for KeymapEditor {
133 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
134 if self.processed_bindings.is_empty() {
135 self.processed_bindings = Self::process_bindings(cx);
136 }
137
138 // let scroll_track_size = px(16.);
139 // let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar {
140 // // magic number
141 // px(3.)
142 // } else {
143 // px(0.)
144 // };
145
146 let row_count = self.processed_bindings.len();
147
148 let theme = cx.theme();
149
150 div()
151 .size_full()
152 .bg(theme.colors().background)
153 .id("keymap-editor")
154 .track_focus(&self.focus_handle)
155 .child(
156 Table::uniform_list(
157 "keymap-editor-table",
158 row_count,
159 cx.processor(|this, range: Range<usize>, window, cx| {
160 range
161 .map(|index| {
162 let binding = &this.processed_bindings[index];
163 let row = [
164 binding.action.clone(),
165 binding.keystroke_text.clone(),
166 binding.context.clone(),
167 // TODO: Add a source field
168 // binding.source.clone(),
169 ];
170
171 // fixme: pass through callback as a row_cx param
172 let striped = false;
173
174 crate::ui_components::table::render_row(
175 index, row, row_count, striped, cx,
176 )
177 })
178 .collect()
179 }),
180 )
181 .header(["Command", "Keystrokes", "Context"])
182 .interactable(&self.table_interaction_state),
183 )
184 // .child(
185 // table
186 // .h_full()
187 // .v_flex()
188 // .child(table.render_header(headers, cx))
189 // .child(
190 // div()
191 // .flex_grow()
192 // .w_full()
193 // .relative()
194 // .overflow_hidden()
195 // .child(
196 // uniform_list(
197 // "keybindings",
198 // row_count,
199 // cx.processor(move |this, range, _, cx| {}),
200 // )
201 // .size_full()
202 // .flex_grow()
203 // .track_scroll(self.scroll_handle.clone())
204 // .with_sizing_behavior(ListSizingBehavior::Auto)
205 // .with_horizontal_sizing_behavior(
206 // ListHorizontalSizingBehavior::Unconstrained,
207 // ),
208 // )
209 // .when(self.vertical_scrollbar.show_track, |this| {
210 // this.child(
211 // v_flex()
212 // .h_full()
213 // .flex_none()
214 // .w(scroll_track_size)
215 // .bg(cx.theme().colors().background)
216 // .child(
217 // div()
218 // .size_full()
219 // .flex_1()
220 // .border_l_1()
221 // .border_color(cx.theme().colors().border),
222 // ),
223 // )
224 // })
225 // .when(self.vertical_scrollbar.show_scrollbar, |this| {
226 // this.child(self.render_vertical_scrollbar(cx))
227 // }),
228 // )
229 // .when(self.horizontal_scrollbar.show_track, |this| {
230 // this.child(
231 // h_flex()
232 // .w_full()
233 // .h(scroll_track_size)
234 // .flex_none()
235 // .relative()
236 // .child(
237 // div()
238 // .w_full()
239 // .flex_1()
240 // // for some reason the horizontal scrollbar is 1px
241 // // taller than the vertical scrollbar??
242 // .h(scroll_track_size - px(1.))
243 // .bg(cx.theme().colors().background)
244 // .border_t_1()
245 // .border_color(cx.theme().colors().border),
246 // )
247 // .when(self.vertical_scrollbar.show_track, |this| {
248 // this.child(
249 // div()
250 // .flex_none()
251 // // -1px prevents a missing pixel between the two container borders
252 // .w(scroll_track_size - px(1.))
253 // .h_full(),
254 // )
255 // .child(
256 // // HACK: Fill the missing 1px 🥲
257 // div()
258 // .absolute()
259 // .right(scroll_track_size - px(1.))
260 // .bottom(scroll_track_size - px(1.))
261 // .size_px()
262 // .bg(cx.theme().colors().border),
263 // )
264 // }),
265 // )
266 // })
267 // .when(self.horizontal_scrollbar.show_scrollbar, |this| {
268 // this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx))
269 // }),
270 // )
271 }
272}
273
274impl SerializableItem for KeymapEditor {
275 fn serialized_item_kind() -> &'static str {
276 "KeymapEditor"
277 }
278
279 fn cleanup(
280 workspace_id: workspace::WorkspaceId,
281 alive_items: Vec<workspace::ItemId>,
282 _window: &mut Window,
283 cx: &mut App,
284 ) -> gpui::Task<gpui::Result<()>> {
285 workspace::delete_unloaded_items(
286 alive_items,
287 workspace_id,
288 "keybinding_editors",
289 &KEYBINDING_EDITORS,
290 cx,
291 )
292 }
293
294 fn deserialize(
295 _project: gpui::Entity<project::Project>,
296 _workspace: gpui::WeakEntity<Workspace>,
297 workspace_id: workspace::WorkspaceId,
298 item_id: workspace::ItemId,
299 window: &mut Window,
300 cx: &mut App,
301 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
302 window.spawn(cx, async move |cx| {
303 if KEYBINDING_EDITORS
304 .get_keybinding_editor(item_id, workspace_id)?
305 .is_some()
306 {
307 cx.update(KeymapEditor::new)
308 } else {
309 Err(anyhow!("No keybinding editor to deserialize"))
310 }
311 })
312 }
313
314 fn serialize(
315 &mut self,
316 workspace: &mut Workspace,
317 item_id: workspace::ItemId,
318 _closing: bool,
319 _window: &mut Window,
320 cx: &mut ui::Context<Self>,
321 ) -> Option<gpui::Task<gpui::Result<()>>> {
322 let Some(workspace_id) = workspace.database_id() else {
323 return None;
324 };
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}