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