diff --git a/governance/README.md b/governance/README.md index f68d6786..c0812e7e 100644 --- a/governance/README.md +++ b/governance/README.md @@ -123,6 +123,7 @@ You might also want, project-permitting: - [D-79: Workspace content search is a pure-Dart in-process engine, outside pql](decisions/architecture.md#d-79-workspace-content-search-is-a-pure-dart-in-process-engine-outside-pql) — _architecture_ - [D-80: `files.read` allows trusted Claude config roots beyond the workspace](decisions/architecture.md#d-80-filesread-allows-trusted-claude-config-roots-beyond-the-workspace) — _architecture_ - [D-81: Right-pane reader load is driven by a retained `ReaderNav`, not per-view state or bus retention](decisions/architecture.md#d-81-right-pane-reader-load-is-driven-by-a-retained-readernav-not-per-view-state-or-bus-retention) — _architecture_ +- [D-82: Keymap sequences are space-separated; matching is a reusable matcher consumed at the interception point](decisions/architecture.md#d-82-keymap-sequences-are-space-separated-matching-is-a-reusable-matcher-consumed-at-the-interception-point) — _architecture_ ## Open questions diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index eac7b078..db6f8a5e 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -379,3 +379,14 @@ Core, rendering, IPC, kernel, panel manager. - **Raised by:** 2026-06-01 — the user reported decisions opening only on the second click, diagnosed the lost-on-mount race, and explicitly chose a "right-pane nav history helper" over a MessageBus fix ("leaving them in the messagebus is the wrong shape"). --- + +### D-82: Keymap sequences are space-separated; matching is a reusable matcher consumed at the interception point +- **Date:** 2026-06-01 +- **Status:** accepted +- **Decision:** A keymap binding's `keys:` may be a **multi-chord sequence** written as a **space-separated string** (`'d d'`, `'g g'`, `'ctrl+k ctrl+s'`): space means "then". The existing forms are unchanged — `+` joins modifiers within one chord, and a YAML **list** (`[ctrl+p, meta+p]`) still means **alternation** ("or"). A leading digit run in normal mode is captured as a **repeat count** and applied by firing the resolved intent N times (not threaded into the intent payload). Sequence *matching* is a reusable two-part facility: `Keymap` answers a **stateless** prefix/exact/none query over its bindings, and a small **stateful `SequenceMatcher`** (pending buffer + count + timeout) wraps it. **Interception lives at the consumer**, not in the global key handler. +- **Rationale:** Real Vim needs `dd`, `gg`, `dw`, `ciw`, `5j` — impossible under single-chord resolution (T-205). Of the candidate separators, every punctuation option (`,` `;` `>`) is *itself a bindable key* (Vim leader, repeat-find, indent), so each would force an escape rule (`\>`). A literal **space never appears as a key spec** — the space key is always spelled `space` — so it separates with zero collisions and no escaping, and matches Vim-doc / VS Code convention (`ctrl+k ctrl+s`). Count-by-repeat keeps intents `const` and payload-free. The interception split is forced by the host: the global dispatch is a passive `KeyboardListener` (returns void, **cannot swallow** events), so normal-mode keys can't be intercepted there before `EditableText` types them — the editor's `Focus.onKeyEvent` (returns `KeyEventResult`) is the only place that can consume them. Putting the matcher there (T-206) keeps T-205 pure, headless, and unit-testable, and avoids the global handler buffering keys it has no power to swallow. +- **Cost / alternatives:** Rejected a **full Vim grammar engine** (operator × motion × text-object × count combinatorics) for the first pass — common operator+motion combos are enumerated as explicit sequence bindings in `vim.yaml` instead, covering the demo surface without a parser. Rejected **comma/`>`/semicolon separators** (escape wart). Rejected **count-in-intent-payload** (would de-`const` every intent and bloat the bridge). Rejected **buffering in the global `resolveEvent`** (it can't swallow, so it would double-handle with the editor). Cost: `KeymapBinding` generalises `chord` → an ordered chord list; a new `SequenceMatcher`; the editor owns interception. +- **Cross-reference:** T-205 (matcher + notation), T-206 (editor interception + motions), T-65 (`vim.yaml`). Builds on the T-117 keymap layer; `vim.*` scope flags from [D-81](#d-81-right-pane-reader-load-is-driven-by-a-retained-readernav-not-per-view-state-or-bus-retention)-era work are set by the Vim mode service (T-207). +- **Raised by:** 2026-06-01 — scoping the Vim preset for a Vim-power-user demo; the user weighed `,`/`;`/`>` separators and flagged the escaping problem, which made space the collision-free choice. + +---