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 SerializableItem for KeymapEditor {
115 fn serialized_item_kind() -> &'static str {
116 "KeymapEditor"
117 }
118
119 fn cleanup(
120 workspace_id: workspace::WorkspaceId,
121 alive_items: Vec<workspace::ItemId>,
122 _window: &mut Window,
123 cx: &mut App,
124 ) -> gpui::Task<gpui::Result<()>> {
125 workspace::delete_unloaded_items(
126 alive_items,
127 workspace_id,
128 "keybinding_editors",
129 &KEYBINDING_EDITORS,
130 cx,
131 )
132 }
133
134 fn deserialize(
135 _project: gpui::Entity<project::Project>,
136 _workspace: gpui::WeakEntity<Workspace>,
137 workspace_id: workspace::WorkspaceId,
138 item_id: workspace::ItemId,
139 _window: &mut Window,
140 cx: &mut App,
141 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
142 cx.spawn(async move |cx| {
143 if KEYBINDING_EDITORS
144 .get_keybinding_editor(item_id, workspace_id)?
145 .is_some()
146 {
147 cx.new(|cx| KeymapEditor::new(cx))
148 } else {
149 Err(anyhow!("No keybinding editor to deserialize"))
150 }
151 })
152 }
153
154 fn serialize(
155 &mut self,
156 workspace: &mut Workspace,
157 item_id: workspace::ItemId,
158 _closing: bool,
159 _window: &mut Window,
160 cx: &mut ui::Context<Self>,
161 ) -> Option<gpui::Task<gpui::Result<()>>> {
162 let Some(workspace_id) = workspace.database_id() else {
163 return None;
164 };
165 Some(cx.background_spawn(async move {
166 KEYBINDING_EDITORS
167 .save_keybinding_editor(item_id, workspace_id)
168 .await
169 }))
170 }
171
172 fn should_serialize(&self, _event: &Self::Event) -> bool {
173 false
174 }
175}
176
177impl Item for KeymapEditor {
178 type Event = ();
179
180 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
181 "Keymap Editor".into()
182 }
183}
184
185impl Render for KeymapEditor {
186 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
187 dbg!("rendering");
188 if self.processed_bindings.is_empty() {
189 self.processed_bindings = Self::process_bindings(cx);
190 }
191
192 let table = Table::new(self.processed_bindings.len());
193
194 let theme = cx.theme();
195 let headers = ["Command", "Keystrokes", "Context"].map(Into::into);
196
197 div().size_full().bg(theme.colors().background).child(
198 table
199 .render()
200 .h_full()
201 .child(table.render_header(headers, cx))
202 .child(
203 uniform_list(
204 cx.entity(),
205 "keybindings",
206 table.row_count,
207 move |this, range, _, cx| {
208 return range
209 .map(|index| {
210 table.render_row(
211 index,
212 [
213 string_cell(
214 this.processed_bindings[index].action.clone(),
215 ),
216 string_cell(
217 this.processed_bindings[index]
218 .keystroke_text
219 .clone(),
220 ),
221 string_cell(
222 this.processed_bindings[index].context.clone(),
223 ),
224 // TODO: Add a source field
225 // string_cell(keybinding.source().to_string()),
226 ],
227 cx,
228 )
229 })
230 .collect();
231 },
232 )
233 .size_full()
234 .flex_grow()
235 .with_sizing_behavior(ListSizingBehavior::Auto)
236 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained),
237 ),
238 )
239 }
240}
241
242/// A table component
243#[derive(Clone, Copy)]
244pub struct Table<const COLS: usize> {
245 striped: bool,
246 width: Length,
247 row_count: usize,
248}
249
250impl<const COLS: usize> Table<COLS> {
251 /// Create a new table with a column count equal to the
252 /// number of headers provided.
253 pub fn new(row_count: usize) -> Self {
254 Table {
255 striped: false,
256 width: Length::Auto,
257 row_count,
258 }
259 }
260
261 /// Enables row striping.
262 pub fn striped(mut self) -> Self {
263 self.striped = true;
264 self
265 }
266
267 /// Sets the width of the table.
268 pub fn width(mut self, width: impl Into<Length>) -> Self {
269 self.width = width.into();
270 self
271 }
272
273 fn base_cell_style(cx: &App) -> Div {
274 div()
275 .px_1p5()
276 .flex_1()
277 .justify_start()
278 .text_ui(cx)
279 .whitespace_nowrap()
280 .text_ellipsis()
281 .overflow_hidden()
282 }
283
284 pub fn render_row(&self, row_index: usize, items: [TableCell; COLS], cx: &App) -> AnyElement {
285 let is_last = row_index == self.row_count - 1;
286 let bg = if row_index % 2 == 1 && self.striped {
287 Some(cx.theme().colors().text.opacity(0.05))
288 } else {
289 None
290 };
291 div()
292 .w_full()
293 .flex()
294 .flex_row()
295 .items_center()
296 .justify_between()
297 .px_1p5()
298 .py_1()
299 .when_some(bg, |row, bg| row.bg(bg))
300 .when(!is_last, |row| {
301 row.border_b_1().border_color(cx.theme().colors().border)
302 })
303 .children(items.into_iter().map(|cell| match cell {
304 TableCell::String(s) => Self::base_cell_style(cx).child(s),
305 TableCell::Element(e) => Self::base_cell_style(cx).child(e),
306 }))
307 .into_any_element()
308 }
309
310 fn render_header(&self, headers: [SharedString; COLS], cx: &mut App) -> impl IntoElement {
311 div()
312 .flex()
313 .flex_row()
314 .items_center()
315 .justify_between()
316 .w_full()
317 .p_2()
318 .border_b_1()
319 .border_color(cx.theme().colors().border)
320 .children(headers.into_iter().map(|h| {
321 Self::base_cell_style(cx)
322 .font_weight(FontWeight::SEMIBOLD)
323 .child(h.clone())
324 }))
325 }
326
327 fn render(&self) -> Div {
328 div().w(self.width).overflow_hidden()
329 }
330}
331
332/// Represents a cell in a table.
333pub enum TableCell {
334 /// A cell containing a string value.
335 String(SharedString),
336 /// A cell containing a UI element.
337 Element(AnyElement),
338}
339
340/// Creates a `TableCell` containing a string value.
341pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
342 TableCell::String(s.into())
343}
344
345/// Creates a `TableCell` containing an element.
346pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
347 TableCell::Element(e.into())
348}
349
350impl<E> From<E> for TableCell
351where
352 E: Into<SharedString>,
353{
354 fn from(e: E) -> Self {
355 TableCell::String(e.into())
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}