summaryrefslogtreecommitdiff
path: root/docs/helix-keys.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/helix-keys.md')
-rw-r--r--docs/helix-keys.md104
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: