ADR 0113: Keybindings as data¶
Status¶
Accepted
Context¶
Every keybinding was a line of C. docs/EXTENSIBILITY.md has called this
"the biggest customization gap" since it was written, and ADR 0046
already flagged that building rebinding for Vim alone would be the wrong
shape — the Ctrl chain is not Vim's.
The bindings lived in two chains of if (event->key() == ...) inside
handleCtrlShortcut and handleAltShortcut, plus eight QShortcuts in
the window. A third copy of the same knowledge sat in the help panel,
written out by hand.
Decision¶
key.<chord> = <command>¶
Not the [keys] section the plan sketched: this config file has no
sections, and it already has two dotted families in lang.<id>.lsp and
filetype.<suffix>. A third matches what is there.
key.ctrl+s = editor.save
key.f5 = editor.compile
key.normal.ctrl+d = vim.half-page-down
key.ctrl+b = none
One registry for built-ins, window actions and plugins¶
CommandRegistry maps a dotted name to a function. The viewport
registers its commands, the window registers the ones that act on the
buffer list, and both go in the same table — which is the point. A user
rebinding Ctrl+S and a plugin bound to a key are then the same
mechanism, and neither needs new API.
Chords are canonicalised in core¶
Ctrl+Shift+F, shift+ctrl+f and CTRL+SHIFT+F are one chord, and the
editor has to agree with all three before it can look one up.
core/src/keymap.c puts modifiers in a fixed order and lowercases
everything, so the lookup is a string compare and the config file's keys
are the same strings a key press builds.
It is core rather than GUI because it is pure text, and therefore testable without a window.
Only chords, never bare keys¶
A binding needs Ctrl, Alt or Meta, or to be a function key. Shift does not count, because Shift+A is typing.
Without that rule a user could bind a and make the editor untypeable,
and every printable keystroke would take a hash lookup on its way to the
buffer. It also means the dispatch can sit before the key switch —
which it must, because that switch claims F1, F12, Escape and the arrows
by key code.
Vim's takeovers become two rows instead of an if¶
key.normal.ctrl+d beats key.ctrl+d. The mode is empty when
vim_mode = false, so a Vim-only binding simply does not match and a
non-Vim user never sees it — the behaviour ADR 0059 argued for, now
stated as data.
The help panel reads the same table¶
It listed the keys by hand, so it would have gone on describing the
defaults after a rebinding — the panel is the user's map of the
keyboard, and a map that lies is worse than none. Each row now asks
which chords run a command, which is also why the reverse lookup
exists. With key.f2 = editor.open the panel reads F2 / Alt+O.
A binding that names nothing is reported¶
Once, when the config is read, in the status bar: key.ctrl+g: no
command called 'editor.no-such-thing'. Saying it when the key is
pressed would be too late and too quiet; saying nothing leaves a dead
key looking like a bug in the editor.
Consequences¶
Ten parser tests in core, all eight mutations caught, and thirteen precedence tests in the GUI.
One mutation escaped at first: dropping the check for a trailing +.
"ctrl+" was already rejected for having no key at all, so the case
that needed the check was "ctrl+f+" — a separator after a key, which
without it parses as ctrl+f. The test existed for the wrong one of
the two.
key.* is not in the project-config allowlist, so a .ase.conf in a
repository you cloned cannot bind keys. That falls out of the default-
deny rule from ADR 0087 rather than being added here, which is what
that rule was for.
Two limits:
Vim's own sequences are not bindable. dd, gg and ciw are
parsed by a stateful Normal-mode dispatcher, not looked up as chords.
Making those data is a different and larger job, and the plan's own
normal:dd example is the part of it left undone.
Window shortcuts are rebuilt only at startup. They are QShortcuts
so that closing a buffer works while a panel holds focus, and a
QShortcut takes its sequence at construction. Editing key.ctrl+w
takes effect on the next launch, where the viewport's own bindings
follow the config's hot-reload immediately.