@@ -14567,11 +14567,14 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"command_palette_hooks",
+ "db",
"editor",
"feature_flags",
"fs",
"gpui",
"log",
+ "paths",
+ "project",
"schemars",
"serde",
"settings",
@@ -17048,6 +17051,7 @@ dependencies = [
"gpui_macros",
"icons",
"itertools 0.14.0",
+ "log",
"menu",
"serde",
"settings",
@@ -0,0 +1,196 @@
+use std::fmt::Write as _;
+
+use db::anyhow::anyhow;
+use gpui::{AppContext as _, EventEmitter, FocusHandle, Focusable, actions};
+use ui::{
+ ActiveTheme as _, App, ParentElement as _, Render, Styled as _, Table, Window, div, string_cell,
+};
+use workspace::{Item, SerializableItem, Workspace, register_serializable_item};
+
+use crate::keybindings::persistence::KEYBINDING_EDITORS;
+
+actions!(zed, [OpenKeymapEditor]);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
+ workspace.register_action(|workspace, _: &OpenKeymapEditor, window, cx| {
+ let open_keymap_editor = cx.new(|cx| KeymapEditor::new(cx));
+ workspace.add_item_to_center(Box::new(open_keymap_editor), window, cx);
+ });
+ })
+ .detach();
+
+ register_serializable_item::<KeymapEditor>(cx);
+}
+
+struct KeymapEditor {
+ focus_handle: FocusHandle,
+}
+
+impl EventEmitter<()> for KeymapEditor {}
+
+impl Focusable for KeymapEditor {
+ fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl KeymapEditor {
+ fn new(cx: &mut gpui::Context<Self>) -> Self {
+ Self {
+ focus_handle: cx.focus_handle(),
+ }
+ }
+}
+
+impl SerializableItem for KeymapEditor {
+ fn serialized_item_kind() -> &'static str {
+ "KeymapEditor"
+ }
+
+ fn cleanup(
+ workspace_id: workspace::WorkspaceId,
+ alive_items: Vec<workspace::ItemId>,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> gpui::Task<gpui::Result<()>> {
+ workspace::delete_unloaded_items(
+ alive_items,
+ workspace_id,
+ "keybinding_editors",
+ &KEYBINDING_EDITORS,
+ cx,
+ )
+ }
+
+ fn deserialize(
+ _project: gpui::Entity<project::Project>,
+ _workspace: gpui::WeakEntity<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: workspace::ItemId,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> gpui::Task<gpui::Result<gpui::Entity<Self>>> {
+ cx.spawn(async move |cx| {
+ if KEYBINDING_EDITORS
+ .get_keybinding_editor(item_id, workspace_id)?
+ .is_some()
+ {
+ cx.new(|cx| KeymapEditor::new(cx))
+ } else {
+ Err(anyhow!("No keybinding editor to deserialize"))
+ }
+ })
+ }
+
+ fn serialize(
+ &mut self,
+ workspace: &mut Workspace,
+ item_id: workspace::ItemId,
+ _closing: bool,
+ _window: &mut Window,
+ cx: &mut ui::Context<Self>,
+ ) -> Option<gpui::Task<gpui::Result<()>>> {
+ let Some(workspace_id) = workspace.database_id() else {
+ return None;
+ };
+ Some(cx.background_spawn(async move {
+ KEYBINDING_EDITORS
+ .save_keybinding_editor(item_id, workspace_id)
+ .await
+ }))
+ }
+
+ fn should_serialize(&self, _event: &Self::Event) -> bool {
+ false
+ }
+}
+
+impl Item for KeymapEditor {
+ type Event = ();
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
+ "Keymap Editor".into()
+ }
+}
+
+impl Render for KeymapEditor {
+ fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
+ let key_bindings_ptr = cx.key_bindings();
+ let lock = key_bindings_ptr.borrow();
+ let key_bindings = lock.bindings();
+
+ let mut table = Table::new(vec!["Command", "Keystrokes", "Context"]);
+ for key_binding in key_bindings {
+ let mut keystroke_text = String::new();
+ for keystroke in key_binding.keystrokes() {
+ write!(&mut keystroke_text, "{} ", keystroke.unparse()).ok();
+ }
+ let keystroke_text = keystroke_text.trim().to_string();
+
+ let context = key_binding
+ .predicate()
+ .map(|predicate| predicate.to_string())
+ .unwrap_or_else(|| "<global>".to_string());
+
+ // dbg!(key_binding.action().name(), &keystroke_text, &context);
+
+ table = table.row(vec![
+ string_cell(key_binding.action().name()),
+ string_cell(keystroke_text),
+ string_cell(context),
+ // TODO: Add a source field
+ // string_cell(keybinding.source().to_string()),
+ ]);
+ }
+
+ let theme = cx.theme();
+
+ div()
+ .size_full()
+ .bg(theme.colors().background)
+ .child(table.striped())
+ }
+}
+
+mod persistence {
+ use db::{define_connection, query, sqlez_macros::sql};
+ use workspace::WorkspaceDb;
+
+ define_connection! {
+ pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
+ &[sql!(
+ CREATE TABLE keybinding_editors (
+ workspace_id INTEGER,
+ item_id INTEGER UNIQUE,
+
+ PRIMARY KEY(workspace_id, item_id),
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;
+ )];
+ }
+
+ impl KeybindingEditorDb {
+ query! {
+ pub async fn save_keybinding_editor(
+ item_id: workspace::ItemId,
+ workspace_id: workspace::WorkspaceId
+ ) -> Result<()> {
+ INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
+ VALUES (?, ?)
+ }
+ }
+
+ query! {
+ pub fn get_keybinding_editor(
+ item_id: workspace::ItemId,
+ workspace_id: workspace::WorkspaceId
+ ) -> Result<Option<workspace::ItemId>> {
+ SELECT item_id
+ FROM keybinding_editors
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+ }
+}