From 9da20917b6f4cf98fd9b5c2c0a904e4f6a8380a9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 7 Jul 2026 09:48:05 -0300 Subject: helix diff testing and feature parity --- docs/helix-keys.md | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 docs/helix-keys.md (limited to 'docs') diff --git a/docs/helix-keys.md b/docs/helix-keys.md new file mode 100644 index 00000000..4a077c2c --- /dev/null +++ b/docs/helix-keys.md @@ -0,0 +1,291 @@ +# 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. + +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. + +## Global semantic divergences (read first) + +- **FULL HELIX MOTION MODEL (phase 5 decision — overrides phase 2's + "keep point motions").** Motions select the range they traverse with + helix's landing conventions: after `w` on "alpha beta" the anchor is + (0,0) and the block cursor sits ON the space at col 5 (not vim's col + 6); a following `d` deletes "alpha ". Which motions select a range and + which collapse to a point follows helix exactly (word/find/till/ + paragraph-class motions select; h/j/k/l-class char and line moves + collapse) — the checked-in helix goldens are the ground truth, verified + case-by-case by `zig build hxdiff`. Motion-created ranges live in + `pane.vsel` marked IMPLICIT (`vsel.explicit = false`); `v` extend mode, + `x`/`X`, terminal `n`/`N` (lookStep) and file `n`/`N` (searchStep) set + EXPLICIT ones. `d`/`c`/`y`/`~`/… act on the active selection either + way — that is the point of the model. The pardes-specific chords + (normal-mode Enter look / Tab execute, the clipboard-`p` intercept) + act on EXPLICIT selections only and fall back to the word under the + cursor when the selection is implicit motion residue. Mode is reported + "select" ONLY while `v` extend mode is on (helix keeps mode normal for + x/%/mi-created selections). +- **Columns are byte offsets** (`cur_col`). ASCII-only harness cases + sidestep this; real UTF-8 parity is out of scope. +- **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 + mutating half is file-pane only or run-only). +- **No soft wrap** in pardes file panes — visual line == textual line, so + helix's visual/textual distinction (`j` vs `gj`) collapses. +- **Undo is snapshot-per-edit-op / per-insert-session**, not a + transaction log. Anything requiring replayable edits (`.`, + macros) needs new machinery. + +## A. Implemented today + +Parity column (phase 5): **helix-verified** = matched field-by-field +(text/mode/cursor/anchor) against the helix goldens by `zig build hxdiff`; +**pardes-specific** = key helix leaves unbound (or a deliberate pardes +binding) — cannot mismatch a golden; **waived** = corpus-covered divergence +with a reason in `test/hxcases/waivers.jsonl`. + +| Key | Behavior (pardes today) | Notes / quirks vs helix | Parity | +| --- | --- | --- | --- | +| `h` `j` `k` `l`, arrows | char left/right, line up/down — collapse the selection to a point (helix) | no soft wrap so `j`/`k` are both visual and textual; sticky col, phantom-line-blocked | helix-verified | +| `w` `b` `e` | select to next word start / prev word start / next word end | full helix model incl. landing conventions (block cursor one before the next word after `w`) and newline/punct/EOF edges | helix-verified | +| `W` `B` `E` | long-word (WORD) variants | same | helix-verified | +| `Home` / `End` | line start / line end | matches `goto_line_start` / `goto_line_end` | helix-verified | +| `0` / `$` / `^` | line start / line end / first non-ws | pardes extras — helix leaves these unbound (spells them `gh`/`gl`/`gs`); `0` is a count digit while a count is pending | pardes-specific | +| `G` | bare `G` is a **no-op**; `G` = goto line n | vim-ism removed (phase 5): helix `goto_line` only acts with a count; `ge` is goto-last-line | helix-verified | +| `gg` / `gg` | goto first line / line n | | helix-verified | +| `ge` | goto last content line | matches `goto_last_line` (ignores the trailing empty line) | helix-verified | +| `gh` / `gl` | line start / line end (last char, not the newline) | | helix-verified | +| `Ctrl-d` / `Ctrl-u` | half page down/up, cursor follows | matches `page_cursor_half_down/up` | helix-verified | +| `Ctrl-f` | full page down | matches `page_down`; file panes only get `Ctrl-b` (see next row) | helix-verified | +| `Ctrl-b` | file panes: full page up. Terminal panes: **raw tty mode toggle** (`opts.tty_toggle`, configurable) | tty toggle is pardes-specific and wins on terminals; harness `pane:"tty"` cases avoid `Ctrl-b` | helix-verified (file) / pardes-specific (tty) | +| `PageUp` / `PageDown` | full page | matches helix `page_up`/`page_down` (view scroll + cursor snap to the scrolloff edge) | helix-verified | +| `zt` / `zz` / `zb` | scroll current line to top / center / bottom | matches `align_view_top/center/bottom` (helix harness pins scrolloff to pardes' 3) | helix-verified | +| `i` `a` | insert at selection start / after selection end | helix semantics: `i` before the selection, `a` selects and appends after it | helix-verified | +| `I` `A` | insert at first non-ws / line end | matches `insert_at_line_start` / `insert_at_line_end` | helix-verified | +| `o` `O` | open line below / above, copying the current line's indent levels; `o` opens n | matches `open_below`/`open_above` | helix-verified | +| `v` | select (extend) mode: motions extend from the fixed anchor; `v` again exits KEEPING the selection | mode "select" is reported only while this is on (helix keeps mode normal for x/%/mi selections) | helix-verified | +| `x` / `x` | select current line / extend one (n) line(s) down, char-range through the newline, cursor ON the `\n` | col-0 vim-ism removed (phase 5); matches `extend_line_below`, mode stays normal | helix-verified | +| `d` | delete selection (implicit or explicit); bare cursor = the 1-wide selection | always yanks (`Alt-d` noyank in B); terminals: drops typed runs only | helix-verified | +| `c` | change (delete + insert mode); linewise selections open a fresh indented line | same shape as `d` | helix-verified | +| `y` | yank selection; bare cursor yanks the 1-wide selection (char under cursor) | line-yank vim-ism removed (phase 5); yank keeps selection AND cursor (helix). Also emits `set_clipboard` (OSC 52 out) | helix-verified | +| `u` / `U` | undo / redo | restores the pre-edit selection (helix); snapshot granularity, no `Alt-u`/`Alt-U` history walking (skipped) | helix-verified | +| `p` (normal) | **async system-clipboard paste**: emits `get_clipboard`, reply arrives as `Event.paste` → sets yank register → pastes | pardes-specific plumbing; placement is helix semantics (B). hxdiff verifies it end-to-end through a fake clipboard that round-trips the yank | helix-verified | +| `Esc` (normal) | cancel pending prefix / exit select mode; the selection is KEPT | helix Esc does not collapse selections; never drops to tty mode | helix-verified | +| `Esc` (insert) | back to normal mode, cursor right after the insertion (no vim left-step) | | helix-verified | +| `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 | +| `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). Insert mode owns `Ctrl-w` = delete-word-back; `Ctrl-w` while editing a tag (tags are always insert) is a no-op | 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`) | +| `/` then `n`/`N` (file panes) | pardes' own grep-into-a-shell-pane search, n/N walk results (EXPLICIT selections) | KEEP, do not touch; not in the corpus (helix `/` is regex search) | pardes-specific | +| `n`/`N` (terminal panes) | lookable-token motion over scrollback (EXPLICIT selections) | KEEP, do not touch | pardes-specific | +| insert: printable text | file: real edit; terminal: typed run splice | | helix-verified (file) | +| insert: `Enter` | newline (keeps indent) | matches `insert_newline` | helix-verified | +| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0 | matches `delete_char_backward`; `Ctrl-h` alias in B | helix-verified | +| insert: `Up` `Down` `Left` `Right` | move cursor | matches helix's "not recommended" insert arrows | helix-verified | + +## B. To implement + +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 ``), `find_op`/`find_ch` +(Alt-. repeat). `pending` also holds `m` `[` `]` and the char-arg ops +`f F t T r`. `Pardes.paste_before` carries `P` across the clipboard round +trip. 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. + +### Counts + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `` digit prefix | count for motions (h/j/k/l/arrows, w/b/e/W/B/E, gj/gk), `x`, `f/t` family + `Alt-.`, `G`/`gg`, `g|`, `]p`/`[p`, `]Space`/`[Space`, `J`, `>`/`<`, `Ctrl-a`/`Ctrl-x` | `0` stays line-start when NO count is pending, count-digit otherwise. `Pane.count` accumulator, capped at 0xffff (ponytail). Digits are literal char args while a prefix waits (`f4` finds '4'). Any non-prefix key consumes the count; prefix setters carry it into their continuation | helix-verified (phase 5) | +| `G`, `gg` | goto line n (1-based, clamped), col 0 | plain `G` is a no-op like helix (vim-ism removed in phase 5; `ge` = last line) | helix-verified (phase 5) | + +### Movement + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `f` / `F` | find next / prev char (on it) | NOT confined to current line. `pending` holds the waiting op (`f`/`F`/`t`/`T`); ASCII targets only (byte columns — ponytail); not found = no move | helix-verified (phase 5) | +| `t` / `T` | till next / prev char (one short of it) | `modal.findChar` handles all four + counts (nth occurrence, till applied after) | helix-verified (phase 5) | +| `Alt-.` | repeat last `f`/`t`/`F`/`T` motion (`Pane.find_op`/`find_ch`), takes a count | decision: repeats ONLY the find family, not `m`/`[`/`]` (helix extends it there; marginal) | helix-verified (phase 5) | +| `g|`, `g|` | goto column n (1 = line start), clamped to the line | | helix-verified (phase 5) | +| `gs` | goto first non-whitespace | alias of the `^` handler | helix-verified (phase 5) | +| `gt` / `gc` / `gb` | goto screen top / center / bottom | view-relative (`pane.scroll()` + `pane.rows`), column kept (clamped) | helix-verified (phase 5) | +| `gj` / `gk` | textual line down / up (+ count) | alias `j`/`k` — no soft wrap | helix-verified (phase 5) | +| `PageUp` / `PageDown` | FULL page (was half) | `Ctrl-u`/`Ctrl-d` stay the half-page pair | helix-verified (phase 5) | + +### Changes + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `r` | replace selection/char with ``, newlines kept | terminals: replaces the typed-run byte under the cursor if any, else no-op (`runByteAt`) | 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; 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) | +| ``Alt-` `` | selection to uppercase | terminals: run byte only | helix-verified (phase 5) | +| `J` | join lines in selection (or cur+next; helix ignores the count) | helix semantics: the newline + next line's leading whitespace collapse to ONE space (first line untrimmed); selection and cursor STAY where they were (vim's cursor-on-the-space removed in phase 5); terminals: no-op | helix-verified (phase 5) | +| `>` / `<` | indent / unindent selected lines (count times) | width = `modal.INDENT_W` = **4 spaces** (helix harness pins its no-language indent style to Spaces(4) to match); `<` also takes one leading tab as a level; empty lines never indented (helix); selection kept, positions mapped through the edit (phase 5) | helix-verified (phase 5) | +| `Ctrl-a` / `Ctrl-x` | increment / decrement the decimal int under the cursor by count | helix-style: under the cursor only (no vim forward scan), `-` handled, i64 saturating, zero-padding width preserved (phase 5); cursor to the last digit, selection kept; terminals: no-op | helix-verified (phase 5) | +| `Alt-d` | delete without yanking | `normalDelete(yank=false)` | helix-verified (phase 5) | +| `Alt-c` | change without yanking | **skipped (phase 2)**: the pardes `Alt-c` window op (move pane to fresh column, any mode) wins — it's load-bearing global UX and the parent contract forbids touching it. `Alt-d` + `i` covers the behavior. Corpus-covered and waived (`alt-c-window-op`) | waived (phase 5) | +| `P` | paste before | same async clipboard round trip as `p`: `Pardes.paste_before` stashes the intent, the `Event.paste` reply consumes it | helix-verified (phase 5) | +| `p`/`P` semantics | yank ending `\n` pastes as whole lines below/above the SELECTION's line span; else inline at the selection's outer edge. The paste (× count) becomes the selection, head on its last char (helix) | terminals keep the run-splice-at-cursor path (before/after collapses). Still not pty-snap-testable (the snapshot harness never answers OSC 52), but hxdiff verifies it end-to-end via its fake clipboard | helix-verified (phase 5) | + +### Selection manipulation + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `;` | collapse selection to cursor | drops vsel/msel, cursor stays | helix-verified (phase 5) | +| `Alt-;` | flip anchor and head | vsel: swap cursor ↔ anchor; msel: cursor to the other end (r0/r1 swapped to keep the cursor-at-r1 invariant) | helix-verified (phase 5) | +| `%` | select whole buffer | vsel anchor 0,0, cursor on the buffer's last char | helix-verified (phase 5) | +| `X` | snap selection to line bounds | vsel → msel over its row span; bare cursor → 1-line msel; msel: already line-wise, no-op | helix-verified (phase 5) | +| `Alt-x` | shrink selection to line bounds | vsel only: partial first/last lines drop out; nothing left collapses the selection; msel: no-op | helix-verified (phase 5) | + +### Match mode (`m` prefix) — plain-text, no tree-sitter + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `mm` | goto matching bracket | dumb text scan with nesting for `()[]{}<>`; ON a bracket only (no TS "nearest pair"); works on terminals via the motion surface | helix-verified (phase 5) | +| `mi` / `ma` | select inside / around textobject | pairs `( ) [ ] { } < >` nesting-aware multi-line; quotes `' " `` ` `` ` **line-scoped** (plain-text strings don't span lines); `w`/`W` word run (+trailing ws around, leading if none); `p` blank-line block (+trailing blanks around). Empty inside (`()`) = no-op. Selections work on terminals; `Pane.pending2` holds the i/a/s/r/d sub-key | helix-verified (phase 5) | +| `ms` | surround selection (or cursor char) with the `` pair; wrap incl. pair becomes the selection | either bracket names its pair; any other ASCII char wraps with itself; file panes only | helix-verified (phase 5) | +| `mr` | replace enclosing `` pair chars with ``'s | `Pane.pending_ch` holds `` while `` pends; file panes only | helix-verified (phase 5) | +| `md` | delete the enclosing `` pair chars | file panes only | helix-verified (phase 5) | + +### View mode (`z` prefix) additions + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `zj` / `zk` (+ `zdown`/`zup`) | scroll view down / up one line, cursor untouched | `pane.scrollBy(±1)` | helix-verified (phase 5) | +| `zc` | center (alias of `zz`) | | helix-verified (phase 5) | +| `z Ctrl-d` / `z Ctrl-u` | half page with cursor | aliases of the bare handlers | helix-verified (phase 5) | +| `z Ctrl-f` / `z Ctrl-b` / `z PageUp` / `z PageDown` | full page | aliases | helix-verified (phase 5) | + +### Unimpaired (plain-text subset) + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `]p` / `[p` | next / prev paragraph (+ count) | helix `goto_next/prev_paragraph`: selects to the boundary (anchor at the origin), blank-line-delimited blocks, count iterates | helix-verified (phase 5) | +| `]Space` / `[Space` | add `` blank lines below / above, cursor staying on its text line | file panes only (shell output immutable) | helix-verified (phase 5) | + +### Insert mode + +All the mutating insert keys follow the pane split: file panes edit content +(`modal.deleteSpan` for the kills), terminal panes edit ONLY the typed run at +the cursor (word = space-delimited within the run). Ctrl-h/j/d are normalized +to Backspace/Enter/Delete and re-dispatched at the top of `handleInsert`. The +text-insert path now requires no ctrl/alt so modifier combos can't leak their +text. + +| Key | Behavior | Notes | Status | +| --- | --- | --- | --- | +| `Ctrl-w` / `Alt-Backspace` | delete word backward (ws then word-class run; at col 0 = the ordinary backspace join) | resolved per proposal: focus prefix restricted to normal/tty; insert owns `Ctrl-w` | helix-verified (phase 5) | +| `Alt-d` / `Alt-Delete` | delete word forward (word run + trailing ws; at EOL eats the newline) | | helix-verified (phase 5) | +| `Ctrl-u` | kill to line start | | helix-verified (phase 5) | +| `Ctrl-k` | kill to line end | | helix-verified (phase 5) | +| `Ctrl-h` | delete prev char (Backspace alias) | | helix-verified (phase 5) | +| `Ctrl-d` / `Delete` | delete next char; at line end joins the next line up | `Key.delete` (0xF0009) added + `mapKey` in tty.zig/gui.zig (vaxis + SDLK) + `forwardKey` sends `ESC[3~` in tty mode | helix-verified (phase 5) | +| `Ctrl-j` | insert newline (Enter alias) | | helix-verified (phase 5) | +| `Home` / `End` | line start / line end past-the-last-char (`goto_line_end_newline`) | file panes (terminal insert cursor rides its run, as before) | helix-verified (phase 5) | +| `PageUp` / `PageDown` | cursor page up / down, col kept (clamped to line) | file panes | helix-verified (phase 5) | +| `Tab` | insert a literal tab (`Key.tab` case + the text path both land `\t`) | file content is correct; NOTE: file-pane RENDERING of a literal tab has no tab-stop expansion (pre-existing — any tab-containing file shows the same); helix smart-tab skipped | helix-verified (phase 5) | + +## C. Skipped + +| Key(s) | Helix behavior | Reason | +| --- | --- | --- | +| `?`, `*`, `Alt-*` | rsearch / selection-as-pattern | search — pardes has its own `/` n N (grep-based, kept as-is); helix regex search machinery not wanted | +| `Space` mode, all rows (`f F e . b j g G k s S d D r a h ' w c C Alt-c p P y Y R / ?`) | pickers, LSP actions, clipboard menu, global search, palette | pickers/space-mode; clipboard already covered by pardes normal-`p` + OSC 52 yank | +| Popup `Ctrl-u`/`Ctrl-d`, Completion menu, Signature help tables | LSP popups | LSP | +| Picker table (all rows), Prompt table (all rows) | picker / prompt internals | pickers — pardes' tag line is its own one-line editor | +| `gn` `gp` `ga` `gm` | next/prev/alternate buffer | buffer nav — pardes panes aren't a buffer list | +| `gw` | word-label jump | label-jump overlay machinery, not core editing | +| `g.` | goto last modification | jumplist/history position tracking | +| Window mode table: `Ctrl-w` + `w v s t f F h j k l q o H J K L ns nv` (+ Ctrl variants) | splits/window management | window mode — pardes has its own Ctrl-w focus + Alt-n/Alt-c + mouse layout drags | +| `Alt-o`/`Alt-up`, `Alt-i`/`Alt-down`, `Alt-p`/`Alt-left`, `Alt-n`/`Alt-right`, `Alt-a`, `Alt-I`, `Alt-e`, `Alt-b` | syntax-node selection | tree-sitter | +| `]f [f ]t [t ]a [a ]c [c ]e [e ]T [T ]x [x` | TS unimpaired jumps | tree-sitter | +| `]g [g ]G [G` | git change jumps | needs VCS diff state | +| `gd gD gy gr gi`, `=`, `]d [d ]D [D`, insert `Ctrl-x` | definition/refs/format/diagnostics/completion | LSP | +| `"` ``, insert `Ctrl-r` | register select / insert | registers — single yank register only | +| `Q` / `q` | record / replay macro | macros — needs replayable input log | +| `Ctrl-i` `Ctrl-o` `Ctrl-s` (normal) | jumplist forward/back/save | jumplist | +| `Alt-u` / `Alt-U` | undo-history earlier/later | history timeline — linear snapshot u/U covers pardes | +| `s S Alt-s`, `Alt-minus Alt-_`, `& _`, `, Alt-,`, `C Alt-C`, `( ) Alt-( Alt-)`, `K Alt-K`, `Alt-:` | regex select/split, merge, align/trim, rotate, keep/remove, copy-to-line, ensure-forward | multicursor machinery — pardes has exactly one selection | +| `Alt-J` | join + select the inserted space | marginal over `J`; multicursor-flavored | +| `\|` `Alt-\|` `!` `Alt-!` `$` | shell pipe/insert/append/keep | shell — pardes executes via Tab / middle-click chords instead | +| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag | +| `gf` | goto file under selection | covered by pardes Enter-look | +| `Ctrl-c` | toggle comments | language-dependent | +| `Ctrl-z` | suspend | pardes IS the terminal multiplexer | +| insert `Ctrl-s` | commit undo checkpoint | undo is per-insert-session snapshots; no sub-session checkpoints | +| `Shift-Tab` (insert), smart-tab semantics | insert tab / smart tab | smart-tab machinery; plain Tab-inserts-tab lands in B | +| `Z` (sticky view mode) | persistent view mode | marginal; `z` one-shots suffice | +| `zm` (view) | align middle horizontally | marginal even with hscroll | +| `.` | repeat last insert | **deferred by decision**: needs recording the insert session's keystrokes and a replay path — a new subsystem; undo is whole-buffer snapshots with no edit log to piggyback on. Revisit after phases 2–5 if the log exists by then for another reason | +| Select/extend mode section (prose) | `v` turns all motions into extenders, `n`/`N` keep selections | implemented for motions (phase 5: `pane.select` + the fixed anchor, differential-verified — see the `v` row in A); helix's search-`n`/`N` extension doesn't apply (pardes `n`/`N` are its own search/look ops) | + +## Differential testing (phase 5) + +pardes is diffed key-for-key against real helix (checkout +`~/05-genizah/helix`, branch `pardes-harness`) over a shared JSON-Lines +case corpus. Both sides speak the same contract: a case is +`{"name","pane","text","keys"}` (helix key notation; `pane` 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. + +Files (all in `test/hxcases/`): + +- `cases.jsonl` — the corpus: 360 cases (255 file + 105 tty). Every + helix-equivalent row of sections A and B has at least a typical and an + edge case; pure navigation/selection bindings get a `pane:"tty"` twin + (same text/keys — helix on that text IS the oracle for tty navigation + parity). No tty twins for content-mutating ops (shell output is + immutable) or doc-marked terminal no-ops. tty case texts keep motions + inside the content (the tty motion surface trims trailing blank rows, + so ge/G-to-last-line style assertions stay off tty). +- `goldens.jsonl` — checked-in helix results, regenerated by `regen.sh` + (runs the `hx-harness` binary from the helix checkout; override with + `$HX_HARNESS`). Only needed when cases change — the diff itself runs + offline. +- `waivers.jsonl` — named exemptions, each with a reason. Two live ones: + `wiX-edit-drops-sel` (anchor-only: helix maps the selection through + insert-mode edits, pardes drops it on the first edit) and + `alt-c-window-op` (the pardes window op deliberately shadows helix + change-noyank). +- `test/hxdiff.zig` builds `pardes-hxdiff`, which drives the sans-IO core + headlessly at 80x24 (22 body rows, matching helix's 22 text rows) with + a fake clipboard that round-trips the yank register (so `y`/`p` + differential-test end-to-end despite the async OSC 52 plumbing). + +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 + sh test/hxcases/regen.sh # regenerate goldens + +The helix half (`hx-harness`) lives on the `pardes-harness` branch: +a full headless `Application` with LSP/tree-sitter/auto-pairs/word- +completion off, scrolloff pinned to pardes' 3, no-language indent style +pinned to Spaces(4), smart-tab off, and the buffer-setup transaction +committed as its own undo revision. Build: +`cargo build --release -p helix-term --features helix-term/integration --bin hx-harness`. -- cgit v1.3