From e67818bdcb76d9ae46f8d937b8f0481d5c778bd1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:41:46 -0300 Subject: [PATCH] keymap_editor: Add a "create keybinding" modal (#46030) 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. --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/default-windows.json | 1 + .../src/action_completion_provider.rs | 136 ++++++ crates/keymap_editor/src/keymap_editor.rs | 446 +++++++++++++++--- 5 files changed, 510 insertions(+), 75 deletions(-) create mode 100644 crates/keymap_editor/src/action_completion_provider.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5ba0005289024d245b5cd13aa1425b761dd23374..e9016ec8270f32829e7c2b4b6526082a4f58a288 100644 --- a/assets/keymaps/default-linux.json +++ b/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", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f9adbcd49bf7b3eae74116ac3da64b3cdfd3b4de..7ec882dbfd432927e059650626036fee2041f73b 100644 --- a/assets/keymaps/default-macos.json +++ b/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", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 931363319e7573c05b0d5d9f455c4e89f747ada9..4cf24f56ec53f9c31a7a9fc907f6abcdbf85da7a 100644 --- a/assets/keymaps/default-windows.json +++ b/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", diff --git a/crates/keymap_editor/src/action_completion_provider.rs b/crates/keymap_editor/src/action_completion_provider.rs new file mode 100644 index 0000000000000000000000000000000000000000..98428baeb2f7b419ba7354130e12f1a4710c8aea --- /dev/null +++ b/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, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> gpui::Task>> { + 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 = 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 = 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, + _position: language::Anchor, + text: &str, + _trigger_in_words: bool, + _cx: &mut Context, + ) -> bool { + text.chars().last().is_some_and(|last_char| { + last_char.is_ascii_alphanumeric() || last_char == '_' || last_char == ':' + }) + } +} diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index be20feaf5f8c1feea5b08fa3a6a3b542b26ef5ce..6f3521877e1384e46aed81c94294177620eba04b 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/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>, 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, humanized_action_names: &HumanizedActionNameCache, cx: &mut App, - ) -> (Vec, Vec) { + ) -> ( + Vec, + Vec, + 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) { @@ -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, + ) { + 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) { 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, context_editor: Entity, + action_editor: Option>, action_arguments_editor: Option>, + action_name_to_static: HashMap, + selected_action_name: Option<&'static str>, fs: Arc, error: Option, keymap_editor: Entity, @@ -2191,6 +2270,9 @@ impl EventEmitter 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 = 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) { + 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) -> 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> { + 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| -> 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) { + 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, ) { + 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) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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, keystrokes: FocusHandle, - action_input: Option, + action_arguments: Option, 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) -> impl IntoElement { - let background_color; - let border_color; + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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()) } }