summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-07 09:48:05 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit9da20917b6f4cf98fd9b5c2c0a904e4f6a8380a9 (patch)
treec7c8c4d1126def565eb0b1d20ad29782eb49c5e7 /docs
parentb2cdd8ee719961b2f037f20bf0f88fad94ca73b4 (diff)
downloadpardes-9da20917b6f4cf98fd9b5c2c0a904e4f6a8380a9.tar.gz
pardes-9da20917b6f4cf98fd9b5c2c0a904e4f6a8380a9.zip
helix diff testing and feature parity
Diffstat (limited to 'docs')
-rw-r--r--docs/helix-keys.md291
1 files changed, 291 insertions, 0 deletions
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**; `<n>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` / `<n>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; `<n>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` / `<n>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 `<from>`), `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 |
+| --- | --- | --- | --- |
+| `<n>` 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) |
+| `<n>G`, `<n>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<ch>` / `F<ch>` | 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<ch>` / `T<ch>` | 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|`, `<n>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<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` | 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<pair>` / `ma<pair>` | 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<ch>` | surround selection (or cursor char) with the `<ch>` 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<from><to>` | replace enclosing `<from>` pair chars with `<to>`'s | `Pane.pending_ch` holds `<from>` while `<to>` pends; file panes only | helix-verified (phase 5) |
+| `md<ch>` | delete the enclosing `<ch>` 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 `<n>` 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 |
+| `"` `<reg>`, 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`.