keymap_ui: Creating keybinds (#34138)

Ben Kunkle created

Adds support for creating new keybinds in the keymap UI. "Create" means
two different things for existing bindings, and unbound actions.
- For existing bindings, it is essentially a duplicate + edit
- For unbound actions, it is the creation of a binding

Release Notes:

- N/A

Change summary

crates/settings_ui/src/keybindings.rs | 50 ++++++++++++++++++++++------
1 file changed, 39 insertions(+), 11 deletions(-)

Detailed changes

crates/settings_ui/src/keybindings.rs 🔗

@@ -45,6 +45,8 @@ actions!(
     [
         /// Edits the selected key binding.
         EditBinding,
+        /// Creates a new key binding for the selected action.
+        CreateBinding,
         /// Copies the action name to clipboard.
         CopyAction,
         /// Copies the context predicate to clipboard.
@@ -466,11 +468,16 @@ impl KeymapEditor {
     }
 
     fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
-        self.edit_selected_keybinding(window, cx);
+        self.open_edit_keybinding_modal(false, window, cx);
     }
 
-    fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(keybind) = self.selected_binding() else {
+    fn open_edit_keybinding_modal(
+        &mut self,
+        create: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(keybind) = self.selected_binding().cloned() else {
             return;
         };
         self.workspace
@@ -479,7 +486,7 @@ impl KeymapEditor {
                 let workspace_weak = cx.weak_entity();
                 workspace.toggle_modal(window, cx, |window, cx| {
                     let modal =
-                        KeybindingEditorModal::new(keybind.clone(), workspace_weak, fs, window, cx);
+                        KeybindingEditorModal::new(create, keybind, workspace_weak, fs, window, cx);
                     window.focus(&modal.focus_handle(cx));
                     modal
                 });
@@ -488,7 +495,11 @@ impl KeymapEditor {
     }
 
     fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context<Self>) {
-        self.edit_selected_keybinding(window, cx);
+        self.open_edit_keybinding_modal(false, window, cx);
+    }
+
+    fn create_binding(&mut self, _: &CreateBinding, window: &mut Window, cx: &mut Context<Self>) {
+        self.open_edit_keybinding_modal(true, window, cx);
     }
 
     fn copy_context_to_clipboard(
@@ -600,6 +611,7 @@ impl Render for KeymapEditor {
             .on_action(cx.listener(Self::focus_search))
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::edit_binding))
+            .on_action(cx.listener(Self::create_binding))
             .on_action(cx.listener(Self::copy_action_to_clipboard))
             .on_action(cx.listener(Self::copy_context_to_clipboard))
             .size_full()
@@ -778,6 +790,7 @@ impl RenderOnce for SyntaxHighlightedText {
 }
 
 struct KeybindingEditorModal {
+    creating: bool,
     editing_keybind: ProcessedKeybinding,
     keybind_editor: Entity<KeystrokeInput>,
     context_editor: Entity<Editor>,
@@ -798,6 +811,7 @@ impl Focusable for KeybindingEditorModal {
 
 impl KeybindingEditorModal {
     pub fn new(
+        create: bool,
         editing_keybind: ProcessedKeybinding,
         workspace: WeakEntity<Workspace>,
         fs: Arc<dyn Fs>,
@@ -864,6 +878,7 @@ impl KeybindingEditorModal {
         });
 
         Self {
+            creating: create,
             editing_keybind,
             fs,
             keybind_editor,
@@ -902,8 +917,11 @@ impl KeybindingEditorModal {
             return;
         }
 
+        let create = self.creating;
+
         cx.spawn(async move |this, cx| {
             if let Err(err) = save_keybinding_update(
+                create,
                 existing_keybind,
                 &new_keystrokes,
                 new_context.as_deref(),
@@ -1142,6 +1160,7 @@ async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp)
 }
 
 async fn save_keybinding_update(
+    create: bool,
     existing: ProcessedKeybinding,
     new_keystrokes: &[Keystroke],
     new_context: Option<&str>,
@@ -1168,7 +1187,7 @@ async fn save_keybinding_update(
         .as_ref()
         .map(|input| input.text.as_ref());
 
-    let operation = if existing.ui_key_binding.is_some() {
+    let operation = if !create {
         settings::KeybindUpdateOperation::Replace {
             target: settings::KeybindUpdateTarget {
                 context: existing_context,
@@ -1190,7 +1209,13 @@ async fn save_keybinding_update(
             },
         }
     } else {
-        anyhow::bail!("Adding new bindings not implemented yet");
+        settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget {
+            context: new_context,
+            keystrokes: new_keystrokes,
+            action_name: &existing.action_name,
+            use_key_equivalents: false,
+            input,
+        })
     };
     let updated_keymap_contents =
         settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
@@ -1378,16 +1403,19 @@ fn build_keybind_context_menu(
             return menu;
         };
 
-        let selected_binding_has_context = selected_binding
+        let selected_binding_has_no_context = selected_binding
             .context
             .as_ref()
             .and_then(KeybindContextString::local)
-            .is_some();
+            .is_none();
+
+        let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none();
 
-        menu.action("Edit Binding", Box::new(EditBinding))
+        menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding))
+            .action("Create", Box::new(CreateBinding))
             .action("Copy action", Box::new(CopyAction))
             .action_disabled_when(
-                !selected_binding_has_context,
+                selected_binding_has_no_context,
                 "Copy Context",
                 Box::new(CopyContext),
             )