keymap_editor: Add a "create keybinding" modal (#46030)

Danilo Leal created

Ever since we launched the keymap editor, we've received feedback about
how "_creating_" a new keybinding was not obvious. At first, I was
confused about this feedback because you can't "create" a keybinding,
you need to rather _assign it_ to an action... so what always made sense
to me was to start with searching for the action, which is an use case
we already support. Regardless, having an easy to reach "create" button,
which essentially still asks for action > keystroke > arguments (if
needed) > and context, feels like a win UX-wise; a bit of a redundant
flow that feels ultimately positive.

So, this PR adds a "Create Keybinding" button to the keymap editor,
which you can reach with `cmd-k`. That will open up a modal with an
action autocomplete, the keystroke recording input, the arguments input
(if needed), and the context input.


https://github.com/user-attachments/assets/86f64314-4685-47bb-bb0d-72ca4c469d1f

Release Notes:

- keymap editor: Added a keybinding creation modal to make it easier to
assign an action to a keystroke.

Change summary

assets/keymaps/default-linux.json                      |   1 
assets/keymaps/default-macos.json                      |   1 
assets/keymaps/default-windows.json                    |   1 
crates/keymap_editor/src/action_completion_provider.rs | 136 +++
crates/keymap_editor/src/keymap_editor.rs              | 446 +++++++++--
5 files changed, 510 insertions(+), 75 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -1205,6 +1205,7 @@
       "alt-c": "keymap_editor::ToggleConflictFilter",
       "enter": "keymap_editor::EditBinding",
       "alt-enter": "keymap_editor::CreateBinding",
+      "ctrl-k": "keymap_editor::OpenCreateKeybindingModal",
       "ctrl-c": "keymap_editor::CopyAction",
       "ctrl-shift-c": "keymap_editor::CopyContext",
       "ctrl-t": "keymap_editor::ShowMatchingKeybinds",

assets/keymaps/default-macos.json 🔗

@@ -1306,6 +1306,7 @@
       "cmd-alt-c": "keymap_editor::ToggleConflictFilter",
       "enter": "keymap_editor::EditBinding",
       "alt-enter": "keymap_editor::CreateBinding",
+      "cmd-k": "keymap_editor::OpenCreateKeybindingModal",
       "cmd-c": "keymap_editor::CopyAction",
       "cmd-shift-c": "keymap_editor::CopyContext",
       "cmd-t": "keymap_editor::ShowMatchingKeybinds",

assets/keymaps/default-windows.json 🔗

@@ -1233,6 +1233,7 @@
       "alt-c": "keymap_editor::ToggleConflictFilter",
       "enter": "keymap_editor::EditBinding",
       "alt-enter": "keymap_editor::CreateBinding",
+      "ctrl-k": "keymap_editor::OpenCreateKeybindingModal",
       "ctrl-c": "keymap_editor::CopyAction",
       "ctrl-shift-c": "keymap_editor::CopyContext",
       "ctrl-t": "keymap_editor::ShowMatchingKeybinds",

crates/keymap_editor/src/action_completion_provider.rs 🔗

@@ -0,0 +1,136 @@
+use collections::HashMap;
+use command_palette;
+use editor::{CompletionProvider, Editor};
+use fuzzy::StringMatchCandidate;
+use gpui::{Context, Entity, SharedString, Window};
+use language::{self, ToOffset};
+use project::{self, CompletionDisplayOptions};
+
+pub struct ActionCompletionProvider {
+    action_names: Vec<&'static str>,
+    humanized_names: HashMap<&'static str, SharedString>,
+}
+
+impl ActionCompletionProvider {
+    pub fn new(
+        action_names: Vec<&'static str>,
+        humanized_names: HashMap<&'static str, SharedString>,
+    ) -> Self {
+        Self {
+            action_names,
+            humanized_names,
+        }
+    }
+}
+
+impl CompletionProvider for ActionCompletionProvider {
+    fn completions(
+        &self,
+        _excerpt_id: editor::ExcerptId,
+        buffer: &Entity<language::Buffer>,
+        buffer_position: language::Anchor,
+        _trigger: editor::CompletionContext,
+        _window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
+        let buffer = buffer.read(cx);
+        let mut count_back = 0;
+
+        for char in buffer.reversed_chars_at(buffer_position) {
+            if char.is_ascii_alphanumeric() || char == '_' || char == ':' {
+                count_back += 1;
+            } else {
+                break;
+            }
+        }
+
+        let start_anchor = buffer.anchor_before(
+            buffer_position
+                .to_offset(&buffer)
+                .saturating_sub(count_back),
+        );
+
+        let replace_range = start_anchor..buffer_position;
+        let snapshot = buffer.text_snapshot();
+        let query: String = snapshot.text_for_range(replace_range.clone()).collect();
+        let normalized_query = command_palette::normalize_action_query(&query);
+
+        let candidates: Vec<StringMatchCandidate> = self
+            .action_names
+            .iter()
+            .enumerate()
+            .map(|(ix, &name)| {
+                let humanized = self
+                    .humanized_names
+                    .get(name)
+                    .cloned()
+                    .unwrap_or_else(|| name.into());
+                StringMatchCandidate::new(ix, &humanized)
+            })
+            .collect();
+
+        let executor = cx.background_executor().clone();
+        let executor_for_fuzzy = executor.clone();
+        let action_names = self.action_names.clone();
+        let humanized_names = self.humanized_names.clone();
+
+        executor.spawn(async move {
+            let matches = fuzzy::match_strings(
+                &candidates,
+                &normalized_query,
+                true,
+                true,
+                action_names.len(),
+                &Default::default(),
+                executor_for_fuzzy,
+            )
+            .await;
+
+            let completions: Vec<project::Completion> = matches
+                .iter()
+                .take(50)
+                .map(|m| {
+                    let action_name = action_names[m.candidate_id];
+                    let humanized = humanized_names
+                        .get(action_name)
+                        .cloned()
+                        .unwrap_or_else(|| action_name.into());
+
+                    project::Completion {
+                        replace_range: replace_range.clone(),
+                        label: language::CodeLabel::plain(humanized.to_string(), None),
+                        new_text: action_name.to_string(),
+                        documentation: None,
+                        source: project::CompletionSource::Custom,
+                        icon_path: None,
+                        match_start: None,
+                        snippet_deduplication_key: None,
+                        insert_text_mode: None,
+                        confirm: None,
+                    }
+                })
+                .collect();
+
+            Ok(vec![project::CompletionResponse {
+                completions,
+                display_options: CompletionDisplayOptions {
+                    dynamic_width: true,
+                },
+                is_incomplete: false,
+            }])
+        })
+    }
+
+    fn is_completion_trigger(
+        &self,
+        _buffer: &Entity<language::Buffer>,
+        _position: language::Anchor,
+        text: &str,
+        _trigger_in_words: bool,
+        _cx: &mut Context<Editor>,
+    ) -> bool {
+        text.chars().last().is_some_and(|last_char| {
+            last_char.is_ascii_alphanumeric() || last_char == '_' || last_char == ':'
+        })
+    }
+}

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -7,6 +7,7 @@ use std::{
     time::{Duration, Instant},
 };
 
+mod action_completion_provider;
 mod ui_components;
 
 use anyhow::{Context as _, anyhow};
@@ -45,6 +46,7 @@ pub use ui_components::*;
 use zed_actions::{ChangeKeybinding, OpenKeymap};
 
 use crate::{
+    action_completion_provider::ActionCompletionProvider,
     persistence::KEYBINDING_EDITORS,
     ui_components::keystroke_input::{
         ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording,
@@ -60,6 +62,8 @@ actions!(
         EditBinding,
         /// Creates a new key binding for the selected action.
         CreateBinding,
+        /// Creates a new key binding from scratch, prompting for the action.
+        OpenCreateKeybindingModal,
         /// Deletes the selected key binding.
         DeleteBinding,
         /// Copies the action name to clipboard.
@@ -426,6 +430,7 @@ struct KeymapEditor {
     humanized_action_names: HumanizedActionNameCache,
     current_widths: Entity<TableColumnWidths<6>>,
     show_hover_menus: bool,
+    actions_with_schemas: HashSet<&'static str>,
     /// In order for the JSON LSP to run in the actions arguments editor, we
     /// require a backing file In order to avoid issues (primarily log spam)
     /// with drop order between the buffer, file, worktree, etc, we create a
@@ -552,6 +557,7 @@ impl KeymapEditor {
             search_query_debounce: None,
             humanized_action_names: HumanizedActionNameCache::new(cx),
             show_hover_menus: true,
+            actions_with_schemas: HashSet::default(),
             action_args_temp_dir: None,
             action_args_temp_dir_worktree: None,
             current_widths: cx.new(|cx| TableColumnWidths::new(cx)),
@@ -720,7 +726,11 @@ impl KeymapEditor {
         zed_keybind_context_language: Arc<Language>,
         humanized_action_names: &HumanizedActionNameCache,
         cx: &mut App,
-    ) -> (Vec<ProcessedBinding>, Vec<StringMatchCandidate>) {
+    ) -> (
+        Vec<ProcessedBinding>,
+        Vec<StringMatchCandidate>,
+        HashSet<&'static str>,
+    ) {
         let key_bindings_ptr = cx.key_bindings();
         let lock = key_bindings_ptr.borrow();
         let key_bindings = lock.bindings();
@@ -797,8 +807,11 @@ impl KeymapEditor {
             processed_bindings.push(ProcessedBinding::Unmapped(action_information));
             string_match_candidates.push(string_match_candidate);
         }
-
-        (processed_bindings, string_match_candidates)
+        (
+            processed_bindings,
+            string_match_candidates,
+            actions_with_schemas,
+        )
     }
 
     fn on_keymap_changed(&mut self, window: &mut Window, cx: &mut Context<KeymapEditor>) {
@@ -809,16 +822,18 @@ impl KeymapEditor {
                 load_keybind_context_language(workspace.clone(), cx).await;
 
             let (action_query, keystroke_query) = this.update(cx, |this, cx| {
-                let (key_bindings, string_match_candidates) = Self::process_bindings(
-                    json_language,
-                    zed_keybind_context_language,
-                    &this.humanized_action_names,
-                    cx,
-                );
+                let (key_bindings, string_match_candidates, actions_with_schemas) =
+                    Self::process_bindings(
+                        json_language,
+                        zed_keybind_context_language,
+                        &this.humanized_action_names,
+                        cx,
+                    );
 
                 this.keybinding_conflict_state = ConflictState::new(&key_bindings);
 
                 this.keybindings = key_bindings;
+                this.actions_with_schemas = actions_with_schemas;
                 this.string_match_candidates = Arc::new(string_match_candidates);
                 this.matches = this
                     .string_match_candidates
@@ -1245,6 +1260,51 @@ impl KeymapEditor {
         self.open_edit_keybinding_modal(true, window, cx);
     }
 
+    fn open_create_keybinding_modal(
+        &mut self,
+        _: &OpenCreateKeybindingModal,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let keymap_editor = cx.entity();
+
+        let action_information = ActionInformation::new(
+            gpui::NoAction.name(),
+            None,
+            &HashSet::default(),
+            cx.action_documentation(),
+            &self.humanized_action_names,
+        );
+
+        let dummy_binding = ProcessedBinding::Unmapped(action_information);
+        let dummy_index = self.keybindings.len();
+
+        let temp_dir = self.action_args_temp_dir.as_ref().map(|dir| dir.path());
+
+        self.workspace
+            .update(cx, |workspace, cx| {
+                let fs = workspace.app_state().fs.clone();
+                let workspace_weak = cx.weak_entity();
+                workspace.toggle_modal(window, cx, |window, cx| {
+                    let modal = KeybindingEditorModal::new(
+                        true,
+                        dummy_binding,
+                        dummy_index,
+                        keymap_editor,
+                        temp_dir,
+                        workspace_weak,
+                        fs,
+                        window,
+                        cx,
+                    );
+
+                    window.focus(&modal.focus_handle(cx), cx);
+                    modal
+                });
+            })
+            .log_err();
+    }
+
     fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context<Self>) {
         let Some(to_remove) = self.selected_binding().cloned() else {
             return;
@@ -1650,6 +1710,7 @@ impl Render for KeymapEditor {
             .on_action(cx.listener(Self::focus_search))
             .on_action(cx.listener(Self::edit_binding))
             .on_action(cx.listener(Self::create_binding))
+            .on_action(cx.listener(Self::open_create_keybinding_modal))
             .on_action(cx.listener(Self::delete_binding))
             .on_action(cx.listener(Self::copy_action_to_clipboard))
             .on_action(cx.listener(Self::copy_context_to_clipboard))
@@ -1690,7 +1751,7 @@ impl Render for KeymapEditor {
                             .child(
                                 h_flex()
                                     .gap_1()
-                                    .min_w_64()
+                                    .min_w_96()
                                     .child(
                                         IconButton::new(
                                             "KeymapEditorToggleFiltersIcon",
@@ -1765,7 +1826,7 @@ impl Render for KeymapEditor {
                                     .child(
                                         h_flex()
                                             .w_full()
-                                            .pl_2()
+                                            .px_1p5()
                                             .gap_1()
                                             .justify_end()
                                             .child(
@@ -1809,15 +1870,30 @@ impl Render for KeymapEditor {
                                                     ),
                                             )
                                             .child(
-                                                Button::new("edit-in-json", "Edit in keymap.json")
-                                                    .style(ButtonStyle::Outlined)
+                                                Button::new("edit-in-json", "Edit in JSON")
+                                                    .style(ButtonStyle::Subtle)
                                                     .on_click(|_, window, cx| {
                                                         window.dispatch_action(
                                                             zed_actions::OpenKeymapFile.boxed_clone(),
                                                             cx,
                                                         );
                                                     })
-                                            ),
+                                            )
+                                            .child(
+                                                Button::new("create", "Create Keybinding")
+                                                    .style(ButtonStyle::Outlined)
+                                                    .key_binding(
+                                                        ui::KeyBinding::for_action_in(&OpenCreateKeybindingModal, &focus_handle, cx)
+                                                            .map(|kb| kb.size(rems_from_px(10.))),
+                                                    )
+                                                    .on_click(|_, window, cx| {
+                                                        window.dispatch_action(
+                                                            OpenCreateKeybindingModal.boxed_clone(),
+                                                            cx,
+                                                        );
+                                                    })
+                                            )
+
                                     )
                             ),
                     )
@@ -1828,7 +1904,7 @@ impl Render for KeymapEditor {
                                 h_flex()
                                     .gap_2()
                                     .child(self.keystroke_editor.clone())
-                                    .child(div().min_w_64()), // Spacer div to align with the search input
+                                    .child(div().min_w_96()), // Spacer div to align with the search input
                             )
                         },
                     ),
@@ -2177,7 +2253,10 @@ struct KeybindingEditorModal {
     editing_keybind_idx: usize,
     keybind_editor: Entity<KeystrokeInput>,
     context_editor: Entity<InputField>,
+    action_editor: Option<Entity<InputField>>,
     action_arguments_editor: Option<Entity<ActionArgumentsEditor>>,
+    action_name_to_static: HashMap<String, &'static str>,
+    selected_action_name: Option<&'static str>,
     fs: Arc<dyn Fs>,
     error: Option<InputError>,
     keymap_editor: Entity<KeymapEditor>,
@@ -2191,6 +2270,9 @@ impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
 
 impl Focusable for KeybindingEditorModal {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
+        if let Some(action_editor) = &self.action_editor {
+            return action_editor.focus_handle(cx);
+        }
         self.keybind_editor.focus_handle(cx)
     }
 }
@@ -2250,16 +2332,53 @@ impl KeybindingEditorModal {
             input
         });
 
-        let action_arguments_editor = editing_keybind.action().has_schema.then(|| {
-            let arguments = editing_keybind
-                .action()
-                .arguments
-                .as_ref()
-                .map(|args| args.text.clone());
+        let has_action_editor = create && editing_keybind.action().name == gpui::NoAction.name();
+
+        let (action_editor, action_name_to_static) = if has_action_editor {
+            let actions: Vec<&'static str> = cx.all_action_names().to_vec();
+
+            let humanized_names: HashMap<&'static str, SharedString> = actions
+                .iter()
+                .map(|&name| (name, command_palette::humanize_action_name(name).into()))
+                .collect();
+
+            let action_name_to_static: HashMap<String, &'static str> = actions
+                .iter()
+                .map(|&name| (name.to_string(), name))
+                .collect();
+
+            let editor = cx.new(|cx| {
+                let input = InputField::new(window, cx, "Type an action name")
+                    .label("Action")
+                    .label_size(LabelSize::Default);
+
+                input.editor().update(cx, |editor, _cx| {
+                    editor.set_completion_provider(Some(std::rc::Rc::new(
+                        ActionCompletionProvider::new(actions, humanized_names),
+                    )));
+                });
+
+                input
+            });
+
+            (Some(editor), action_name_to_static)
+        } else {
+            (None, HashMap::default())
+        };
+
+        let action_has_schema = editing_keybind.action().has_schema;
+        let action_name_for_args = editing_keybind.action().name;
+        let action_args = editing_keybind
+            .action()
+            .arguments
+            .as_ref()
+            .map(|args| args.text.clone());
+
+        let action_arguments_editor = action_has_schema.then(|| {
             cx.new(|cx| {
                 ActionArgumentsEditor::new(
-                    editing_keybind.action().name,
-                    arguments,
+                    action_name_for_args,
+                    action_args.clone(),
                     action_args_temp_dir,
                     workspace.clone(),
                     window,
@@ -2269,6 +2388,7 @@ impl KeybindingEditorModal {
         });
 
         let focus_state = KeybindingEditorModalFocusState::new(
+            action_editor.as_ref().map(|e| e.focus_handle(cx)),
             keybind_editor.focus_handle(cx),
             action_arguments_editor
                 .as_ref()
@@ -2283,7 +2403,10 @@ impl KeybindingEditorModal {
             fs,
             keybind_editor,
             context_editor,
+            action_editor,
             action_arguments_editor,
+            action_name_to_static,
+            selected_action_name: None,
             error: None,
             keymap_editor,
             workspace,
@@ -2291,6 +2414,76 @@ impl KeybindingEditorModal {
         }
     }
 
+    fn add_action_arguments_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(action_editor) = &self.action_editor else {
+            return;
+        };
+
+        let action_name_str = action_editor.read(cx).editor.read(cx).text(cx);
+        let current_action = self.action_name_to_static.get(&action_name_str).copied();
+
+        if current_action == self.selected_action_name {
+            return;
+        }
+
+        self.selected_action_name = current_action;
+
+        let Some(action_name) = current_action else {
+            if self.action_arguments_editor.is_some() {
+                self.action_arguments_editor = None;
+                self.rebuild_focus_state(cx);
+                cx.notify();
+            }
+            return;
+        };
+
+        let (action_has_schema, temp_dir) = {
+            let keymap_editor = self.keymap_editor.read(cx);
+            let has_schema = keymap_editor.actions_with_schemas.contains(action_name);
+            let temp_dir = keymap_editor
+                .action_args_temp_dir
+                .as_ref()
+                .map(|dir| dir.path().to_path_buf());
+            (has_schema, temp_dir)
+        };
+
+        let currently_has_editor = self.action_arguments_editor.is_some();
+
+        if action_has_schema && !currently_has_editor {
+            let workspace = self.workspace.clone();
+
+            let new_editor = cx.new(|cx| {
+                ActionArgumentsEditor::new(
+                    action_name,
+                    None,
+                    temp_dir.as_deref(),
+                    workspace,
+                    window,
+                    cx,
+                )
+            });
+
+            self.action_arguments_editor = Some(new_editor);
+            self.rebuild_focus_state(cx);
+            cx.notify();
+        } else if !action_has_schema && currently_has_editor {
+            self.action_arguments_editor = None;
+            self.rebuild_focus_state(cx);
+            cx.notify();
+        }
+    }
+
+    fn rebuild_focus_state(&mut self, cx: &App) {
+        self.focus_state = KeybindingEditorModalFocusState::new(
+            self.action_editor.as_ref().map(|e| e.focus_handle(cx)),
+            self.keybind_editor.focus_handle(cx),
+            self.action_arguments_editor
+                .as_ref()
+                .map(|args_editor| args_editor.focus_handle(cx)),
+            self.context_editor.focus_handle(cx),
+        );
+    }
+
     fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
         if self
             .error
@@ -2305,7 +2498,25 @@ impl KeybindingEditorModal {
         }
     }
 
+    fn get_selected_action_name(&self, cx: &App) -> anyhow::Result<&'static str> {
+        if let Some(selector) = self.action_editor.as_ref() {
+            let action_name_str = selector.read(cx).editor.read(cx).text(cx);
+
+            if action_name_str.is_empty() {
+                anyhow::bail!("Action name is required");
+            }
+
+            self.action_name_to_static
+                .get(&action_name_str)
+                .copied()
+                .ok_or_else(|| anyhow::anyhow!("Action '{}' not found", action_name_str))
+        } else {
+            Ok(self.editing_keybind.action().name)
+        }
+    }
+
     fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> {
+        let action_name = self.get_selected_action_name(cx)?;
         let action_arguments = self
             .action_arguments_editor
             .as_ref()
@@ -2319,7 +2530,7 @@ impl KeybindingEditorModal {
             })
             .transpose()?;
 
-        cx.build_action(self.editing_keybind.action().name, value)
+        cx.build_action(action_name, value)
             .context("Failed to validate action arguments")?;
         Ok(action_arguments)
     }
@@ -2419,13 +2630,31 @@ impl KeybindingEditorModal {
         let create = self.creating;
         let keyboard_mapper = cx.keyboard_mapper().clone();
 
-        cx.spawn(async move |this, cx| {
-            let action_name = existing_keybind.action().name;
-            let humanized_action_name = existing_keybind.action().humanized_name.clone();
+        let action_name = self
+            .get_selected_action_name(cx)
+            .map_err(InputError::error)?;
+
+        let humanized_action_name: SharedString =
+            command_palette::humanize_action_name(action_name).into();
+
+        let action_information = ActionInformation::new(
+            action_name,
+            None,
+            &HashSet::default(),
+            cx.action_documentation(),
+            &self.keymap_editor.read(cx).humanized_action_names,
+        );
 
+        let keybind_for_save = if create {
+            ProcessedBinding::Unmapped(action_information)
+        } else {
+            existing_keybind
+        };
+
+        cx.spawn(async move |this, cx| {
             match save_keybinding_update(
                 create,
-                existing_keybind,
+                keybind_for_save,
                 &action_mapping,
                 new_action_args.as_deref(),
                 &fs,
@@ -2474,13 +2703,57 @@ impl KeybindingEditorModal {
         Ok(())
     }
 
+    fn is_any_editor_showing_completions(&self, window: &Window, cx: &App) -> bool {
+        let is_editor_showing_completions =
+            |focus_handle: &FocusHandle, editor_entity: &Entity<Editor>| -> bool {
+                focus_handle.contains_focused(window, cx)
+                    && editor_entity.read_with(cx, |editor, _cx| {
+                        editor
+                            .context_menu()
+                            .borrow()
+                            .as_ref()
+                            .is_some_and(|menu| menu.visible())
+                    })
+            };
+
+        self.action_editor.as_ref().is_some_and(|action_editor| {
+            let focus_handle = action_editor.read(cx).focus_handle(cx);
+            let editor_entity = action_editor.read(cx).editor();
+            is_editor_showing_completions(&focus_handle, editor_entity)
+        }) || {
+            let focus_handle = self.context_editor.read(cx).focus_handle(cx);
+            let editor_entity = self.context_editor.read(cx).editor();
+            is_editor_showing_completions(&focus_handle, editor_entity)
+        } || self
+            .action_arguments_editor
+            .as_ref()
+            .is_some_and(|args_editor| {
+                let focus_handle = args_editor.read(cx).focus_handle(cx);
+                let editor_entity = &args_editor.read(cx).editor;
+                is_editor_showing_completions(&focus_handle, editor_entity)
+            })
+    }
+
     fn key_context(&self) -> KeyContext {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("KeybindEditorModal");
         key_context
     }
 
+    fn key_context_internal(&self, window: &Window, cx: &App) -> KeyContext {
+        let mut key_context = self.key_context();
+
+        if self.is_any_editor_showing_completions(window, cx) {
+            key_context.add("showing_completions");
+        }
+
+        key_context
+    }
+
     fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        if self.is_any_editor_showing_completions(window, cx) {
+            return;
+        }
         self.focus_state.focus_next(window, cx);
     }
 
@@ -2490,6 +2763,9 @@ impl KeybindingEditorModal {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        if self.is_any_editor_showing_completions(window, cx) {
+            return;
+        }
         self.focus_state.focus_previous(window, cx);
     }
 
@@ -2548,18 +2824,24 @@ impl KeybindingEditorModal {
 }
 
 impl Render for KeybindingEditorModal {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        self.add_action_arguments_input(window, cx);
+
         let theme = cx.theme().colors();
         let matching_bindings_count = self.get_matching_bindings_count(cx);
+        let key_context = self.key_context_internal(window, cx);
+        let showing_completions = key_context.contains("showing_completions");
 
         v_flex()
             .w(rems(34.))
             .elevation_3(cx)
-            .key_context(self.key_context())
-            .on_action(cx.listener(Self::focus_next))
-            .on_action(cx.listener(Self::focus_prev))
+            .key_context(key_context)
             .on_action(cx.listener(Self::confirm))
             .on_action(cx.listener(Self::cancel))
+            .when(!showing_completions, |this| {
+                this.on_action(cx.listener(Self::focus_next))
+                    .on_action(cx.listener(Self::focus_prev))
+            })
             .child(
                 Modal::new("keybinding_editor_modal", None)
                     .header(
@@ -2571,25 +2853,36 @@ impl Render for KeybindingEditorModal {
                                 .gap_0p5()
                                 .border_b_1()
                                 .border_color(theme.border_variant)
-                                .child(Label::new(
-                                    self.editing_keybind.action().humanized_name.clone(),
-                                ))
-                                .when_some(
-                                    self.editing_keybind.action().documentation,
-                                    |this, docs| {
-                                        this.child(
-                                            Label::new(docs)
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted),
-                                        )
-                                    },
-                                ),
+                                .when(!self.creating, |this| {
+                                    this.child(Label::new(
+                                        self.editing_keybind.action().humanized_name.clone(),
+                                    ))
+                                    .when_some(
+                                        self.editing_keybind.action().documentation,
+                                        |this, docs| {
+                                            this.child(
+                                                Label::new(docs)
+                                                    .size(LabelSize::Small)
+                                                    .color(Color::Muted),
+                                            )
+                                        },
+                                    )
+                                })
+                                .when(self.creating, |this| {
+                                    this.child(Label::new("Create Keybinding"))
+                                }),
                         ),
                     )
                     .section(
                         Section::new().child(
                             v_flex()
                                 .gap_2p5()
+                                .when_some(
+                                    self.creating
+                                        .then_some(())
+                                        .and_then(|_| self.action_editor.as_ref()),
+                                    |this, selector| this.child(selector.clone()),
+                                )
                                 .child(
                                     v_flex()
                                         .gap_1()
@@ -2678,15 +2971,21 @@ struct KeybindingEditorModalFocusState {
 
 impl KeybindingEditorModalFocusState {
     fn new(
+        action_editor: Option<FocusHandle>,
         keystrokes: FocusHandle,
-        action_input: Option<FocusHandle>,
+        action_arguments: Option<FocusHandle>,
         context: FocusHandle,
     ) -> Self {
         Self {
             handles: Vec::from_iter(
-                [Some(keystrokes), action_input, Some(context)]
-                    .into_iter()
-                    .flatten(),
+                [
+                    action_editor,
+                    Some(keystrokes),
+                    action_arguments,
+                    Some(context),
+                ]
+                .into_iter()
+                .flatten(),
             ),
         }
     }
@@ -2916,23 +3215,23 @@ impl ActionArgumentsEditor {
 }
 
 impl Render for ActionArgumentsEditor {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let background_color;
-        let border_color;
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let settings = theme::ThemeSettings::get_global(cx);
+        let colors = cx.theme().colors();
+
+        let border_color = if self.is_loading {
+            colors.border_disabled
+        } else if self.focus_handle.contains_focused(window, cx) {
+            colors.border_focused
+        } else {
+            colors.border_variant
+        };
+
         let text_style = {
-            let colors = cx.theme().colors();
-            let settings = theme::ThemeSettings::get_global(cx);
-            background_color = colors.editor_background;
-            border_color = if self.is_loading {
-                colors.border_disabled
-            } else {
-                colors.border_variant
-            };
             TextStyleRefinement {
                 font_size: Some(rems(0.875).into()),
                 font_weight: Some(settings.buffer_font.weight),
                 line_height: Some(relative(1.2)),
-                font_style: Some(gpui::FontStyle::Normal),
                 color: self.is_loading.then_some(colors.text_disabled),
                 ..Default::default()
             }
@@ -2941,20 +3240,17 @@ impl Render for ActionArgumentsEditor {
         self.editor
             .update(cx, |editor, _| editor.set_text_style_refinement(text_style));
 
-        v_flex().w_full().child(
-            h_flex()
-                .min_h_8()
-                .min_w_48()
-                .px_2()
-                .py_1p5()
-                .flex_grow()
-                .rounded_lg()
-                .bg(background_color)
-                .border_1()
-                .border_color(border_color)
-                .track_focus(&self.focus_handle)
-                .child(self.editor.clone()),
-        )
+        h_flex()
+            .min_h_8()
+            .min_w_48()
+            .px_2()
+            .flex_grow()
+            .rounded_md()
+            .bg(cx.theme().colors().editor_background)
+            .border_1()
+            .border_color(border_color)
+            .track_focus(&self.focus_handle)
+            .child(self.editor.clone())
     }
 }