diff options
Diffstat (limited to 'docs/helix-keys.md')
| -rw-r--r-- | docs/helix-keys.md | 104 |
1 files changed, 78 insertions, 26 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md index b307ae30..81bc0248 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -1,21 +1,34 @@ # pardes ↔ helix keybinding plan Living tracking doc for the helix-parity effort. Every key / table row in -`helix/book/src/keymap.md` (checkout: `~/05-genizah/helix`, HEAD 278b24389) -appears in exactly one of the three sections below. As of phase 5 every -helix-equivalent row in A and B is differentially verified against real -helix (see "Differential testing" at the bottom); the two waivers are -listed in their rows. +`helix/book/src/keymap.md` appears in exactly one of the three sections +below. The keymap was read at UPSTREAM commit 278b24389 of the genizah +checkout (`~/05-genizah/helix`); that checkout now sits on the local +`pardes-harness` branch, whose three commits add only the harness, so +`book/src/keymap.md` is byte-identical at its tip (694e7dfd). As of phase 5 +every helix-equivalent row in A and B is differentially verified against real +helix (see "Differential testing" at the bottom). Five of the six waivers are +named in the row they belong to; the sixth, `wiX-edit-drops-sel`, belongs to no +single key — it is the anchor-only divergence every insert-mode edit shares — +and is described in the Files list at the bottom instead. -Code map (line numbers approximate): `src/pardes.zig` — `Key` ~296, -`handleKey` ~1171 (intercept order is load-bearing, see comment there), -`setPaneRange` ~1566 (helix range → pane state), `handleNormal` ~1736, -`enterInsert` ~2171, `handleInsert` ~2278, `normalDelete` ~2600, -`normalYank` ~2686, `normalPaste` ~2727, `normalChange` ~2799, undo ~3386. -Pure text math: `src/modal.zig` (the `hx*` family is the helix-semantics -layer: gap offsets + ranges over the flat text). Differential harness: -`test/hxdiff.zig` + `test/hxcases/{cases,goldens,waivers}.jsonl` + -`test/hxcases/regen.sh` — see "Differential testing" at the bottom. +Code map, by SYMBOL — line numbers rot, names do not. Body-normal key +RECOGNITION is `src/normal_input.zig`: a pure state machine over `Role` (one +per bound command, matched against `src/config.zig`'s chord lists by +`Pardes.normalInput`), a `Prefix`, a `MatchSub` and a count, emitting a +semantic `Action` that both the text and PDF adapters consume. It knows +nothing about panes or text. `src/pardes.zig` then EXECUTES: `Key`, +`handleKey` (intercept order is load-bearing, see the comment there), +`handleNormal` (marshals `Pane`'s compact fields in and out of +`normal_input.State` through `paneNormalState` / `putPaneNormalState`), +`executeNormalAction`, `setPaneRange` (helix range → pane state), +`enterInsert`, `handleInsert` / `insertKey`, `insertTab`, `normalDelete`, +`normalYank`, `normalPaste`, `normalChange`, `replaySels`, `multiOnce`. Undo +and redo are per pane kind, in `file_pane` and `term_pane`. Pure text math: +`src/modal.zig` (the `hx*` family is the helix-semantics layer: gap offsets + +ranges over the flat text). Differential harness: `test/hxdiff.zig` + +`test/hxcases/*.jsonl` + `test/hxcases/regen.sh` — see "Differential testing" +at the bottom. ## Global semantic divergences (read first) @@ -41,6 +54,15 @@ layer: gap offsets + ranges over the flat text). Differential harness: cursor/range endpoint is snapped to an extended-grapheme boundary. Rendering, mouse input and vertical motion translate those offsets through terminal-cell widths, so combining sequences and wide glyphs remain single cursor cells. + Three functions in `modal.zig` do the snapping — `graphemeStart` (repair an + offset back onto a boundary), `nextGrapheme`, `prevGrapheme` — each with an + arithmetic ASCII fast path over the UAX #29 segmenter. All three carry the + same exclusion by hand, because GB3 is the one UAX #29 rule that joins two + ASCII scalars: a CR takes a following LF into the same cluster. `nextGrapheme` + and `prevGrapheme` did not spell it out and so disagreed with `graphemeStart` + by exactly one byte on a CRLF file — a head could step to the offset between + CR and LF and be repaired straight back. Fixed; the test "GB3 keeps CR-LF one + cluster for every grapheme step" is what holds the three in agreement. - **Terminal panes: shell output is immutable.** Edit ops only ever drop/alter typed insertion runs. All "To implement" edit ops follow the same rule (motion/selection parts work on the full motion surface; the @@ -97,7 +119,7 @@ language-backend queries, and the shell pipe. | `Enter` (normal) | acme **look** chord: EXPLICIT selection, else file-ish word under cursor | pardes-specific, keep (helix normal-mode Enter unbound). Covers helix `gf`. Implicit motion residue falls back to the cursor word | pardes-specific | | `Tab` (normal) | acme **execute** chord | pardes-specific, keep; explicit-selection rule as Enter | pardes-specific | | `:` (normal, body) | focuses the pane's OWN tag as a one-line editor in **normal** mode, parked at the first EDITABLE column: motions (`w` `b` `e` `W` `B` `E`, `0` `$` `^`, arrows, `Home`/`End`) walk the whole rendered tag, `y` yanks the selection, `Enter`/`Tab` look/execute it (else the file-ish word under the cursor), `i`/`a`/`I`/`A` enter insert, `Esc` hands the body back. `h`/`j`/`k`/`l` are NOT motion here — a tagline is a place in the LAYOUT, so they run the same `Left`/`Down`/`Up`/`Right` builtins and land on the neighbouring pane's TAGLINE, still in normal mode (nothing that way = stay put, EXCEPT `k` off the topmost tagline — see the next row); the arrows keep the in-tag motion | helix `:` is command mode (section C); pardes' commands are acme words that live in the tag. `tag_col`/`tag_anchor` are columns of the RENDERED tag (prefix ++ tail) — one coordinate space, so the live mode+path prefix is selectable, yankable and executable, while every edit op (typing, `Backspace`, `i`/`a`/`I`/`A`) measures from the first editable column and is inert inside it | pardes-specific | -| `k` (tag normal, topmost tagline) | focuses the TOPBAR — row 0, the global tagline (`New Newcol Find Grep Help Tutor Dump NextColor Debug Kill`, plus `Restore <path>` once a dump exists). It is its own one-line normal mode: `h`/`l` and the arrows by grapheme (row 0 has no window left or right to walk to), `w`/`b`/`e`/`W`/`B`/`E` and `0`/`$`/`^` by word, `Enter`/`Tab` runs the word under the cursor through the same dispatch a MIDDLE click on it uses, `j` drops back onto the topmost pane's tagline, `Esc` leaves. No insert mode and no selection — the bar is chrome with no tail to own | pardes-specific. The topbar is not a pane, so `focusDir` can never reach it: this is a fallback on the `.Up` branch of the tagline hop, from a TAGLINE only (a body's `SPC w k`/`Ctrl-w k` keep their pane-to-pane meaning). Its whole state is one global `topbar_col: ?u16` (row 0 has no pane to hang it on), cleared by any mouse press and BEFORE the chord dispatch, because `Kill` up here frees the session the way `Del` frees a pane. The motion vocabulary is literally the tag's — both call `lineMotion` | pardes-specific | +| `k` (tag normal, topmost tagline) | focuses the TOPBAR — row 0, the global tagline (`config.topbar_str`, today `New Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill`, plus `Restore <path>` once a dump exists). It is its own one-line normal mode: `h`/`l` and the arrows by grapheme (row 0 has no window left or right to walk to), `w`/`b`/`e`/`W`/`B`/`E` and `0`/`$`/`^` by word, `Enter`/`Tab` runs the word under the cursor through the same dispatch a MIDDLE click on it uses, `j` drops back onto the topmost pane's tagline, `Esc` leaves. No insert mode and no selection — the bar is chrome with no tail to own | pardes-specific. The topbar is not a pane, so `focusDir` can never reach it: this is a fallback on the `.Up` branch of the tagline hop, from a TAGLINE only (a body's `SPC w k`/`Ctrl-w k` keep their pane-to-pane meaning). Its whole state is one global `topbar_col: ?u16` (row 0 has no pane to hang it on), cleared by any mouse press and BEFORE the chord dispatch, because `Kill` up here frees the session the way `Del` frees a pane. The motion vocabulary is literally the tag's — both call `lineMotion` | pardes-specific | | `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — normal/tty modes only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; kept alongside the leader because a pane in raw **tty** mode never sees `SPC` (the shell owns it), so this is the only keyboard way out of one. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode (`:`) it moves focus to the neighbour's BODY, while the bare letters `h/j/k/l` there move to its TAGLINE (next row) | pardes-specific | | `Alt-n` | new terminal below (any mode) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific | | `Alt-c` | move active terminal to a fresh column (any mode) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) | @@ -122,13 +144,19 @@ Status: `todo` → set to `done (phase N)` as rows land. All rows verified against keymap.md. Edit ops on terminal panes obey the immutable-output rule (typed runs only) — same as `d`/`c` today. -Phase 2 state added to `Pane`: `count` (accumulator, capped 0xffff), -`pending2` (m-mode sub-key), `pending_ch` (mr's `<from>`), `find_op`/`find_ch` -(Alt-. repeat). `pending` also holds `m` `[` `]` and the char-arg ops -`f F t T r`. New pure text math in modal.zig (`findChar`, `matchBracket`, -`paragraphFwd/Bwd`, textobject/surround ranges, `replaceRange/Chars`, -`changeCase`, `joinLine`, `indentLines`, `adjustNumber`, `deleteSpan`, -`advanceBy`) with inline tests; `zig build unit-test` runs them. +Phase 2's recognition state is now `normal_input.State`: a count (accumulator, +capped 0xffff), a `Prefix` (`g` `z` `m` `f` `F` `t` `T` `r` `]` `[`), a +`MatchSub` (m-mode's `i`/`a`/`s`/`r`/`d`) and one `held_char` for `mr`'s +`<from>`. `Pane` keeps the same four fields it always did — `count`, +`pending`, `pending2`, `pending_ch` — but purely as compact per-pane storage, +marshalled in and out by `paneNormalState` / `putPaneNormalState`; the +comments on them say so. `find_op`/`find_ch` (the `Alt-.` repeat target) stay +on `Pane`, because the parser emits `repeat_find` without remembering what was +found. Pure text math in `modal.zig`: `findChar`, `matchBracket`, +`paragraphFwd`/`paragraphBwd`/`paragraphRange`, textobject/surround ranges, +`replaceRange`/`replaceChars`, `changeCase`, `joinLine`, `indentLines`, +`adjustNumber`, `deleteSpan`, `advanceBy` — each with inline tests that +`zig build unit-test` runs. ### Counts @@ -154,7 +182,7 @@ Phase 2 state added to `Pane`: `count` (accumulator, capped 0xffff), | Key | Behavior | Notes | Status | | --- | --- | --- | --- | -| `r<ch>` | replace selection/char with `<ch>`, newlines kept | terminals: replaces the typed-run byte under the cursor if any, else no-op (`runByteAt`) | helix-verified (phase 5) | +| `r<ch>` | replace selection/char with `<ch>`, newlines kept | terminals: replaces the typed-run byte under the cursor if any, else no-op — one code path for both kinds, since `normalReplaceChar` goes through `Pardes.editText`/`setEditText`, which route to a file's content or to `term_pane`'s typed-run overlay | helix-verified (phase 5) | | `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection, head on its last char | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) | | `~` | switch case of selection/char, selection kept | terminals: run byte only | helix-verified (phase 5) | | `` ` `` | selection to lowercase | terminals: run byte only | helix-verified (phase 5) | @@ -331,7 +359,12 @@ case corpus. Both sides speak the same contract: a case is `"file"`/`"tty"`, ignored by helix), a result is the full final buffer text, the mode (`normal`/`insert`/`select` — select only for `v` extend mode), and the block-cursor positions of the primary selection's head -(`cursor`) and other end (`anchor`), 0-based row + byte col. +(`cursor`) and other end (`anchor`), 0-based row + byte col. With more than +one selection the result line grows two more fields — `sels`, every range in +document order measured the same way, and `primary`, the index of the one +`cursor`/`anchor` describe. Both are omitted at one selection, which is why +every golden written before multiple cursors existed is still byte-for-byte +valid. Files (all in `test/hxcases/`): @@ -354,14 +387,33 @@ Files (all in `test/hxcases/`): (one yank register, not one value per range), and `sel-regex-caret` / `sel-regex-dot-newline` (mvzr is not the Rust regex crate: no multi-line `^`/`$`, and `.` matches a newline). -- `test/hxdiff.zig` builds `pardes-hxdiff`, which drives the core - headlessly at 80x24 (22 body rows, matching helix's 22 text rows). +- `parity.jsonl` — 80 further cases, used only by the parity gate below. +- `parity-waivers.jsonl` — 13 named exemptions for the parity gate, in three + classes: one deliberate pardes binding (`ctrl-b-page`, since `Ctrl-b` IS the + tty toggle), eight pty VIEWPORT divergences (a terminal's view cannot scroll + below the vt's live grid bottom, so the cursor snaps into a different + scrolloff band — no text differs), and four case texts a pty cannot hold + verbatim (a literal TAB the emulator expands, a file with no trailing + newline, an all-whitespace last row the dump trims). +- `smoke.jsonl` — 20 cases referenced by nothing in the tree: no build step, + no script. Either wire it up or delete it. + +The pardes half is `test/hxdiff.zig`, which builds `pardes-hxdiff` and drives +the core headlessly at 80x24 (22 body rows, matching helix's 22 text rows). Run it: zig build hxdiff # cases + goldens + waivers, field-by-field; # unwaivered mismatch = per-case report + exit 1 zig build hxdiff -- test/hxcases/cases.jsonl # results to stdout, no diff + zig build hxparity # the SAME binary in --parity mode: cases.jsonl + # + parity.jsonl, each case run twice over the + # same text and keys (once in a file pane, once + # in a pty pane), both result lines must match. + # No goldens — the file pane IS the oracle, so + # "editing a shell pane behaves like editing a + # file" cannot drift. The case's own "pane" field + # is ignored; exemptions in parity-waivers.jsonl sh test/hxcases/regen.sh # regenerate goldens The helix half (`hx-harness`) lives on the `pardes-harness` branch: |
