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