Skip to content

input: Make completion and code-action menu keys rebindable - #3491

Closed
hlcfan wants to merge 4 commits into
longbridge:mainfrom
hlcfan:input-menu-keys
Closed

hlcfan wants to merge 4 commits into
longbridge:mainfrom
hlcfan:input-menu-keys

Conversation

@hlcfan

@hlcfan hlcfan commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #3490 (first two commits). Only the last two commits are new here; this stays a draft until that one merges.

Description

The completion and code-action menus consume MoveUp, MoveDown, Enter and Escape directly. An application therefore cannot rebind the menu keys (say ctrl-n / ctrl-p) without also changing caret movement and editing in every input, and cannot say "only while a menu is open".

This adds four dedicated actions and a key context for them:

  • OverlayMenuNext, OverlayMenuPrevious, OverlayMenuAccept, OverlayMenuDismiss.
  • The input adds menu_open to its key context while a menu is open. Default bindings in Input && menu_open: up, down, enter, shift-enter, escape. They are registered last because GPUI breaks ties between equally deep contexts by registration order; with no menu open nothing changes for caret or editing keys.
  • One generic overlay_menu_action handler is registered for every input kind and hands the action to the open menu, bubbling when unclaimed. It replaces the single-line menu_up / menu_down handlers.
  • CompletionMenu and CodeActionMenu accept the new actions in addition to the old ones, so existing custom bindings keep working.
  • Tab is deliberately not bound: it indents in editors and moves focus in inputs. Applications can add it.

An application rebinds with the ordinary keymap:

cx.bind_keys([
    KeyBinding::new("ctrl-n", OverlayMenuNext, Some("Input && menu_open")),
    KeyBinding::new("ctrl-p", OverlayMenuPrevious, Some("Input && menu_open")),
]);

Public API

  • gpui_base::input::{OverlayMenuNext, OverlayMenuPrevious, OverlayMenuAccept, OverlayMenuDismiss} (re-exported by gpui_component::input) — unit actions the open completion or code-action menu handles: select next / previous, accept the selected item, dismiss.
  • Key context menu_open, added to an input's key context while a completion or code-action menu is open; default bindings are in Input && menu_open.

Breaking Changes

None. The menus keep handling MoveUp, MoveDown, Enter and Escape. The default keys behave as before, now through the new actions while a menu is open.

How to Test

cargo test -p gpui-base -p gpui-component --lib
cargo clippy -p gpui-base -p gpui-component

menu_keys_follow_the_keymap_and_can_be_rebound drives the real keymap on an InputState: Escape dismisses, Enter accepts, and an added ctrl-y binding to OverlayMenuAccept in Input && menu_open accepts only while the menu is open. The editor completion and code-action tests pass unchanged (1333 base, 667 component).

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

The completion menu, its overlay and its key routing existed only for
`EditorState`: `present_completion_items`, `dismiss_completion_overlay` and
`insert_completion` were defined on `InputBaseState<EditorMode>`, and
`CompletionMenu` and `CaretAnchoredPopup` took an `Entity<EditorState>`. An
application that wants autocomplete in a one-line field, such as a URL bar
that completes `{{variables}}`, had no way to get it without turning the
field into a multi-line code editor.

`InputState` now hosts the same host-driven menu. The application decides
when to complete and what to offer; the field only shows the items, routes
keys to the menu and applies the chosen edit.

Base (`gpui-base`)
- `InputMode` gets `FieldExtras`, which holds a `CompletionMenuState`, in
  place of `()`. A plain input pays for one small struct; textareas are
  unchanged.
- New `InputState` methods mirroring the editor's: `present_completion_items`,
  `dismiss_completion_overlay`, `insert_completion`, `completion_menu_state`,
  `set_overlay_action_handler`, `has_overlay_action_handler` and
  `route_overlay_action`.
- `InputMode` overrides `hide_context_menu`, `is_context_menu_open` and
  `handle_context_menu_action`, so an edit, caret move or blur closes the
  menu and Enter/Escape reach it first, exactly as in the editor.
- The text-edit resolution in `insert_completion` moves into a shared
  `apply_completion_item` on `InputBaseState<M>`, used by both modes, so the
  two cannot drift. `CompletionMenuState::bump` becomes `pub(crate)` for it.
- Register `up`/`down` handlers on single-line inputs. They were registered
  only for multi-line ones, so the arrow keys never reached an open menu.
  `menu_up`/`menu_down` give the open menu the key and otherwise call
  `cx.propagate()`, so single-line inputs without a menu behave as before
  and parents (for example a command palette) still see the key.

Component (`gpui-component`)
- `CompletionMenu` and `CaretAnchoredPopup` become generic over the input
  kind. A crate-private `CompletionMode` trait carries the three things that
  differ: inserting an item, dismissing the overlay and the maximum width
  (the editor reads its LSP option; a field uses the default 320 px).
  `CompletionMenu<M = EditorMode>` keeps existing call sites inferring.
- `OverlayMode` gains an associated `Lsp` type and a `collect_floating`
  hook, so each kind declares the popovers it builds. The editor keeps its
  completion, code-action, hover and diagnostic set, a field builds only the
  completion menu, and a textarea builds none.
- The completion sync that diffed signatures and re-showed the menu is
  extracted into `sync_completion` and shared by the editor and the field.

Tests
- `single_line_input_hosts_a_completion_menu` presents items on an
  `InputState`, checks the menu is materialized as a floating overlay, that
  Escape closes it, that actions fall through when no menu is open, and that
  `insert_completion` replaces the typed prefix.
- gpui-base (1333) and gpui-component (666) lib tests pass; clippy is clean.
The menus consumed `MoveUp`, `MoveDown`, `Enter` and `Escape` directly, so
an application could not rebind the menu keys without also changing caret
movement and editing in the input, and had no way to say "only while a menu
is open". Single-line inputs additionally needed ad hoc `menu_up` and
`menu_down` handlers because they never registered `up`/`down` at all.

Add four dedicated actions: `OverlayMenuNext`, `OverlayMenuPrevious`,
`OverlayMenuAccept` and `OverlayMenuDismiss`.

- The input adds a `menu_open` key context while a completion or
  code-action menu is open (`M::is_context_menu_open`), so bindings can
  target `Input && menu_open`.
- Default bindings in that context: `up`, `down`, `enter`, `shift-enter`
  and `escape`. They are registered last because GPUI breaks ties between
  equally deep contexts by registration order, so they win over the plain
  `MoveUp`/`Enter`/... bindings only while a menu is open. Nothing changes
  for caret or editing keys when no menu is open.
- A single generic `overlay_menu_action` handler is registered for every
  input kind and hands the action to the open menu through
  `handle_context_menu_action`; an unclaimed action bubbles to the host.
  It replaces the single-line `menu_up`/`menu_down` handlers.
- `CompletionMenu` and `CodeActionMenu` accept the new actions in addition
  to the old ones, so existing custom bindings keep working. Accept and
  dismiss close the overlay like Enter and Escape do.
- The new actions are exported from `gpui_component::input`.

An application rebinds the keys with the ordinary keymap:

    cx.bind_keys([
        KeyBinding::new("ctrl-n", OverlayMenuNext, Some("Input && menu_open")),
        KeyBinding::new("ctrl-p", OverlayMenuPrevious, Some("Input && menu_open")),
    ]);

`tab` is deliberately not bound by default: it indents in editors and moves
focus in inputs, so accepting on it is left to the application.

Tests
- `menu_keys_follow_the_keymap_and_can_be_rebound` drives the real keymap
  on an `InputState`: Escape dismisses, Enter accepts, and an added
  `ctrl-y` binding accepts only while the menu is open.
- gpui-base (1333) and gpui-component (667) lib tests pass; clippy is clean.
@hlcfan hlcfan closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant