1use std::{cell::RefCell, fmt::Write as _, rc::Rc};
2
3use db::anyhow::anyhow;
4use gpui::{
5 AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Keymap,
6 Subscription, Task, actions,
7};
8use ui::{
9 ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
10 Table, Window, div, string_cell,
11};
12use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
13
14use crate::keybindings::persistence::KEYBINDING_EDITORS;
15
16actions!(zed, [OpenKeymapEditor]);
17
18pub fn init(cx: &mut App) {
19 let keymap_event_channel = KeymapEventChannel::new();
20 cx.set_global(keymap_event_channel);
21
22 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
23 workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
24 let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx));
25 workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
26 });
27 })
28 .detach();
29
30 register_serializable_item::<KeymapEditor>(cx);
31}
32
33enum KeymapEvent {
34 KeymapChanged,
35}
36
37pub struct KeymapEventChannel {}
38
39impl EventEmitter<KeymapEvent> for KeymapEventChannel {}
40impl Global for KeymapEventChannel {}
41
42impl KeymapEventChannel {
43 fn new() -> Self {
44 Self {}
45 }
46
47 pub fn trigger_keymap_changed(cx: &mut App) {
48 cx.update_global(|_event_channel: &mut Self, _| {
49 dbg!("updating global");
50 *_event_channel = Self::new();
51 });
52 }
53}
54
55struct KeymapEditor {
56 focus_handle: FocusHandle,
57 _keymap_subscription: Subscription,
58 processed_bindings: Vec<ProcessedKeybinding>,
59}
60
61impl EventEmitter<()> for KeymapEditor {}
62
63impl Focusable for KeymapEditor {
64 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
65 self.focus_handle.clone()
66 }
67}
68
69impl KeymapEditor {
70 fn new(cx: &mut gpui::Context<Self>) -> Self {
71 let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(|this, cx| {
72 let key_bindings = Self::process_bindings(cx);
73 this.processed_bindings = key_bindings;
74 });
75 Self {
76 focus_handle: cx.focus_handle(),
77 _keymap_subscription,
78 processed_bindings: vec![],
79 }
80 }
81
82 fn process_bindings(cx: &mut Context<Self>) -> Vec<ProcessedKeybinding> {
83 let key_bindings_ptr = cx.key_bindings();
84 let lock = key_bindings_ptr.borrow();
85 let key_bindings = lock.bindings();
86
87 let mut processed_bindings = Vec::new();
88
89 for key_binding in key_bindings {
90 let mut keystroke_text = String::new();
91 for keystroke in key_binding.keystrokes() {
92 write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
93 }
94 let keystroke_text = keystroke_text.trim().to_string();
95
96 let context = key_binding
97 .predicate()
98 .map(|predicate| predicate.to_string())
99 .unwrap_or_else(|| "<global>".to_string());
100
101 processed_bindings.push(ProcessedKeybinding {
102 keystroke_text: keystroke_text.into(),
103 action: key_binding.action().name().into(),
104 context: context.into(),
105 })
106 }
107 processed_bindings
108 }
109}
110
111struct ProcessedKeybinding {
112 keystroke_text: SharedString,
113 action: SharedString,
114 context: SharedString,
115}
116
117impl SerializableItem for KeymapEditor {
118 fn serialized_item_kind() -> &'static str {
119 "KeymapEditor"
120 }
121
122 fn cleanup(
123 workspace_id: workspace::WorkspaceId,
124 alive_items: Vec<workspace::ItemId>,
125 _window: &mut Window,
126 cx: &mut App,
127 ) -> gpui::Task<gpui::Result<()>> {
128 workspace::delete_unloaded_items(
129 alive_items,
130 workspace_id,
131 "keybinding_editors",
132 &KEYBINDING_EDITORS,
133 cx,
134 )
135 }
136
137 fn deserialize(
138 _project: gpui::Entity<project::Project>,
139 _workspace: gpui::WeakEntity<Workspace>,
140 workspace_id: workspace::WorkspaceId,
141 item_id: workspace::ItemId,
142 _window: &mut Window,
143 cx: &mut App,
144 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
145 cx.spawn(async move |cx| {
146 if KEYBINDING_EDITORS
147 .get_keybinding_editor(item_id, workspace_id)?
148 .is_some()
149 {
150 cx.new(|cx| KeymapEditor::new(cx))
151 } else {
152 Err(anyhow!("No keybinding editor to deserialize"))
153 }
154 })
155 }
156
157 fn serialize(
158 &mut self,
159 workspace: &mut Workspace,
160 item_id: workspace::ItemId,
161 _closing: bool,
162 _window: &mut Window,
163 cx: &mut ui::Context<Self>,
164 ) -> Option<gpui::Task<gpui::Result<()>>> {
165 let Some(workspace_id) = workspace.database_id() else {
166 return None;
167 };
168 Some(cx.background_spawn(async move {
169 KEYBINDING_EDITORS
170 .save_keybinding_editor(item_id, workspace_id)
171 .await
172 }))
173 }
174
175 fn should_serialize(&self, _event: &Self::Event) -> bool {
176 false
177 }
178}
179
180impl Item for KeymapEditor {
181 type Event = ();
182
183 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
184 "Keymap Editor".into()
185 }
186}
187
188impl Render for KeymapEditor {
189 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
190 dbg!("rendering");
191 if self.processed_bindings.is_empty() {
192 self.processed_bindings = Self::process_bindings(cx);
193 }
194
195 let mut table = Table::new(vec!["Command", "Keystrokes", "Context"]);
196 for key_binding in &self.processed_bindings {
197 table = table.row(vec![
198 string_cell(key_binding.action.clone()),
199 string_cell(key_binding.keystroke_text.clone()),
200 string_cell(key_binding.context.clone()),
201 // TODO: Add a source field
202 // string_cell(keybinding.source().to_string()),
203 ]);
204 }
205
206 let theme = cx.theme();
207
208 div()
209 .size_full()
210 .bg(theme.colors().background)
211 .child(table.striped())
212 }
213}
214
215mod persistence {
216 use db::{define_connection, query, sqlez_macros::sql};
217 use workspace::WorkspaceDb;
218
219 define_connection! {
220 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
221 &[sql!(
222 CREATE TABLE keybinding_editors (
223 workspace_id INTEGER,
224 item_id INTEGER UNIQUE,
225
226 PRIMARY KEY(workspace_id, item_id),
227 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
228 ON DELETE CASCADE
229 ) STRICT;
230 )];
231 }
232
233 impl KeybindingEditorDb {
234 query! {
235 pub async fn save_keybinding_editor(
236 item_id: workspace::ItemId,
237 workspace_id: workspace::WorkspaceId
238 ) -> Result<()> {
239 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
240 VALUES (?, ?)
241 }
242 }
243
244 query! {
245 pub fn get_keybinding_editor(
246 item_id: workspace::ItemId,
247 workspace_id: workspace::WorkspaceId
248 ) -> Result<Option<workspace::ItemId>> {
249 SELECT item_id
250 FROM keybinding_editors
251 WHERE item_id = ? AND workspace_id = ?
252 }
253 }
254 }
255}