Behaviour and resolution logic for buffer-type-specific keybindings.
A keymap scope determines which keybindings are active based on the type of view the user is interacting with. Think of scopes as Neovim's which-key groups: flat, explicit declarations of what keys do in a given context.
Design rules
The keymap follows Neovim's flat model, not Emacs's composed hierarchy. See AGENTS.md § "Keymap Architecture" for the full rationale. Three rules:
Keymap is the single authority. If a key resolves through a scope trie, the command runs. Commands never re-check context internally. Don't bind a command in a scope where it shouldn't run.
Scopes are flat. No implicit inheritance or minor-mode stacking. Shared bindings come in through bulk registration helpers that merge named binding groups at compile time. See #1278.
Derived scope. The active scope should follow from what's on screen, not from a manually managed field. (Target architecture; today
workspace.keymap_scopeis still a field.)
Built-in scopes
:editor— normal text editing (default):agent— agent chat view (Board zoom or side panel):file_tree— file tree panel:git_status— git status panel
Resolution layers
Each scope module implements this behaviour and declares its own keybindings as trie data. Keymap resolution walks layers in priority order:
- User overrides for the active scope + vim state
- Vim-state-specific bindings from the scope module
- Shared bindings that apply across all vim states for the scope
:not_found(caller decides what to do: self-insert, passthrough, etc.)
Global bindings (leader sequences, Ctrl+S) and the Mode FSM fallback are handled by the caller, not by this module.
Context parameter
The keymap/2 callback receives a context keyword list. Phase 1 always
passes []. Phase 2 (#215) will pass [filetype: :elixir] so the editor
scope can return filetype-specific bindings. Agent and file_tree scopes
ignore context.
Summary
Types
Extra context for scope keymap resolution (e.g., filetype).
A single help binding: {key_string, description}.
A group of help bindings with a category label.
Result of resolving a key through the scope system.
A scope name atom.
Vim state relevant to scope resolution.
Callbacks
Returns a human-readable name for display (e.g., "Agent").
Returns categorized help groups for the ? help overlay.
Returns the shared binding groups this scope includes.
Returns the keybinding trie for a specific vim state.
Returns the atom name of this scope (e.g., :agent).
Called when this scope becomes active. Initialize scope-specific state.
Called when this scope is deactivated. Clean up scope-specific state.
Returns bindings that apply regardless of vim state.
Functions
Returns all registered scope names.
Returns help groups for the given scope and focus context.
Returns the scope module for a given scope name.
Resolves a key through the scope's keybinding layers.
Resolves a key against a specific trie node (for multi-key sequences).
Types
@type context() :: keyword()
Extra context for scope keymap resolution (e.g., filetype).
A single help binding: {key_string, description}.
@type help_group() :: {String.t(), [help_binding()]}
A group of help bindings with a category label.
@type resolve_result() :: {:command, atom()} | {:prefix, Minga.Keymap.Bindings.node_t()} | :not_found
Result of resolving a key through the scope system.
{:command, atom()}— execute this named command{:prefix, Bindings.node_t()}— key is a prefix; more keys needed:not_found— scope has no binding for this key
@type scope_name() :: :editor | :agent | :file_tree | :git_status
A scope name atom.
@type vim_state() :: :normal | :insert | :input_normal | :cua
Vim state relevant to scope resolution.
Callbacks
@callback display_name() :: String.t()
Returns a human-readable name for display (e.g., "Agent").
@callback help_groups(focus :: atom()) :: [help_group()]
Returns categorized help groups for the ? help overlay.
Each group is a {category_label, [{key_string, description}]} tuple.
The focus parameter lets scopes return different help content depending
on the current UI context (e.g., :chat vs :file_viewer in the agent
scope).
Return [] to indicate no help overlay is available for this scope.
Returns the shared binding groups this scope includes.
Each entry is either a group name atom or a {group_name, opts} tuple
with exclusion options. Used for introspection and documentation; the
actual merge happens in the scope's trie-building functions.
Optional callback. Returns [] by default.
@callback keymap(vim_state(), context()) :: Minga.Keymap.Bindings.node_t()
Returns the keybinding trie for a specific vim state.
The context parameter is a keyword list that phase 1 passes as [].
Phase 2 will pass [filetype: :elixir] for filetype-specific bindings.
@callback name() :: scope_name()
Returns the atom name of this scope (e.g., :agent).
Called when this scope becomes active. Initialize scope-specific state.
Called when this scope is deactivated. Clean up scope-specific state.
Functions
@spec all_scopes() :: [scope_name()]
Returns all registered scope names.
@spec help_groups(scope_name(), atom()) :: [help_group()]
Returns help groups for the given scope and focus context.
Delegates to the scope module's help_groups/1 callback.
Returns [] if the scope is not found.
@spec module_for(scope_name()) :: module() | nil
Returns the scope module for a given scope name.
@spec resolve_key(scope_name(), vim_state(), Minga.Keymap.Bindings.key(), context()) :: resolve_result()
Resolves a key through the scope's keybinding layers.
Walks layers in priority order:
- User overrides for the scope + vim state (from
Keymap.Active) - Vim-state-specific bindings for the active scope
- Shared bindings for the active scope
- Returns
:not_foundif no scope binding matches
Global bindings (leader sequences, Ctrl+S) and Mode.process fallback are handled by the caller, not by this function.
@spec resolve_key_in_node(Minga.Keymap.Bindings.node_t(), Minga.Keymap.Bindings.key()) :: resolve_result()
Resolves a key against a specific trie node (for multi-key sequences).
Used when continuing a prefix sequence within a scope. The caller tracks which trie node to continue from.