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 let source = key_binding
110 .meta()
111 .map(|meta| settings::KeybindSource::from_meta(meta).name().into());
112
113 processed_bindings.push(ProcessedKeybinding {
114 keystroke_text: keystroke_text.into(),
115 action: key_binding.action().name().into(),
116 context: context.into(),
117 source,
118 })
119 }
120 processed_bindings
121 }
122}
123
124struct ProcessedKeybinding {
125 keystroke_text: SharedString,
126 action: SharedString,
127 context: SharedString,
128 source: Option<SharedString>,
129}
130
131impl Item for KeymapEditor {
132 type Event = ();
133
134 fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
135 "Keymap Editor".into()
136 }
137}
138
139impl Render for KeymapEditor {
140 fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
141 if self.processed_bindings.is_empty() {
142 self.processed_bindings = Self::process_bindings(cx);
143 }
144
145 let row_count = self.processed_bindings.len();
146
147 let theme = cx.theme();
148
149 div()
150 .size_full()
151 .bg(theme.colors().background)
152 .id("keymap-editor")
153 .track_focus(&self.focus_handle)
154 .child(
155 Table::new()
156 .interactable(&self.table_interaction_state)
157 .header(["Command", "Keystrokes", "Context", "Source"])
158 .uniform_list(
159 "keymap-editor-table",
160 row_count,
161 cx.processor(move |this, range: Range<usize>, _window, cx| {
162 range
163 .map(|index| {
164 let binding = &this.processed_bindings[index];
165 let row = [
166 binding.action.clone(),
167 binding.keystroke_text.clone(),
168 binding.context.clone(),
169 binding.source.clone().unwrap_or_default(),
170 ];
171
172 // fixme: pass through callback as a row_cx param
173 let striped = false;
174
175 crate::ui_components::table::render_row(
176 index, row, row_count, striped, cx,
177 )
178 })
179 .collect()
180 }),
181 ),
182 )
183 }
184}
185
186impl SerializableItem for KeymapEditor {
187 fn serialized_item_kind() -> &'static str {
188 "KeymapEditor"
189 }
190
191 fn cleanup(
192 workspace_id: workspace::WorkspaceId,
193 alive_items: Vec<workspace::ItemId>,
194 _window: &mut Window,
195 cx: &mut App,
196 ) -> gpui::Task<gpui::Result<()>> {
197 workspace::delete_unloaded_items(
198 alive_items,
199 workspace_id,
200 "keybinding_editors",
201 &KEYBINDING_EDITORS,
202 cx,
203 )
204 }
205
206 fn deserialize(
207 _project: gpui::Entity<project::Project>,
208 _workspace: gpui::WeakEntity<Workspace>,
209 workspace_id: workspace::WorkspaceId,
210 item_id: workspace::ItemId,
211 window: &mut Window,
212 cx: &mut App,
213 ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
214 window.spawn(cx, async move |cx| {
215 if KEYBINDING_EDITORS
216 .get_keybinding_editor(item_id, workspace_id)?
217 .is_some()
218 {
219 cx.update(KeymapEditor::new)
220 } else {
221 Err(anyhow!("No keybinding editor to deserialize"))
222 }
223 })
224 }
225
226 fn serialize(
227 &mut self,
228 workspace: &mut Workspace,
229 item_id: workspace::ItemId,
230 _closing: bool,
231 _window: &mut Window,
232 cx: &mut ui::Context<Self>,
233 ) -> Option<gpui::Task<gpui::Result<()>>> {
234 let Some(workspace_id) = workspace.database_id() else {
235 return None;
236 };
237 Some(cx.background_spawn(async move {
238 KEYBINDING_EDITORS
239 .save_keybinding_editor(item_id, workspace_id)
240 .await
241 }))
242 }
243
244 fn should_serialize(&self, _event: &Self::Event) -> bool {
245 false
246 }
247}
248
249mod persistence {
250 use db::{define_connection, query, sqlez_macros::sql};
251 use workspace::WorkspaceDb;
252
253 define_connection! {
254 pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
255 &[sql!(
256 CREATE TABLE keybinding_editors (
257 workspace_id INTEGER,
258 item_id INTEGER UNIQUE,
259
260 PRIMARY KEY(workspace_id, item_id),
261 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
262 ON DELETE CASCADE
263 ) STRICT;
264 )];
265 }
266
267 impl KeybindingEditorDb {
268 query! {
269 pub async fn save_keybinding_editor(
270 item_id: workspace::ItemId,
271 workspace_id: workspace::WorkspaceId
272 ) -> Result<()> {
273 INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
274 VALUES (?, ?)
275 }
276 }
277
278 query! {
279 pub fn get_keybinding_editor(
280 item_id: workspace::ItemId,
281 workspace_id: workspace::WorkspaceId
282 ) -> Result<Option<workspace::ItemId>> {
283 SELECT item_id
284 FROM keybinding_editors
285 WHERE item_id = ? AND workspace_id = ?
286 }
287 }
288 }
289}