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