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
|
# 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` (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 |
| `:` (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 (`Kill Newcol Tutor Debug NextColor Dump`, 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 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 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) |
### 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 |
| `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, and `:` is bound to focusing it (section A) |
| `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).
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`.
|