feat(protocol): one global keymap every mode inherits from (improvise-636d)

`?` resolved in 2 of 17 modes: bound once on Normal and reached by
RecordsNormal through the single inheritance edge. It was missing from all
three panels, TileSelect, SearchMode, ImportWizard and Help itself.
Meanwhile `bind_panel_toggles` had two call sites and `:` was written into
two spines while `?` was in one — nothing kept the spines in agreement.

`global_keymap()` binds `?`, `:` and the three panel toggles, and all
seventeen modes are built `Keymap::with_parent(global)`. The answer to "which
modes must not inherit" is **none**: `lookup_local` is exhausted before the
parent, so the eight text-entry modes and SearchMode shadow every global key
with their own `AnyChar`, and ImportWizard with `Any`. Only Help writes an
override, and it is a decision — `?` closes the overlay.

The global binds Char keys only, because `AnyChar` is what does the
shadowing; F1 therefore stays on Normal although it runs the same command.
`TextEntry::with_parent` is folded into `new`, which now takes the parent
every text-entry mode has.

Four guards: the chain ends at one global (Arc::ptr_eq, not by value), the
global is Char-only and prefix-free, no mode binds a toggle of its own, and
every global key resolves in every mode and types where it must. Each was
proved by injecting its fault; evidence in context/test-evidence/protocol.md
records the two cases where a neighbouring test stayed green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 17:48:36 -07:00
parent 523d57197c
commit 11320f771a
4 changed files with 360 additions and 71 deletions
+84
View File
@@ -718,3 +718,87 @@ Faults injected, each against the fixed code:
`attempt to add with overflow`.
- the same, release: the 65536 arm fails with `left: [5]`, `right: [32]` —
the wrap reported as the minimum width.
---
## `src/keymap.rs` — the global (tests live in `improvise-command`)
The keymap's tests sit with `dispatch` in
`improvise-command/src/command/keymap.rs`, because checking a bound name needs
a registry (`context/code/protocol.md` §6). The table itself is this crate's,
so its evidence is here.
### every_modes_keymap_chain_ends_at_the_same_global
Before improvise-636d `Keymap::with_parent` had **two** call sites in
seventeen modes, and `?` therefore resolved in **2 of 17** — not in any panel,
not in TileSelect, SearchMode or ImportWizard, and not in Help itself.
The comparison is `Arc::ptr_eq`, and that is the whole point: the defect being
removed is *several tables that agree today*, which any by-value comparison
would accept. The test also asserts no mode **is** the global, so a keymap set
that made Normal the root — inheritance "working" with sixteen children and
one privileged parent that is also a mode — fails rather than passes.
Fault injected against the fixed code: `SearchMode` rebuilt as
`Keymap::new()`. It fails with `SearchMode does not reach the global — its
chain roots elsewhere`. Note what that injection also showed, which is why the
sibling test below is not redundant:
`every_global_key_resolves_in_every_mode_and_is_shadowed_where_it_must_be`
stayed **green** under it, because SearchMode's own `AnyChar` answers `?` with
`append-char` either way — the expected answer. Resolution alone cannot see a
missing inheritance edge behind a catch-all; only the chain can.
### the_global_binds_only_unmodified_char_keys
`KeyPattern::AnyChar` matches `KeyCode::Char` and nothing else, so the
shadowing that makes the global free in the eight text-entry modes is a
property of the **key kind**, not of the binding. A global `F1` would reach
the parent mid-edit and throw away a half-typed cell; a global `Ctrl-x` would
miss `lookup_local`'s `Char`-with-`NONE` retry, which is what a shifted `?`
relies on. The test asserts the shape rather than a list of keys, plus a count
derived from `Panel::ALL` so a fourth panel does not need editing here.
It also refuses a `Binding::Prefix` in the global, because a prefix reached
through a parent would be resolved by `Keymaps::prefix_keymap` in seventeen
modes at once — `prefix_keymaps_are_flat` checks nesting, not location.
Fault injected: `km.bind_args(KeyCode::F(1), none, "enter-mode", vec!["help"])`
added to `global_keymap`. This test fails naming `F(1)`, and — separately and
for the better reason — the shadowing test fails with
`Editing: F(1) must reach append-char, not the global`.
### no_mode_binds_a_panel_toggle_of_its_own
`bind_panel_toggles` was called **twice** (Normal and `PanelKeymap::new`)
while `:` was written into two spines and `?` into one. Nothing held those
three in agreement; the asymmetry between them was the only evidence they had
drifted, and it was in the source rather than in a test.
This is the mechanical check that the duplication is gone, expressed as a
property rather than as a grep: a mode's **own** `bindings()` may not contain a
toggle key, so re-adding a second call site fails here whichever keymap it is
added to. A grep for the call count could not say that.
Fault injected: `bind_panel_toggles(&mut normal)` restored in
`default_keymaps`. Fails with `Normal binds 'F' itself; the global already
does`. Note `every_mode_with_panel_toggles_binds_all_of_them` — which already
existed — stays green under that injection, because it goes through `lookup`,
which walks parents and so cannot tell a local binding from an inherited one.
### every_global_key_resolves_in_every_mode_and_is_shadowed_where_it_must_be
The half no existing test could state: `every_hint_key_resolves_in_its_mode`
checks a mode's hint line against its own map and is blind to a key no hint
mentions, which is every global key in sixteen of seventeen modes.
The keys are read **off the global** rather than listed again here, so adding
a sixth global key extends the check with no edit. The expectation table is
derived too — the typing modes come from `AppMode::text_entry()`, not from a
literal list — so the one thing stated by hand is Help's `?`, which is the
only decision.
Help's arm asserts **both** that `?` closes the overlay and that the binding is
Help's **own**: without the second assertion the arm would also pass if Help
had no `?` at all and something upstream happened to answer `enter-mode
normal`, which is precisely the state the ticket found.