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