summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/design.typ2
-rw-r--r--docs/detached.md6
-rw-r--r--docs/fs.md197
-rw-r--r--docs/helix-keys.md128
-rw-r--r--docs/open-questions.md34
-rw-r--r--docs/selections.md123
-rw-r--r--docs/tags.md10
7 files changed, 405 insertions, 95 deletions
diff --git a/docs/design.typ b/docs/design.typ
index caa35b37..a6bf56a3 100644
--- a/docs/design.typ
+++ b/docs/design.typ
@@ -1917,7 +1917,7 @@ delayed, theme-derived highlight of the exact side-effect-free selection that
Look would expand; it neither focuses nor installs that selection, and pointer
leave/input/content invalidation cancels it. Wheel: scroll hovered pane, batched.
-*Modes.* normal: helix motions (`h j k l w b e W B E 0 $ ^`, `f F t T` and
+*Modes.* normal: helix motions (`h j k l w b e W B E 0 gl ^`, `f F t T` and
`Alt-.`, counts, `gg ge gh gs gl g| G`, `Ctrl-d/u/f/b`, `zt zz zb zj zk`),
insert entries (`i a I A o O`), selections (`v` extend — displayed as a fourth
mode name, "select" — `x`/`X`/`Alt-x`, `%`, `;`/`Alt-;`, `_`), MULTIPLE CURSORS
diff --git a/docs/detached.md b/docs/detached.md
index 2111e3dc..46e93fd5 100644
--- a/docs/detached.md
+++ b/docs/detached.md
@@ -17,6 +17,12 @@ Inside an editor, `Attach work` (`SPC s a`) switches the current window to
that session. `Detach` (`SPC s D`) closes only that frontend. It does not
turn a local editor into a detached session.
+A message stays until the next key or click dismisses it. With no frontend
+attached there is none to come, so a detached session drops each pane's
+messages once the newest has been up `MessageLinger` on the clock, checked
+by the session's own loop between the editor's steps; /screen then shows
+what is current.
+
## Files and transport
The frontend socket is `pardes-detached-<name>.sock` under
diff --git a/docs/fs.md b/docs/fs.md
index a2d8a9b2..86f44501 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -62,8 +62,10 @@ private namespace:
A `9ns --unix` mount lives in the private namespace of the command it runs,
and nothing outside that command sees it. The mount everyone on the machine
shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running
-editor posts itself there, so `/mnt/9p/pardes/<pid>/` is that editor's tree
-for any process. A new pane made through `pane/new` is a scratch named
+editor posts itself there, so `$NINE_MOUNT/pardes/<pid>/` is that editor's tree
+for any process. 9ns exports `$NINE_MOUNT` to everything it starts, so a
+script checks that variable to know the mount is there, and takes `<pid>` from
+the name of `$PARDES_9P` (`pardes-9p-<pid>.sock`). A new pane made through `pane/new` is a scratch named
`<dir>/+New` until it is given a name, and closing a column's last pane
leaves such a `+New` in its place (`Delcol` closes the column).
@@ -82,9 +84,11 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
/README this guide, also src/fs-help.txt
/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
/status pid, version and pane count
-/look write a line: a right click on it at the active pane; read: the serials it touched
+/look write a line: a right click on it at the active pane; read: the serials the last
+ look, exec or ctl write touched (made, else acted at)
/exec write a line: a middle click; read the same serials
-/log recent events, one a line: new|del|rename|save <serial> <name>, msg <serial|-> <text>;
+/log recent events, one a line: new|del|rename|save <serial> <name>, msg <serial|-> <text>,
+ dump|restore <path>, err <serial|-> <file>: <why>;
write follow to that open to wait for more
/screen rendered screen JSON; frozen per open handle
/listeners the session's dial addresses
@@ -106,27 +110,47 @@ builtins, one a line, at whichever pane has the keyboard as each runs --
`Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`; `Exit`,
which quits the editor as acme's does (it refuses once, naming each pane with
unsaved text, `<name>: Modified (Exit again to discard)`, and a second
-`Exit` with nothing edited since quits, throwing that text away; a scratch under 100 bytes is not asked about; `Restore`, which
-replaces every pane, asks the same first); and `Kill`, which
+`Exit` with nothing edited since quits, throwing that text away; a scratch
+under 100 bytes is not asked about; `Restore`, which replaces every pane,
+asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in
+`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`)
+and logs `dump <path>`, and `Restore` with no path takes the last one; a
+Restore puts a new editor under every client, so the write of it is
+answered and then every connection is hung up, their fids naming the old
+editor's panes (through a 9ns mount the write usually fails with
+ECONNRESET all the same: 9ns fails a request whose reply is already in once
+the hang-up breaks its next send, so trust the log): dial again, and the new
+log names the restored panes and
+`restore <path>`. The answer has 200 ms to leave before the cut, so a slow
+client may see only the cut; the log's `restore <path>` is what says the
+Restore happened. Keeping connections across it would mean carrying serials
+and opens into the new editor, which acme, whose Load only adds windows,
+never needed); and `Kill`, which
does not quit but stops commands, as acme's does: bare, every command pardes
started, and `Kill make ls`, those whose line begins with one of the words. A
-command pardes started is a line it typed into a terminal (a word written to
-`exec`, a middle click on one, a `pty/run`), from its shell's start mark (C)
-to its end mark (D); Kill sends its foreground job SIGTERM -- acme posts the
+command pardes started is a command pane's, until its child exits -- Kill
+sends its whole process group SIGTERM -- or a line it typed into a terminal (a
+word written to `exec`, a middle click on one, a `pty/run`), from its shell's
+start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the
"kill" note, which ends a process -- and never signals the shell itself;
in a shell running without job control (`set +m`) the job shares the
shell's group, so there is none to signal: Kill says `Kill: no job to
-signal`, and a write of it to `ctl` fails with that --
+signal`, and a write of it to `ctl` fails with that; with nothing running
+it says `Kill: nothing running`; and only the
+foreground job, so of `sleep 30; echo done` the `echo` still runs once the
+`sleep` is stopped --
and reads every setting there is, one a line, in the words a write of it
takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare
for the default directory, `LocationsConfig ...`), so writing what it reads
back changes nothing; platform and startup facts are `/status`'s and the
Config window's, not settings. A pane's `ctl` takes the builtins that act on
-a pane (`Del`, `Save f`, `Collapse`, `Find pat`) beside acme's `get`, `lock`
-and `unlock`. The column words are pane words too, acting on the column that
-pane is in: `Delcol`, `Collapse`, `DelAbove`, `DelBelow` and the focus moves
-`Left`/`Right`/`Up`/`Down` from it. `Joincol` and `Newcol` are the root's:
-they act on the column of the pane with the keyboard. The words are case-sensitive and do not alias: acme's verbs
+a pane (`Del`, `Save f`, `Collapse`, which folds that pane, `Find pat`)
+beside acme's `get`, `lock` and `unlock`. The column words are pane words
+too, acting on the column that pane is in: `Delcol`, `DelAbove`, `DelBelow`
+and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and
+`Newcol` are the root's: they act on the column of the pane with the
+keyboard, and `Joincol` needs a column to its right (`Joincol: no column to
+the right`). The words are case-sensitive and do not alias: acme's verbs
are lowercase and the builtins keep their tag spelling, so `Get` is no word
and `del` none either.
@@ -135,14 +159,16 @@ in Plan 9's words for a ctl (kernel/misc/parse.c:82-97), quoting the line:
`unknown control message "X"`; `wrong #args in control message "X"` for an
argument to a builtin that takes none, or none to one that needs it (`Msg`,
`Mount`, `Find`, a setting's value but a switch's, which flips bare);
-`bad value in control message "X"` for a setting's value it does not take;
+`bad value in control message "X"` for a setting's value it does not take
+(for `Theme`, naming the themes that share the name's first letter, since
+all of them, `ThemeSel`'s list, are too many for an error);
and `not a session control message "X"` or `not a window control message
"X"` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
refused here has done nothing. A line that then fails as it runs fails the
write with the error the editor reports for it and the line, e.g. `Mount:
-AlreadyMounted "Mount peer /tmp/s"` (EIO); so does `control message needs
-its argument "Save"`, for a builtin that would have asked at a prompt (a
-`Save` on a scratch) rather than open one nobody is there to answer. The
+AlreadyMounted "Mount peer /tmp/s"` (EIO), and `control message needs its
+argument "Save"` (EINVAL), for a builtin that would have asked at a prompt
+(a `Save` on a scratch) rather than open one nobody is there to answer. The
lines before a failing one have taken effect and those after it never run,
which is what acme's ctl loop does (editors/acme/xfid.c:600-790). An error
that only happens as the editor performs what a line asked for -- a `Save`
@@ -164,7 +190,7 @@ the keyboard no pane does: `/focus` reads empty and every pane's `ctl` says
`notcurrent`. A write gives the keyboard and nothing else: a folded pane
stays folded (unfold it with `Collapse` on its ctl), as rio keeps `current`
apart from `unhide`. A serial no pane has fails with `no such
-window`; anything but a number, with `ill-formed control message`.
+pane`; anything but a number, with `ill-formed control message`.
A pane is made by **opening** `/pane/new`, and closed by Tremove on
`/pane/<n>` (`rmdir`), which is the only remove the tree serves; Tcreate is
@@ -213,13 +239,57 @@ same tree without leaving the process.
- a line written to `exec` is a middle click: a command word from
`src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`,
`Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...),
- or anything else, which is typed into a terminal: the pane itself when it
- is a terminal at its prompt, otherwise a terminal in the pane's directory
- that is at its prompt, and failing both a new one made below the last
- column. That is not an error, whatever the shell makes of the line, and a
- misspelled builtin word ends up there too (whether it should is an open
- question, docs/open-questions.md). `echo Tty > pane/<n>/ctl` makes a
- terminal in that pane's directory outright.
+ or anything else, a command line. Written at a terminal at its prompt it
+ is typed into that shell. From anywhere else -- a file, a scratch, a tag,
+ a terminal whose tty a program holds -- it runs as a command pane: a
+ terminal whose child is the root ctl's `Shell` (fish unless set) run
+ with `-c` and the line, in the pane's directory,
+ which shows its output and then `exit N` (its tag reads `<dir> (<line>)
+ running`, then `exit N`), and stays. The command is over when its process
+ exits, as in acme, not when its terminal closes: a job it left in the
+ background prints on below `exit N` until it lets go of the pty, and a
+ command that lets go of its terminal early runs on to its own exit. The directory's next command runs in
+ that pane once it is done, below what it showed, after a `% <line>` line;
+ one still running gets a second pane. Before the next command the pane
+ leaves any alternate screen and turns off the modes a program left on
+ (mouse reports, bracketed paste, a hidden cursor); a command that clears
+ the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts
+ the next command there too. The log says `run <serial> <line>` and `exit
+ <serial> <N|?>`; `exec` reads back the command pane's serial; Kill ends
+ its whole process group; a line is at most 1 KB. A misspelled word is a
+ command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes
+ an interactive terminal in that pane's directory.
+
+A terminal can be bound as a language's REPL: `Repl python` in its tag or
+on its `ctl` (the language names are the syntax table's, or a code fence's
+alias such as `py`, in any case; `Repl -` unbinds,
+`Repl` bare says the binding, `Repl python` again changes nothing). Its tag
+shows its id, `python-a`, `python-b` for the next, a freed letter reused,
+and so does the end of its `ctl` line, after `current`/`notcurrent`.
+A builtin's word still runs first, bound or not: `Del` clicked in the file
+closes its pane. Then any other exec made by a gesture on the body of a file
+in that language -- a
+middle click, the execute key, on a selection or a single word, even `make`
+in a comment -- or on the REPL's own body is typed into the REPL instead of
+run: bracketed paste when its program asked for it (DECSET 2004), else line
+by line, where a blank line inside a Python block ends the block (said
+once), then Enter. The message row says `→ python-a` in the tag's name tint
+and the log `send <from> <to> <id>`. With several REPLs bound for the
+language the pane asks which, on its notice band, one key answering and Esc
+sending nowhere; nothing is remembered. A REPL takes text only while its
+program has the terminal: a command pane's until its command is done (the
+binding goes with it, and a done one cannot be bound), an interactive
+terminal's while a program other than its shell holds the tty -- after
+Ctrl-D the shell would run the text, so nothing is sent and the pane says
+so. Still commands, whatever is bound: a word in a tag (so
+the tag is how to run `make` from that file), `Exec <text>` run by name
+(typed, a 2-1 chord onto `Exec`, a `ctl` line) and a command word @`cmd` in
+the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec`
+is no gesture and is never sent: a script writes to the REPL pane's
+`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`
+then `\r`), since line by line a blank line ends a Python block and Python
+3.14's REPL auto-indents each line typed into it. The REPL gets the text
+wherever it is -- at a `pdb` or `input()` prompt too. Bindings are not dumped, so a Restore leaves none.
The root's pair clicks at the active pane and `/pane/<n>/look` and
`/pane/<n>/exec` at that pane. Blank lines are skipped, and every other line
@@ -235,8 +305,21 @@ writing it renames the buffer; a relative name resolves against the pane's
directory. `body` appends on write and replaces on truncating open. `sel`
reads the selected text and writing it replaces the selection. `errors`
appends to the directory's `+Errors` pane. Holding `event` open redirects the
-pane's Look and Exec clicks to that client; writing a record back performs the
-action. `ctl` reads acme's window status line — serial, tag length, body
+pane's Look and Exec clicks to that client, and so does a line written to the
+pane's own `look` or `exec`, or to the root's while that pane has the
+keyboard, a click with no place in the text: an `F` record at `0 0` carrying
+the line. So a client holding `event` that wants a command run gets its own
+exec back as a record: it runs it through `ctl`, or writes the record back.
+Writing a record back performs the action, as the click would have (acme's
+xfideventwrite): a body `X` goes to a REPL bound for the text as a middle
+click does, where an `F` record, a line written to `exec`, runs as the
+command it was. acme takes back only `<origin>
+<action><q0> <q1>`, the text of that range; pardes takes the record whole as
+it was read too, and for an empty range acts on its text, which is how such
+a line is done. A click in a file's body carries the offsets of the text it
+took; one in a terminal's body cannot, since that body is a history
+snapshot, and is also at `0 0` with its text. A click that takes no text
+sends nothing, as in acme. `ctl` reads acme's window status line — serial, tag length, body
length, a reserved zero, the dirty flag, the width in cells, the font and the
tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`):
whether the pane has the keyboard. It takes the pane's builtins (below),
@@ -280,10 +363,16 @@ lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as
lines, so `^` and `$` match at the start and end of any line, `.` and a
negated class never match a newline, and `$` also matches at the end of a
text with no final newline. A pattern that names a newline (`\n`) runs over
-the whole text instead, its `.` kept to one line. `/re/` searches forward
-from the end of the current range to `limit` if one is set, and otherwise
-wraps to the start of the text; `?re?` finds the last match ending before
-the range, wrapping to the text's last. The match is the leftmost, but of
+the whole text instead, its `.` kept to one line. An expression is
+evaluated from the current address, the range last written to `addr` (or
+left by the last `data` write, just past it), as acme evaluates it from
+`w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is
+the selection's own file), and `#9/re/` searches from `#9`; in `/a/;/b/`
+the second search starts at the end of the first, as acme's `;` does, where
+`/a/,/b/` starts both at the current address. `/re/` searches
+forward from the end of the current range to `limit` if one is set, and
+otherwise wraps to the start of the text; `?re?` and `-/re/` find the last
+match ending before the range, wrapping to the text's last. The match is the leftmost, but of
the alternatives at that place mvzr takes the first that matches where sam
takes the longest (`/gam|gamma/` finds `gam`); in a search begun in the
middle of a line, `^` inside an alternation can match there; and in a
@@ -291,7 +380,8 @@ pattern that spans lines, `^`, `$` and `[^...]` keep mvzr's own meaning.
mvzr backtracks without bound of its own (`a?` twenty times then twenty
`a`s is 2^20 steps from each place it tries), and a search holds the editor, so pardes patches a
step budget into mvzr's matcher (build.zig): a search that spends it,
-about 300 ms, fails with `regular expression search took too long` rather
+about 300 ms, fails with `regular expression search gave up, backtracking
+past its step budget` rather
than answer a match it is not sure of. Ordinary patterns spend a few
thousand steps; what runs out is exponential backtracking, and a quadratic
pattern over a very long line (`\s*(\w+)\s*=` over 20 KB of letters).
@@ -301,7 +391,10 @@ is the one place both call), so `^` there also means a line's start.
An address that does not evaluate fails the write with why: `bad address
syntax`, `no match for regexp`, `address out of range`, `bad regular
-expression` or `regular expression search took too long`. A failed write to `addr` leaves no address at all, where acme
+expression`, `regular expression search gave up, ...`, or sam's
+`addresses out of order` for a range that ends before it starts
+(`#100,#50`), which acme lets through. A failed write to `addr` leaves no
+address at all, where acme
keeps the old one: until an address is written or `addr` is truncated,
reading `addr`, and reading, writing or truncating `data` and `xdata`, fail
with `no address: the last one written to addr failed`, so a script that
@@ -315,9 +408,12 @@ pane.
`tag` reads the whole tag as the pane shows it: the computed path, dirty
marker or PDF page, then the text you may edit. A write appends to that text,
newlines included, and a tag with more than one line takes a row per line on
-screen; truncating `tag` clears it, as acme's `cleartag` does. The clearing
-is an edit of the tag like a typed one and its undo history is kept: `u` in
-the tag brings back the text it cleared.
+screen; truncating `tag` clears it, as acme's `cleartag` does -- the default
+words (`Del`, `Put` and the rest) with it, since they are that text until
+you edit it, so `echo Make > tag` leaves only `Make`; append with `>>` to
+keep them. The clearing is an edit of the tag like a typed one and its undo
+history is kept: `u` in the tag brings back the text it cleared, words
+included.
Stats report real lengths for `index`, `status`, `look`, `exec`, `listeners`,
`name`, `body`, `tag`, `sel`, `ctl`, the range files and the flag files, and
@@ -333,7 +429,9 @@ an open renders its frame; stat the entry.
`/log` is one ring (64 KiB) that records whether or not anyone reads it:
`new`, `del`, `rename` (a terminal's too, as its shell changes directory,
-since a terminal is named by its directory) and `save <serial> <name>`, and `msg <serial|-> <text>`
+since a terminal is named by its directory) and `save <serial> <name>`,
+`dump <path>` when a Dump is written and `restore <path>` in a Restore's
+new log after its panes' `new`s, and `msg <serial|-> <text>`
for every line the editor says, repeats included (with `verbose` on, that
includes each builtin announcing itself as it runs), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
@@ -342,12 +440,25 @@ reply to, usually `Invalid argument`, and this is the reason (`err 3 addr:
no match for regexp`). The same err again, before any follower has read
the first, is that record counted (`err 3 addr: no match for regexp (x4)`),
so a client retrying a failing write does not push the rest out of the
-ring; a follower that read it gets each repeat. There is no per-pane error file to read instead:
+ring; a follower that read it gets each repeat. Through a kernel mount a client sees only an errno, which 9ns reads from the
+error's words (cloud9's 9ns/src/nine.zig, `enameToErrno`): a malformed write
+-- an unknown or ill-formed control message, `bad address syntax`, `bad
+regular expression` -- is EINVAL; a lock another open holds, EBUSY; a pane
+gone, ENOENT; a well-formed write that fails -- `no match for regexp`,
+`address out of range`, `addresses out of order`, a search that gave up,
+`<name>: Modified (Exit again to discard)` -- EIO. The err record has the
+words. There is no per-pane error file to read instead:
acme's `errors` only takes text, and one record stream is simpler to watch
than a file per pane. A `msg` said while a
pane is being made can precede that pane's `new`; panes present at boot are
recorded before anything else.
-Control characters in a record become spaces, so a record is one line. An
+Control characters in a record become spaces, so a record is one line.
+(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n>
+<text>\n`, whose text may hold newlines; read `n` bytes of it rather than
+up to a newline -- bytes here, where acme counts runes. Every offset and
+count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count
+follows them rather than switch alone, so an acme library reads pardes
+correctly for ASCII text and not beyond it.) An
open freezes the ring's text the way `/screen` freezes a frame: reads walk it
and end. Writing `follow` to that same open makes reads past it wait for the
next record, one per read; a follower the ring outran reads `lost N` first.
@@ -399,7 +510,9 @@ for ever: cancel the read (interrupt it, or close the open) to give up;
`error not run` when the shell refused
the line without running it (a fish syntax error; the line is taken back off
the prompt); `error shell gone` when the pane closed or its shell was
-replaced; `error no prompt marks` for a shell pardes could not instrument.
+replaced; `error no prompt marks` for a shell pardes could not instrument;
+`error command done; not a shell` (or `error a command runs here, not a
+shell`) on a command pane, whose child is its command.
It relies on the OSC 133 marks pardes injects into bash and fish, tagged
`aid=pardes` so fish's own marks and a nested shell's are ignored. A second
line on an open whose command still runs fails the write. `exec zsh` or a
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index 41f9b364..c703a3da 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -7,10 +7,17 @@ checkout (`~/05-genizah/helix`); that checkout now sits on the local
`pardes-harness` branch, whose three commits add only the harness, so
`book/src/keymap.md` is byte-identical at its tip (694e7dfd). As of phase 5
every helix-equivalent row in A and B is differentially verified against real
-helix (see "Differential testing" at the bottom). Five of the six waivers are
-named in the row they belong to; the sixth, `wiX-edit-drops-sel`, belongs to no
-single key — it is the anchor-only divergence every insert-mode edit shares —
-and is described in the Files list at the bottom instead.
+helix (see "Differential testing" at the bottom). Every waiver is named in the
+row it belongs to.
+
+**The reference helix is fixed at one commit:** the `hx-harness` built from
+`694e7dfdd` on the genizah checkout's `pardes-harness` branch, which is upstream
+`278b24389` (2026-06-29, `25.07-905`) plus the three harness commits. "helix"
+in this document means that build, not the 25.07.1 release, which differs on
+counted `Alt-(`/`Alt-)` and on `&`. Upstream master was 82 commits further on
+2026-09-28 (`079a789e`); of those only `416a0e09` (continuing a comment in an
+injected `comment` layer) touches the editing code, and it needs tree-sitter,
+which the harness runs without.
Code map, by SYMBOL — line numbers rot, names do not. Body-normal key
RECOGNITION is `modal.Normal` in `src/modal.zig`: a state machine over `Role` (one
@@ -73,8 +80,8 @@ at the bottom.
`gj` the textual one. With the toggle off a line is one visual row and the
two pairs are the same motion.
- **Undo is snapshot-per-edit-op / per-insert-session**, not a
- transaction log. Anything requiring replayable edits (`.`,
- macros) needs new machinery.
+ transaction log. `.` and macros replay KEYS (`Macro.zig`), not
+ edits, so they need none.
## A. Implemented today
@@ -93,7 +100,7 @@ language-backend queries, and the shell pipe.
| `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 | `0` and `^` are pardes extras — helix leaves those two unbound (it spells them `gh`/`gs`). `$` is NOT unbound in helix: it is `shell_keep_pipe` (`keymap/default.rs`), and taking it for line-end is a deliberate divergence — see the `$` row in C. It is the one shell command still unimplemented: it needs a per-selection verdict, where the runner's answer is atomic. `0` is a count digit while a count is pending | pardes-specific (`$`: deliberate) |
+| `0` / `^` | line start / first non-ws | pardes extras: helix leaves both unbound (it spells them `gh`/`gs`), so they collide with nothing. Line end is helix's own `gl` and End. `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 |
@@ -103,6 +110,8 @@ language-backend queries, and the shell pipe.
| `Ctrl-b` | file panes: full page up. Terminal panes: **toggle raw tty/editor mode** (`opts.tty_toggle`, configurable; `Shift-Esc` also enters). In raw tty Shift-Esc goes to the child | 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 |
+| `Z` | sticky view mode: the `z` keys stay armed after each until Esc, which only leaves the mode; other keys do nothing meanwhile | helix's sticky view node | helix-verified |
+| `zm` | scroll the unwrapped view so the cursor's column is in its middle | helix `align_view_middle`; with soft wrap on there is no horizontal scroll, and nothing happens | out of corpus (the harness's text never scrolls sideways) |
| `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 |
@@ -110,9 +119,9 @@ language-backend queries, and the shell pipe.
| `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). Writes the DEFAULT REGISTER and nothing else — the system clipboard is `SPC y`, which is helix's own split and so moves this row TOWARDS helix, not away: a `d` of one character can no longer clobber what the desktop was holding | 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, and only the register — nothing on this path reads or writes the system clipboard. `SPC p` is the word that does, and on a tty its read is OSC 52, which most terminals refuse: an honest no-op there rather than a paste of the wrong text | helix-verified |
+| `y` | yank each range's text into the register (`"<reg>` names one, else `"`), one value per range | line-yank vim-ism removed (phase 5); yank keeps selection AND cursor (helix). The system clipboard is `"+y` / `SPC y`, helix's own split: a `d` of one character never clobbers what the desktop holds | helix-verified |
+| `u` / `U` | undo / redo | restores the pre-edit selection (helix); snapshot granularity, `Alt-u`/`Alt-U` are the same two: helix walks a history tree by time with them, and pardes's history is linear, so earlier and later ARE undo and redo | helix-verified |
+| `p` (normal) | paste the register after each range: value i at range i, the last value repeated when there are fewer | helix `paste_impl`. `"+p` asks the system clipboard (`SPC p`); on a tty that read is OSC 52, which most terminals refuse: an honest no-op rather than a paste of the wrong text | helix-verified |
| `Esc` (body normal) | clear a pending modal prefix / exit select mode, keeping the selection, then run `Last`: hop to the pane you were in before this one, whichever kind it was, exactly like `SPC j j` — so held down it alternates between two panes, two files as readily as a file and its shell. A PDF pane is the ONE exception: there Esc is the document's own cancel (drop the mouse selection and the search overlay, stay where you are reading) and `Shift-Esc` is the hop out, while raw tty only intercepts unmodified Esc at a detected shell prompt | 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 |
@@ -120,16 +129,16 @@ language-backend queries, and the shell pipe.
| `:` (normal, body or tag) | in the body, focuses the pane's tag in normal mode at its remembered cursor (the first time, on `Save`); in the tag, goes back to the body; in a column or workspace tag, back to the active pane. The tag's normal and insert modes ARE the body's: every motion, selection, edit and undo works there, and `0` goes to the line's start, the path's. Tab runs the word under the cursor or the selection and Enter looks it up (in a column or workspace tag Enter runs it too), and either hands the keyboard back to the body first. Clicks choose a new cursor position and type into the tag. | Each tag keeps its own cursor during the session. The computed path/marker/page is reachable and yankable but read-only: an edit into it is refused, and typing into a file's path drafts a new name. File-name changes are staged as described in [editable tags](tags.md). | pardes-specific |
| `Ctrl-w k` / `SPC w k` (pane with nothing above) | focuses its column's tag, then the workspace tag; with `ColumnTags` disabled it goes directly to the workspace. `Ctrl-w j` walks back to the panes, `Ctrl-w h`/`l` walk the column tags. Headers edit exactly as a pane tag does. | Column commands target that column's active pane, or its first pane when coming from elsewhere. Workspace and column text are independently editable and persist in dumps. See [editable tags](tags.md). | pardes-specific |
| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — editor normal mode 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; Raw **tty** mode forwards Ctrl-w to the child. 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, as from a body, and from the top pane `Up` reaches the column and workspace tags | pardes-specific |
-| `Alt-n` | new terminal below (outside raw tty) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific |
+| `Alt-n` | new terminal below (outside raw tty) | shadows helix `Alt-n` (select next sibling), which pardes spells `Alt-right` alone | pardes-specific |
| `Alt-c` | move active terminal to a fresh column (outside raw tty) | 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) | starts the pardes LEADER: a key path runs the same builtin words used by tags and the topbar. The main groups are `f` files, `h` docs, `c` columns, `t` toggles/effects, `a` panel animations, `s` session, `j` jumps, `l` language, and `w` directional focus; `SPC ?` lists every path and `<prefix> ?` lists one group in `+Help`. Neither `Exit` (quits the editor, acme's Exit) nor `Kill` (stops the commands pardes typed into terminals, acme's Kill) has a leader path: both are the topbar's and the root `ctl`'s. The pending path appears on the active pane's transient body/message row. Esc or an unmapped key abandons it. Paths, Help rows, and dispatch are all generated from the builtin registry at comptime; the leader applies only to a BODY in normal mode because tags and tty programs own their input | pardes-specific; helix spends Space on pickers/LSP (section C), while pardes uses acme-style executable words |
-| `SPC y` `SPC Y` `SPC p` `SPC P` `SPC R` | helix's clipboard menu on helix's own letters: yank the selection to the system clipboard (`ClipYank`) or the PRIMARY selection alone (`ClipYankMain`), paste the system clipboard after (`ClipPaste`) / before (`ClipPasteBefore`) the selection, replace the selection with it (`ClipReplace`) | the ONLY five words in pardes that touch the desktop's clipboard — `y`/`d`/`c`/`p`/`P`/`R` and the acme cut/paste chords are the internal register alone. Builtins rather than bare chords because a leader path names a builtin: they land in Help's index and are executable words like every other verb. One divergence: pardes keeps a single register VALUE where helix keeps one per range, so `SPC y` at N cursors joins them with newlines (`Pardes.setYank`, the divergence `msel-yank-paste` already waives). On a tty the write is OSC 52 out and the READ is OSC 52 back, which many terminals refuse or gate — so `SPC y` works there and `SPC p` can be a no-op | out of corpus |
+| `SPC y` `SPC Y` `SPC p` `SPC P` `SPC R` | helix's clipboard menu on helix's own letters: yank the selection to the system clipboard (`ClipYank`) or the PRIMARY selection alone (`ClipYankMain`), paste the system clipboard after (`ClipPaste`) / before (`ClipPasteBefore`) the selection, replace the selection with it (`ClipReplace`) | the same as `"+y`, `"+p`, `"+P`, `"+R`: register `+` (and `*`) IS the desktop's clipboard, the only register that reaches it. `SPC y` writes `+` alone, as helix's does, not the default register too. Builtins rather than bare chords because a leader path names a builtin: they land in Help's index and are executable words like every other verb. The clipboard holds one text, so N values go out joined by newlines, as helix sends them. On a tty the write is OSC 52 out and the READ is OSC 52 back, which many terminals refuse or gate — so `SPC y` works there and `SPC p` can be a no-op | out of corpus |
| a paste from the OUTER terminal | one `Event.paste`, spliced in at the cursor | the tty shell enables bracketed paste and coalesces `paste_start`..`paste_end` into a single event; before that the bytes arrived as individual key presses and normal mode RAN them, which is how a pasted `d` deleted a line. The bytes deliberately never enter the yank register — clipboard and default register are separate stores in both directions | pardes-specific |
| `/` (any pane) | pardes' own plain-substring search into a `+Search` output buffer: the pattern is typed on a line of its own on the pane's notice band, Enter fills the buffer, and its rows are ordinary look targets. Enter also GOES to the first row — the buffer is focused and then the step `n` is and the look Enter is run in it (`Pardes.lookFirstHit`), so `/foo` lands on the first hit with the matched span selected. A pattern that matched nothing opens its empty buffer and moves nothing | KEEP, do not touch; not in the corpus (helix `/` is regex search). Find and Grep answer with OTHER files and deliberately do NOT jump. The stepping half is the next row | pardes-specific |
| `n` / `N` (any pane) | MOTION, not a jump: move the SELECTION to the next / previous look-able text and open NOTHING. Enter — the look chord — on what it leaves selected is what opens it | KEEP, do not touch; not in the corpus (helix's `n`/`N` walk regex search hits). What a step selects is the pane's GRAIN (`output_pane.Grain`, read in `Pardes.lookSpanIn`): in FREE TEXT — a terminal, a file, a PDF, a prose answer buffer — the largest whitespace-delimited run `look.resolve` can act on (`look.lookableSpan`, wrapper punctuation peeled off both ends), several to a line; in a RESULTS BUFFER one stop per ROW, the largest run its head resolves as (`look.lookableLineSpan`), because a row there IS one location and the words after it are the match rather than a second place to go; in a COMMAND list the whole line. The walk is a RING across PANES: every pane that has performed a Look, most recent first (`Pardes.look_src`), then the output buffers that have not, newest first, and only when both are empty the active pane. Exhausting a pane enters the next at its first (forward) / last (backward) span and the end wraps to the start, so `N` is the exact inverse of `n`. What it lands on becomes an EXPLICIT `vsel` with the cursor on its FIRST column, in the pane the walk focuses. ONE motion in every pane kind and every buffer kind — a PDF steps the `+Search` buffer its own search filled, `n` to select the row and Enter to jump. The single thing a buffer may change is that grain, and it changes it by BEING a kind of buffer rather than by a branch: `output_pane.Traits.steps` (a list of locations) makes a step take one row at a time, and `Traits.commands` makes it take the WHOLE LINE, because a command list (`ThemeSel`/`FontSel`) holds words to run and there is no path inside `Theme gruvbox` to pick out. Tab on what `n` selected wears the theme, which is the same middle click on the row is. `]d`/`[d` are helix's diagnostic motions, a different binding, and they do still jump to each diagnostic (`docs/lsp.md`) | pardes-specific |
| insert: printable text | file: real edit; terminal: typed run splice | | helix-verified (file) |
| insert: `Enter` | newline; keeps the current full indent levels and adds one 4-space logical tab when the text before the cursor ends in `(`, `[`, `{`, or `)` (including `})`) | plain lines match `insert_newline`; delimiter heuristic is pardes-specific | helix-verified (plain) / pardes-specific (delimiter) |
-| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0 | matches `delete_char_backward`; `Ctrl-h` alias in B | helix-verified |
+| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0; in a line's leading blanks, back to the previous 4-column indent stop (a whole unit when on one; a tab still goes alone) | matches `delete_char_backward` and its dedent; `Ctrl-h` alias in B | helix-verified |
| insert: `Up` `Down` `Left` `Right` | move cursor | matches helix's "not recommended" insert arrows | helix-verified |
| `gd` `gD` `gy` `gi` `gr` | LSP definition / declaration / type-definition / implementation / references. ONE answer jumps straight there; several fill `+Search`, where n/N walk and Enter opens | in-process ZLS (`src/lsp/lsp_zls.zig`), `.zig` only — on a file the backend does not speak these do nothing at all, with no error row. Ctrl+left-click is the mouse spelling of `gd` | out of corpus |
| `]d` `[d` / `]D` `[D` | step the diagnostics list / go to its last or first; if no list is up, asking the backend for one is part of the press | | out of corpus |
@@ -139,6 +148,10 @@ language-backend queries, and the shell pipe.
| `A-\|` | the same, and the output is DISCARDED — the text is not touched at all | helix `shell_pipe_to`. For a command run for its effect. Marker `\|-` | out of corpus |
| `!` | run with NO stdin, insert the output BEFORE each selection | helix `shell_insert_output`. Runs ONCE and every cursor gets that one answer, as helix does — ten cursors and `date` give ten identical stamps. Marker `!` | out of corpus |
| `A-!` | the same, appended AFTER each selection | helix `shell_append_output`. Marker `!+` | out of corpus |
+| `$` | keep only the selections a shell command exits 0 on: each range's text on its stdin, one run per range, its output discarded; the primary stays if kept, else the last kept range takes over, and keeping none changes nothing | helix `shell_keep_pipe`. The runner's answer is all or nothing, so the command runs as `(cmd) >/dev/null; echo $?` and the status comes back as the output. Marker `$` | out of corpus |
+| `Q` / `"<reg>Q` | record the keys that follow into `@` (or the named register) until the next `Q`; kept as helix writes a macro, in key notation (`xt,S=<ret>_<A-(>`), so `"@p` pastes it and a yanked text can be replayed | helix `record_macro` | helix-verified |
+| `q` / `<n>q` / `"<reg>q` | type a register's keys again, n times. Replayed keys go to the modal handling only: Esc does not hop panes, Enter and Tab neither look nor execute, and Space does not open the leader | helix `replay_macro`; `Macro.zig`, `Pardes.replayKeys` | helix-verified |
+| `.` / `<n>.` | repeat the last insert session: the normal command that entered it once, the keys typed n times, then leave insert mode the way it was left | helix `repeat_last_insert`; the same key log as macros | helix-verified |
## B. To implement
@@ -173,6 +186,8 @@ found. Pure text math in `modal.zig`: `findChar`, `matchBracket`,
| `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) |
+| `g.` | go to where the last edit ended (a deletion's start, an insertion's end), every range there, extending in select mode | helix `goto_last_modification`. The position is `Text.last_edit`, set by `edit.setEditText` from where the old and new texts part; helix's is the last history revision's, so with nothing edited yet pardes stays put where the harness's helix goes to the end of its setup text | helix-verified |
+| `gw` | label the words in view (two word characters or more) with two letters each, nearest the cursor first, one forward and one back in turn; typing a label selects its word (stretching the selection to it in select mode), any other key takes the labels away | helix `goto_word`, alphabet a-z (`config.jump_label_alphabet`). The labels are painted over the body's cells in `body_layer.renderBody`, in the selection colours; the words live on `Pane.jump` | helix-verified |
| `gt` / `gc` / `gb` | goto screen top / center / bottom | view-relative (`pane.scroll()` + `pane.rows`), column kept (clamped) | helix-verified (phase 5) |
| `gj` / `gk` | VISUAL line down / up (+ count) | follows the wrapped body's own breaks (`file_pane.visualRow`, the same walk `fillBody` renders), keeping the goal column INSIDE the row; the last row of a line steps into the next line's first. Wrap off = one row per line, and this IS `j`/`k`. helix assigns the pair the other way round (its `j` is the visual one) | pardes-specific |
| `PageUp` / `PageDown` | FULL page (was half) | `Ctrl-u`/`Ctrl-d` stay the half-page pair | helix-verified (phase 5) |
@@ -182,11 +197,12 @@ found. Pure text math in `modal.zig`: `findChar`, `matchBracket`,
| 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 — one code path for both kinds, since `normalReplaceChar` goes through `Pardes.editText`/`setEditText`, which route to a file's content or to `term_pane`'s typed-run overlay | helix-verified (phase 5) |
-| `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection, head on its last char | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) |
+| `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection in the direction the replaced one had (head on its last char, or its first when backward) | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) |
| `~` | switch case of selection/char, selection kept | terminals: run byte only | helix-verified (phase 5) |
| `` ` `` | selection to lowercase | terminals: run byte only | helix-verified (phase 5) |
| ``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) |
+| `Alt-J` | join like `J`, then select the spaces the join put in (one bare cursor each); a join that put none in keeps the selection | helix `join_selections_space`. `J` and `Alt-J` are one edit over every range, so a line two ranges share is joined once | helix-verified |
| `>` / `<` | 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) |
@@ -200,6 +216,9 @@ found. Pure text math in `modal.zig`: `findChar`, `matchBracket`,
| --- | --- | --- | --- |
| `;` | 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) |
+| `Alt-:` | make every range point forward | helix `ensure_selections_forward` | helix-verified |
+| `Alt-o`/`Alt-up`, `Alt-i`/`Alt-down`, `Alt-p`/`Alt-left`, `Alt-right`, `Alt-a`, `Alt-I`, `Alt-e`, `Alt-b` | walk the file's syntax tree from every range: grow to the enclosing node (and remember what it grew from), shrink back to that or to the first child, the previous / next sibling (anonymous ones included, so often a comma), every named sibling or child, the enclosing named node's end / start (stretching to it in select mode) | helix `expand_selection` and kin, on a whole-file parse (`File.node_tree`) made on the first such key after an edit and reused until the next; a range the tree says nothing about, and every range of a file without a grammar, stays. Checked against the installed hx on JSON (`normal.zig` test): the harness runs without grammars, so it only proves the plain-text no-op | helix-verified (plain text), hx-checked (JSON) |
+| `]f [f ]t [t ]a [a ]c [c ]T [T ]e [e ]x [x`, `mi`/`ma` + `f t a c T e x` | function, class, argument, comment, test, entry, element: jump to the next one starting after the cursor / the previous one ending before it (count times; select mode stretches to it), or select the smallest one around the cursor, inside or around | helix `goto_ts_object` / `textobject_treesitter`, from helix's own `textobjects.scm` vendored in `vendor/queries` (MPL-2.0), with its `#eq?`/`#match?` predicates; `syntax.objectAt`/`objectNext` on the file's kept parse. Zig's query is helix's ported by hand to the zig grammar pardes builds, and a node that ends in its line's newline (that grammar's comments) is taken without it, as helix's grammars have it. Grammars without a query (fortran, markdown, powershell) or whose query does not compile against pardes's grammar (erlang) have none, and the keys do nothing there. Checked against the installed hx on Python, JSON and Zig (`normal.zig` test) | hx-checked |
| `%` | 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) |
@@ -223,10 +242,12 @@ grew `sels` + `primary`, emitted only when there is more than one range.
| `_` | 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 |
+| `y` with several ranges | one value per range, pasted back value i at range i by `p`/`P`/`R` and insert `Ctrl-r`; the acme cut/paste chords and a paste into a terminal take the values joined by newlines | `Registers.zig` | helix-verified |
+| `"<reg>` | the next command's register: any character names one of its own. Computed ones: `_` swallows writes and reads nothing (`"_d`), `#` is each range's number from 1, `.` each range's text, `%` the file's name, `/` the last `s`/`S` pattern; `+`/`*` the system clipboard. A count typed before `"` stays the command's | helix `select_register` | helix-verified |
+| insert `Ctrl-r <reg>` | type this range's value of the register (`Ctrl-r #` numbers the cursors). Esc after `Ctrl-r` only cancels it. `Ctrl-r +` reads what pardes last put on the clipboard, not the desktop's | helix `insert_register` | helix-verified |
+| insert mode with a selection | every range is carried through each edit as helix maps it (`Range::map`): typing at the head of an `i` range slides it, typing at the end of an `a` range stretches it, Enter slides or stretches it by what the cursor moved, and an arrow key collapses it. Esc after `a` pulls each range's end back one character (helix `restore_cursor`) | `edit.insertKey`, `Text.restore_cursor` | helix-verified |
+| `&` | align selections into columns: the k-th range of each line is column k, and spaces go in before each range until its head reaches the column's widest head, in display cells (`File.rawDisplayCol`) | helix `align_selections` as of the harness's helix (25.07.1 grouped columns differently). A range over several lines refuses. A TAB counts as `tab_width` cells, not up to the next stop | helix-verified |
+| `Alt-)` / `Alt-(` / `<n>Alt-)` | rotate the CONTENTS of the selections forward / back by one range (n ranges), in one edit; each range comes back over the text it now holds and the primary moves with its text | helix `rotate_selection_contents_*` as of the harness's helix: 25.07.1 read the count as a GROUP size instead (rotate by one within each run of n ranges) and left the primary where it was, which is what helix-golf's `invert_dictionary_2` relies on | helix-verified |
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
@@ -260,10 +281,10 @@ keeps the matches it found so far.
| `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 |
+| `^` and `$` | hold at every line's start and end | helix compiles with `multi_line(true)`; pardes gets the same by searching each line as its own haystack (`src/regexp.zig`) | helix-verified |
+| `.` | never matches a newline | as the Rust regex crate: a pattern without `\n` searches line by line, and one that names `\n` has its `.`s made `[^\n]` (`src/regexp.zig`) | helix-verified |
+| many matches | up to `memory.limits.selections` ranges: 1024 on the desktop, 64 on the board; matches past it are dropped | helix has no limit. The room for them is allocated when a second range appears and given back at one range, so a single cursor costs nothing | helix-verified up to the limit |
+| `K<pat>` / `Alt-K<pat>` Enter | keep only the ranges a match starts inside / only those none does; primary 0, and keeping none leaves the selection alone | helix `keep_selections` / `remove_selections`, on the same prompt as `s`/`S` (markers `Keep /`, `Remove /`). The range is searched as `s` searches it, line by line with its lines' context, where helix matches the range's text alone: a `^` right at a range that starts mid-line matches in helix and not here | helix-verified |
### `Ctrl-c` — toggle comments
@@ -283,7 +304,7 @@ keeps the matches it found so far.
| --- | --- | --- | --- |
| `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) |
+| `ms<ch>` | surround selection (or cursor char) with the `<ch>` pair; wrap incl. pair becomes the selection, keeping the direction it had | 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) |
@@ -301,7 +322,7 @@ keeps the matches it found so far.
| 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) |
+| `]Space` / `[Space` | add `<n>` blank lines below the selection's last line / above its first, the selection staying on its text | file panes only (shell output immutable) | helix-verified (phase 5) |
### Insert mode
@@ -324,6 +345,8 @@ text.
| `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` | indent to the next 4-column stop with SPACES (`insertTab`, `edit.zig`: `pad = INDENT_W - col % INDENT_W`) — no `\t` byte ever reaches the file | helix's Spaces indent style; helix smart-tab skipped. After a `.` in a file the backend speaks, Tab instead asks for completion and only indents if the answer is empty (section A) | helix-verified (phase 5) |
+| insert `Tab` after text / `Shift-Tab` | in a file with a grammar, Tab after text on its line leaves the syntax node the cursor is in (to the enclosing named node's end); Shift-Tab always indents | helix `smart_tab` / `insert_tab`. The reference pins smart tab off, so a file without a grammar indents as the harness's helix does, where stock helix would do nothing there. Smart tab is checked against the installed hx on JSON (`normal.zig` test) | helix-verified (plain text), hx-checked (JSON) |
+| insert `Ctrl-s` | commit an undo checkpoint: `u` after the session goes back to the text as it was here, a second `u` to before the session | helix `commit_undo_checkpoint`; a snapshot pushed mid-session | helix-verified |
## C. Skipped
@@ -334,29 +357,13 @@ text.
| 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 |
+| `]g [g ]G [G` | git change jumps | **deferred by decision**: needs a diff base per file (jj `@-` or git HEAD, fetched by the host on open and save) and line hunks from diffz; the design is written up, the work waits |
| insert `Ctrl-x` | completion menu | completion exists, but not as a popup: insert-mode Tab straight after a `.` opens a buffer of candidate DECLARATIONS (section A). helix's menu itself is skipped |
-| `"` `<reg>`, insert `Ctrl-r` | register select / insert | registers — one yank register and no way to name a second; the system clipboard is not spelled as a register here either, it is the five `SPC` commands in A |
-| `Q` / `q` | record / replay macro | macros — needs replayable input log |
| `Ctrl-s` (normal) | save jumplist position | jumplist itself is implemented (`Ctrl-o`/`Ctrl-i`, section A); only the explicit save point is skipped |
-| `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-to (output discarded), insert output, append output, keep-by-exit-status | shell — `\|` (`shell_pipe`, output replaces the selection) is implemented in section A; these four are the other members of helix's shell family. `$` is additionally taken for line-end here (section A) |
| `:` | 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` is the look-ring motion, which REPLACES the selection with the span it lands on) |
## Differential testing (phase 5)
@@ -389,21 +396,40 @@ Files (all in `test/hxcases/`):
(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).
+- `waivers.jsonl` — named exemptions, each with a reason. One live one:
+ `alt-c-window-op` (the pardes window op deliberately shadows helix
+ change-noyank). `wiX-edit-drops-sel` and `msel-append` went when insert
+ mode began carrying every range through its edits, `msel-yank-paste` with
+ registers of one value per range, `sel-regex-caret` and
+ `sel-regex-dot-newline` when `s`/`S` began searching line by line.
- `parity.jsonl` — 80 further cases, used only by the parity gate below.
-- `parity-waivers.jsonl` — 13 named exemptions for the parity gate, in three
+- `parity-waivers.jsonl` — 19 named exemptions for the parity gate, in three
classes: one deliberate pardes binding (`ctrl-b-page`, since `Ctrl-b` IS the
tty toggle), eight pty VIEWPORT divergences (a terminal's view cannot scroll
below the vt's live grid bottom, so the cursor snaps into a different
- scrolloff band — no text differs), and four case texts a pty cannot hold
+ scrolloff band — no text differs), and five case texts a pty cannot hold
verbatim (a literal TAB the emulator expands, a file with no trailing
- newline, an all-whitespace last row the dump trims).
+ newline, an all-whitespace last row the dump trims). One more,
+ `dot-append-count`: `a` at a file's end grows it by a newline (helix
+ append_mode), which a terminal's text, not the editor's to grow, does not.
+ The sticky-view cases join the viewport class.
+- `golf.jsonl` + `golf-goldens.jsonl` — every example on helix-golf
+ (github.com/nik-rev/helix-golf, imported at d78c18c): one case per numbered
+ step of each walkthrough, its keys being the command up to and including
+ that step, so the first failing case names the step that diverges. A
+ step that starts over with `%` also gets `-fromNN` cases replaying the rest
+ of the command from helix's own text there, so a later step is still
+ tested when an earlier one misses. The goldens are
+ `hx-harness test/hxcases/golf.jsonl`; `zig build hxgolf` runs them.
+ `golf-waivers.jsonl` pins the seven `reverse_golf_example` steps from `""N`
+ on: they walk the search register with `*`, `N` and `n`, which in pardes
+ are the acme look ring, by decision. All ten examples reproduce their published result in hx 25.07.1 under
+ the site's own conditions (a file of the example's language, auto-pairs
+ on). Under the harness two do not, and their goldens are the harness's
+ anyway, which pardes follows: `csv_to_sql` needs auto-pairs (off in the
+ harness and absent in pardes) to close its `VALUES (`, and
+ `invert_dictionary_2` needs 25.07.1's `<count>Alt-(` (rotate within groups
+ of count) where the reference helix rotates by count.
- `smoke.jsonl` — 20 cases referenced by nothing in the tree: no build step,
no script. Either wire it up or delete it.
diff --git a/docs/open-questions.md b/docs/open-questions.md
index db491213..d4b5699c 100644
--- a/docs/open-questions.md
+++ b/docs/open-questions.md
@@ -6,7 +6,23 @@ picked up without redoing the research.
## Where does an unknown command word run?
Raised 2026-09-27 in the review of pardes against acme (Plan 9 source at
-`~/05-genizah/principia-softwarica`). Undecided.
+`~/05-genizah/principia-softwarica`).
+
+**Decided 2026-09-28: a command pane.** Clicked or written at an interactive
+terminal at its prompt, the line is typed into that shell, whose state the
+clicker can see. From anywhere else it runs as its own terminal pane whose
+child is the root ctl's `Shell` run with `-c` and the line, in the pane's
+directory: full emulation
+(colours, `less`, `vim`, `sudo`'s prompt work), the exit status from waiting
+on the child (`exit N`, `exit 127` for a misspelling, no prompt marks
+needed), Kill signalling its process group, `run`/`exit` records in the log,
+and `exec` answering its serial. One command pane per directory is reused
+once done, and keeps what it showed, as acme appends to `+Errors` and never
+clears it (util.c:213-221). Chosen over acme's process-into-`+Errors` because
+it keeps interactive programs working without a streaming runner, and over
+a PATH check before typing into a shell, which misjudges builtins, aliases
+and functions and leaves the shell's state in the way. The analysis below is
+what it was decided from.
**Today.** A middle-click, an `exec` write or a tag word that is not a builtin
is typed into a terminal pane: the pane itself when it takes a command line,
@@ -42,3 +58,19 @@ behaviour inside a chosen terminal: a line in, `exit N` and its output out.
The planned root `ctl` (session builtins) and pane `ctl` (pane builtins)
refuse unknown words, whatever is decided here; the question is only what
`exec` and the middle-click do with them.
+
+## Where does an exec from a code file go?
+
+**Decided 2026-09-28: to a REPL bound for its language, when one is.**
+`Repl python` binds a terminal; then an exec made by a gesture (a middle
+click, the execute key, a selection or a single word) on the body of a file
+in that language, or on the REPL's own body, is typed into the REPL
+instead of run. What stays a command whatever is bound: a builtin's word,
+a word in a tag, `Exec <text>` run by name, a command word @`cmd` in the
+text, and a 9P `exec` write (a script writes the REPL's `pty/data`). An
+event record written back is done as the click it was, REPL and all. With
+several bound the pane asks which, one key answering, and remembers
+nothing. A REPL takes text only while its program has the terminal, and a
+done command pane cannot be bound. Chosen over a per-file or per-directory
+binding, which would need a place to keep and show it; bindings are not
+dumped. `docs/fs.md` has the behaviour.
diff --git a/docs/selections.md b/docs/selections.md
new file mode 100644
index 00000000..5cc1df1a
--- /dev/null
+++ b/docs/selections.md
@@ -0,0 +1,123 @@
+# Selections
+
+How normal mode holds and changes its selections, and where that differs from
+helix's `Selection` (a list of ranges over gap offsets, with a primary index).
+Key-by-key behaviour is in docs/helix-keys.md; this is the model underneath.
+"helix" here is the reference build docs/helix-keys.md names: `hx-harness`
+at `694e7dfdd`, upstream `278b24389`.
+
+## What a selection is
+
+Every `Text` (src/Text.zig) has its own: a pane's body, its tag, a prompt's
+answer, a column's or the workspace's tag. A selection is one primary range
+plus others, up to `memory.limits.selections` in all (1024 on the
+desktop, 64 on the board).
+
+- The PRIMARY is where the cursor always was: `cur_row`/`cur_col` is the
+ head, `vsel.row`/`vsel.col` the anchor while `vsel.active`, and a bare
+ cursor (inactive `vsel`) is the one-character range under it. `msel` is
+ the older whole-line form (`r0`..`r1`), kept for search-result highlights.
+- The others are `sels[0..nsel]`, in room allocated when a second range
+ appears and given back when the selection is one range again (a column's
+ or the workspace's tag allocates from `Text.gpa`, any other text from its
+ pane's), each a `SelRange` of head and anchor plus
+ its own `j`/`k` goal column, in document order. The primary is not in the
+ list; `Text.ranges` slots it in and returns its index.
+- Ends are CELLS, the characters a block cursor sits on, where helix's are
+ gaps between characters. `Text.cellRange`, `cellOffRange` and `rangeCells`
+ convert. A range covers at least one character, as in helix (min width 1),
+ so there are no empty ranges.
+- `vsel.explicit` says the selection was made on purpose (`v`, `x`, `%`,
+ `s`, `n`/`N`, ...) rather than left behind by a motion. Only pardes reads
+ it: the look (Enter) and execute (Tab) chords act on explicit selections
+ and on the word under the cursor otherwise.
+- `select` is `v` extend mode. `restore_cursor` says the insert session
+ began with `a`, so Esc gives each range back the character it was
+ stretched by.
+
+`Text.setRanges` is the one writer of a whole selection: it sorts by start,
+merges ranges that overlap or share a start (helix `normalize`), follows the
+primary through the merges, and keeps the first `max_selections`. `setRange` writes one
+range and drops the others.
+
+## Making, splitting, merging, rotating, removing
+
+- Made several by `C`/`Alt-C` (copy to the next/previous line), `s` (every
+ match inside each range), `S` (the pieces between matches), `Alt-s` (one
+ per line), `<n>o`/`<n>O`, and `Alt-J` (the spaces a join put in). `s`/`S`
+ re-run on every keystroke from the selection the prompt opened on
+ (`Pane.sel_snap`); Esc is that snapshot restored.
+- `K`/`Alt-K` keep or drop the ranges a regex matches in, on the `s` prompt;
+ `Alt-:` turns every range forward.
+- `Alt-minus` merges all into one, `Alt-_` the consecutive ones, `_` trims
+ whitespace (dropping all-blank ranges).
+- `)`/`(` rotate which range is primary; `Alt-)`/`Alt-(` rotate the TEXT
+ between ranges, the primary moving with its text.
+- `,` keeps only the primary, `Alt-,` removes it (the next range takes over).
+- After `s`, `S` and `Alt-s` the primary is the first range, which is
+ helix's behaviour (its own TODO), not a choice.
+
+## How a command reaches every range
+
+A parsed action has a scope (`modal.Normal.Action.scope`).
+
+- `per_selection` (motions, `d`, `c`, `y`, `p`, `r`, `~`, `>`, `ms`, ...):
+ `normal.replaySels` runs the ONE-range handler once per range, last range
+ first, each pass seeing a single selection. A pass's result is stored as
+ distances from the END of the text, which an edit at an earlier range
+ cannot move; `setRanges` then puts them back together. The mode, prefix
+ and select state that survive are the primary's pass. Insert-mode keys are
+ replayed the same way.
+- `once` (the `multi` family, `s`/`S`, `J`/`Alt-J`, `Alt-(`/`Alt-)`, `&`,
+ `Ctrl-c`, undo/redo, builtins, search): one function takes `Text.ranges`,
+ makes one edit and hands the mapped ranges to `setRanges`, the way a helix
+ command builds one transaction.
+- Anything that reaches outside the text (a builtin, a language query, a
+ look) runs once from the primary and drops back to one cursor
+ (`normal.multiOnce`).
+
+## Insert mode
+
+Each range lives on through insert mode. Every insert-mode edit happens at
+the cursor, so `edit.insertKey` runs it on the bare cursor and then carries
+the range through what it did to the text, with helix's `Range::map` rules:
+at an insertion, a backward range's head and an empty range move past it
+and a forward range's head stays before it, so typing slides an `i` range
+and stretches an `a` range. Enter slides or stretches the range by what the
+cursor moved; an arrow key collapses it. The other ranges get the same by
+the replay.
+
+## Undo, registers, repeat
+
+- One keystroke is one undo step however many ranges it edited: only the
+ first replay pass records (`pushUndo`).
+- An undo snapshot (`File.Snapshot`) holds the content and the PRIMARY
+ range. Undo puts the primary back and leaves the other ranges where they
+ were; helix restores the whole selection of that revision.
+- Registers (`Registers.zig`) hold one value per range. A yank under the
+ replay fills slot `Pardes.multi_index` of `multi_count`, and `p`, `P`, `R`
+ and insert `Ctrl-r` read slot i at range i, the last value standing in
+ for missing ones. `"<reg>` names the register (`Pardes.register` for the
+ length of one command); `_ # . % /` are computed or special and `+`/`*`
+ is the system clipboard. The acme chords and a paste into a terminal read
+ the default register joined by newlines. The clipboard's read is an
+ answer from the shell that comes later, so `"+p` pastes once, at the
+ primary, and insert `Ctrl-r +` types what pardes last put on the
+ clipboard rather than asking the desktop.
+- `Alt-.` repeats the last `f`/`t`/`F`/`T`. `.` and macros (`Q`/`q`) are
+ keys typed again (`Macro.zig`), so whatever those keys do to the ranges
+ happens again: there is no edit log underneath.
+
+## Where it differs from helix
+
+- Direction of a one-character range: both of its cells are the same cell,
+ so it has none. helix can flip one (`Alt-;`) and the commands that read
+ the head (`&`, the next extend) see the difference.
+- Count: at most `memory.limits.selections` ranges (1024 on the desktop, 64
+ on the board); matches past that are dropped silently.
+- `n`/`N` walk the look ring (acme), not regex search hits, and there is no
+ `*`; helix-golf's `*`, `""N` and `n` do not apply.
+- `s`/`S` match as sam does (docs/helix-keys.md, "Regex selection"): each
+ line is its own haystack, so `^`/`$` hold at every line and a class like
+ `\s` does not reach the newline unless the pattern names `\n`.
+- Undo restores only the primary (above).
diff --git a/docs/tags.md b/docs/tags.md
index 810ae010..f1e4a420 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -6,6 +6,16 @@ pane (or its first pane when focus comes from another column). This makes
`New`, `Tty`, `Find`, and `Grep` available beside the work they act on.
`New` appears only in the column tag by default; pane tags keep their own
save, terminal, close, and collapse commands. `Tty` opens a new embedded terminal.
+In a terminal's own tag the word names the shell it runs, `Tty+fish`, tinted
+with the tag's name colour: `Tty+arg` is one word a tag can hold for `Tty
+arg`, so clicking it opens another terminal on that shell, as `Tty fish`
+would. Only a builtin that declares `plus_arg` reads a `+` so; `Tty` is the
+one, since other arguments (a dump's name) may contain `+`. A command pane's tag reads `<dir> (<line>) running` or `exit N`
+and offers `Kill`. `Repl python` in a terminal's tag binds it as Python's
+REPL, its id, `python-a`, beside the Tty word: a middle click or the execute
+key on a `.py` body then types the text into it rather than running it, and
+several bound ask which. The tag's own words, `Exec <text>` by name and a
+command word @`cmd` still run as commands (docs/fs.md).
Pane and column command text leave a small gap after their aligned drag grips.
GUI pane mode symbols are centered by their visible ink; changing the font or
its size refreshes the cached measurements.