summaryrefslogtreecommitdiff
path: root/docs/helix-keys.md
blob: 23f8f92634e7fab03014dd430acf2fdb37e0dd73 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# 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)
  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; `Shift-Esc` is a second, fixed binding for the same toggle) | 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) | paste the core's yank register after the selection | helix default-register semantics; yank still mirrors OUT via OSC 52 (`set_clipboard`) but `p` never reads the system clipboard back — most terminals refuse the OSC 52 read, which left `p` a silent no-op | helix-verified |
| `Esc` (body normal) | clear a pending modal prefix / exit select mode, keeping the selection, then run `Toggleterm`: hop between the most recently focused document and terminal, exactly like `SPC w t` | Pardes-specific focus binding layered on helix's cleanup. A leader path, tag, topbar or search owns Esc while it is active; raw tty forwards it | pardes-specific (cleanup 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 |
| `:` (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 |
| `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`) |
| `Space` (normal, body) | the pardes LEADER: a key path from here runs a BUILTIN with no arguments — the same builtins the topbar and the tags hold. `SPC k` Kill, `SPC d` Del, `SPC f s` Save, `SPC f n` New (an empty temporary file focused in the calling pane's column), `SPC f f` Find (fd over the pane's directory, results into `+Search`), `SPC f g` Grep (grep -R over the CONTENTS of every pane's directory, the ones another pane already covers dropped, rows relative to the asking pane's own directory, results into `+Search`; like Find it takes an ARGUMENT — `Grep foo` executed, or a selection chorded onto the word, searches that and skips the input), `SPC h t` Tutor, `SPC c n`/`SPC c d` Newcol/Delcol, `SPC t d/c/n/r` Debug/Colors/NextColor/Crt, `SPC t t` ThemeSel (the whole theme ring as `Theme <name>` rows in an output buffer — the one list whose rows are COMMANDS, so n/N execute each row instead of looking it and stepping is a live preview), `SPC t p/l/a` Petscii/Palette/Ascii (an image pane's renderer toggles — the words its tag used to spell out, so the bar stays a bar), `SPC s d`/`SPC s r` Dump/Restore, `SPC w h/j/k/l` Left/Down/Up/Right (directional pane focus — the `Ctrl-w` prefix's four moves as builtins), `SPC w t` Toggleterm (focus hops between the most recently focused document — file, image or `+Search`/`+Help` buffer — and the most recently focused terminal; the same key hops back). `?` at ANY depth opens the Help builtin listing what the prefix can still reach (`SPC ?` = all, `SPC h ?` = the docs group), into a `+Help` output buffer. The pending path shows at the right edge of the active pane's tag; Esc — or any unmapped key — abandons it | helix spends Space on pickers/LSP (section C); pardes has neither, and acme's builtins are what a leader is for. Enum-derived: the paths, the Help lines and the execute dispatch all fold out of one `Builtin` enum at comptime. Normal mode on a BODY only — tags are always insert, tty keys belong to the program | pardes-specific |
| `/` then `n`/`N` (any pane) | pardes' own plain-substring search into a `+Search` output buffer, n/N walk the result rows and look them (EXPLICIT selections) | KEEP, do not touch; not in the corpus (helix `/` is regex search) | pardes-specific |
| `n`/`N` (terminal panes, no search armed) | 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`. 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 yank-register path as `p` | 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). Pinned by yankpaste.snap and the hxdiff yp cases | 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) |

### Multiple cursors

helix's `Selection` is a LIST of ranges with a primary index. pardes keeps the
PRIMARY where it always was (`cur_row`/`cur_col` + `vsel`) and the other ranges
in `Pane.sels`; an ordinary key is REPLAYED once per range (`replaySels`), so
every motion and operator above works at every cursor without being rewritten.
With one cursor nothing is replayed and nothing changed. The result contract
grew `sels` + `primary`, emitted only when there is more than one range.

| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `C` / `Alt-C` / `<n>C` | copy the selection to the next / previous line | helix `copy_selection_on_line`, incl. skipping lines too short to hold the column. **Byte columns, not visual ones** (a TAB counts as one) | helix-verified |
| `,` / `Alt-,` | keep only the primary / remove the primary | `Alt-,` on a lone cursor is a no-op (helix errors) | helix-verified |
| `)` / `(` / `<n>)` | rotate the primary forward / backward (wrapping) | ranges unchanged, only which one is primary | helix-verified |
| `Alt-s` | split the selection on newlines | helix `split_on_newline`; the newlines themselves drop out, primary becomes 0 (helix's own TODO) | helix-verified |
| `Alt-minus` / `Alt-_` | merge all ranges into one / merge the consecutive ones | helix `merge_selections` / `merge_consecutive_ranges` | helix-verified |
| `_` | trim whitespace off both ends of every range | empty and all-whitespace ranges drop; nothing left = collapse + keep primary (helix) | helix-verified |
| `<n>o` / `<n>O` | open n lines, one cursor per line | helix `open` with a count; this is why the `o-count` golden gained a `sels` field | helix-verified |
| every motion/operator | acts at every cursor | replayed last-range-first, so an edit never disturbs a range still waiting; each pass's result is remembered as a distance from the END of the text, which an earlier edit cannot move | helix-verified (56 msel-* cases) |
| `y` with several ranges | joins the ranges' text with newlines into the ONE register | helix keeps a register VALUE per range and pastes value[i] at range[i]. Waived (`msel-yank-paste`) | waived |
| `a` … `Esc` with several ranges | the primary's appended-over span is restored; the others collapse to bare cursors | `Pane.append_at` is a single field. Waived (`msel-append`) | waived |
| `&` | align selections into a column | **skipped**: needs visual (tab-expanded) columns, which nothing else in pardes measures |
| `Alt-(` / `Alt-)` | rotate the CONTENTS of the selections | skipped: a separate feature from rotating which range is primary |

Anything that reaches outside the buffer — a builtin, a language query — runs
once from the primary and drops back to a single cursor rather than firing per
range (`multiOnce`). Undo restores the primary from the snapshot and leaves the
other cursors where they are (`FileSnap` holds one range).

### Regex selection (`s` / `S`) — the interactive pair

Both arm the tag input the same way `/` does — the pattern is typed into the
tag tail after a marker, no popup — and both re-run on EVERY keystroke, from
the selection the prompt opened on (`Pane.sel_snap`). That is what makes the
selection a live preview, what makes typing a pattern one character at a time
land where pasting it whole would, and what makes Esc a plain restore. Enter
re-runs the final pattern down the same path, so a submit can never disagree
with what is on screen. Engine: **mvzr** (`build.zig.zon`), a bytecode VM that
compiles a runtime pattern with no allocator.

| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `s<pat>` Enter | select every match INSIDE each range | helix `select_on_matches`; a match sitting right off a range's end is dropped (what `\b` and empty matches produce there). No match, no pattern, or one that will not compile = the selection is left alone (helix's "nothing selected") | helix-verified |
| `S<pat>` Enter | split each range on its matches | helix `split_on_matches`; the pieces BETWEEN the matches, including the empty one a leading match produces | helix-verified |
| `s`/`S` then Esc | back to the selection the prompt opened on | the empty pattern applies nothing, so cancelling IS the restore — one path, not a second one | helix-verified |
| an all-lowercase pattern | matches case-blind | helix's smart-case. mvzr has no such flag, so the surface is lowercased instead (ASCII folding is byte-for-byte, so the offsets are identical) | helix-verified |
| `^` and `$` | assert at the SCAN position, not at a line | helix compiles with `multi_line(true)`, so its `^` is every line start. Waived (`sel-regex-caret`) | waived |
| `.` | matches a newline like any other byte | the Rust regex crate excludes `\n` by default; mvzr does not. `[^\n]` is the workaround and agrees in both. Waived (`sel-regex-dot-newline`) | waived |
| more than 64 matches | the ones past `MAX_SELS` are dropped | the ceiling the whole selection model has, not this key's |
| `K` / `Alt-K` | keep / remove ranges matching a regex | **skipped**: the same prompt, filtering instead of splitting — worth adding next |

### `Ctrl-c` — toggle comments

| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `Ctrl-c` | comment or uncomment every line the WHOLE selection touches | helix `toggle_comments`. One decision for the whole set — one uncommented non-blank line and everything gets commented — which is why it is a `wholeKey` and not a per-cursor replay | helix-verified |
| … at several cursors | each line once, in order | helix's `min_next_line`: two cursors on one line comment it once | helix-verified |
| … with mixed indents | the token goes in at the SHALLOWEST indent in the set | helix's `min`, quirks included: a deeper line is commented mid-whitespace | helix-verified |
| … uncommenting | one space after the token goes too, unless some line lacks it | helix's `margin` | helix-verified |
| … blank lines | skipped entirely, and they do not vote on commented-or-not | helix | helix-verified |
| which token | by file EXTENSION, from `config.comment_tokens` | the same notion of "language" `src/syntax.zig` picks grammars with. No extension (a terminal, an output buffer) or an unlisted one gets `config.comment_token_default` = `#`, which is helix's own `DEFAULT_COMMENT_TOKEN` and therefore what the oracle answers | helix-verified |
| block comments (`/* */`) | **skipped**: helix only reaches them when a language declares block tokens and no line tokens; every language in the table has a line comment |

### 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 (plain substring into an output buffer, 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 | helix's space mode is pickers + LSP + a clipboard menu, none of which exist here — but the KEY is taken: pardes' own leader runs the acme builtins (section A). Clipboard: yank mirrors out via OSC 52, `p` pastes the register |
| 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 |
| `K Alt-K`, `Alt-:` | regex keep/remove, ensure-forward | `K`/`Alt-K` are the same prompt `s`/`S` now have, filtering instead of splitting (`s S` moved to "Regex selection" in A, the rest of the family to "Multiple cursors") |
| `&`, `Alt-(` / `Alt-)` | align selections, rotate selection CONTENTS | see the multiple-cursors table for why |
| `Alt-J` | join + select the inserted space | marginal over `J` |
| `\|` `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, and `:` is bound to focusing it (section A) |
| `gf` | goto file under selection | covered by pardes Enter-look |
| `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: 481 cases (360 file + 121 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. Six live ones:
  `wiX-edit-drops-sel` and `msel-append` (helix maps selections through
  insert-mode edits, pardes does not), `alt-c-window-op` (the pardes
  window op deliberately shadows helix change-noyank), `msel-yank-paste`
  (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 sans-IO 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
    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`.