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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user