diff options
174 files changed, 7081 insertions, 1092 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 393b0b0f..0643e209 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -13,24 +13,29 @@ relative to that root unless linked. ## Find the session through the mount -This machine keeps a FUSE mount of every posted session under `/mnt/9p`: -`/mnt/9p/pardes/<pid>/` is one session's served root. Inside a pane, +Check `$NINE_MOUNT` first. `9ns --mntgen` sets it for every process it starts +(an interactive shell on this machine runs inside one), so a set `$NINE_MOUNT` +means the posted-9P registry is mounted there, usually `/mnt/9p`, and each +running editor is a directory `$NINE_MOUNT/pardes/<pid>/`. Inside a pane, `$PARDES_9P` is that session's socket (`/run/user/1000/pardes-9p-<pid>.sock`), -so its pid is the directory to use, and `$PARDES_PANE` is the calling pane's -serial: +which names the directory, and `$PARDES_PANE` is the calling pane's serial: ```sh -m=/mnt/9p/pardes/$(basename "$PARDES_9P" .sock | sed 's/^pardes-9p-//') +[ -n "$NINE_MOUNT" ] || echo 'no 9P mount: use the Python client below' +s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock} cat "$m/index" cat "$m/pane/$PARDES_PANE/body" ``` +From there the editor is files: `cat`, `echo >`, `ls` and shell scripts are +the whole interface, and nothing below needs more than they do. + Entries for dead sessions stay listed and answer `Input/output error` on any access, so name the session you mean rather than globbing or taking the newest. `cat "$m/index"` is the cheapest liveness check, and `cat "$m/status"` reports the `pid`, `version` and `panes` of a session new enough to serve it. -`/mnt/9p/pardes` is the registry of posted sessions, mounted once for the +`$NINE_MOUNT/pardes` is the registry of posted sessions, mounted once for the machine. It is not the per-pane kernel mount the `Tty9p` builtin makes, which gives one pane's shell `$PARDES_MOUNT`; see [docs/v9fs.md](../../../docs/v9fs.md) for that. Either mountpoint serves the same tree. A `9ns --unix SOCKET -- cmd` @@ -51,7 +56,9 @@ $m/exec write a line = a middle click: an editor command word, or a shell $m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `error ...`, on the same open: exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3 (a fresh terminal: waits for its first prompt; if none comes, interrupt the read) -$m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg <serial|-> <text> +$m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg <serial|-> <text>, + run <serial> <line>, exit <serial> <N|?>, send <from> <to> <repl-id>, + dump|restore <path>, err <serial|-> <file>: <why> (exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not) $m/screen the rendered screen as JSON, frozen per open $m/listeners this session's dial addresses @@ -59,11 +66,14 @@ $m/focus the serial of the pane with the keyboard (empty while a column/wor echo a serial into it to move the keyboard (a folded pane stays folded) $m/ctl the settings, one a line as a write takes them; write a setting or a session builtin (Newcol, Dump, Theme x; Exit QUITS the editor, Kill [word...] stops the - commands pardes typed into terminals; Exit and Restore refuse once per + commands pardes started (command panes, lines it typed into shells), a word + matching a command line's first word; Exit and Restore refuse once per unsaved pane, `<name>: Modified (Exit again to discard)`, and the same - word again DISCARDS that text -- not a retry, unlike lock's `file in use`); a pane's builtins (Del, Save f, - and the column words Delcol, Collapse, which act on that pane's column) go to - $m/pane/<n>/ctl + word again DISCARDS that text -- not a retry, unlike lock's `file in use`; + Kill signals only the foreground job, so of `sleep 30; echo done` the + echo still runs; Joincol needs a column to the right of the keyboard's); + a pane's builtins (Del, Save f, Collapse, which folds that pane, and the + column word Delcol, which closes that pane's column) go to $m/pane/<n>/ctl $m/commands every builtin: `Word`, `Word arg`, then `root` or `pane` (which ctl takes it) $m/pane/new open it to make a pane (a scratch named <dir>/+New), read names it; rmdir $m/pane/<n> closes it; closing a column's last pane leaves a +New there @@ -114,9 +124,22 @@ active pane from the root and at that pane from `$m/pane/<n>/look` and made, or the pane it focused or acted on. A command that fails is reported in the editor, not as a write error, so inspect the resulting pane, index, message or screen; only a malformed line fails the write itself. A word no builtin -knows (a typo included) is typed into a terminal -- the pane itself if it is a -terminal at its prompt, else one in the pane's directory, made if need be -- -and is not reported as an error. Every refused 9P write adds an `err <serial|-> +knows (a typo included) is a command line: written at a terminal at its +prompt it is typed into that shell; from anywhere else it runs as a command +pane, a terminal whose child is the root ctl's `Shell` (fish unless set) +running `-c` the line in the pane's directory, which ends showing `exit N` (a typo: `exit 127`) and logs `run +<serial> <line>` and `exit <serial> <N|?>` -- `exec` reads back its serial, +so follow `log` for the exit. The directory's next command reuses a finished +command pane, below what it showed. A terminal bound as a REPL (`Repl +python` on its ctl) takes the middle clicks made on a `.py` body instead, +but never a 9P write: a script sends code by writing the REPL pane's +`pty/data`, and runs a command from such a file with `Exec <text>` or the +tag. A bound terminal's `ctl` line ends with its id (`python-a`); an event +record written back from such a file's body goes to its REPL, as the click +would. `Repl -` unbinds; a bare `Repl` says the binding. Multi-line code +written to `pty/data` should be a bracketed paste, `\e[200~<code>\e[201~` +then `\r`: sent line by line, a blank line ends a Python block, and Python +3.14's REPL auto-indents each line it is typed. Every refused 9P write adds an `err <serial|-> <file>: <why>` record to `$m/log`; through a mount the write itself only says `Invalid argument`. @@ -147,9 +170,13 @@ is why copying one onto another is all that acme's `addr=dot`, `dot=addr` and `/pattern/`, `2+1`), whose regexps are mvzr's searched as sam searches: `^`/`$` match at any line's start and end, `.` and `[^...]` never match a newline, the leftmost match wins (the first alternative there, not the -longest), `/re/` wraps unless `limit` is set, and a search that backtracks +longest). An expression is evaluated from the current address (the last one +written, or just past the last `data` write): `.` is that address, not the +selection, `/re/` searches on from its end and wraps unless `limit` is set, +`?re?` or `-/re/` searches back, `#100,#50` fails `addresses out of order`, +and a search that backtracks past a step budget (about 300 ms) fails with `regular expression search -took too long`. A failed address says +gave up, ...`. A failed address says why (`no match for regexp`, `address out of range`) and leaves no address: `data` refuses until the next good one, so a missed target is never written at the old one. Moving `dot` scrolls the pane to it. `limit` bounds a @@ -209,11 +236,28 @@ For live terminal output or plugin events, use `open` / `read_fid` / `close`, not the read-until-EOF helper. `pty/data` captures output while held open; it is not a history replay. Both files are shared, consuming queues, not per-client broadcasts, so a slow reader loses older data. Holding `event` open -intercepts that pane's Look and Exec clicks, so it is not a passive logger: for -Look/Exec records whose offsets identify the intended text, forward the short -record `<origin><action><q0> <q1>\n`, not the whole report with flags and text. -Expansion and chord reports need explicit handling, and terminal-body events -can carry text only in the report, which a short writeback cannot reproduce. +intercepts that pane's Look and Exec clicks -- and lines written to that pane's +own `look`/`exec`, or to the root's while it has the keyboard, as `F` records +at `0 0` with the text, and clicks in a terminal's body, also at `0 0` -- so +it is not a passive logger: a helper holding `event` that writes its own +pane's exec gets its command back as a record; run it through `ctl` or write +the record back. To have a record done, write it back: the short form +`<origin><action><q0> <q1>\n` acts on that range's text, and the whole record +as read acts on its text when the range is empty (the only way for a record +at `0 0`). Chord reports need explicit handling. A record is +`<origin><action><q0> <q1> <flag> <n> <text>\n` and its text may hold +newlines: read `n` bytes of text, never up to the next newline (acme counts +runes; pardes counts bytes, as all its offsets are). +A Restore puts a new editor under every client: the Restore write is +answered, then every connection is hung up (their fids name the old +editor's panes); dial again, and the new log names the restored panes then +`restore <path>` -- the authority, since a slow client may see the cut +before the answer. Through a 9ns mount the Restore write itself usually +fails with ECONNRESET although the Restore went ahead: 9ns fails a request +whose reply is already in when the hang-up breaks its next send. Trust the +log. `Dump` writes `pardes-<date>-<time>.zon`, the time in UTC, under +`DumpDir` (`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`) and logs +`dump <path>`. Read the event implementation before building an interceptor. Close handles in `finally`, and disconnect after a socket timeout. The service shares four connection slots and 32 screen/terminal-history snapshot handles. @@ -129,6 +129,7 @@ zig build hxdiff-test comparator, allocation and CLI regression checks zig build hxdiff-live compare against a freshly run hx-harness zig build hxdiff-update update reference results only after comparison passes zig build hxparity file-pane vs pty-pane editing parity +zig build hxgolf every helix-golf example, step by step, against helix zig build mupdf-check compile, link, render and search docs/design.pdf zig build web-snap browser highlighting and touch interactions zig build web-driver-test browser driver timeouts and cleanup @@ -228,6 +229,7 @@ sketch; `docs/design.pdf` is a retained rendering/search fixture and may lag it. | `docs/detached.md` | one core, many frontends, over a unix socket | | `docs/config.md` | build options and runtime configuration | | `docs/tags.md` | editable workspace, column and pane tags; filename drafts | +| `docs/selections.md` | the normal-mode selection model and how it differs from helix's | | `docs/fs.md` | default 9P service, Look resolution, and named mounts | | `docs/lsp.md` | in-process ZLS and external language servers | | `docs/lsp-evaluation.md` | why that backend, measured against the alternatives | @@ -388,6 +388,10 @@ pub fn build(b: *std.Build) void { const query_path = dep.path(g.query); const query = std.Io.Dir.cwd().readFileAlloc(io, query_path.getPath(b), b.allocator, .limited(0x100000)) catch @panic("read " ++ g.name ++ " highlights"); ts_queries.addOption([]const u8, g.name ++ "_highlights", query); + // helix's textobject queries, vendored (vendor/queries); a + // grammar without one gets none + const objects = std.Io.Dir.cwd().readFileAlloc(io, b.pathFromRoot("vendor/queries/" ++ g.name ++ "/textobjects.scm"), b.allocator, .limited(0x100000)) catch ""; + ts_queries.addOption([]const u8, g.name ++ "_textobjects", objects); const lib = b.addLibrary(.{ .name = "tree-sitter-" ++ g.name, @@ -1103,6 +1107,13 @@ pub fn build(b: *std.Build) void { const hxparity_step = b.step("hxparity", "run the file-vs-pty editing parity suite (-- [--waivers w.jsonl] <cases.jsonl>...)"); hxparity_step.dependOn(&run_hxparity.step); + // every helix-golf example, one case per step of its walkthrough + const run_hxgolf = b.addRunArtifact(hxdiff); + run_hxgolf.has_side_effects = true; + run_hxgolf.addArgs(&.{ "--strict", "test/hxcases/golf.jsonl", "test/hxcases/golf-goldens.jsonl", "test/hxcases/golf-waivers.jsonl" }); + run_hxgolf.setCwd(b.path(".")); + b.step("hxgolf", "compare the helix-golf examples against their Helix results").dependOn(&run_hxgolf.step); + const lspbench = b.addExecutable(.{ .name = "pardes-lspbench", .root_module = b.createModule(.{ 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 @@ -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. diff --git a/src/9p_io.zig b/src/9p_io.zig index d6eef9d4..2ca11a87 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -174,6 +174,16 @@ pub const Listener = struct { core: *pardes.Pardes, runner: Runner = undefined, stopping: std.atomic.Value(bool) = .init(false), + /// A connection wrote the Restore and has yet to answer it. + restore_writer: std.atomic.Value(bool) = .init(false), + /// Connections accepted so far, and the count each slot's connection + /// was accepted at: a Restore cuts those accepted before it (`reset`), + /// and a new client in a freed slot has a later stamp. Written on the + /// connection's task as it opens, read with the turn. + accepts: std.atomic.Value(u64) = .init(0), + accepted: [max_conns]std.atomic.Value(u64) = @splat(.init(0)), + /// Connections accepted before this count are being cut by a Restore. + cut_before: u64 = 0, tcp_address: ?std.Io.net.IpAddress = null, quic: if (quic_enabled) ?quic.Listener else void = if (quic_enabled) null else {}, quic_address: ?std.Io.net.IpAddress = null, @@ -207,6 +217,16 @@ pub const Listener = struct { /// answers. A request that would change a pane while the editor is out /// in a syscall mid-step is parked in the engine instead, and retried /// when the turn is next given up between steps (`wakeParked`). + fn onOpened(ctx: ?*anyopaque, conn: *Runner.Conn) void { + const l = of(ctx); + l.accepted[conn.index].store(l.accepts.fetchAdd(1, .acq_rel) + 1, .release); + } + + /// A live connection accepted before the Restore under way. + fn cutting(l: *Listener, conn: *Runner.Conn) bool { + return conn.live() and l.accepted[conn.index].load(.acquire) <= l.cut_before and l.cut_before != 0; + } + fn onServe(ctx: ?*anyopaque, conn: *Runner.Conn, req: pardes.ctlfs.Req) void { const l = of(ctx); // Once `stop` has begun the editor is tearing down and may never @@ -217,6 +237,12 @@ pub const Listener = struct { } const quiet = pardes.turn.take(); defer pardes.turn.give(); + // A connection a Restore is cutting (`reset`) is the old editor's + // client: its fids name the old editor's panes and opens. + if (l.cutting(conn)) { + const refused = pardes.ctlfs.Reply.fail(req.tag, pardes.ctlfs.E.IO); + return conn.reply(&refused, ""); + } if (!quiet and pardes.ctlfs.needsQuiet(req)) { pardes.turn.parked = true; const later: pardes.ctlfs.Reply = .{ .tag = req.tag, .status = .again }; @@ -267,13 +293,20 @@ pub const Listener = struct { // and `deinit` lets the answer out before it cuts the connections. if (core.quit) return conn.reply(&reply, ""); const restoring = core.restore_req != null; + // Set with the turn still held, so `reset` sees it and waits. + if (restoring) l.restore_writer.store(true, .release); + defer if (restoring) l.restore_writer.store(false, .release); if (core.effects_len != 0) pardes.turn.awaitSettled(epoch); + // The Restore's own write is answered once it is done, and before + // `reset` hangs every connection up, this one too, so the writer + // hears that it happened rather than a cut; a failed one leaves the + // connection and says why on the message row and in the log. + if (restoring) { + if (l.core == core) pardes.turn.awaitRestored(restores); + return conn.reply(&reply, ""); + } // Once a wait returns `core` may be gone: a Restore meanwhile put a - // replacement in (`reset`) and is hanging this connection up, which - // is a Restore's answer. Only one that failed leaves this connection - // here to answer. - if (l.core != core) return; - if (restoring) pardes.turn.awaitRestored(restores); + // replacement in and is hanging this connection up. if (l.core != core) return; conn.reply(&reply, ""); } @@ -484,9 +517,9 @@ pub const Listener = struct { /// Hangs every connection up, so that `replacement` starts with no /// client holding anything. From here requests are answered from the /// replacement -- a client that connects meanwhile is a client of the - /// replacement -- and every task waiting on the old core is let go: it - /// finds the core changed and answers nothing, its connection being - /// hung up. The connections pay their releases on their own tasks, so + /// replacement -- and every task waiting on the old core is let go: the + /// Restore's own writer answers, the rest find the core changed and + /// answer nothing, their connections being hung up. The connections pay their releases on their own tasks, so /// the editor rests while they do. pub fn reset(l: *Listener, replacement: *pardes.Pardes) void { for (0..quic_slots) |i| l.drop(@intCast(i)); @@ -496,13 +529,38 @@ pub const Listener = struct { replacement.fs.tcp_address = l.tcp_address; replacement.fs.quic_address = l.quic_address; if (comptime supported) { - l.runner.closeAll(); + // The Restore's writer answers as it wakes (`onServe`), and its + // answer gets 200 ms to leave before the cut; every other request + // from the old editor's clients meanwhile is refused, since the + // fids it names are the old editor's. A client that connects now + // is the replacement's, and stays. + l.cut_before = l.accepts.load(.acquire); pardes.turn.settle(); pardes.turn.restoreSettled(); pardes.turn.rest(); + const flush_by = nowMs() +| 200; + while (nowMs() < flush_by) { + const pending = l.restore_writer.load(.acquire) or for (&l.runner.conns) |*conn| { + if (!l.cutting(conn)) continue; + conn.lock(); + const n = conn.engine.output().len; + conn.unlock(); + if (n != 0) break true; + } else false; + if (!pending) break; + Client.nap(1); + } + for (&l.runner.conns) |*conn| if (l.cutting(conn)) conn.close(); const deadline = nowMs() +| 2000; - while (l.runner.count() != 0 and nowMs() < deadline) Client.nap(1); + while (nowMs() < deadline) { + const left = for (&l.runner.conns) |*conn| { + if (l.cutting(conn)) break true; + } else false; + if (!left) break; + Client.nap(1); + } pardes.turn.wake(); + l.cut_before = 0; } l.collectOs(); for (&l.conns) |*conn| conn.accepted_ms = 0; @@ -758,7 +816,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [ l.runner.init(.{ .io = io, .root = pardes.ctlfs.root, - .handler = .{ .ctx = l, .serve = Listener.onServe }, + .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened }, .greet_timeout_ms = Listener.greet_deadline_ms, }); const entry_name = if (named.len != 0) named else fallback; diff --git a/src/File.zig b/src/File.zig index 26585f77..b8368482 100644 --- a/src/File.zig +++ b/src/File.zig @@ -70,9 +70,19 @@ pub const State = struct { tree_context: bool = false, context_declarations: []syntax.ContextDeclaration = &.{}, context_revision: ?u32 = null, + /// The whole file's parse for the keys that walk its nodes (Alt-o and + /// kin), made on the first of them after an edit and kept until the next + /// one: `syntax.parseTree`'s, opaque here. + node_tree: ?*anyopaque = null, + node_tree_revision: u32 = 0, + /// helix's object_selections: what Alt-o grew from, for Alt-i to go back + /// to; gpa-owned ranges and the primary's index. + node_history: std.ArrayList(NodeStep) = .empty, history: *History, }; +pub const NodeStep = struct { ranges: []modal.Selection, primary: usize }; + pub fn dumpPane( arena: std.mem.Allocator, pane: *const Pane, @@ -817,6 +827,9 @@ pub fn deinit(p: *Pardes, pane: *Pane, file: *State) void { if (file.highlights.len > 0) p.tree_sitter_gpa.free(file.highlights); locations.freeRows(p.gpa, file.location_rows); if (file.context_declarations.len > 0) p.tree_sitter_gpa.free(file.context_declarations); + if (file.node_tree) |tree| syntax.freeTree(tree); + for (file.node_history.items) |step| p.gpa.free(step.ranges); + file.node_history.deinit(p.gpa); for (file.history.undo[0..file.history.undo_len]) |snap| p.gpa.free(snap.content); for (file.history.redo[0..file.history.redo_len]) |snap| p.gpa.free(snap.content); p.gpa.destroy(file.history); @@ -1130,7 +1143,7 @@ pub fn drawGutter(p: *Pardes, s: *Surface, pane: *Pane, r: pardes.Rect, tx: u16, const goff = pane.scroll(); const gcur = Terminal.gridCursor(pane); const gcrow = if (pane.body.cur_pinned) pane.body.cur_row else @as(i32, gcur.y) + goff; - const typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side); + const typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice); const cur_line: i32 = if (active and typing_body) gcrow else std.math.minInt(i32); // the body's first row, the way renderPane derives it (Tagbottom) const body_y = p.bodyTop(pane, r); diff --git a/src/Macro.zig b/src/Macro.zig new file mode 100644 index 00000000..7475992e --- /dev/null +++ b/src/Macro.zig @@ -0,0 +1,176 @@ +//! Keys kept to be typed again. `Q` records the keys that follow into a +//! register until the next `Q`, and `q` types them again (helix macros); +//! `.` types again the last insert session together with the normal command +//! that began it (helix repeat_last_insert). A macro is kept in its register +//! as helix writes one, in key notation (`xt,S=<ret>_<A-(>`), so it can be +//! pasted, edited and yanked back like any text. +const std = @import("std"); +const pardes = @import("pardes.zig"); +const Key = pardes.Key; + +const Macro = @This(); + +/// The register `Q` is recording into, 0 when it is not. +recording: u21 = 0, +recorded: std.ArrayList(Stored) = .empty, +/// The keys of the normal command under way (a count, `"<reg>`, a prefix +/// and the key that completes it), dropped when it completes without +/// entering insert mode. +command: std.ArrayList(Stored) = .empty, +/// `.`'s: the command that began the last insert session and every key +/// typed in it, the one that left it included. +last_insert: std.ArrayList(Stored) = .empty, +/// Where in `last_insert` the typed keys begin, after the command's. +typed_from: usize = 0, +/// Keys are going into `last_insert`. +inserting: bool = false, +/// How deep macros are being typed again; `.`'s replay is `repeating`. +replaying: u8 = 0, +repeating: bool = false, + +/// A key as it can be typed again: what it was, not the text it came with. +pub const Stored = struct { + cp: u21, + ctrl: bool = false, + alt: bool = false, + shift: bool = false, + + pub fn of(key: Key) Stored { + return .{ .cp = key.cp, .ctrl = key.ctrl, .alt = key.alt, .shift = key.shift }; + } + + /// The key again, its text made from its codepoint into `buf`. + pub fn typed(s: Stored, buf: *[4]u8) Key { + var k: Key = .{ .cp = s.cp, .ctrl = s.ctrl, .alt = s.alt, .shift = s.shift }; + if (!s.ctrl and !s.alt and s.cp >= 0x20 and s.cp != Key.backspace and s.cp < 0xF0000) { + const n = std.unicode.utf8Encode(s.cp, buf) catch return k; + k.text = buf[0..n]; + } + return k; + } +}; + +pub fn deinit(m: *Macro, gpa: std.mem.Allocator) void { + m.recorded.deinit(gpa); + m.command.deinit(gpa); + m.last_insert.deinit(gpa); + m.* = .{}; +} + +/// A typed key, each of its characters one key: what a host delivers as +/// one key event with a run of text is that run typed. +pub fn append(list: *std.ArrayList(Stored), gpa: std.mem.Allocator, key: Key) void { + const view = std.unicode.Utf8View.init(key.text) catch null; + if (key.text.len == 0 or key.ctrl or key.alt or view == null) { + list.append(gpa, .of(key)) catch {}; + return; + } + var it = view.?.iterator(); + while (it.nextCodepoint()) |cp| list.append(gpa, .{ .cp = cp, .shift = key.shift }) catch {}; +} + +const names = [_]struct { cp: u21, name: []const u8 }{ + .{ .cp = Key.escape, .name = "esc" }, + .{ .cp = Key.enter, .name = "ret" }, + .{ .cp = Key.tab, .name = "tab" }, + .{ .cp = Key.backspace, .name = "backspace" }, + .{ .cp = Key.delete, .name = "del" }, + .{ .cp = ' ', .name = "space" }, + .{ .cp = '<', .name = "lt" }, + .{ .cp = '>', .name = "gt" }, + .{ .cp = '-', .name = "minus" }, + .{ .cp = Key.up, .name = "up" }, + .{ .cp = Key.down, .name = "down" }, + .{ .cp = Key.left, .name = "left" }, + .{ .cp = Key.right, .name = "right" }, + .{ .cp = Key.home, .name = "home" }, + .{ .cp = Key.end, .name = "end" }, + .{ .cp = Key.page_up, .name = "pageup" }, + .{ .cp = Key.page_down, .name = "pagedown" }, +}; + +/// Keys in helix's notation: a plain character as itself, anything else +/// in angle brackets with its modifiers (`<C-r>`, `<A-(>`, `<ret>`). +pub fn encode(gpa: std.mem.Allocator, keys: []const Stored) ![]u8 { + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(gpa); + for (keys) |k| { + var name: ?[]const u8 = null; + for (names) |n| if (n.cp == k.cp) { + name = n.name; + }; + // `-` is only spelled out after a modifier, where helix needs it + if (k.cp == '-' and !k.ctrl and !k.alt) name = null; + var buf: [4]u8 = undefined; + const bare = if (name) |n| n else buf[0..(std.unicode.utf8Encode(k.cp, &buf) catch continue)]; + if (!k.ctrl and !k.alt and name == null) { + try out.appendSlice(gpa, bare); + continue; + } + try out.append(gpa, '<'); + if (k.ctrl) try out.appendSlice(gpa, "C-"); + if (k.alt) try out.appendSlice(gpa, "A-"); + try out.appendSlice(gpa, bare); + try out.append(gpa, '>'); + } + return out.toOwnedSlice(gpa); +} + +/// The keys helix notation names; what it cannot read ends the list. +pub fn decode(gpa: std.mem.Allocator, text: []const u8) ![]Stored { + var out: std.ArrayList(Stored) = .empty; + errdefer out.deinit(gpa); + var i: usize = 0; + while (i < text.len) { + const len = std.unicode.utf8ByteSequenceLength(text[i]) catch break; + if (i + len > text.len) break; + if (text[i] != '<') { + try out.append(gpa, .{ .cp = std.unicode.utf8Decode(text[i..][0..len]) catch break }); + i += len; + continue; + } + const close = std.mem.indexOfScalarPos(u8, text, i + 1, '>') orelse break; + var name = text[i + 1 .. close]; + i = close + 1; + var k: Stored = .{ .cp = 0 }; + while (name.len > 2 and name[1] == '-') : (name = name[2..]) switch (name[0]) { + 'C' => k.ctrl = true, + 'A' => k.alt = true, + 'S' => k.shift = true, + else => break, + }; + for (names) |n| if (std.mem.eql(u8, n.name, name)) { + k.cp = n.cp; + }; + if (k.cp == 0) k.cp = std.unicode.utf8Decode(name) catch break; + try out.append(gpa, k); + } + return out.toOwnedSlice(gpa); +} + +test "a macro's keys go into helix notation and come back the same" { + const gpa = std.testing.allocator; + const keys = [_]Stored{ + .{ .cp = 'x' }, + .{ .cp = 't' }, + .{ .cp = ',' }, + .{ .cp = 'S' }, + .{ .cp = '=' }, + .{ .cp = Key.enter }, + .{ .cp = '_' }, + .{ .cp = '(', .alt = true }, + .{ .cp = 'r', .ctrl = true }, + .{ .cp = '<' }, + .{ .cp = ' ' }, + .{ .cp = '-' }, + .{ .cp = '-', .alt = true }, + .{ .cp = 'λ' }, + .{ .cp = Key.escape }, + }; + const text = try encode(gpa, &keys); + defer gpa.free(text); + try std.testing.expectEqualStrings("xt,S=<ret>_<A-(><C-r><lt><space>-<A-minus>λ<esc>", text); + const back = try decode(gpa, text); + defer gpa.free(back); + try std.testing.expectEqualSlices(Stored, &keys, back); +} diff --git a/src/Messages.zig b/src/Messages.zig index b7153e5c..ea62041f 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -181,10 +181,12 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void { pane.pushOlderMessage(); if (pane.msg_len == 0) pane.msg_slot = pane.freeMessageSlot(); pane.msg_announcement = false; + pane.msg_sent = false; pane.msg_status = !stack; const arrives = pane.msg_len == 0 or pane.msg_life.phase == .leaving; for (text[0..n], pane.msg[0..n]) |c, *cell| cell.* = if (c < 0x20 or c == 0x7f) ' ' else c; pane.msg_len = @intCast(n); + pane.msg_said_ms = nowMs(); p.needs_frame = true; if (arrives) { pane.msg_life = .{ .phase = if (p.settings.message_animation and n > 0) .entering else .shown }; @@ -198,6 +200,35 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void { } } +fn nowMs() i64 { + if (comptime !pardes.hosted) return 0; + var ts: std.c.timespec = undefined; + if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) return 0; + return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000); +} + +/// With no frontend attached no input dismisses a message, and a detached +/// session's /screen and messages kept what was said an hour ago: each +/// pane's lines go once its newest has been up `message_linger_ms`, on the +/// clock, checked as the core is stepped and by the detached session's loop +/// between steps -- not as a 9P request is served, since a read is answered +/// while a step is out and must change nothing. +/// ponytail: a pane's older lines go with its newest, not each on its own +/// clock; the render pipeline's clock will give every line its own. +pub fn expireUnattended(p: *Pardes) void { + if (!p.unattended) return; + const now = nowMs(); + for (p.panes) |slot| { + const pane = slot orelse continue; + if (pane.msg_len == 0 and pane.msg_older_len == 0) continue; + if (now - pane.msg_said_ms < p.settings.message_linger_ms) continue; + pane.msg_len = 0; + pane.msg_life = .{}; + pane.msg_older_len = 0; + p.needs_frame = true; + } +} + /// The key or click that used to clear the message row now starts each /// line's exit: it stays `message_linger_ms`, then dissolves. A line /// already on its way out keeps its own timing. @@ -410,23 +441,51 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage { return &m.log[(first + i) % limits.message_log]; } +/// `<operation>: <the error in words>`: FileNotFound reads "file not found". pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void { var buf: [256]u8 = undefined; - const text = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch operation; - reportFailure(p, id, text); + var w = std.Io.Writer.fixed(&buf); + w.print("{s}: ", .{operation}) catch {}; + for (@errorName(err), 0..) |c, i| { + if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break; + w.writeByte(std.ascii.toLower(c)) catch break; + } + reportFailure(p, id, w.buffered()); +} + +/// The longest start of `text` that fits `max` bytes and ends between +/// words (or, with no space to cut at, between characters). +pub fn clip(text: []const u8, max: usize) []const u8 { + if (text.len <= max) return text; + if (std.mem.lastIndexOfScalar(u8, text[0 .. max + 1], ' ')) |sp| return text[0..sp]; + var n = max; + while (n > 0 and text[n] & 0xC0 == 0x80) n -= 1; + return text[0..n]; } /// reportError with the words already chosen. pub fn reportFailure(p: *Pardes, id: usize, text: []const u8) void { - // A builtin a ctl write runs: its first error is also the write's. + // A builtin a ctl write runs: its first error is also the write's, cut + // between words. if (p.fs.no_prompt and p.fs.failure_len == 0) { - const n = @min(text.len, p.fs.failure.len); - @memcpy(p.fs.failure[0..n], text[0..n]); + const kept = if (text.len > p.fs.failure.len) clip(text, p.fs.failure.len - 3) else text; + @memcpy(p.fs.failure[0..kept.len], kept); + var n = kept.len; + if (kept.len < text.len) { + @memcpy(p.fs.failure[n..][0..3], "..."); + n += 3; + } p.fs.failure_len = @intCast(n); } setMessage(p, id, text); } +test "an error reads as words and a failure is cut between them" { + try std.testing.expectEqualStrings("go", clip("go haskell", 4)); + try std.testing.expectEqualStrings("go haskell", clip("go haskell", 10)); + try std.testing.expectEqualStrings("\u{e9}", clip("\u{e9}\u{e9}", 3)); +} + /// The one place that decides which transient lines a pane is showing and /// in what order. Everything downstream reads the answer: the body layer /// so it does not paint under them, and the paint pass so it can stack @@ -528,14 +587,14 @@ test "Msg writes the transient row by hand, bare or with text, and input ends it // ...on the pane that ran it, and nowhere else try std.testing.expectEqualStrings("", row(p, 0)); - // Bare, it reports ITSELF through reportError — the `<operation>: <Error>` + // Bare, it reports ITSELF through reportError — the `<operation>: <error in words>` // every failed save and refused Look arrives in. try std.testing.expect(p.executeBuiltinLine(2, "Msg")); - try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2)); + try std.testing.expectEqualStrings("Msg: no message", row(p, 2)); // Input starts its exit: it lingers, dissolves, and is gone. p.update(.{ .key = .{ .cp = Key.escape } }); - try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2)); + try std.testing.expectEqualStrings("Msg: no message", row(p, 2)); for (0..200) |_| p.update(.tick); try std.testing.expectEqualStrings("", row(p, 2)); // ...and with neither linger nor animation it is as old as your last input. @@ -554,6 +613,31 @@ test "Msg writes the transient row by hand, bare or with text, and input ends it try std.testing.expectEqual(p.panes[2].?.msg.len, row(p, 2).len); } +test "with no frontend attached a message goes once it has lingered, on the clock" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = p.panes[p.active].?; + p.setMessage(p.active, "said"); + p.setMessage(p.active, "said again"); + // Attached: it waits for input, however long. + pane.msg_said_ms -= 60_000; + p.update(.tick); + try std.testing.expect(pane.msg_len > 0); + // Detached with nobody attached: past its linger it is gone, and the + // line above it with it. + p.unattended = true; + // A read served meanwhile changes nothing; the session's loop expires. + _ = p.serveFs(.{ .tag = 1, .op = .read, .node = @intFromEnum(ctlfs.TopFile.index), .size = 64 }); + try std.testing.expect(pane.msg_len > 0); + expireUnattended(p); + try std.testing.expectEqual(@as(u16, 0), pane.msg_len); + try std.testing.expectEqual(@as(u8, 0), pane.msg_older_len); + // A fresh one stays until its linger is up. + p.setMessage(p.active, "fresh"); + p.update(.tick); + try std.testing.expect(pane.msg_len > 0); +} + test "a message falls in, lingers past the input that dismisses it, and dissolves" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); @@ -733,6 +817,11 @@ test "a builtin announces itself on the message row, and Verbose silences it" { // for its announcement, which is correct and not what this is testing. try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap")); try std.testing.expectEqualStrings("Wrap", pane.msg[0..pane.msg_len]); + // A value it does not take is said, in place of the announcement. + const wrapped = p.settings.wrap; + try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap maybe")); + try std.testing.expectEqualStrings("Wrap: takes on or off", pane.msg[0..pane.msg_len]); + try std.testing.expectEqual(wrapped, p.settings.wrap); // The notice list is what the paint pass reads, so the announcement has to // reach it, not just the buffer. diff --git a/src/Output.zig b/src/Output.zig index 6e1765a7..201b5491 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -749,7 +749,7 @@ pub fn resetBody(p: *Pardes, pane: *Pane) void { pane.body.select = false; pane.sel = @splat(.{}); pane.body.sticky_col = -1; - pane.body.append_at = null; + pane.body.restore_cursor = false; pane.body.normal.clear(); pane.look_at = null; if (p.look_hover_wait) |wait| if (wait.serial == pane.serial) { diff --git a/src/Pipe.zig b/src/Pipe.zig index 1ad7b826..48e98b7b 100644 --- a/src/Pipe.zig +++ b/src/Pipe.zig @@ -35,6 +35,7 @@ pub fn pipeMarker(how: modal.Normal.PipeBehavior) []const u8 { .ignore => config.pipe_marker_to, .insert => config.pipe_marker_insert, .append => config.pipe_marker_append, + .keep => config.pipe_marker_keep, }; } @@ -54,10 +55,10 @@ pub const PendingPipe = struct { cwd: []u8, inputs: []selection_pipe.Input, ranges: [panes.Text.max_selections]modal.Selection, - primary: u8, + primary: u16, explicit: bool, how: modal.Normal.PipeBehavior, - nranges: u8, + nranges: u16, pub fn deinit(wait: *PendingPipe, gpa: std.mem.Allocator) void { gpa.free(wait.command); @@ -117,7 +118,13 @@ pub fn submitPipe(p: *Pardes, id: usize) void { p.gpa.free(inputs); return; } - const command_copy = p.gpa.dupe(u8, command) catch { + // `$` wants a verdict per selection where the runner's is all or + // nothing: the command runs in a subshell whose exit status becomes the + // output, so the runner always succeeds and the answer says which kept + const command_copy = (if (how == .keep) + std.fmt.allocPrint(p.gpa, "(\n{s}\n) >/dev/null; echo $?", .{command}) + else + p.gpa.dupe(u8, command)) catch { for (inputs) |input| p.gpa.free(@constCast(input.bytes)); p.gpa.free(inputs); return; @@ -202,7 +209,7 @@ fn pipeCut(wait: *const PendingPipe, content: []const u8, i: usize) ?struct { lo const hi = @max(range.anchor, range.head); if (hi > content.len) return null; return switch (wait.how) { - .replace, .ignore => .{ .lo = lo, .hi = hi }, + .replace, .ignore, .keep => .{ .lo = lo, .hi = hi }, .insert => .{ .lo = lo, .hi = lo }, .append => .{ .lo = hi, .hi = hi }, }; @@ -260,6 +267,21 @@ pub fn pipeResponse( if (changed) return p.reportError(wait.pane, "pipe", error.FileChangedWhileFiltering); if (wait.how == .ignore) return; + if (wait.how == .keep) { + // helix shell_keep_pipe: the primary stays if kept, else the last + // kept range takes over; keeping none changes nothing + var kept: [panes.Text.max_selections]modal.Selection = undefined; + var m: usize = 0; + var pri: ?usize = null; + for (0..wait.nranges) |i| { + if (!std.mem.eql(u8, std.mem.trim(u8, outputs[i], " \n"), "0")) continue; + if (i == wait.primary) pri = m; + kept[m] = wait.ranges[i]; + m += 1; + } + if (m == 0) return; + return t.setRanges(content, kept[0..m], &.{}, pri orelse m - 1, wait.explicit); + } const n = wait.nranges; var total_output: usize = 0; @@ -366,6 +388,43 @@ test "| in a tag pipes the tag's own text, and undo there brings it back" { try std.testing.expect(std.mem.startsWith(u8, pardes.tagline.curTail(pane), " Save ")); } +test "$ keeps the selections the command exits 0 on, and a real shell gives that verdict" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const pane = try p.setTestFile("aa bb cc\n"); + const ranges = [_]modal.Selection{ + .{ .anchor = 0, .head = 2 }, + .{ .anchor = 3, .head = 5 }, + .{ .anchor = 6, .head = 8 }, + }; + pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 1, true); + p.update(.{ .key = .{ .cp = '$' } }); + try std.testing.expectEqualStrings(config.pipe_marker_keep, pane.promptText().?); + p.update(.{ .key = .{ .cp = 'g', .text = "grep -v b" } }); + p.update(.{ .key = .{ .cp = Key.enter } }); + const id = nextPipeEffect(p) orelse return error.MissingPipeEffect; + const request = p.pipe.pipeRequest(id) orelse return error.MissingPipeRequest; + try std.testing.expectEqual(@as(usize, 3), request.inputs.len); + // the wrapper turns each run's status into its output, as the runner sees it + const job = try selection_pipe.Job.copy(gpa, request); + defer job.deinit(gpa); + var response = selection_pipe.runJob(gpa, std.testing.io, job); + defer response.deinit(gpa); + try std.testing.expect(response.success); + try std.testing.expectEqualStrings("0\n", response.outputs[0]); + try std.testing.expectEqualStrings("1\n", response.outputs[1]); + p.update(.{ .pipe_resp = .{ .id = id, .success = true, .outputs = response.outputs } }); + try std.testing.expectEqualStrings("aa bb cc\n", pane.file.?.content); + var out: [panes.Text.max_selections]modal.Selection = undefined; + const got = pane.body.ranges(pane.file.?.content, 0, &out); + try std.testing.expectEqual(@as(usize, 2), got.n); + try std.testing.expectEqual(@as(usize, 6), out[1].anchor); + // the primary was dropped: the last kept range takes over + try std.testing.expectEqual(@as(usize, 1), got.pri); +} + test "selection pipe replaces all ranges atomically and undo restores them" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .tty_only = true }); @@ -394,7 +453,7 @@ test "selection pipe replaces all ranges atomically and undo restores them" { p.update(.{ .pipe_resp = .{ .id = id, .success = true, .outputs = outputs } }); try std.testing.expectEqualSlices(u8, "AA bb \n", pane.file.?.content); try std.testing.expectEqual(@as(usize, 1), pane.file.?.history.undo_len); - try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 1), pane.body.nsel); try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expectEqual(@as(i32, 6), pane.body.sels[0].col); try std.testing.expect(!denied.has_induced_failure); diff --git a/src/Registers.zig b/src/Registers.zig new file mode 100644 index 00000000..d2a42434 --- /dev/null +++ b/src/Registers.zig @@ -0,0 +1,106 @@ +//! helix's registers: named stores of text, each holding one VALUE PER RANGE +//! of the selection that wrote it. `"` is the default, `+` the one the +//! system clipboard is copied from, `/` the last s/S pattern, `@` the last +//! macro; any other character names a register of its own. The registers +//! computed from the editor rather than stored (`_`, `#`, `.`, `%`) and the +//! clipboard's paste side are read in edit.zig, which has the pane at hand. +const std = @import("std"); + +const Registers = @This(); + +list: std.ArrayList(Reg) = .empty, + +pub const Reg = struct { + name: u21, + /// gpa-owned, one per range, in document order + values: [][]u8, + /// the values joined by newlines, for the readers that take one text + /// (the acme chords, a paste into a terminal, the clipboard); gpa-owned, + /// made on first read + joined: ?[]u8 = null, +}; + +pub const default = '"'; + +pub fn deinit(r: *Registers, gpa: std.mem.Allocator) void { + for (r.list.items) |*reg| freeReg(gpa, reg); + r.list.deinit(gpa); + r.* = .{}; +} + +fn freeReg(gpa: std.mem.Allocator, reg: *Reg) void { + for (reg.values) |v| gpa.free(v); + gpa.free(reg.values); + if (reg.joined) |j| gpa.free(j); +} + +pub fn get(r: *Registers, name: u21) ?*Reg { + for (r.list.items) |*reg| if (reg.name == name) return reg; + return null; +} + +/// Value `i` of register `name`: a register holding fewer values than there +/// are ranges repeats its last (helix paste_impl). +pub fn value(r: *Registers, name: u21, i: usize) ?[]const u8 { + const reg = r.get(name) orelse return null; + if (reg.values.len == 0) return null; + return reg.values[@min(i, reg.values.len - 1)]; +} + +/// Store value `i` of `n`. `fresh` starts the register over with `n` empty +/// values; the replay across ranges passes it on its first pass only, so +/// every range fills its own slot. +pub fn put(r: *Registers, gpa: std.mem.Allocator, name: u21, bytes: []const u8, i: usize, n: usize, fresh: bool) void { + const copy = gpa.dupe(u8, bytes) catch return; + const reg = r.get(name) orelse blk: { + r.list.append(gpa, .{ .name = name, .values = &.{} }) catch { + gpa.free(copy); + return; + }; + break :blk &r.list.items[r.list.items.len - 1]; + }; + if (fresh or reg.values.len != n) { + const values = gpa.alloc([]u8, n) catch { + gpa.free(copy); + return; + }; + for (values) |*v| v.* = &.{}; + freeReg(gpa, reg); + reg.* = .{ .name = name, .values = values }; + } + gpa.free(reg.values[i]); + reg.values[i] = copy; + if (reg.joined) |j| gpa.free(j); + reg.joined = null; +} + +/// The whole register as one text: its values joined by newlines, no +/// newline added after a value that already ends in one. +pub fn text(r: *Registers, gpa: std.mem.Allocator, name: u21) ?[]const u8 { + const reg = r.get(name) orelse return null; + if (reg.joined) |j| return j; + var out: std.ArrayList(u8) = .empty; + for (reg.values, 0..) |v, i| { + if (i > 0 and !std.mem.endsWith(u8, reg.values[i - 1], "\n")) out.append(gpa, '\n') catch return null; + out.appendSlice(gpa, v) catch return null; + } + reg.joined = out.toOwnedSlice(gpa) catch return null; + return reg.joined; +} + +test "a register holds a value per range and joins them for one-text readers" { + const gpa = std.testing.allocator; + var r: Registers = .{}; + defer r.deinit(gpa); + try std.testing.expect(r.value(default, 0) == null); + // a replay fills the last range first + r.put(gpa, default, "two", 1, 2, true); + r.put(gpa, default, "one\n", 0, 2, false); + try std.testing.expectEqualStrings("one\n", r.value(default, 0).?); + try std.testing.expectEqualStrings("two", r.value(default, 1).?); + try std.testing.expectEqualStrings("two", r.value(default, 5).?); + try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?); + r.put(gpa, 'a', "x", 0, 1, true); + try std.testing.expectEqualStrings("x", r.text(gpa, 'a').?); + try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?); +} diff --git a/src/Terminal.zig b/src/Terminal.zig index 62a77ca8..91c23305 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -718,7 +718,7 @@ pub fn feedOutput(p: *Pardes, pane: *Pane, bytes: []const u8) void { if (comptime !enabled) return; if (pane.terminal == null) return; const has_positions = pane.ovl != null or pane.body.cur_pinned or pane.body.vsel.active or - pane.body.msel.active or pane.body.nsel > 0 or pane.body.append_at != null or pane.look_at != null or + pane.body.msel.active or pane.body.nsel > 0 or pane.body.last_edit != null or pane.look_at != null or pane.body.ed_undo_len > 0 or pane.body.ed_redo_len > 0 or pointerRowCount(pane) > 0; // Sticky bottom, as every other terminal does it: output drags the // viewport down only when the viewport was already on the last row, so @@ -844,7 +844,7 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void { extra[nextra + 1] = &selection.arow; nextra += 2; } - if (pane.body.append_at) |*at| { + if (pane.body.last_edit) |*at| { extra[nextra] = &at.row; nextra += 1; } @@ -933,6 +933,13 @@ test "raw terminal output needs no position tracking allocations" { try std.testing.expectEqual(pins, pane.terminal.?.vt.screens.active.pages.countTrackedPins()); } +/// A full-screen program has the emulator on its alternate screen. +pub fn onAlternateScreen(pane: *const Pane) bool { + if (comptime !enabled) return false; + const state = pane.terminal orelse return false; + return state.vt.screens.active_key == .alternate; +} + pub fn promptInputReady(pane: *const Pane) bool { if (comptime !enabled) return false; const state = pane.terminal orelse return false; @@ -1118,7 +1125,7 @@ pub fn enterTty(p: *Pardes, id: usize) void { // keeps its modal endpoint for the next return to editor mode. pane.body.cur_pinned = selected; pane.body.select = false; - pane.body.append_at = null; + pane.body.restore_cursor = false; pane.body.sticky_col = -1; pane.body.normal.clear(); } @@ -1380,7 +1387,7 @@ pub fn editText(p: *Pardes, pane: *Pane, lo: i32, hi: i32, col: i32) ?EditText { /// Consume a rewritten overlay, freeing the terminal edit text it replaces. pub fn setEditText(p: *Pardes, pane: *Pane, new: []u8) void { - const overlay = if (pane.ovl) |*value| value else return p.gpa.free(new); + const overlay = if (pane.ovl) |*value| value else return pardes.edit.retireEditText(p, new); for (0..pane.sel.len) |button| { pane.clearPointerSelection(button); pane.sel[button].state = .none; diff --git a/src/Text.zig b/src/Text.zig index f6780935..ab23bf8e 100644 --- a/src/Text.zig +++ b/src/Text.zig @@ -11,6 +11,7 @@ const std = @import("std"); const panes = @import("panes.zig"); const modal = @import("modal.zig"); const config = @import("config.zig"); +const memory = @import("memory.zig"); const Pane = panes.Pane; const File = panes.File; const Terminal = panes.Terminal; @@ -36,7 +37,7 @@ pub const CharSel = struct { explicit: bool = false, }; -pub const max_selections = 64; +pub const max_selections = memory.limits.selections; pub const SelRange = struct { row: i32, @@ -59,13 +60,26 @@ cur_row: i32 = 0, cur_col: i32 = 0, msel: LineSel = .{}, vsel: CharSel = .{}, -sels: [max_selections - 1]SelRange = undefined, -nsel: u8 = 0, +/// The ranges beside the primary, `nsel` of them: room for max_selections - 1 +/// made when a second range appears and given back when the selection is one +/// range again, so a single cursor holds nothing here. +sels: []SelRange = &.{}, +nsel: u16 = 0, +/// What a column's or the workspace's tag allocates its ranges from; a text +/// with a pane uses the pane's. +gpa: ?std.mem.Allocator = null, select: bool = false, /// sticky goal column for j/k runs (helix old_visual_position): any /// non-vertical range write resets it to -1. sticky_col: i32 = -1, -append_at: ?struct { row: i32, col: i32 } = null, +/// The insert session began with `a`: on Esc every range gives back the +/// one character it was stretched by (helix restore_cursor). +restore_cursor: bool = false, +/// Where the last edit ended, a deletion's start or an insertion's end, in +/// surface rows: for `g.` (helix last_edit_pos). +last_edit: ?struct { row: i32, col: i32 } = null, +/// insert `Ctrl-r` was pressed: the next key names the register to insert +reg_wait: bool = false, normal: modal.Normal.State = .{}, /// last f/F/t/T motion, for Alt-. repeat find_op: u8 = 0, @@ -93,10 +107,39 @@ pub fn pane(t: *Text) ?*Pane { /// Free what a text owns: its characters and the buffers its undo keeps. pub fn deinit(t: *Text, gpa: std.mem.Allocator) void { + if (t.sels.len > 0) gpa.free(t.sels); if (t.own) |own| gpa.free(own); for (t.ed_undo[0..t.ed_undo_len]) |item| if (item.ovl) |overlay| gpa.free(overlay.text); for (t.ed_redo[0..t.ed_redo_len]) |item| if (item.ovl) |overlay| gpa.free(overlay.text); - t.* = .{ .what = t.what }; + t.* = .{ .what = t.what, .gpa = t.gpa }; +} + +fn allocator(t: *Text) ?std.mem.Allocator { + return if (t.pane()) |w| w.gpa else t.gpa; +} + +/// Room for the ranges beside the primary; false when there is none to be had. +pub fn reserveSels(t: *Text) bool { + if (t.sels.len > 0) return true; + const gpa = t.allocator() orelse return false; + t.sels = gpa.alloc(SelRange, max_selections - 1) catch return false; + return true; +} + +/// The ranges beside the primary as they were (a refused edit's undoing). +pub fn restoreSels(t: *Text, kept: []const SelRange) void { + if (kept.len == 0) return t.dropSels(); + if (!t.reserveSels()) return; + @memcpy(t.sels[0..kept.len], kept); + t.nsel = @intCast(kept.len); +} + +/// Back to one range: the room for the others goes. +pub fn dropSels(t: *Text) void { + t.nsel = 0; + if (t.sels.len == 0) return; + if (t.allocator()) |gpa| gpa.free(t.sels); + t.sels = &.{}; } /// Bring the cursor into view (acme's textshow). Only a body scrolls to it; @@ -178,7 +221,7 @@ pub fn setRange(t: *Text, text: []const u8, row0: i32, r0: modal.Selection, expl .explicit = explicit or t.select, }; t.msel.active = false; - t.nsel = 0; // writing ONE range means the selection IS that range + t.dropSels(); // writing ONE range means the selection IS that range t.cur_pinned = true; t.sticky_col = -1; t.normal.clear(); @@ -255,6 +298,7 @@ pub fn setRanges(t: *Text, text: []const u8, in: []const modal.Selection, sticky } n = k + 1; setRange(t, text, 0, r[pri], explicit); + if (n > 1 and !t.reserveSels()) return; // no room: the primary alone t.sticky_col = st[pri]; var w: usize = 0; for (r[0..n], 0..) |x, idx| { @@ -324,6 +368,15 @@ pub fn multiSelAction(t: *Text, text: []const u8, kind: modal.Normal.Multi, cnt: } return setRanges(t, text, out[0..m], &.{}, pri, expl); } + if (kind == .ensure_forward) { + // helix ensure_selections_forward: every range points forward + for (rs[0..n]) |*r| { + const from = @min(r.anchor, r.head); + r.head = @max(r.anchor, r.head); + r.anchor = from; + } + return setRanges(t, text, rs[0..n], &.{}, got.pri, expl); + } if (kind == .split_newline) { // helix selection::split_on_newline — one range per line the // selection covers, the newlines themselves left out diff --git a/src/body_layer.zig b/src/body_layer.zig index e9407719..a0195fb8 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -408,9 +408,32 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane } } + // `gw`'s labels, two letters over the start of each word + if (pane.jump.items.len > 0) labels: { + const flat = edit.flatSurface(p, &pane.body) catch break :labels; + const letters = config.jump_label_alphabet; + const style: CellStyle = .{ .fg = .{ .rgb = th.sel_fg }, .bg = .{ .rgb = th.sel_bg }, .bold = true }; + for (pane.jump.items, 0..) |word, i| { + if (pane.jump_first) |first| if (i / letters.len != first) continue; + var at = @min(word.anchor, word.head); + for ([_]u8{ letters[i / letters.len], letters[i % letters.len] }) |letter| { + const pos = modal.positionAt(flat, at); + const row: i32 = @intCast(pos.row); + const wp = pane.wrapRow(row, @intCast(pos.col)); + const cx = if (pane.file != null) + @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, row, wp.at, @intCast(pos.col)) + else + panes.File.lineDisplayOffset(modal.lineSlice(flat, pos.row), @intCast(@max(0, wp.at)), pos.col); + if (wp.row >= 0 and wp.row < body_h and cx >= 0 and cx < tw) + s.set(tx + @as(u16, @intCast(cx)), body_y + @as(u16, @intCast(wp.row)), &.{letter}, style); + at = modal.nextGrapheme(flat, at); + } + } + } + // cursor: tracks the shell cursor until pinned by a click or a key // (the tag's or a prompt's cursor wins while that is being typed into) - if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side)) { + if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice)) { if (pane.body.mode != .tty) { const cur = panes.Terminal.gridCursor(pane); const goff = panes.Terminal.gridOffset(pane); diff --git a/src/builtins.zig b/src/builtins.zig index 1864086f..23d853c8 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -44,6 +44,10 @@ pub const Ctx = struct { id: usize, txt: []const u8, arg: ?[]const u8, + /// Made by a gesture on the pane's body -- a middle click, the execute + /// key -- as acme's execute() asks of its Text (exec.c:122,169): an + /// Exec from a body may be a bound REPL's (exec.zig routeToRepl). + from_body: bool = false, }; fn isEnabled(comptime T: type) bool { @@ -145,6 +149,16 @@ pub const registry = struct { return true; } + /// Whether the word takes its argument joined by a `+` as one word a + /// tag can hold and a click take whole (`Tty+fish`). Only a builtin + /// that says `pub const plus_arg = true;`: others have arguments a `+` + /// may be in (`Dump+2024.zon` is a name, not Dump's argument). + pub fn plusArg(b: Builtin()) bool { + inline for (manualBuiltinList(), 0..) |T, i| + if (@intFromEnum(b) == i) return @hasDecl(T, "plus_arg") and T.plus_arg; + return false; + } + /// Whether the word means nothing without its argument (`Mount`, /// `Msg`, a setting's value), or would ask for it at a prompt (`Find`): /// a ctl refuses it bare before any line of the write runs. A builtin @@ -226,16 +240,57 @@ test "Exit asks once about unsaved text, and quits when asked again" { try std.testing.expect(p.quit); } +/// A dump file that exists, for a Restore to get past reading it. +fn testDump(buf: []u8) ![]const u8 { + var tmp = std.testing.tmpDir(.{}); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = ".{}" }); + var dir: [4096]u8 = undefined; + const at = dir[0..try tmp.dir.realPath(std.testing.io, &dir)]; + return std.fmt.bufPrint(buf, "Restore {s}/d.zon", .{at}); +} + test "Restore asks about unsaved text as Exit does, and restores when asked again" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const pane = try p.setTestFile("saved\n"); pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified - try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon")); + var line_buf: [4200]u8 = undefined; + const line = try testDump(&line_buf); + try std.testing.expect(p.executeBuiltinLine(p.active, line)); try std.testing.expect(p.restore_req == null); try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Restore again to discard)")); - try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /tmp/some.dump.zon")); - try std.testing.expectEqualStrings("/tmp/some.dump.zon", p.restore_req.?); + try std.testing.expect(p.executeBuiltinLine(p.active, line)); + try std.testing.expectEqualStrings(line["Restore ".len..], p.restore_req.?); +} + +test "a Restore of a dump that cannot be read says so, before the unsaved-text warning" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = try p.setTestFile("saved\n"); + pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified + p.fs.no_prompt = true; + defer p.fs.no_prompt = false; + try std.testing.expect(p.executeBuiltinLine(p.active, "Restore /nope/missing.zon")); + try std.testing.expect(p.restore_req == null); + try std.testing.expectEqualStrings("Restore: /nope/missing.zon: no such file", p.fs.failure[0..p.fs.failure_len]); + // No warning spent: the Exit after it still asks. + try std.testing.expect(pane.discard_warned == null); +} + +test "a Restore's warning is not an Exit's: each word is warned on its own" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = try p.setTestFile("saved\n"); + pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified + var line_buf: [4200]u8 = undefined; + try std.testing.expect(p.executeBuiltinLine(p.active, try testDump(&line_buf))); + try std.testing.expect(p.restore_req == null); + // The Restore warned; an Exit after it has not been, and asks. + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(!p.quit); + try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Exit again to discard)")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Exit")); + try std.testing.expect(p.quit); } test "Kill stops the commands pardes typed, bare all and by name those that match, and never quits" { @@ -264,8 +319,9 @@ test "Kill stops the commands pardes typed, bare all and by name those that matc } }; p.host = .{ .vtable = &killed.vtable }; - // Nothing running: nothing to stop, and never the shell. + // Nothing running: nothing to stop, and never the shell; said. try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); + try std.testing.expectEqualStrings("Kill: nothing running", p.panes[term].?.msg[0..p.panes[term].?.msg_len]); _ = exec.execute(p, term, "make -j8"); while (p.nextEffect()) |_| {} // Typed but not started yet (no C mark): not running. @@ -346,7 +402,7 @@ pub const Exec = struct { pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were - _ = exec.execute(c.p, c.id, c.arg orelse return); + _ = exec.executeFrom(c.p, c.id, c.arg orelse return, c.from_body); } }; @@ -356,21 +412,22 @@ pub const Exec = struct { /// what would throw unsaved text away: each pane holding some nobody has /// been told about says so, `<name>: Modified (Exit again to discard)`, /// naming the word asking, since repeating it throws the text away, and is -/// then clean for the -/// asking, as winclean clears `dirty`; so the same word again, with -/// nothing edited since, goes ahead. A scratch of under 100 bytes is no -/// loss worth the question, as winclean does not ask for one. -fn warnModified(c: Ctx, asking: []const u8) bool { +/// then clean for the asking, as winclean clears `dirty`; so the same word +/// again, with nothing edited since, goes ahead. Only the same word: a +/// Restore a script was refused does not spend the warning an Exit owes +/// the person at the keyboard. A scratch of under 100 bytes is no loss +/// worth the question, as winclean does not ask for one. +fn warnModified(c: Ctx, asking: Pane.Discarding) bool { var modified = false; for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; const f = pane.file orelse continue; if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue; if (f.output != null and f.content.len < 100) continue; - if (pane.discard_warned == f.revision) continue; - pane.discard_warned = f.revision; + if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == asking) continue; + pane.discard_warned = .{ .revision = f.revision, .by = asking }; var buf: [limits.host_path_cap + 48]u8 = undefined; - c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ f.path, asking }) catch f.path); + c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ f.path, @tagName(asking) }) catch f.path); modified = true; } return modified; @@ -381,7 +438,7 @@ fn warnModified(c: Ctx, asking: []const u8) bool { pub const Exit = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { - if (warnModified(c, "Exit")) return; + if (warnModified(c, .Exit)) return; c.p.quit = true; c.p.emit(.quit); } @@ -403,6 +460,21 @@ pub const Kill = struct { var jobless = false; for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; + // A command pane's child is the command: the whole of its + // process group goes, as acme's Kill ends the process. + if (pane.command) |line| if (!pane.command_done) { + const said = std.mem.trim(u8, line, " \t"); + const first = said[0 .. std.mem.indexOfAny(u8, said, " \t") orelse said.len]; + if (names.len > 0) { + var words = std.mem.tokenizeAny(u8, names, " \t"); + while (words.next()) |w| { + if (std.mem.eql(u8, w, first)) break; + } else continue; + } + c.p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .serial = pane.serial, .sig = .term } }); + stopped = true; + continue; + }; if (!panes.Terminal.commandRunning(pane)) continue; const sent = pane.sent_command.?; if (names.len > 0) { @@ -417,7 +489,10 @@ pub const Kill = struct { } if (stopped) return; if (jobless) return c.p.reportFailure(c.id, "Kill: no job to signal (its shell runs it without job control)"); - if (names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand); + if (names.len > 0) + c.p.reportFailure(c.id, "Kill: no running command has that first word") + else // said, but no failure: a script's Kill has nothing left to do + c.p.setMessage(c.id, "Kill: nothing running"); } }; @@ -432,11 +507,27 @@ pub const Restore = struct { pub const scope: Scope = .session; pub const takes_arg = true; pub fn run(c: Ctx) void { - const path = c.arg orelse (c.p.last_dump orelse return); - if (path.len > c.p.restore_buf.len) return; + const path = c.arg orelse (c.p.last_dump orelse return c.p.reportFailure(c.id, "Restore: no dump yet; name one")); + if (path.len > c.p.restore_buf.len) return c.p.reportFailure(c.id, "Restore: that name is too long"); + // A dump that cannot be read is said now, and fails a ctl write: the + // host reads it only after this step, where nobody hears. First, + // so a Restore that could never happen spends no warning. + if (comptime pardes.hosted) { + const bytes = @import("fs.zig").readRestore(c.p.gpa, path, c.p.settings.dump_dir.get()) catch |err| { + var buf: [limits.host_path_cap + 64]u8 = undefined; + const why = switch (err) { + error.FileNotFound => "no such file", + error.PermissionDenied => "permission denied", + error.IsDirectory => "a directory, not a dump", + else => @errorName(err), + }; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: {s}", .{ path, why }) catch "Restore: cannot read the dump"); + }; + c.p.gpa.free(bytes); + } // acme's Load adds a dump's windows to the ones there; a Restore // replaces them all, so it asks what Exit asks first. - if (warnModified(c, "Restore")) return; + if (warnModified(c, .Restore)) return; @memcpy(c.p.restore_buf[0..path.len], path); c.p.restore_req = c.p.restore_buf[0..path.len]; } @@ -703,7 +794,8 @@ pub const Newcol = struct { }; /// Close the pane. `Del k` gives its rows to the open pane above it and -/// `Del j` to the one below, each falling back to the other side. +/// `Del j` to the one below, each falling back to the other side. Focus on +/// it goes with its rows. pub const Del = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { @@ -716,7 +808,7 @@ pub const Del = struct { else if (std.mem.eql(u8, side, "j")) below orelse above else if (side.len > 0) - return c.p.reportError(c.id, "Del k|j", error.InvalidArgument) + return c.p.reportFailure(c.id, "Del: takes k, j or nothing") else null; // Someone at the keyboard with a real choice to make is asked, on the @@ -734,8 +826,6 @@ pub const Del = struct { return; } c.p.removePane(c.id, to) catch |err| return c.p.reportError(c.id, "close", err); - // Del k|j is a direction: focus lands where the rows went. - if (to) |gainer| c.p.active = gainer; } }; @@ -787,9 +877,24 @@ pub const DelBelow = struct { /// A shell in the calling pane's directory, raw from the first frame. On every /// pane's tagline: the fast path from wherever you are to a prompt there. +/// A terminal below the caller in its directory: the configured shell, or +/// the one named, `Tty fish`, as a terminal's own tag word `Tty+fish` says. pub const Tty = struct { + pub const takes_arg = true; + pub const plus_arg = true; pub fn run(c: Ctx) void { - exec.spawnTty(c.p, c.id); + const arg = std.mem.trim(u8, c.arg orelse "", " \t"); + // A shell that is not there is said, where the host would quietly + // start its fallback in its place. + if (comptime pardes.hosted) if (arg.len > 0) { + var buf: [std.fs.max_path_bytes]u8 = undefined; + if (@import("host_io.zig").Shell.find(arg, &buf) == null) { + var said: [320]u8 = undefined; + return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Tty: no shell \"{s}\" (a name on the usual paths, or a path to one)", .{arg[0..@min(arg.len, 200)]}) catch "Tty: no such shell"); + } + }; + const pane = exec.spawnTty(c.p, c.id) orelse return; + if (arg.len > 0) pane.shell = c.p.gpa.dupe(u8, arg) catch null; } }; @@ -802,10 +907,72 @@ pub const Tty9p = struct { }; /// Fold the active pane's column into the one on its right, keeping its panes. +/// Binds this terminal as a language's REPL, by the language's name as the +/// syntax table has it: an exec made on the body of a file in that +/// language -- a middle click, the execute key, a single word like `ls` +/// as much as a selection -- and one made on this terminal's own body, is +/// typed into it (bracketed paste if its program asked for that, else line +/// by line, then Enter) instead of run. `Repl python` binds (again, it is +/// already); `Repl -` unbinds; `Repl` bare says what it is bound as. With +/// several bound for a language, the exec asks which. +/// Still commands, whatever is bound: a word in a tag, `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. +pub const Repl = struct { + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const pane = c.pane; + // A terminal with its emulator -- not a document pane -- and not + // a command pane whose command, the REPL it would be, is done. + if (!pane.isTerminal() or pane.terminal == null) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL"); + if (pane.command != null and pane.command_done) return c.p.reportFailure(c.id, "Repl: its command is done; a REPL is a running program"); + const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + var buf: [320]u8 = undefined; + var idbuf: [48]u8 = undefined; + if (arg.len == 0) { + if (pane.repl == null) return c.p.setMessage(c.id, "Repl: not bound"); + return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + } + if (std.mem.eql(u8, arg, "-")) { + pane.repl = null; + return c.p.setMessage(c.id, "Repl: unbound"); + } + const manifest = @import("grammar_manifest.zig"); + const grammars = manifest.all; + // Named as a code fence names it: `py`, `Python`, `sh`. + const named: ?u8 = if (manifest.byName(arg)) |i| if (grammars[i].exts.len > 0) @intCast(i) else null else null; + const lang: u8 = named orelse { + var w = std.Io.Writer.fixed(&buf); + w.print("Repl: no language \"{s}\"; - or one of:", .{@import("Messages.zig").clip(arg, 24)}) catch {}; + for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break; + return c.p.reportFailure(c.id, @import("Messages.zig").clip(w.buffered(), 256)); + }; + if (pane.repl) |r| if (r.lang == lang) + return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + // The lowest letter no other REPL of the language has: freed ones + // come back. + var letter: u8 = 'a'; + while (letter <= 'z') : (letter += 1) { + const taken = for (c.p.panes) |slot| { + const other = slot orelse continue; + if (other != pane) if (other.repl) |r| if (r.lang == lang and r.letter == letter) break true; + } else false; + if (!taken) break; + } + if (letter > 'z') return c.p.reportFailure(c.id, "Repl: every letter of the language is taken"); + pane.repl = .{ .lang = lang, .letter = letter }; + pane.repl_warned = false; + c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl"); + } +}; + /// The horizontal mirror of the vertical stacking `New` does. pub const Joincol = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { + // The keyboard's column joins the one to its right; the last has none. + const at = layout.findPane(c.p, c.p.active) orelse return; + if (at.col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right"); layout.joinCol(c.p); } }; diff --git a/src/colors.zig b/src/colors.zig index 0065d5ef..aeff7b00 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -317,6 +317,19 @@ fn invalidateThemeDependentRasters(p: *Pardes) void { }; } +/// Themes whose names share `name`'s first letter, a line of them, for a +/// Theme given a name there is none of: all of them (ThemeSel) are too many +/// for a message or a refused write. +pub fn themesNear(buf: []u8, name: []const u8) []const u8 { + var w = std.Io.Writer.fixed(buf); + for (themes) |t| { + if (name.len > 0 and std.ascii.toLower(t.name[0]) != std.ascii.toLower(name[0])) continue; + if (w.buffered().len + t.name.len + 1 > buf.len) break; + w.print("{s}{s}", .{ if (w.buffered().len > 0) " " else "", t.name }) catch break; + } + return w.buffered(); +} + pub fn setThemeIndex(p: *Pardes, index: usize) void { if (index >= themes.len or (p.custom_theme == null and p.theme_file_path.get().len == 0 and diff --git a/src/config.zig b/src/config.zig index d1e5f23d..186b29a1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -49,6 +49,7 @@ pub const leader_path = paths: { .Mode = null, .Togglettymode = null, .Collapse = null, + .Repl = null, .Kill = null, .Exit = null, .ClipYank = "y", @@ -249,6 +250,7 @@ pub const pane_builtins_str = "Tty Collapse Del"; pub const pdf_pane_builtins_str = "Tty PdfSections PdfTint Collapse Del"; pub const file_pane_builtins_str = "Save Tty Collapse Del"; pub const terminal_pane_builtins_str = "Tty Save Mode Filter Collapse Del"; +pub const command_pane_builtins_str = "Kill Save Collapse Del"; // Each mode badge is one codepoint. pub const tag_normal = " "; @@ -265,10 +267,15 @@ pub const pipe_selection: []const Chord = &.{.{ .cp = '|' }}; pub const pipe_selection_to: []const Chord = &.{.{ .cp = '|', .alt = true }}; pub const insert_output: []const Chord = &.{.{ .cp = '!' }}; pub const append_output: []const Chord = &.{.{ .cp = '!', .alt = true }}; +pub const keep_pipe: []const Chord = &.{.{ .cp = '$' }}; +pub const macro_record: []const Chord = &.{.{ .cp = 'Q' }}; +pub const macro_replay: []const Chord = &.{.{ .cp = 'q' }}; +pub const repeat_insert: []const Chord = &.{.{ .cp = '.' }}; // Armed inputs are parsed from their distinct, nonempty tag markers. pub const pipe_marker_to = " |-"; pub const pipe_marker_insert = " !"; pub const pipe_marker_append = " !+"; +pub const pipe_marker_keep = " $"; pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }}; // Del from the keyboard on a pane with open panes both above and below it @@ -408,6 +415,8 @@ pub const symbol_marker = " WsSymbols /"; pub const save_marker = " Save "; pub const select_marker = " Select /"; pub const split_marker = " Split /"; +pub const keep_marker = " Keep /"; +pub const remove_marker = " Remove /"; pub const pipe_marker = " |"; pub const search_buffer = "+Search"; @@ -432,6 +441,7 @@ pub const errors_buffer = "+Errors"; // Modal bindings below are checked against Helix by hxdiff and hxparity. pub const goto_prefix = 'g'; pub const view_prefix = 'z'; +pub const view_prefix_sticky = 'Z'; pub const match_prefix = 'm'; pub const replace_prefix = 'r'; pub const next_prefix = ']'; @@ -453,7 +463,7 @@ pub const next_long_word_start: []const Chord = &.{.{ .cp = 'W' }}; pub const prev_long_word_start: []const Chord = &.{.{ .cp = 'B' }}; pub const next_long_word_end: []const Chord = &.{.{ .cp = 'E' }}; pub const line_start: []const Chord = &.{ .{ .cp = '0' }, .{ .cp = Key.home } }; -pub const line_end: []const Chord = &.{ .{ .cp = '$' }, .{ .cp = Key.end } }; +pub const line_end: []const Chord = &.{.{ .cp = Key.end }}; pub const line_first_nonws: []const Chord = &.{.{ .cp = '^' }}; // Bare G does nothing; ge reaches the last line. pub const goto_line: []const Chord = &.{.{ .cp = 'G' }}; @@ -465,6 +475,7 @@ pub const page_up: []const Chord = &.{ .{ .cp = 'b', .ctrl = true }, .{ .cp = Ke pub const view_top: []const Chord = &.{.{ .cp = 't' }}; pub const view_center: []const Chord = &.{ .{ .cp = 'z' }, .{ .cp = 'c' } }; pub const view_bottom: []const Chord = &.{.{ .cp = 'b' }}; +pub const view_middle: []const Chord = &.{.{ .cp = 'm' }}; pub const view_scroll_down: []const Chord = &.{ .{ .cp = 'j' }, .{ .cp = Key.down } }; pub const view_scroll_up: []const Chord = &.{ .{ .cp = 'k' }, .{ .cp = Key.up } }; @@ -479,6 +490,10 @@ pub const goto_column: []const Chord = &.{.{ .cp = '|' }}; pub const goto_view_top: []const Chord = &.{.{ .cp = 't' }}; pub const goto_view_center: []const Chord = &.{.{ .cp = 'c' }}; pub const goto_view_bottom: []const Chord = &.{.{ .cp = 'b' }}; +pub const goto_last_modification: []const Chord = &.{.{ .cp = '.' }}; +pub const goto_word: []const Chord = &.{.{ .cp = 'w' }}; +/// the jump labels' letters, helix's jump_label_alphabet +pub const jump_label_alphabet = "abcdefghijklmnopqrstuvwxyz"; pub const goto_definition: []const Chord = &.{.{ .cp = 'd' }}; pub const goto_declaration: []const Chord = &.{.{ .cp = 'D' }}; pub const goto_type_definition: []const Chord = &.{.{ .cp = 'y' }}; @@ -491,11 +506,19 @@ pub const match_around = 'a'; pub const surround_add = 's'; pub const surround_replace = 'r'; pub const surround_delete = 'd'; +pub const register_prefix = '"'; pub const goto_paragraph: []const Chord = &.{.{ .cp = 'p' }}; pub const add_newline: []const Chord = &.{.{ .cp = ' ' }}; pub const goto_diagnostic: []const Chord = &.{.{ .cp = 'd' }}; pub const goto_diagnostic_end: []const Chord = &.{.{ .cp = 'D' }}; +pub const goto_function: []const Chord = &.{.{ .cp = 'f' }}; +pub const goto_class: []const Chord = &.{.{ .cp = 't' }}; +pub const goto_parameter: []const Chord = &.{.{ .cp = 'a' }}; +pub const goto_comment: []const Chord = &.{.{ .cp = 'c' }}; +pub const goto_test: []const Chord = &.{.{ .cp = 'T' }}; +pub const goto_entry: []const Chord = &.{.{ .cp = 'e' }}; +pub const goto_xml_element: []const Chord = &.{.{ .cp = 'x' }}; pub const insert: []const Chord = &.{.{ .cp = 'i' }}; pub const append: []const Chord = &.{.{ .cp = 'a' }}; @@ -525,6 +548,19 @@ pub const trim_sels: []const Chord = &.{.{ .cp = '_' }}; pub const select_regex: []const Chord = &.{.{ .cp = 's' }}; pub const split_regex: []const Chord = &.{.{ .cp = 'S' }}; +pub const keep_regex: []const Chord = &.{.{ .cp = 'K' }}; +pub const remove_regex: []const Chord = &.{.{ .cp = 'K', .alt = true }}; +pub const ensure_forward: []const Chord = &.{.{ .cp = ':', .alt = true }}; +// helix's tree-sitter selection; Alt-n stays the new terminal, so the next +// sibling is Alt-right alone +pub const expand_node: []const Chord = &.{ .{ .cp = 'o', .alt = true }, .{ .cp = Key.up, .alt = true } }; +pub const shrink_node: []const Chord = &.{ .{ .cp = 'i', .alt = true }, .{ .cp = Key.down, .alt = true } }; +pub const prev_sibling: []const Chord = &.{ .{ .cp = 'p', .alt = true }, .{ .cp = Key.left, .alt = true } }; +pub const next_sibling: []const Chord = &.{.{ .cp = Key.right, .alt = true }}; +pub const all_siblings: []const Chord = &.{.{ .cp = 'a', .alt = true }}; +pub const all_children: []const Chord = &.{.{ .cp = 'I', .alt = true }}; +pub const parent_node_end: []const Chord = &.{.{ .cp = 'e', .alt = true }}; +pub const parent_node_start: []const Chord = &.{.{ .cp = 'b', .alt = true }}; pub const delete: []const Chord = &.{.{ .cp = 'd' }}; pub const delete_noyank: []const Chord = &.{.{ .cp = 'd', .alt = true }}; @@ -537,13 +573,19 @@ pub const switch_case: []const Chord = &.{.{ .cp = '~' }}; pub const to_lowercase: []const Chord = &.{.{ .cp = '`' }}; pub const to_uppercase: []const Chord = &.{.{ .cp = '`', .alt = true }}; pub const join_lines: []const Chord = &.{.{ .cp = 'J' }}; +pub const join_select_space: []const Chord = &.{.{ .cp = 'J', .alt = true }}; +pub const rotate_contents_fwd: []const Chord = &.{.{ .cp = ')', .alt = true }}; +pub const rotate_contents_back: []const Chord = &.{.{ .cp = '(', .alt = true }}; +pub const align_selections: []const Chord = &.{.{ .cp = '&' }}; pub const indent: []const Chord = &.{.{ .cp = '>' }}; pub const unindent: []const Chord = &.{.{ .cp = '<' }}; pub const format: []const Chord = &.{.{ .cp = '=' }}; pub const increment: []const Chord = &.{.{ .cp = 'a', .ctrl = true }}; pub const decrement: []const Chord = &.{.{ .cp = 'x', .ctrl = true }}; -pub const undo: []const Chord = &.{.{ .cp = 'u' }}; -pub const redo: []const Chord = &.{.{ .cp = 'U' }}; +// pardes's history is linear, so helix's earlier/later (Alt-u, Alt-U) are +// undo and redo themselves +pub const undo: []const Chord = &.{ .{ .cp = 'u' }, .{ .cp = 'u', .alt = true } }; +pub const redo: []const Chord = &.{ .{ .cp = 'U' }, .{ .cp = 'U', .alt = true } }; pub const comment_toggle: []const Chord = &.{.{ .cp = 'c', .ctrl = true }}; // Body-normal Esc runs Last; other modes cancel input or leave insert mode. @@ -552,6 +594,8 @@ pub const escape: []const Chord = &.{.{ .cp = Key.escape }}; pub const insert_backspace_alias: []const Chord = &.{.{ .cp = 'h', .ctrl = true }}; pub const insert_enter_alias: []const Chord = &.{.{ .cp = 'j', .ctrl = true }}; pub const insert_delete_alias: []const Chord = &.{.{ .cp = 'd', .ctrl = true }}; +pub const insert_register: []const Chord = &.{.{ .cp = 'r', .ctrl = true }}; +pub const insert_checkpoint: []const Chord = &.{.{ .cp = 's', .ctrl = true }}; pub const delete_word_backward: []const Chord = &.{ .{ .cp = 'w', .ctrl = true }, .{ .cp = Key.backspace, .alt = true } }; pub const delete_word_forward: []const Chord = &.{ .{ .cp = 'd', .alt = true }, .{ .cp = Key.delete, .alt = true } }; pub const kill_to_line_start: []const Chord = &.{.{ .cp = 'u', .ctrl = true }}; diff --git a/src/detached/client.zig b/src/detached/client.zig index 9fa85fb4..8c9a2560 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -843,6 +843,7 @@ test "detached Restore keeps attached frontends and follows queued frames with a const fd = h.session.clients[c.slot].fd; try testing.expectError(error.BadDumpMagic, h.session.restore( ".{ .magic = \"not-a-pardes-dump\", .theme = \"dark\", .screen = .{ .cols = 60, .rows = 16 } }", + "test", )); try testing.expectEqual(before, h.session.core); try testing.expectEqual(fd, h.session.clients[c.slot].fd); @@ -853,7 +854,7 @@ test "detached Restore keeps attached frontends and follows queued frames with a defer testing.allocator.free(saved); while (h.core.nextEffect()) |_| {} _ = try h.core.setTestFile("changed after dump\n"); - try h.session.restore(saved); + try h.session.restore(saved, "test"); h.core = h.session.core; try testing.expect(h.core != before); try testing.expectEqual(fd, h.session.clients[c.slot].fd); diff --git a/src/detached/server.zig b/src/detached/server.zig index 9e18e125..5374bf73 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -70,8 +70,18 @@ const Pty = struct { pid: posix.pid_t = 0, kill_at: i64 = 0, out: std.ArrayListUnmanaged(u8) = .empty, + /// A command pane's child, watched to its exit (host_io.watchExit); + /// the pty stays open, unpolled after its end of file, until both its + /// end of file and its exit. + cmd: host_io.CommandWatch = .{}, }; +/// A watched child exited: wake the loop, which takes it (`takeExits`). +fn wakeForExit(ctx: ?*anyopaque) void { + const box: *Mailbox = @ptrCast(@alignCast(ctx.?)); + box.signal(); +} + const RetiredShell = struct { pid: posix.pid_t = 0, kill_at: i64 = 0 }; const Completion = union(enum) { @@ -190,6 +200,7 @@ pub const Session = struct { } s.mailbox.wake = pair; pardes.lsp.setStatusSink(s, lspStatus); + host_io.setExitWake(&s.mailbox, wakeForExit); } fn cancelWorkers(s: *Session) void { @@ -235,8 +246,8 @@ pub const Session = struct { return batch.len != 0 or batch.status != null; } - pub fn restore(s: *Session, bytes: []const u8) !void { - const replacement = try dump.restore(s.core, bytes); + pub fn restore(s: *Session, bytes: []const u8, from: []const u8) !void { + const replacement = try dump.restore(s.core, bytes, from); s.cancelWorkers(); for (0..s.ptys.len) |pane| s.closePty(@intCast(pane)); s.harvest(); @@ -257,6 +268,7 @@ pub const Session = struct { pub fn deinit(s: *Session) void { if (s.mailbox.wake[0] >= 0) pardes.lsp.setStatusSink(null, null); + if (s.mailbox.wake[0] >= 0) host_io.setExitWake(null, null); s.cancelWorkers(); for (s.mailbox.wake) |fd| if (fd >= 0) { _ = libc.close(fd); @@ -479,7 +491,8 @@ pub const Session = struct { s.core.screen_w, s.ninep, ) catch |err| return s.core.reportError(pane, "shell", err); - s.ptys[pane] = .{ .fd = child.file.handle, .pid = child.pid }; + const command = if (s.core.panes[pane]) |pn| pn.command != null else false; + s.ptys[pane] = .{ .fd = child.file.handle, .pid = child.pid, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; setNonblock(child.file.handle); var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); @@ -518,7 +531,8 @@ pub const Session = struct { const s = of(ctx); if (pane >= s.ptys.len) return; const pt = s.ptys[pane]; - if (pt.fd < 0) return; + // A command whose exit is recorded is reaped: its group may be another's. + if (pt.fd < 0 or pt.cmd.exited) return; host_io.signalTty(pt.pid, pt.fd, sig); } @@ -687,11 +701,33 @@ pub const Session = struct { } fn paneEof(s: *Session, pane: u8) void { + const pt = &s.ptys[pane]; + // A command's pty stays open until its child has exited: closing + // it would hang up one that runs on without it. + // Its exit, if it came first, is told now its output is in. + if (pt.cmd.watched) return host_io.commandEof(s.core, &s.ptys, pane, s, closeWatched); + // Unwatched, a command's exit is read here, as its end. + const unwatched = if (s.core.panes[pane]) |pn| pn.command != null else false; + var status: ?u8 = null; + if (unwatched) { + status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire + } s.closePty(pane); s.harvest(); + if (unwatched) s.core.update(.{ .exited = .{ .pane = pane, .status = status } }); s.core.update(.{ .eof = .{ .pane = pane } }); } + /// The command panes' exits, told once their output is in (host_io). + fn takeExits(s: *Session) void { + host_io.takeExits(s.core, &s.ptys, s, closeWatched); + } + + fn closeWatched(s: *Session, id: usize) void { + s.closePty(@intCast(id)); + } + fn harvest(s: *Session) void { const now = monotonicMs(); for (&s.retired_shells) |*shell| reapShell(&shell.pid, &shell.kill_at, now); @@ -780,6 +816,10 @@ pub const Session = struct { const completed = s.ninep_wake.swap(false, .acq_rel) or drained; for (&s.clients) |*c| if (c.fd >= 0) s.flush(c); const regridded = s.reconcile(); + // Nobody attached, nobody to dismiss a message: the loop expires + // them itself, between the editor's steps (expireUnattended). + s.core.unattended = s.primary() == null; + pardes.Messages.expireUnattended(s.core); const now = monotonicMs(); var fds: [poll_slots]libc.pollfd = undefined; @@ -807,7 +847,7 @@ pub const Session = struct { n += 1; } for (&s.ptys, 0..) |*pt, pane| { - if (pt.fd < 0) continue; + if (pt.fd < 0 or pt.cmd.eof) continue; fds[n] = .{ .fd = pt.fd, .events = if (pt.out.items.len != 0) poll_in | poll_out else poll_in, @@ -854,6 +894,7 @@ pub const Session = struct { virtual.* = @max(virtual.*, s.core.nextWake() orelse virtual.*); }; _ = s.drainCompletions(true); + s.takeExits(); s.expire(monotonicMs()); s.harvest(); s.reloadWatched(); @@ -914,6 +955,11 @@ pub const Session = struct { due = if (due) |d| @min(d, at9) else at9; } } + // Unattended messages go on the clock: look again in a while. + if (s.core.unattended) for (s.core.panes) |slot| if (slot) |pane| if (pane.msg_len > 0) { + due = if (due) |d| @min(d, now + 100) else now + 100; + break; + }; const at = due orelse return null; return @intCast(@max(0, @min(at - now, std.math.maxInt(c_int)))); } @@ -1135,6 +1181,17 @@ pub const Session = struct { } }; +/// SIGTERM, SIGINT or SIGHUP: quit as Exit does, through the loop, so its +/// sockets are unlinked on the way out rather than left for the next +/// session to trip on. The handler only sets the flag and wakes the loop. +var quit_signalled = std.atomic.Value(bool).init(false); +var quit_wake: c_int = -1; + +fn onQuitSignal(_: std.posix.SIG) callconv(.c) void { + quit_signalled.store(true, .release); + if (quit_wake >= 0) _ = libc.send(quit_wake, "q", 1, nosignal); +} + pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void { const gpa = init.gpa; const allocs = pardes.memory.init(gpa); @@ -1186,7 +1243,11 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void core.host = h; while (core.nextEffect()) |effect| core.perform(effect); session.in_loop = true; - while (!session.core.quit) { + quit_wake = session.mailbox.wake[1]; + const on_quit: std.posix.Sigaction = .{ .handler = .{ .handler = onQuitSignal }, .mask = std.posix.sigemptyset(), .flags = 0 }; + for ([_]std.posix.SIG{ .HUP, .INT, .TERM }) |sig| std.posix.sigaction(sig, &on_quit, null); + defer quit_wake = -1; + while (!session.core.quit and !quit_signalled.load(.acquire)) { pardes.turn.restoreSettled(); try session.core.pump(h); if (session.core.quit) break; @@ -1196,7 +1257,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void break :restore; }; defer gpa.free(bytes); - session.restore(bytes) catch |err| session.core.reportError(session.core.active, "Restore", err); + session.restore(bytes, path) catch |err| session.core.reportError(session.core.active, "Restore", err); } } } @@ -1236,7 +1297,7 @@ test "detached queued results preserve current requests and are discarded before try std.testing.expect(s.pipe_tasks.add(.{ .id = 77, .future = .{ .any_future = null, .result = {} } })); s.mailbox.post(.{ .pipe = .{ .id = 77, .success = true, .outputs = outputs } }); Session.lspStatus(&s, "old status"); - try s.restore(saved); + try s.restore(saved, "test"); try std.testing.expect(s.lsp_task == null); try std.testing.expectEqual(@as(usize, 0), s.pipe_tasks.len); try std.testing.expect(!s.drainCompletions(true)); diff --git a/src/detached/wire.zig b/src/detached/wire.zig index 32cd5229..b1b81eda 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -884,7 +884,7 @@ fn clientTag(msg: ClientMsg) ClientTag { return switch (msg) { .event => |ev| switch (ev) { // Machine-local reports and 9P requests belong to the session owner. - .output, .eof, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, + .output, .eof, .exited, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, inline else => |_, t| @field(ClientTag, @tagName(t)), }, inline else => |_, t| @field(ClientTag, @tagName(t)), @@ -1011,7 +1011,7 @@ pub fn encodeClient(out: []u8, msg: ClientMsg) Error![]const u8 { .touch_scroll => |v| try w.putF32(v), .pointer_leave => {}, // See `clientTag`: no tag, so nothing to encode. - .output, .eof, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, + .output, .eof, .exited, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, }, } try finishMessage(&w, at); @@ -1141,7 +1141,7 @@ pub fn clientBound(msg: ClientMsg) usize { .paste => |b| b.len, .command => |line| line.len, // See `clientTag`: not on this wire in this direction. - .output, .eof, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, + .output, .eof, .exited, .lsp_resp, .pipe_resp, .file_changed, .tick => unreachable, }, }; } diff --git a/src/draw.zig b/src/draw.zig index 0dbf5bae..2d757635 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -598,9 +598,10 @@ fn joinCells(s: *Surface, x: u16, y: u16, cells: []const Cell, from: usize, w: u fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct { lo: usize, hi: usize } { if (pane.isTerminal()) { - var words = std.mem.tokenizeAny(u8, tag[prefix_len..], " \t\r\n"); + // `tag` is the first row; a prefix longer than it ran to a second. + var words = std.mem.tokenizeAny(u8, tag[@min(prefix_len, tag.len)..], " \t\r\n"); while (words.next()) |word| { - if (!std.mem.eql(u8, word, "Tty")) continue; + if (!std.mem.eql(u8, word, "Tty") and !std.mem.startsWith(u8, word, "Tty+")) continue; const lo = @intFromPtr(word.ptr) - @intFromPtr(tag.ptr); return .{ .lo = lo, .hi = lo + word.len }; } @@ -611,7 +612,7 @@ fn tagTintRange(pane: *const Pane, tag: []const u8, prefix_len: usize) ?struct { if (pane.prompt == .name) pane.input.own orelse file.path else file.path else if (pane.image) |state| blk: { // Image tags put renderer controls before the path. - start = std.mem.lastIndexOf(u8, tag[0..prefix_len], state.path) orelse return null; + start = std.mem.lastIndexOf(u8, tag[0..@min(prefix_len, tag.len)], state.path) orelse return null; break :blk state.path; } else if (comptime pdf_enabled) blk: { break :blk if (pane.pdf) |state| state.path else return null; @@ -772,13 +773,16 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region) const motion: Messages.MessageMotion = if (Messages.noticeLife(pane, i)) |life| Messages.messageMotion(p, life) else .{}; const page = th.bg orelse chrome.tag_bg; const bg = Messages.blendRgb(chrome.tag_bg, page, motion.alpha); - const fg = Messages.blendRgb(chrome.tag_fg, page, motion.alpha); + // Where an exec was sent to a REPL reads in the tag's name tint, set + // apart from what the editor says. + const ink = if (kind == .message and pane.msg_sent) chrome.tag_name_fg else chrome.tag_fg; + const fg = Messages.blendRgb(ink, page, motion.alpha); // Nothing to say, or too narrow for a layer: a blank chip on the grid. if (text.len == 0 or r.w <= TAG_TEXT_INSET) { s.fill(region.rect.x, region.rect.y, chip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); if (text.len == 0) return; const printed = Messages.printRight(s, region.rect.x, region.rect.y, chip -| 1, text, .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline }); - if (kind != .prompt or id != p.active or pane.prompt == .del_side) return; + if (kind != .prompt or id != p.active or pane.prompt == .del_side or pane.prompt == .repl_choice) return; const prompt_col = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); if (prompt_col < printed.dropped) return; s.cursor = .{ .x = @intCast(printed.left + (prompt_col - printed.dropped)), .y = region.rect.y, .bar = pane.input.mode == .insert }; @@ -807,7 +811,7 @@ fn joinNotice(p: *Pardes, s: *Surface, arena: std.mem.Allocator, region: Region) layer.slide = motion.slide; layer.fade = motion.alpha; // The caret rides the band, where the prompt's glyphs are. - if (kind == .prompt and id == p.active and pane.prompt != .del_side) caret: { + if (kind == .prompt and id == p.active and pane.prompt != .del_side and pane.prompt != .repl_choice) caret: { const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]); const dropped = shown - panes.File.displayWidth(kept); if (typed < dropped) break :caret; diff --git a/src/dump.zig b/src/dump.zig index 508e7300..954a3270 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -671,11 +671,15 @@ pub fn dumpState(p: *Pardes) !void { p.emit(.write_dump); } -pub fn restore(p: *Pardes, zon_bytes: []const u8) !*Pardes { +/// The replacement core for a Restore of the dump at `from`, whose log says +/// so after its panes' `new`s, for a client that reconnects to read. +pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes { var opts = p.opts; opts.cols = p.screen_w; opts.rows = p.screen_h; - return initDump(p.gpa, opts, zon_bytes, p); + const replacement = try initDump(p.gpa, opts, zon_bytes, p); + pardes.ctlfs.events.notePath(replacement, "restore", from); + return replacement; } pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8) !*Pardes { @@ -744,7 +748,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo p.installPane(i, restored); try restored.setOwnedCwd(t.cwd); if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty; - if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .cwd = .from(t.cwd) } }); + if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } }); break :terminal restored; }, .file => try pardes.panes.File.restore(p, i, src), diff --git a/src/edit.zig b/src/edit.zig index 9044d7ed..84bbb292 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -13,6 +13,7 @@ const body_layer = @import("body_layer.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const lsp = @import("lsp/lsp.zig"); +const syntax = @import("syntax.zig"); const platform = pardes.platform; const Pane = panes.Pane; const Text = panes.Text; @@ -26,23 +27,75 @@ const hit = pardes.hit; const sel_slot = pardes.sel_slot; const Effect = pardes.Effect; const Pardes = pardes.Pardes; +const Registers = pardes.Registers; +/// Write `text` as this range's value of the register the running command +/// names (`"<reg>`), the default one otherwise. `_` swallows it, and the +/// registers computed from the editor (`#`, `.`, `%`) cannot be written. +/// `+` and `*` are the system clipboard: the value is sent there too. pub fn setYank(p: *Pardes, text: []const u8) void { - if (p.multi_on and !p.multi_first) { - const old = p.yank orelse ""; - const sep: []const u8 = if (text.len > 0 and text[text.len - 1] == '\n') "" else "\n"; - const joined = std.fmt.allocPrint(p.gpa, "{s}{s}{s}", .{ text, sep, old }) catch return; - if (p.yank) |y| p.gpa.free(y); - p.yank = joined; - return; + var name: u21 = if (p.register != 0) p.register else Registers.default; + switch (name) { + '_', '#', '.', '%' => return, + '*' => name = '+', + else => {}, } - if (p.yank) |y| p.gpa.free(y); - p.yank = p.gpa.dupe(u8, text) catch null; + const one = !p.multi_on; + p.registers.put(p.gpa, name, text, if (one) 0 else p.multi_index, if (one) 1 else p.multi_count, one or p.multi_first); + if (name == '+' and (one or p.multi_first)) p.emit(.{ .set_clipboard = {} }); } pub fn setClipboard(p: *Pardes, text: []const u8) void { + const named = p.register; + p.register = '+'; + defer p.register = named; setYank(p, text); - if (!p.multi_on or p.multi_first) p.emit(.{ .set_clipboard = {} }); +} + +/// Value `i` of register `name` as the command at range `i` reads it, the +/// computed registers included: `#` is the range's number from 1, `.` its +/// text, `%` the file's name, `_` nothing. Scratch- or register-owned. +pub fn registerValue(p: *Pardes, t: *Text, name: u21) ?[]const u8 { + const i = if (p.multi_on) p.multi_index else 0; + return switch (name) { + '_' => null, + '#' => std.fmt.allocPrint(p.scratch.allocator(), "{d}", .{i + 1}) catch null, + '.' => selText(p, t), + '%' => if (t.what == .body) if (t.pane()) |pane| (if (pane.file) |f| f.path else null) else null else null, + else => p.registers.value(name, i), + }; +} + +/// the text of the range under the cursor: the selection, or the cursor's +/// own character +fn selText(p: *Pardes, t: *Text) []const u8 { + if (t.msel.active) return yankRows(p, t, @min(t.msel.r0, t.msel.r1), @max(t.msel.r0, t.msel.r1)); + const save = t.vsel; + defer t.vsel = save; + if (!t.vsel.active) t.vsel = .{ .active = true, .row = t.cur_row, .col = t.cur_col, .explicit = false }; + return vselText(p, t); +} + +/// `p` / `P` / `R` from the register the command names: this range's value +/// of it. The clipboard's is asked of the shell and pasted when it answers, +/// once, at the primary. +pub fn pasteRegister(p: *Pardes, t: *Text, how: enum { after, before, replace }, count: usize) void { + const name: u21 = if (p.register != 0) p.register else Registers.default; + if (name == '+' or name == '*') { + if (!normal.multiOnce(p)) return; + const pane = t.pane() orelse return; + return clipRequest(p, p.paneIdOf(pane) orelse return, switch (how) { + .after => .after, + .before => .before, + .replace => .replace, + }); + } + const y = registerValue(p, t, name) orelse return; + switch (how) { + .after => pasteText(p, t, y, false, count), + .before => pasteText(p, t, y, true, count), + .replace => replaceWithText(p, t, y), + } } /// Where a `SPC p` / `SPC P` / `SPC R` goes once the shell answers. @@ -87,7 +140,7 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { const pane = p.panes[id] orelse return; if (req) |r| if (pane.serial != r.serial) return; p.active = id; - if (pane.prompt != .none and pane.prompt != .del_side) { + if (pane.prompt != .none and pane.prompt != .del_side and pane.prompt != .repl_choice) { // an answer is one line: a newline in it would only be submitting it const line = p.scratch.allocator().alloc(u8, bytes.len) catch return; var n: usize = 0; @@ -96,7 +149,7 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { n += 1; }; if (n > 0) insertKey(p, &pane.input, .{ .cp = line[0], .text = line[0..n] }); - if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split); + if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.mode); return; } if (pane.focus == .tag) return pasteInto(p, &pane.tag, bytes); @@ -114,10 +167,16 @@ fn pasteInto(p: *Pardes, t: *Text, bytes: []const u8) void { if (bytes.len == 0) return; const typed: pardes.Key = .{ .cp = bytes[0], .text = bytes }; if (t.what == .tag and t.mode == .insert) if (t.pane()) |pane| if (tagline.typedIntoName(p, pane, typed)) return; - const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel }; + const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel }; + var kept_sels: [Text.max_selections - 1]Text.SelRange = undefined; + const kept_n = t.nsel; + @memcpy(kept_sels[0..kept_n], t.sels[0..kept_n]); t.refused = false; if (t.mode == .insert) insertKey(p, t, typed) else pasteText(p, t, bytes, false, 1); - if (t.refused) t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel = kept; + if (t.refused) { + t.cur_row, t.cur_col, t.vsel, t.msel = kept; + t.restoreSels(kept_sels[0..kept_n]); + } } pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void { @@ -125,6 +184,10 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void { if (pv.selection_text.len > 0) setClipboard(p, pv.selection_text); return; }; + // helix's `"+y`: the clipboard register, and only it + const named = p.register; + p.register = '+'; + defer p.register = named; if (pane.body.nsel > 0 and !main_only) { normal.replaySels(p, &pane.body, .{ .normal = .{ .edit = .{ .kind = .yank, .count = 1 } } }); } else { @@ -133,7 +196,6 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void { normalYank(p, &pane.body); pane.body.nsel = others; } - p.emit(.{ .set_clipboard = {} }); } /// tag + '\n' + body: the full selectable pane text, a row to a screen row @@ -286,30 +348,24 @@ pub fn currentSelText(p: *Pardes, pane: *Pane) ?[]const u8 { pub fn exitInsert(p: *Pardes, t: *Text) void { t.mode = .normal; + t.reg_wait = false; t.msel.active = false; t.normal.clear(); - if (t.append_at) |aa| { - t.append_at = null; - const text = flatSurface(p, t) catch return; - const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); - const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) }); - const back = @max(a_off, modal.prevGrapheme(text, gap)); - const bc = modal.positionAt(text, back); - t.cur_row = @intCast(bc.row); - t.cur_col = @intCast(bc.col); - t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; - t.cur_pinned = true; - t.show(); - for (t.sels[0..t.nsel]) |*s| { - const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) }); - const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap); - const bp = modal.positionAt(text, b2); - s.row = @intCast(bp.row); - s.col = @intCast(bp.col); - s.arow = s.row; - s.acol = s.col; - } + if (!t.restore_cursor) return; + t.restore_cursor = false; + // helix enter_normal_mode: every range, from its start to the char + // before its end when it points forward, else to its end + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + const text = flatSurface(p, t) catch return; + var rs: [Text.max_selections]modal.Selection = undefined; + const got = t.ranges(text, 0, &rs); + for (rs[0..got.n]) |*r| { + const from = @min(r.anchor, r.head); + const to = @max(r.anchor, r.head); + const head = if (r.head > r.anchor) @max(from, modal.prevGrapheme(text, to)) else to; + r.* = .{ .anchor = from, .head = head }; } + t.setRanges(text, rs[0..got.n], &.{}, got.pri, expl); } pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { @@ -415,7 +471,9 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void { if (t.what == .tag) { // A tag is edited as it is shown, but only what follows its // prefix is its own: an edit that changed the prefix is refused. - defer p.gpa.free(new); + // The shown text stays alive until the next such edit, since every + // caller reads the new text back, as a body's is kept. + defer retireEditText(p, new); const pane = t.pane() orelse return; const prefix = tagline.pathPrefix(p.scratch.allocator(), pane) catch return; if (!std.mem.startsWith(u8, new, prefix)) { @@ -432,11 +490,25 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void { t.own = new; return; } - const pane = t.pane() orelse return p.gpa.free(new); + const pane = t.pane() orelse return retireEditText(p, new); + // what changed ended where the texts agree again from their ends + const old: []const u8, const row0: i32 = if (pane.file) |f| .{ f.content, 0 } else if (pane.ovl) |o| .{ o.text, o.row } else .{ "", 0 }; + const front = std.mem.indexOfDiff(u8, old, new) orelse @min(old.len, new.len); + var back: usize = 0; + while (back < @min(old.len, new.len) - front and old[old.len - 1 - back] == new[new.len - 1 - back]) back += 1; + const end = modal.positionAt(new, new.len - back); + t.last_edit = .{ .row = @as(i32, @intCast(end.row)) + row0, .col = @intCast(end.col) }; if (pane.file) |*f| return panes.File.setContent(p, f, new); panes.Terminal.setEditText(p, pane, new); } +/// Take a text an edit made that nothing keeps, holding it until the next +/// one: the edit's caller still reads it to place the cursor. +pub fn retireEditText(p: *Pardes, new: []u8) void { + if (p.retired_edit) |old| p.gpa.free(old); + p.retired_edit = new; +} + pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_above }; /// Files insert only inside existing rows and at grapheme boundaries. @@ -462,7 +534,7 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { // `u` undoes the whole session and restores what was selected pushUndo(p, t); t.select = false; - t.append_at = null; + t.restore_cursor = false; t.sticky_col = -1; const text = flatSurface(p, t) catch { t.mode = .insert; @@ -488,12 +560,25 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { const lo_col = if (b) |bb| bb.lo_col else t.cur_col; const hi_row = if (b) |bb| bb.hi_row else t.cur_row; const hi_col = if (b) |bb| bb.hi_col else t.cur_col; - t.append_at = .{ .row = lo_row, .col = lo_col }; - const gap = modal.nextGrapheme(text, modal.offsetAt(text, .{ .row = @intCast(@max(0, hi_row)), .col = @intCast(@max(0, hi_col)) })); - const gc = modal.positionAt(text, gap); + t.restore_cursor = true; + const hi = modal.offsetAt(text, .{ .row = @intCast(@max(0, hi_row)), .col = @intCast(@max(0, hi_col)) }); + const gap = modal.nextGrapheme(text, hi); + // helix append_mode: a range that reaches the end of the text + // gets a newline after it to append before (a terminal's text + // is not the editor's to grow) + const terminal = t.what == .body and if (t.pane()) |pane| pane.isTerminal() else false; + var now = text; + if (hi < text.len and gap >= text.len and !terminal) { + const grown = std.mem.concat(p.gpa, u8, &.{ text, "\n" }) catch return; + setEditText(p, t, grown); + now = grown; // what setEditText keeps; `text` may be gone + } + const gc = modal.positionAt(now, gap); t.cur_row = @intCast(gc.row); t.cur_col = @intCast(gc.col); - t.vsel = .{ .active = b != null, .row = lo_row, .col = lo_col, .explicit = false }; + // helix append_mode: the range reaches one past its end, which + // is where typing goes; a bare cursor becomes a range too + t.vsel = .{ .active = true, .row = lo_row, .col = lo_col, .explicit = false }; t.cur_pinned = true; }, .line_start => { @@ -565,6 +650,87 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { if (hit(key, config.insert_backspace_alias)) return insertKey(p, t, .{ .cp = Key.backspace }); if (hit(key, config.insert_enter_alias)) return insertKey(p, t, .{ .cp = Key.enter }); if (hit(key, config.insert_delete_alias)) return insertKey(p, t, .{ .cp = Key.delete }); + // Ctrl-r <reg>: type this range's value of the register (helix + // insert_register) + if (t.reg_wait) { + t.reg_wait = false; + if (key.ctrl or key.alt or key.cp >= 0xF0000) return; + const y = registerValue(p, t, key.cp) orelse return; + if (y.len > 0) insertKey(p, t, .{ .cp = key.cp, .text = y }); + return; + } + if (hit(key, config.insert_register)) { + t.reg_wait = true; + return; + } + // helix commit_undo_checkpoint: `u` after the session goes back to here + if (hit(key, config.insert_checkpoint)) return pushUndo(p, t); + // helix maps every range through each insert-mode edit (Range::map): + // the edits below work on the bare cursor, and the range the cursor + // heads is carried through what they did to the text afterwards + if (!t.vsel.active) return insertEdit(p, t, key); + const old = flatSurface(p, t) catch return insertEdit(p, t, key); + const r = t.primaryRange(old, 0); + const c = modal.selectionCursor(old, r); + const old_len = old.len; + const explicit = t.vsel.explicit; + t.vsel.active = false; + insertEdit(p, t, key); + if (t.mode != .insert) return; + const new = flatSurface(p, t) catch return; + const c2 = modal.offsetAt(new, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); + // every insert-mode edit happens at the cursor: an insertion there, or + // a deletion that ends there (backward) or starts there (forward) + if (key.cp == Key.enter) { + // helix insert_newline: an appending range stretches by what the + // cursor moved, any other slides by it + const d = @as(isize, @intCast(c2)) - @as(isize, @intCast(c)); + const head: usize = @intCast(@as(isize, @intCast(r.head)) + d); + const anchor: usize = if (c > r.anchor) r.anchor else @intCast(@as(isize, @intCast(r.anchor)) + d); + return setInsertRange(t, new, .{ .anchor = anchor, .head = head }, explicit); + } + var at: usize = c; + var del: usize = 0; + var ins: usize = 0; + if (new.len > old_len and c2 == c + (new.len - old_len)) { + ins = new.len - old_len; + } else if (new.len < old_len and c2 + (old_len - new.len) == c) { + at = c2; + del = old_len - new.len; + } else if (new.len < old_len and c2 == c) { + del = old_len - new.len; + } else if (new.len != old_len or c2 != c) { + return; // a motion: the range collapses onto the cursor (helix) + } + // helix Assoc: the head sticks after an insertion at it when the range + // points backward or is empty, the anchor when it points forward + const fwd = r.anchor < r.head; + setInsertRange(t, new, .{ + .anchor = mapThroughEdit(r.anchor, at, del, ins, !fwd and r.anchor != r.head), + .head = mapThroughEdit(r.head, at, del, ins, fwd), + }, explicit); +} + +fn setInsertRange(t: *Text, new: []const u8, mapped: modal.Selection, explicit: bool) void { + const cells = Text.rangeCells(new, mapped); + const cc = modal.positionAt(new, cells.cur); + const ac = modal.positionAt(new, cells.anc); + t.cur_row = @intCast(cc.row); + t.cur_col = @intCast(cc.col); + t.vsel = .{ .active = cells.anc != cells.cur, .row = @intCast(ac.row), .col = @intCast(ac.col), .explicit = explicit }; +} + +/// A position through one edit at `at` that deleted `del` bytes and put +/// `ins` in (one of the two is 0). A position exactly at an insertion stays +/// before it when `before` is set, else moves past it. +fn mapThroughEdit(pos: usize, at: usize, del: usize, ins: usize, before: bool) usize { + if (pos < at) return pos; + if (del > 0) return if (pos < at + del) at else pos - del; + if (pos == at and before) return pos; + return pos + ins; +} + +fn insertEdit(p: *Pardes, t: *Text, key: Key) void { t.vsel.active = false; if (!t.cur_pinned) pinPaneCursor(p, t); _ = clampFileCursor(t); @@ -679,12 +845,29 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { switch (key.cp) { Key.enter => { const line = modal.lineSlice(text, c.row); + const before = line[0..@min(c.col, line.len)]; + // helix insert_newline: after nothing but blanks, the line + // itself moves down, blanks and all; else the blanks before the + // cursor go and the new line takes the indent + if (std.mem.indexOfNone(u8, before, " \t") == null) { + const new = modal.insertAt(p.gpa, text, .{ .row = c.row, .col = 0 }, "\n") catch return; + setEditText(p, t, new); + t.cur_row += 1; + t.cur_pinned = true; + t.show(); + return; + } const indent = modal.newlineIndentWidth(line, c.col); const arena = p.scratch.allocator(); const block = arena.alloc(u8, 1 + indent) catch return; block[0] = '\n'; @memset(block[1..], ' '); - const new = modal.insertAt(p.gpa, text, c, block) catch return; + // a tag keeps its blanks: they part its words, and the last + // may be its read-only prefix's + const kept = if (t.what == .body) std.mem.trimEnd(u8, before, " \t").len else c.col; + const cut = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = kept }, c) catch return; + defer p.gpa.free(cut); + const new = modal.insertAt(p.gpa, cut, .{ .row = c.row, .col = kept }, block) catch return; setEditText(p, t, new); t.cur_row += 1; t.cur_col = @intCast(indent); @@ -694,8 +877,19 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { Key.backspace => { if (t.cur_col > 0) { const line = modal.lineSlice(text, c.row); - const prev = modal.prevGrapheme(line, c.col); - const new = modal.deleteChar(p.gpa, text, .{ .row = c.row, .col = prev }) catch return; + var prev = modal.prevGrapheme(line, c.col); + // in leading blanks, back to the previous indent stop (helix + // dedent): a whole unit when on one, else the part past it + const lead = line[0..@min(c.col, line.len)]; + if (std.mem.indexOfNone(u8, lead, " \t") == null and prev < line.len and line[prev] != '\t') { + var width: usize = 0; + for (lead) |ch| width += if (ch == '\t') modal.INDENT_W else 1; + var drop = width % modal.INDENT_W; + if (drop == 0) drop = modal.INDENT_W; + prev = c.col; + while (drop > 0 and prev > 0 and line[prev - 1] == ' ') : (drop -= 1) prev -= 1; + } + const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = prev }, c) catch return; setEditText(p, t, new); t.cur_col = @intCast(prev); t.cur_pinned = true; @@ -742,6 +936,17 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { p.lspRequest(p.active, .completion, ""); if (p.lsp_seq != seq) return; } + // helix smart_tab: after text, Tab leaves the syntax node the + // cursor is in; only where the file has a grammar, since the + // reference pins smart tab off, which a file without one agrees + // with. Shift-Tab always types the tab. + const after_text = std.mem.indexOfNone(u8, ln[0..@min(c.col, ln.len)], " \t") != null; + if (!key.shift and after_text and t.what == .body) smart: { + const pane = t.pane() orelse break :smart; + const f = pane.file orelse break :smart; + if (!syntax.supportsPath(f.path)) break :smart; + return normal.nodeSelect(p, pane, t, .parent_end); + } insertTab(p, t); }, else => {}, @@ -1121,31 +1326,45 @@ pub fn normalReplaceChar(p: *Pardes, t: *Text, ch: u21) void { /// `R`: replace the selection (or the cursor char) with the DEFAULT /// register. `SPC R` is the same verb over the system clipboard. -pub fn normalReplaceYank(p: *Pardes, t: *Text) void { - replaceWithText(p, t, p.yank orelse return); +/// The acme chords' replace: the default register as one text. +pub fn chordReplace(p: *Pardes, t: *Text) void { + replaceWithText(p, t, p.registers.text(p.gpa, Registers.default) orelse return); } -/// The pasted text becomes the selection, head on its last char (which for -/// a trailing-newline `y` is the '\n' cell of the last full line). +/// The pasted text becomes the selection, in the direction the replaced one +/// had: a forward head lands on its last char (which for a trailing-newline +/// `y` is the '\n' cell of the last full line), a backward one on its first. fn replaceWithText(p: *Pardes, t: *Text, y: []const u8) void { if (y.len == 0) return; t.select = false; const eb = editTextEol(p, t, selRows(t)) orelse return; const r = selRange(t, eb.text, eb.row0); + const back = t.vsel.active and (t.cur_row < t.vsel.row or (t.cur_row == t.vsel.row and t.cur_col < t.vsel.col)); pushUndo(p, t); const new = modal.replaceRange(p.gpa, eb.text, r.a, r.b, y) catch return; setEditText(p, t, new); t.msel.active = false; - t.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; const end = modal.advanceBy(r.a, y); + var last_row: i32 = undefined; + var last_col: i32 = undefined; if (end.col > 0) { - t.cur_row = @as(i32, @intCast(end.row)) + eb.row0; - t.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col)); + last_row = @as(i32, @intCast(end.row)) + eb.row0; + last_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col)); } else { - // the yank ended in '\n': the cursor lands ON that newline - t.cur_row = @as(i32, @intCast(end.row -| 1)) + eb.row0; - t.cur_col = @intCast(modal.lineSlice(new, end.row -| 1).len); + // the yank ended in '\n': its last cell is that newline + last_row = @as(i32, @intCast(end.row -| 1)) + eb.row0; + last_col = @intCast(modal.lineSlice(new, end.row -| 1).len); } + const first_row = @as(i32, @intCast(r.a.row)) + eb.row0; + const first_col: i32 = @intCast(r.a.col); + t.vsel = .{ + .active = modal.nextGrapheme(y, 0) < y.len, + .row = if (back) last_row else first_row, + .col = if (back) last_col else first_col, + .explicit = false, + }; + t.cur_row = if (back) first_row else last_row; + t.cur_col = if (back) first_col else last_col; t.cur_pinned = true; t.sticky_col = -1; t.show(); @@ -1201,53 +1420,213 @@ fn applyChanges(p: *Pardes, text: []const u8, chs: []const TextChange) ![]u8 { return out; } -pub fn normalJoin(p: *Pardes, t: *Text) void { +/// `J` / `Alt-J`: join every range's lines with the next, in ONE edit +/// (helix join_selections): the newline and the next line's leading blanks +/// become one space, or nothing when that line is blank, and a line two +/// ranges share is joined once. `Alt-J` then selects the spaces it put in. +pub fn normalJoin(p: *Pardes, t: *Text, select_space: bool) void { + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; // a join always eats the newline of its last line, so the buffer has - // to reach one row PAST the selection - const sr = selRows(t); - const eb = editText(p, t, sr.lo_row, sr.hi_row + 1, -1) orelse return; + // to reach one row PAST every range + var b = selRows(t); + for (t.sels[0..t.nsel]) |s| { + b.lo_row = @min(b.lo_row, @min(s.row, s.arow)); + b.hi_row = @max(b.hi_row, @max(s.row, s.arow)); + } + const eb = editText(p, t, b.lo_row, b.hi_row + 1, -1) orelse return; const text = eb.text; - const range = t.primaryRange(text, eb.row0); - const span = Text.rangeLineSpan(text, range); + var rs: [Text.max_selections]modal.Selection = undefined; + const got = t.ranges(text, eb.row0, &rs); const nlines = modal.cursorLineCount(text); - var end = span.end; - if (span.start == end) end = @min(end + 1, nlines - 1); - if (end <= span.start) return; const arena = p.scratch.allocator(); - const chs = arena.alloc(TextChange, end - span.start) catch return; + const chs = arena.alloc(TextChange, nlines) catch return; var chs_len: usize = 0; - var l = span.start; - while (l < end) : (l += 1) { - const from = modal.lineEndOffset(text, l); - var to = if (l + 1 >= nlines) text.len else modal.lineStartOffset(text, l + 1); - while (to < text.len and (text[to] == ' ' or text[to] == '\t')) to += 1; - const sep: []const u8 = if (to == modal.lineEndOffset(text, @min(l + 1, nlines - 1))) "" else " "; - chs[chs_len] = .{ .from = from, .to = to, .ins = sep }; - chs_len += 1; + var next: usize = 0; // lines below this one are joined already + for (rs[0..got.n]) |r| { + const span = Text.rangeLineSpan(text, r); + var end = span.end; + if (span.start == end) end = @min(end + 1, nlines - 1); + var l = @max(span.start, next); + while (l < end) : (l += 1) { + const from = modal.lineEndOffset(text, l); + var to = if (l + 1 >= nlines) text.len else modal.lineStartOffset(text, l + 1); + while (to < text.len and (text[to] == ' ' or text[to] == '\t')) to += 1; + const sep: []const u8 = if (to == modal.lineEndOffset(text, @min(l + 1, nlines - 1))) "" else " "; + chs[chs_len] = .{ .from = from, .to = to, .ins = sep }; + chs_len += 1; + } + next = @max(next, end); } if (chs_len == 0) return; pushUndo(p, t); - const cur_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) }); - const anc_off = if (t.vsel.active) - modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row - eb.row0)), .col = @intCast(@max(0, t.vsel.col)) }) - else - cur_off; const new = applyChanges(p, text, chs[0..chs_len]) catch return; - const nc = mapThroughChanges(chs[0..chs_len], cur_off); - const na = mapThroughChanges(chs[0..chs_len], anc_off); + // every range's cells ride through the edit; Alt-J replaces them with + // the spaces, when it put any in + var cells: [Text.max_selections]Text.SelRange = undefined; + var n: usize = 0; + var pri = got.pri; + if (select_space) { + var delta: i64 = 0; + for (chs[0..chs_len]) |ch| { + const at: usize = @intCast(@as(i64, @intCast(ch.from)) + delta); + delta += @as(i64, @intCast(ch.ins.len)) - @as(i64, @intCast(ch.to - ch.from)); + if (ch.ins.len == 0 or n == cells.len) continue; + const pos = modal.positionAt(new, at); + cells[n] = .{ .row = @as(i32, @intCast(pos.row)) + eb.row0, .col = @intCast(pos.col), .arow = @as(i32, @intCast(pos.row)) + eb.row0, .acol = @intCast(pos.col) }; + n += 1; + } + pri = 0; + } + if (n == 0) { + for (rs[0..got.n]) |r| { + const c = Text.rangeCells(text, r); + const cc = modal.positionAt(new, mapThroughChanges(chs[0..chs_len], c.cur)); + const ac = modal.positionAt(new, mapThroughChanges(chs[0..chs_len], c.anc)); + cells[n] = .{ + .row = @as(i32, @intCast(cc.row)) + eb.row0, + .col = @intCast(cc.col), + .arow = @as(i32, @intCast(ac.row)) + eb.row0, + .acol = @intCast(ac.col), + }; + n += 1; + } + pri = got.pri; + } setEditText(p, t, new); - const cc = modal.positionAt(new, nc); - const ac = modal.positionAt(new, na); - t.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; - t.cur_col = @intCast(cc.col); - if (t.vsel.active) { - t.vsel.row = @as(i32, @intCast(ac.row)) + eb.row0; - t.vsel.col = @intCast(ac.col); + const t2 = flatSurface(p, t) catch return; + for (cells[0..n], 0..) |s, i| rs[i] = Text.cellRange(t2, s.arow, s.acol, s.row, s.col); + t.setRanges(t2, rs[0..n], &.{}, pri, expl); +} + +/// `Alt-)` / `Alt-(`: rotate the TEXT of the selections forward / back by +/// one range (count ranges), in one edit (helix reorder_selection_contents). +/// Each range comes back over the text it now holds, in its own direction, +/// and the primary moves with the text it had. +pub fn normalRotateContents(p: *Pardes, t: *Text, forward: bool, cnt: usize) void { + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + var b = selRows(t); + for (t.sels[0..t.nsel]) |s| { + b.lo_row = @min(b.lo_row, @min(s.row, s.arow)); + b.hi_row = @max(b.hi_row, @max(s.row, s.arow)); } - t.msel.active = false; - t.cur_pinned = true; - t.sticky_col = -1; - t.show(); + const eb = editTextEol(p, t, b) orelse return; + const text = eb.text; + var rs: [Text.max_selections]modal.Selection = undefined; + const got = t.ranges(text, eb.row0, &rs); + const n = got.n; + if (n < 2) return; + const by = @min(cnt, n); + var chs: [Text.max_selections]TextChange = undefined; + for (rs[0..n], 0..) |r, i| { + const src = rs[if (forward) (i + n - by) % n else (i + by) % n]; + chs[i] = .{ + .from = @min(r.anchor, r.head), + .to = @max(r.anchor, r.head), + .ins = text[@min(src.anchor, src.head)..@max(src.anchor, src.head)], + }; + } + pushUndo(p, t); + const new = applyChanges(p, text, chs[0..n]) catch return; + var cells: [Text.max_selections]Text.SelRange = undefined; + var delta: i64 = 0; + for (chs[0..n], rs[0..n], 0..) |ch, r, i| { + const from: usize = @intCast(@as(i64, @intCast(ch.from)) + delta); + delta += @as(i64, @intCast(ch.ins.len)) - @as(i64, @intCast(ch.to - ch.from)); + const to = from + ch.ins.len; + const c = Text.rangeCells(new, if (r.anchor > r.head) .{ .anchor = to, .head = from } else .{ .anchor = from, .head = to }); + const cc = modal.positionAt(new, c.cur); + const ac = modal.positionAt(new, c.anc); + cells[i] = .{ + .row = @as(i32, @intCast(cc.row)) + eb.row0, + .col = @intCast(cc.col), + .arow = @as(i32, @intCast(ac.row)) + eb.row0, + .acol = @intCast(ac.col), + }; + } + setEditText(p, t, new); + const t2 = flatSurface(p, t) catch return; + for (cells[0..n], 0..) |s, i| rs[i] = Text.cellRange(t2, s.arow, s.acol, s.row, s.col); + t.setRanges(t2, rs[0..n], &.{}, if (forward) (got.pri + by) % n else (got.pri + n - by) % n, expl); +} + +/// `&`: pad the selections into columns, in one edit (helix +/// align_selections). The k-th range of every line is a column; spaces go in +/// before each range so its head lands on the column's widest head, measured +/// in display cells. A range over several lines aligns nothing. +pub fn normalAlign(p: *Pardes, t: *Text) void { + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + var b = selRows(t); + for (t.sels[0..t.nsel]) |s| { + b.lo_row = @min(b.lo_row, @min(s.row, s.arow)); + b.hi_row = @max(b.hi_row, @max(s.row, s.arow)); + } + const eb = editTextEol(p, t, b) orelse return; + const text = eb.text; + var rs: [Text.max_selections]modal.Selection = undefined; + const got = t.ranges(text, eb.row0, &rs); + const n = got.n; + var row: [Text.max_selections]usize = undefined; + var col: [Text.max_selections]usize = undefined; + var widths: [Text.max_selections]usize = @splat(0); + var line: usize = std.math.maxInt(usize); + var k: usize = 0; + var run: usize = 0; + for (rs[0..n], 0..) |r, i| { + row[i] = modal.lineAtOffset(text, r.head); + if (modal.lineAtOffset(text, r.anchor) != row[i]) return; + const ls = modal.lineStartOffset(text, row[i]); + col[i] = panes.File.rawDisplayCol(text[ls..modal.lineEndOffset(text, row[i])], r.head - ls); + if (row[i] != line) { + line = row[i]; + k = 0; + run = 0; + } + widths[k] = @max(widths[k], col[i] - run); + run = col[i]; + k += 1; + } + t.select = false; // helix exit_select_mode + // each column's head position, left to right + for (1..n) |i| widths[i] += widths[i - 1]; + const arena = p.scratch.allocator(); + var chs: [Text.max_selections]TextChange = undefined; + var m: usize = 0; + line = std.math.maxInt(usize); + for (rs[0..n], 0..) |r, i| { + if (row[i] != line) { + line = row[i]; + k = 0; + run = 0; + } + const pad = widths[k] - col[i] - run; + k += 1; + run += pad; + if (pad == 0) continue; + const spaces = arena.alloc(u8, pad) catch return; + @memset(spaces, ' '); + const at = @min(r.anchor, r.head); + chs[m] = .{ .from = at, .to = at, .ins = spaces }; + m += 1; + } + if (m == 0) return; + pushUndo(p, t); + const new = applyChanges(p, text, chs[0..m]) catch return; + var cells: [Text.max_selections]Text.SelRange = undefined; + for (rs[0..n], 0..) |r, i| { + const c = Text.rangeCells(text, r); + const cc = modal.positionAt(new, mapThroughChanges(chs[0..m], c.cur)); + const ac = modal.positionAt(new, mapThroughChanges(chs[0..m], c.anc)); + cells[i] = .{ + .row = @as(i32, @intCast(cc.row)) + eb.row0, + .col = @intCast(cc.col), + .arow = @as(i32, @intCast(ac.row)) + eb.row0, + .acol = @intCast(ac.col), + }; + } + setEditText(p, t, new); + const t2 = flatSurface(p, t) catch return; + for (cells[0..n], 0..) |s, i| rs[i] = Text.cellRange(t2, s.arow, s.acol, s.row, s.col); + t.setRanges(t2, rs[0..n], &.{}, got.pri, expl); } pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void { @@ -1439,9 +1818,12 @@ pub fn normalAdjustNumber(p: *Pardes, t: *Text, delta: i64) void { /// `]Space` / `[Space`: add blank lines below/above, the cursor staying /// on its text line (the selection rides along) pub fn addNewline(p: *Pardes, t: *Text, below: bool, cnt: usize) void { - const eb = editText(p, t, t.cur_row, t.cur_row, -1) orelse return; + // above the selection's first line, below its last (helix line_range) + const sr = selRows(t); + const at = if (below) sr.hi_row else sr.lo_row; + const eb = editText(p, t, at, at, -1) orelse return; pushUndo(p, t); - const row: usize = @intCast(@max(0, t.cur_row - eb.row0)); + const row: usize = @intCast(@max(0, at - eb.row0)); const arena = p.scratch.allocator(); const nl = arena.alloc(u8, cnt) catch return; @memset(nl, '\n'); @@ -1449,10 +1831,14 @@ pub fn addNewline(p: *Pardes, t: *Text, below: bool, cnt: usize) void { const new = modal.insertAt(p.gpa, eb.text, .{ .row = row, .col = if (below) llen else 0 }, nl) catch return; setEditText(p, t, new); if (!below) { - t.cur_row += @intCast(cnt); // the line moved down; stay on it - if (t.vsel.active and t.vsel.row >= @as(i32, @intCast(row))) t.vsel.row += @intCast(cnt); - } else if (t.vsel.active and t.vsel.row > @as(i32, @intCast(row))) { - t.vsel.row += @intCast(cnt); + // the whole selection moved down; stay on it + const n: i32 = @intCast(cnt); + t.cur_row += n; + if (t.vsel.active) t.vsel.row += n; + if (t.msel.active) { + t.msel.r0 += n; + t.msel.r1 += n; + } } t.cur_pinned = true; t.show(); @@ -1472,6 +1858,25 @@ fn pairFor(ch: u21) ?struct { o: u8, c: u8 } { pub fn textobjectSelect(p: *Pardes, t: *Text, text: []const u8, obj: u21, around: bool) void { const range = t.primaryRange(text, 0); + // helix's tree-sitter objects, from the grammar's textobjects query + const object: ?modal.Normal.Object = switch (obj) { + 'f' => .function, + 't' => .class, + 'a' => .parameter, + 'c' => .comment, + 'T' => .@"test", + 'e' => .entry, + 'x' => .xml_element, + else => null, + }; + if (object) |o| { + if (t.what != .body) return; + const pane = t.pane() orelse return; + const f = if (pane.file) |*file| file else return; + const tree = normal.fileTree(p, f) orelse return; + const s = syntax.objectAt(f.path, tree, text, o, around, modal.selectionCursor(text, range)) orelse return; + return t.setRange(text, 0, .{ .anchor = s.from, .head = s.to }, false); + } switch (obj) { 'w', 'W' => { const r = modal.selectWord(text, range, around, obj == 'W'); @@ -1512,16 +1917,22 @@ pub fn surroundAdd(p: *Pardes, t: *Text, ch: u21) void { t.select = false; const eb = editTextEol(p, t, selRows(t)) orelse return; const r = selRange(t, eb.text, eb.row0); + // the wrapped range keeps the direction it had (helix with_direction) + const back = t.vsel.active and (t.cur_row < t.vsel.row or (t.cur_row == t.vsel.row and t.cur_col < t.vsel.col)); pushUndo(p, t); const close_col = modal.nextGrapheme(modal.lineSlice(eb.text, r.b.row), r.b.col); var new = modal.insertAt(p.gpa, eb.text, .{ .row = r.b.row, .col = close_col }, &[1]u8{pr.c}) catch return; setEditText(p, t, new); new = modal.insertAt(p.gpa, new, .{ .row = r.a.row, .col = r.a.col }, &[1]u8{pr.o}) catch return; setEditText(p, t, new); + const open_row = @as(i32, @intCast(r.a.row)) + eb.row0; + const open_col: i32 = @intCast(r.a.col); + const close_row = @as(i32, @intCast(r.b.row)) + eb.row0; + const close_at: i32 = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0)); t.msel.active = false; - t.vsel = .{ .active = true, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; - t.cur_row = @as(i32, @intCast(r.b.row)) + eb.row0; - t.cur_col = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0)); + t.vsel = .{ .active = true, .row = if (back) close_row else open_row, .col = if (back) close_at else open_col, .explicit = false }; + t.cur_row = if (back) open_row else close_row; + t.cur_col = if (back) open_col else close_at; t.cur_pinned = true; t.sticky_col = -1; t.show(); @@ -1760,16 +2171,20 @@ test "only the SPC clipboard commands cross to the system clipboard" { _ = drainedEffect(p, .set_clipboard); p.update(.{ .key = .{ .cp = 'y' } }); - try std.testing.expect(p.yank != null and p.yank.?.len > 0); + const plain = p.registers.text(gpa, Registers.default) orelse return error.MissingYank; + try std.testing.expect(plain.len > 0); try std.testing.expect(!drainedEffect(p, .set_clipboard)); p.update(.{ .key = .{ .cp = 'd' } }); try std.testing.expect(!drainedEffect(p, .set_clipboard)); - // `SPC y` is the one that does, and it puts the SAME text there that `y` - // put in the register — it is the ordinary yank path plus the mirror. + // `SPC y` is the one that does: helix's `"+y`, the ordinary yank into + // the clipboard register instead of the default one. p.update(.{ .key = .{ .cp = ' ' } }); p.update(.{ .key = .{ .cp = 'y' } }); - const yanked = p.yank orelse return error.MissingYank; + const yanked = try gpa.dupe(u8, p.registers.text(gpa, Registers.default) orelse ""); + defer gpa.free(yanked); + const clip: []const u8 = p.registers.text(gpa, '+') orelse ""; + try std.testing.expect(clip.len > 0); try std.testing.expect(drainedEffect(p, .set_clipboard)); const before = pane.file.?.content.len; @@ -1783,14 +2198,14 @@ test "only the SPC clipboard commands cross to the system clipboard" { try std.testing.expect(std.mem.indexOf(u8, pane.file.?.content, "PASTED") != null); // ...and it did NOT land in the register on its way past, nor echo back // out to the clipboard it came from. - try std.testing.expectEqualStrings(yanked, p.yank orelse ""); + try std.testing.expectEqualStrings(yanked, p.registers.text(gpa, Registers.default) orelse ""); try std.testing.expect(!drainedEffect(p, .set_clipboard)); // An UNSOLICITED paste — the outer terminal's bracketed paste, a Cmd-V — // is the same event with no request behind it, and means paste after. p.update(.{ .paste = "UNASKED" }); try std.testing.expect(std.mem.indexOf(u8, pane.file.?.content, "UNASKED") != null); - try std.testing.expectEqualStrings(yanked, p.yank orelse ""); + try std.testing.expectEqualStrings(yanked, p.registers.text(gpa, Registers.default) orelse ""); // A request the shell never answers dies at the next keystroke rather // than firing late into whatever pane is focused by then. diff --git a/src/esp32p4.zig b/src/esp32p4.zig index c583de54..b7725b81 100644 --- a/src/esp32p4.zig +++ b/src/esp32p4.zig @@ -613,7 +613,7 @@ fn boardNow(_: ?*anyopaque) u64 { } /// The `Gpio` word's one seam to the board. Nothing here knows what a pad is; it forwards, and -/// answers false when the firmware brought none, which is what puts "gpio: NoPads" on the message +/// answers false when the firmware brought none, which is what puts "gpio: no pads" on the message /// row rather than a trap. fn gpioToggle(_: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool { const f = host_gpio orelse return false; diff --git a/src/exec.zig b/src/exec.zig index ad0f7b9b..ee7c31e9 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -16,6 +16,11 @@ const colors = @import("colors.zig"); const themes = colors.themes; const builtins = @import("builtins.zig"); const config = @import("config.zig"); +const grammars = @import("grammar_manifest.zig").all; +const python_lang = blk: { + @setEvalBranchQuota(20_000); + break :blk @import("grammar_manifest.zig").byName("python").?; +}; const pdf_enabled = panes.Pdf.enabled; const font_picker = pardes.font_picker; const hosted = pardes.hosted; @@ -30,7 +35,8 @@ const Pardes = pardes.Pardes; pub fn takesCommandLine(p: *const Pardes, id: usize) bool { const pane = p.panes[id] orelse return false; - if (!pane.isTerminal()) return false; + // A command pane's child is its command, never a shell to type at. + if (!pane.isTerminal() or pane.command != null) return false; // A mounted shell sits behind sudo's process supervisor. Its OSC 133 // prompt marks identify input readiness; the launcher's executable is // not the shell executable used by the ordinary process heuristic. @@ -67,7 +73,8 @@ pub fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool { var primary_active: ?usize = null; for (texts[0..made], 0..) |txt, i| { - runBuiltin(p, cmd, id, "", txt); + // Every selection is the body's, each one sent as the first is. + runBuiltinFrom(p, cmd, id, "", txt, true); if (i == got.pri) primary_active = p.active; const still = p.panes[id] orelse break; if (still.serial != serial) break; @@ -290,8 +297,8 @@ fn askWrite(p: *Pardes, id: usize, serial: u32, path: []const u8) void { /// Tty: a shell in the caller's directory, raw from the first frame, /// stacked below the caller like Alt-n's new shell. -pub fn spawnTty(p: *Pardes, from: usize) void { - spawnTtyWithMount(p, from, false); +pub fn spawnTty(p: *Pardes, from: usize) ?*Pane { + return spawnTtyWithMount(p, from, false); } pub fn spawnV9fsTty(p: *Pardes, from: usize) void { @@ -299,13 +306,19 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { return p.reportError(from, "Tty9p", error.LinuxRequired); if (p.fs.socket_path.len == 0) return p.reportError(from, "Tty9p", error.SessionSocketRequired); - spawnTtyWithMount(p, from, true); + _ = spawnTtyWithMount(p, from, true); } -fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { - const src = p.panes[from] orelse return; - const free = p.freeSlot() orelse return; - const nt = p.newShell(free, Pardes.paneDir(src)) catch return; +fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { + const src = p.panes[from] orelse return null; + const free = p.freeSlot() orelse { + p.reportFailure(from, "Tty: no pane free"); + return null; + }; + const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| { + p.reportError(from, "Tty", err); + return null; + }; nt.greet = !v9fs; nt.v9fs_on_spawn = v9fs; nt.body.mode = .tty; @@ -315,6 +328,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { layout.splitBelow(p, parent, nt); p.active = free; p.stillborn_joiner = nt.serial; + return nt; } /// when a doc lands in `col`, a lone pristine shell there is clutter — drop @@ -337,7 +351,7 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void { panes.Terminal.scrollbar(tt).total > tt.rows) return; p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err); layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows - layout.absorbVWeight(p, tty_id, null); + _ = layout.absorbVWeight(p, tty_id, null); layout.removePane(p, tty_id); p.panes[tty_id] = null; if (p.active == tty_id) p.active = keep_id; @@ -362,34 +376,156 @@ pub fn replaceStillborn(p: *Pardes) void { if (out.from != .cmd or out.from.cmd != .New) return; if (file.revision != 0 or file.content.len != 0) return; if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return; - const was_active = p.active == old_id; layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err); - if (was_active) p.active = new_id; } -fn ttyForDir(p: *Pardes, dir: []const u8) ?usize { - for (p.panes, 0..) |slot, i| if (slot) |tt| { - if (!std.mem.eql(u8, tt.cwdSlice(), dir)) continue; - if (takesCommandLine(p, i)) return i; +// ---- REPLs ---- +// +// A terminal is bound as a language's REPL by `Repl python` (builtins.zig). +// An exec made by a gesture on the body of a file in that language -- or on +// the REPL's own body -- is sent to it as input rather than run: all of it, +// a word like `ls` too. The tag's words, `Exec` by name and a command word +// `@`...`` still run as commands. + +/// A file pane's language, as grammar_manifest names it, by its index. +pub fn langOf(pane: *const Pane) ?u8 { + const f = pane.file orelse return null; + return @intCast(@import("grammar_manifest.zig").byPath(f.path) orelse return null); +} + +/// A bound REPL whose program is there to take input: a command pane's +/// while its command runs; an interactive terminal's while a program, not +/// the shell, has its tty (the host's `tty_taken`) -- a python3 left with +/// Ctrl-D leaves the shell, which would run `import os` as ImageMagick's. +pub fn replLive(p: *const Pardes, id: usize) bool { + const pane = p.panes[id] orelse return false; + if (pane.repl == null) return false; + if (pane.command != null) return !pane.command_done; + return p.hostTtyTaken(id); +} + +/// `python-a`. +pub fn replId(buf: []u8, pane: *const Pane) []const u8 { + const r = pane.repl orelse return ""; + return std.fmt.bufPrint(buf, "{s}-{c}", .{ grammars[r.lang].name, r.letter }) catch ""; +} + +const Routed = union(enum) { none, sent: usize, asked }; + +/// The REPL bound here has no program running to take the text: said, +/// and nothing sent or run. +fn replGone(p: *Pardes, from: usize, repl: *const Pane) Routed { + var idbuf: [48]u8 = undefined; + var said: [128]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {s} has no program running; nothing sent", .{replId(&idbuf, repl)}) catch "Repl: no program running"); + return .asked; +} + +/// Where an exec from `from`'s body goes when a REPL is bound for it: the +/// one there is, or a question when there are several. +fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { + const src = p.panes[from] orelse return .none; + if (src.repl != null) { + if (!replLive(p, from)) return replGone(p, from, src); + sendToRepl(p, from, from, text); + return .{ .sent = from }; + } + const lang = langOf(src) orelse return .none; + var bound: [26]usize = undefined; + var n: usize = 0; + var gone: ?usize = null; + for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and n < bound.len) { + if (!replLive(p, i)) { + gone = i; + continue; + } + bound[n] = i; + n += 1; }; - const free = p.freeSlot() orelse return null; - const nt = p.newShell(free, dir) catch return null; - nt.greet = false; - const rc = if (p.ncol > 0) p.ncol - 1 else 0; - if (p.col_n[rc] > 0) { - const src = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]); - const f = layout.findPane(p, src).?; - layout.splitBelow(p, src, nt); - layout.insert(p, f.col, f.idx + 1, free); - } else layout.insert(p, rc, p.col_n[rc], free); - return free; + if (n == 0) return if (gone) |i| replGone(p, from, p.panes[i].?) else .none; + if (n == 1) { + sendToRepl(p, from, bound[0], text); + return .{ .sent = bound[0] }; + } + // Several: asked on the pane's notice band, as Del asks, one key + // answering; nothing is remembered of the answer. + const owned = p.gpa.dupe(u8, text) catch return .none; + if (p.repl_ask) |old| p.gpa.free(old.text); + p.repl_ask = .{ .from = src.serial, .lang = lang, .text = owned }; + for (bound[0..n]) |i| p.repl_ask.?.offered[p.panes[i].?.repl.?.letter - 'a'] = p.panes[i].?.serial; + var w = std.Io.Writer.fixed(&src.question); + w.print("send to {s}:", .{grammars[lang].name}) catch {}; + for (bound[0..n]) |i| w.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {}; + w.writeAll(" (Esc: none)") catch {}; + src.question_len = @intCast(w.buffered().len); + src.prompt = .repl_choice; + p.active = from; + return .asked; +} + +/// Types `text` into the REPL in `to`, bracketed if its program asked for +/// that, then Enter; says so on `from`'s message row and in the log. +pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { + const dst = p.panes[to] orelse return; + const src = p.panes[from] orelse return; + var idbuf: [48]u8 = undefined; + const id = replId(&idbuf, dst); + // Lines end in \n alone (a \r\n would give Python a blank line that + // ends its block), and no ESC or other control byte but a tab goes: an + // ESC[201~ in the text would end a bracketed paste early. + const clean = p.scratch.allocator().alloc(u8, text.len) catch return; + var n: usize = 0; + for (text, 0..) |c, i| { + if (c == '\r') { + if (i + 1 < text.len and text[i + 1] == '\n') continue; + clean[n] = '\n'; + } else if (c < ' ' and c != '\t' and c != '\n' or c == 0x7f) continue else clean[n] = c; + n += 1; + } + // Sent whole or not at all: a send cut short would leave the REPL in a + // paste, or with half a block. + if (n + 16 > p.writeRoom(to)) { + var said: [128]u8 = undefined; + return p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {d} bytes is more than can be sent at once; send less", .{n}) catch "Repl: too long to send"); + } + edit.typeToTty(p, to, dst, clean[0..n]); + p.emitWrite(to, "\r"); + // Line by line, a blank line inside a Python block ends the block. + if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and + std.mem.indexOfScalar(u8, text, '\n') != null) + { + dst.repl_warned = true; + var warn: [128]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&warn, "Repl: {s} takes no bracketed paste; a blank line in a block ends it", .{id}) catch "Repl: no bracketed paste"); + } + var said: [64]u8 = undefined; + p.setMessage(from, std.fmt.bufPrint(&said, "\u{2192} {s}", .{id}) catch "sent"); + src.msg_sent = true; + var rec: [96]u8 = undefined; + pardes.ctlfs.events.notePath(p, "send", std.fmt.bufPrint(&rec, "{d} {d} {s}", .{ src.serial, dst.serial, id }) catch return); } +/// The longest line a command pane runs. +pub const command_max = 1024; + const max_exec_depth = 8; +/// A word no builtin knows (acme's exec.c run()). Clicked in an interactive +/// terminal, it is typed into that shell, whose state the click can see; +/// from anywhere else it runs as its own command pane, a terminal whose +/// child is `$SHELL -c` the line, which shows its output and `exit N` when +/// done, and which Kill signals. Answers the pane it went to. pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize { + return executeFrom(p, id, txt, false); +} + +/// `execute` for text a gesture took from `id`'s body when `from_body`: +/// such text may be a bound REPL's. Run by name (`Exec ...`, whose line +/// comes back through `execute`) or written as a command word, never. +pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usize { const pane = p.panes[id] orelse return null; + const gesture = from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null; const cmd = commandText(txt); if (cmd.len == 0) return null; if (p.exec_depth == 0) p.noteHaptic(.exec); @@ -397,16 +533,129 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize { if (p.exec_depth >= max_exec_depth) return null; p.exec_depth += 1; defer p.exec_depth -= 1; - const dst = (if (takesCommandLine(p, id)) id else ttyForDir(p, Pardes.paneDir(pane))) orelse return null; - panes.Terminal.padOutputBelowEdits(p, dst); - panes.Terminal.noteCommand(p.panes[dst].?, cmd); - if (panes.Terminal.queuePendingCommand(p.panes[dst].?, cmd) catch |err| { + if (gesture) switch (routeToRepl(p, id, cmd)) { + .none => {}, + .sent => |to| return to, + .asked => return null, + }; + if (!takesCommandLine(p, id)) return runCommand(p, id, cmd); + panes.Terminal.padOutputBelowEdits(p, id); + panes.Terminal.noteCommand(pane, cmd); + if (panes.Terminal.queuePendingCommand(pane, cmd) catch |err| { p.reportError(id, "queue command", err); - return dst; - }) return dst; - p.emitWrite(dst, cmd); - p.emitWrite(dst, "\r"); - return dst; + return id; + }) return id; + p.emitWrite(id, cmd); + p.emitWrite(id, "\r"); + return id; +} + +/// Runs `line` in a command pane for `from`'s directory: `from` itself if +/// it is one whose command is done, else the directory's command pane that +/// is done, else a new one below the last pane of the rightmost column. +/// A pane reused keeps what it showed: the new command runs below a line +/// naming it, as acme appends to +Errors and never clears it (util.c:213). +fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { + // The board and the web page start no processes: a command pane there + // would say running for ever, and fill the slots. + if (comptime !hosted) { + p.reportFailure(from, "exec: not a builtin, and this build runs no commands"); + return null; + } + if (line.len > command_max) { + p.reportFailure(from, "exec: a command line is at most 1024 bytes"); + return null; + } + const src = p.panes[from] orelse return null; + const dir = Pardes.paneDir(src); + // Not one whose pty a job it left behind still prints to: reusing it + // would hang that job up. + const reuse: ?usize = if (src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| { + const other = slot orelse continue; + if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir)) break i; + } else null; + if (reuse) |id| { + const pane = p.panes[id].?; + const owned = p.gpa.dupe(u8, line) catch |err| { + p.reportError(from, "exec", err); + return null; + }; + p.gpa.free(pane.command.?); + pane.command = owned; + pane.command_done = false; + pane.command_status = null; + pane.command_pty = true; + pane.body.mode = .tty; + // What the last program left the emulator in goes first: the + // alternate screen left (only if it is there: leaving restores a + // saved cursor), mouse reports and bracketed paste off, the cursor + // shown, colours reset. (Not DECSTR: ghostty's stream does not + // implement it.) + if (panes.Terminal.onAlternateScreen(pane)) panes.Terminal.feedOutput(p, pane, "\x1b[?1049l"); + panes.Terminal.feedOutput(p, pane, "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?25h\x1b[0m"); + echoCommand(p, pane, line); + p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); + noteRun(p, pane, "run", line); + return id; + } + const free = p.freeSlot() orelse { + p.reportFailure(from, "exec: no pane free for the command"); + return null; + }; + const pane = p.newCommand(free, dir, line) catch |err| { + p.reportError(from, "exec", err); + return null; + }; + echoCommand(p, pane, line); + const rc = if (p.ncol > 0) p.ncol - 1 else 0; + if (p.col_n[rc] > 0) { + const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]); + const f = layout.findPane(p, parent).?; + layout.splitBelow(p, parent, pane); + layout.insert(p, f.col, f.idx + 1, free); + } else layout.insert(p, rc, p.col_n[rc], free); + noteRun(p, pane, "run", line); + return free; +} + +/// `% line` above a command's output, every command, as a shell echoes +/// what it was given; its newlines the terminal's, so a multi-line command +/// does not staircase. +fn echoCommand(p: *Pardes, pane: *Pane, line: []const u8) void { + var buf: [2 * command_max + 8]u8 = undefined; + var w = std.Io.Writer.fixed(&buf); + w.writeAll(if (panes.Terminal.gridCursor(pane).x != 0) "\r\n% " else "% ") catch {}; + for (line) |c| (if (c == '\n') w.writeAll("\r\n") else w.writeByte(c)) catch {}; + w.writeAll("\r\n") catch {}; + panes.Terminal.feedOutput(p, pane, w.buffered()); +} + +/// `run <serial> <word>` or `exit <serial> <N|?>` in the log. +/// The whole line, one record as `msg` carries its text: its control +/// characters, a newline too, become spaces (events.pushLog). A new pane is +/// announced first, so `new` comes before the `run` in it. +fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void { + pardes.ctlfs.events.announce(p); + var buf: [command_max + 16]u8 = undefined; + pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, std.mem.trim(u8, said, " \t\r\n") }) catch return); +} + +/// A command pane's child is gone: the pane stays, showing what it said +/// and how it ended, and is the next command's for its directory. +pub fn commandDone(p: *Pardes, id: usize, status: ?u8) void { + const pane = p.panes[id] orelse return; + pane.command_done = true; + // A REPL it was is gone with its command; the pane's next is not one. + pane.repl = null; + pane.command_status = status; + pane.body.mode = .normal; + var buf: [16]u8 = undefined; + const code = if (status) |n| std.fmt.bufPrint(&buf, "{d}", .{n}) catch "?" else "?"; + var said: [32]u8 = undefined; + const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else ""; + panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&said, "{s}exit {s}\r\n", .{ lead, code }) catch "exit ?\r\n"); + noteRun(p, pane, "exit", code); + p.needs_frame = true; } /// Trim and unwrap clickable command-word notation once for every command @@ -420,7 +669,12 @@ pub fn commandText(txt: []const u8) []const u8 { pub fn executeBuiltinLine(p: *Pardes, id: usize, txt: []const u8) bool { const cmd = commandText(txt); if (cmd.len == 0 or p.exec_depth >= max_exec_depth) return false; - const sp = std.mem.indexOfAny(u8, cmd, " \t"); + // `Word arg`, or for a word that says so, `Word+arg` (`Tty+fish`). + const plus = std.mem.indexOfScalar(u8, cmd, '+'); + var sp = std.mem.indexOfAny(u8, cmd, " \t"); + if (sp == null) if (plus) |at| if (std.meta.stringToEnum(Builtin, cmd[0..at])) |head| { + if (builtins.registry.plusArg(head)) sp = at; + }; const bi: ?Builtin = std.meta.stringToEnum(Builtin, cmd) orelse blk: { const head = std.meta.stringToEnum(Builtin, cmd[0 .. sp orelse break :blk null]) orelse break :blk null; break :blk if (builtins.registry.takesArg(head)) head else null; @@ -441,6 +695,11 @@ pub fn applyStartupConfig(p: *Pardes) void { } pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void { + runBuiltinFrom(p, b, id, txt, arg, false); +} + +/// `runBuiltin` saying whether a gesture on the pane's body made it. +pub fn runBuiltinFrom(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8, from_body: bool) void { if (!normal.multiOnce(p)) return; // a builtin is per-keystroke, never per-cursor const pane = p.panes[id] orelse return; // A builtin says its own name before it runs, so a tagline click is @@ -450,7 +709,7 @@ pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]co p.setMessage(id, @tagName(b)); pane.msg_announcement = true; } - const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg }; + const c: builtins.Ctx = .{ .p = p, .pane = pane, .id = id, .txt = txt, .arg = arg, .from_body = from_body }; builtins.registry.dispatch(b, c); } @@ -467,6 +726,12 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] colors.setThemeIndex(p, i); return; }; + // A config line is not answered on screen (applyStartupConfig + // runs with announce off); a click or a ctl write is. + if (!p.announce) return; + var near: [96]u8 = undefined; + var text: [200]u8 = undefined; + return p.reportFailure(p.active, std.fmt.bufPrint(&text, "Theme: no theme \"{s}\"; like it: {s}; ThemeSel lists all", .{ want[0..@min(want.len, 40)], colors.themesNear(&near, want) }) catch "Theme: no such theme"); }, .font => { if (comptime !font_picker) return; @@ -476,7 +741,11 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[] if (!p.settings.requestFont(matches[0].path, matches[0].name, spec.size_hundredths)) return; p.font_request_taken = false; }, - else => _ = p.settings.apply(setting, arg), + else => if (!p.settings.apply(setting, arg) and p.announce) { + var text: [96]u8 = undefined; + const takes = if (setting.action == .toggle) "takes on or off" else "does not take that value"; + p.reportFailure(p.active, std.fmt.bufPrint(&text, "{s}: {s}", .{ setting.word, takes }) catch takes); + }, } if (p.settings.colors != previous_colors) { for (p.panes) |slot| { @@ -570,7 +839,7 @@ test "the acme chords act once per selection, not once on the primary" { .{ .anchor = 7, .head = 13 }, }; pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); - try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 1), pane.body.nsel); const before = p.ncol; p.update(.{ .key = .{ .cp = Key.tab } }); // config.exec_key: Exec @@ -578,7 +847,7 @@ test "the acme chords act once per selection, not once on the primary" { // other cursor, so one keystroke over two cursors made one column. try std.testing.expectEqual(before + 2, p.ncol); // ...and the chord consumed the selection exactly as it does with one. - try std.testing.expectEqual(@as(u8, 0), p.panes[0].?.body.nsel); + try std.testing.expectEqual(@as(u16, 0), p.panes[0].?.body.nsel); } test "a switch clicked with a selection held flips unless the selection says on or off" { diff --git a/src/fs-help.txt b/src/fs-help.txt index 689d8839..57ef7b62 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -5,18 +5,18 @@ index one line per pane: serial, kind (text|term|pdf|image), dirty flag, n status pid, version and pane count look write a line: a right click on it at the active pane; read: the serials it touched exec write a line: a middle click, an editor command word or a shell line; read the same -log events: new/del/rename/save <serial> <name>, msg <serial|-> <text>, err <serial|-> <why> +log events: new/del/rename/save <serial> <name>, run/exit/send <serial> ..., dump/restore <path>, msg, err screen rendered screen as JSON, frozen from open to close listeners the session's dial addresses focus the serial of the pane with the keyboard; write a serial to give it the keyboard -ctl settings, one a line as a write takes them; or Newcol, Dump, Exit (quits), Kill (stops cmds) +ctl settings, one a line; or Newcol, Dump, Exit (quits; asks once), Kill [word] (stops what pardes ran, or what starts with word) commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it pane/new open it to make a pane (named +New); the read answers that pane's serial pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only: appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes os/, src/ the host filesystem; the editor's own sources (-Dembed-sources=true builds only) -Below, $m is the mount (/mnt/9p/pardes/<pid> via 9ns --mntgen; PARDES_MOUNT in a Tty9p shell): +Below, $m is the mount: $NINE_MOUNT/pardes/<pid> when 9ns --mntgen set $NINE_MOUNT; PARDES_MOUNT in a Tty9p shell. cat $m/index which panes exist n=$(cat $m/pane/new) make a pane, take its serial @@ -35,11 +35,11 @@ Below, $m is the mount (/mnt/9p/pardes/<pid> via 9ns --mntgen; PARDES_MOUNT in a Pitfalls, one each: Each open of pane/new makes another pane (ls, stat, find never do); rmdir closes even a dirty one. - addr takes #n,#m or sam-style /regexp/ (^ $ per line); a failed one makes data refuse until reset. + addr takes #n,#m or /regexp/ from the current address (^ $ per line); a failed one refuses data. data: > replaces the addr range, : > deletes it, a 2nd > inserts (addr moved); body: > empties all. - tag reads the path, then the tag's own text; writes append to that text; truncating clears it. + tag reads the path, then its own text; writes append; truncating clears it, default words too. A terminal's body is a history snapshot frozen per open; pty/data is the live stream. - exec: a word no builtin knows is typed into a terminal in the pane's directory, and not an error. - A refused write reads as EINVAL through a mount: the log's err record says why. - lock/unlock on a pane's ctl need a held fd (exec 3>ctl); a lock someone holds fails: retry. - Closing a column's last pane leaves a +New in its place; Delcol closes the column. + exec: a non-builtin line runs as a command pane (ctl's Shell -c; exit N shown, logged), a shell's typed in. + Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why. + lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane -> +New. + Repl python on a terminal's ctl: a .py body's clicks go to it (Repl - unbinds, Repl says it); tags stay commands. @@ -1261,11 +1261,11 @@ pub const Namespace = struct { /// ctl answers a command's error (editors/acme/xfid.c:700). no_prompt: bool = false, refused: bool = false, - failure: [96]u8 = undefined, - failure_len: u8 = 0, + failure: [256]u8 = undefined, + failure_len: u16 = 0, /// A refusal that quotes the message it refuses, as Plan 9's cmderror /// does (kernel/misc/parse.c:82); answered at once (src/9p_io.zig). - ename: [128]u8 = undefined, + ename: [320]u8 = undefined, /// Something a held read may be waiting on changed since they were last /// answered: a record queued, a run answered, a pane gone. news: bool = false, diff --git a/src/grammar_manifest.zig b/src/grammar_manifest.zig index 0303af17..d6666729 100644 --- a/src/grammar_manifest.zig +++ b/src/grammar_manifest.zig @@ -2,6 +2,8 @@ //! runtime syntax registry. Keep target symbols and query values out of this //! file: the build runner imports it as host code. +const std = @import("std"); + pub const Tier = enum { zig, minimal, full }; pub const Grammar = struct { @@ -49,3 +51,44 @@ pub const all = [_]Grammar{ .{ .name = "typst", .dep = "ts_typst", .exts = &.{ ".typ", ".typst" }, .tier = .full, .scanner = true, .query = "queries/typst/highlights.scm" }, .{ .name = "zig", .dep = "ts_zig", .exts = &.{ ".zig", ".zon" }, .tier = .zig }, }; + +/// The names a language goes by besides its grammar's: a fence's `py`, +/// a `Repl sh`. +pub const aliases = [_]struct { []const u8, []const u8 }{ + .{ "js", "javascript" }, + .{ "jsx", "javascript" }, + .{ "mjs", "javascript" }, + .{ "py", "python" }, + .{ "sh", "bash" }, + .{ "shell", "bash" }, + .{ "zsh", "bash" }, + .{ "bash", "bash" }, + .{ "rs", "rust" }, + .{ "c++", "cpp" }, + .{ "cxx", "cpp" }, + .{ "cc", "cpp" }, + .{ "cs", "c_sharp" }, + .{ "kt", "kotlin" }, + .{ "rb", "ruby" }, + .{ "ml", "ocaml" }, + .{ "hs", "haskell" }, +}; + +/// The grammar a language name means: its own name or an alias, in any case. +pub fn byName(name: []const u8) ?usize { + var canonical = name; + for (aliases) |a| if (std.ascii.eqlIgnoreCase(name, a[0])) { + canonical = a[1]; + break; + }; + for (all, 0..) |g, i| if (std.ascii.eqlIgnoreCase(canonical, g.name)) return i; + return null; +} + +/// The grammar a path's extension selects, in any case. +pub fn byPath(path: []const u8) ?usize { + const ext = std.fs.path.extension(path); + if (ext.len == 0) return null; + for (all, 0..) |g, i| for (g.exts) |e| if (std.ascii.eqlIgnoreCase(ext, e)) return i; + return null; +} diff --git a/src/gui/gui.zig b/src/gui/gui.zig index aa39cf32..5bc1ea6d 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -816,8 +816,17 @@ const Pty = struct { kill_at: i64 = 0, reader: ?std.Thread = null, stop: [2]c_int = .{ -1, -1 }, + /// A command pane's child, watched to its exit (host_io.watchExit); + /// the pty stays open until both its end of file and its exit. + cmd: host_io.CommandWatch = .{}, }; +/// A watched child exited: wake the loop, which takes it (`takeExits`). +fn wakeForExit(ctx: ?*anyopaque) void { + const q: *Queue = @ptrCast(@alignCast(ctx.?)); + q.wake(); +} + const RetiredShell = struct { pid: libc.pid_t = 0, kill_at: i64 = 0 }; const Msg = union(enum) { @@ -1298,7 +1307,7 @@ test "GUI PTY Restore joins a real reader waiting for queue space" { try std.testing.expect(host_io.writeFd(child.file.handle, "printf 'after-full'; exit\n")); try PtyTests.waitBlocked(&queue); const old_serial = core.panes[0].?.serial; - const replacement = try dump.restore(core, core.dump_out.?); + const replacement = try dump.restore(core, core.dump_out.?, "test"); shell.stopPtys(); core.deinit(); core = replacement; @@ -1627,7 +1636,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); - try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "WorkersBusy") != null); + try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "workers busy") != null); try std.testing.expectEqual(@as(usize, 0), failing.alloc_index); try std.testing.expect(!failing.has_induced_failure); try std.testing.expectEqual(rejected, queue.lsp_id.?); @@ -2617,6 +2626,8 @@ fn localSession( shell.threads_ok = true; pardes.lsp.setStatusSink(&queue, lspStatusSink); defer pardes.lsp.setStatusSink(null, null); + host_io.setExitWake(&queue, wakeForExit); + defer host_io.setExitWake(null, null); while (!core.quit) { pardes.turn.restoreSettled(); @@ -2642,7 +2653,7 @@ fn localSession( break :blk; }; defer gpa.free(bytes); - const nc = dump.restore(core, bytes) catch |err| { + const nc = dump.restore(core, bytes, rp) catch |err| { core.reportError(core.active, "Restore", err); break :blk; }; @@ -2942,6 +2953,8 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void { shell.threads_ok = true; pardes.lsp.setStatusSink(&queue, lspStatusSink); defer pardes.lsp.setStatusSink(null, null); + host_io.setExitWake(&queue, wakeForExit); + defer host_io.setExitWake(null, null); setStdinRaw() catch {}; // stdin may be a pipe, not a pty — best effort @@ -3999,8 +4012,20 @@ const Shell = struct { break :vt v; }; + /// The command panes' exits, told once their output is in (host_io). + fn takeExits(s: *Shell) void { + host_io.takeExits(s.core, s.ptys, s, closeWatched); + } + + fn closeWatched(s: *Shell, id: usize) void { + s.closePty(@intCast(id)); + s.saw_event = true; + } + fn drainQueue(s: *Shell) void { s.reconcilePtys(); + // After this batch's output, which an exit is told behind. + defer s.takeExits(); var msgs = s.queue.take(); var check_files = false; for (msgs.slice()) |m| switch (m) { @@ -4011,14 +4036,36 @@ const Shell = struct { s.saw_event = true; }, .eof => |e| { - if (s.gens[e.pane] == e.gen) { - s.closePty(e.pane); + if (s.gens[e.pane] == e.gen) eof: { if (e.failure) |err| { + s.closePty(e.pane); if (s.core.panes[e.pane]) |pane| { pane.body.mode = .normal; s.core.reportError(e.pane, "terminal reader", err); } - } else s.core.update(.{ .eof = .{ .pane = e.pane } }); + break :eof; + } + const pt = if (s.ptys[e.pane]) |*pt| pt else { + s.core.update(.{ .eof = .{ .pane = e.pane } }); + break :eof; + }; + // A command's pty stays open until its child has exited: + // closing it would hang up one that runs on without it. + // Its exit, if it came first, is told now its output is in. + if (pt.cmd.watched) { + host_io.commandEof(s.core, s.ptys, e.pane, s, closeWatched); + break :eof; + } + // Unwatched, a command's exit is read here, as its end. + const unwatched = if (s.core.panes[e.pane]) |pane| pane.command != null else false; + var status: ?u8 = null; + if (unwatched) { + status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire + } + s.closePty(e.pane); + if (unwatched) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); + s.core.update(.{ .eof = .{ .pane = e.pane } }); } s.saw_event = true; }, @@ -4325,7 +4372,8 @@ fn spawnPane(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { if (s.ptys[pane] != null) return s.core.reportError(pane, "shell", error.WorkersBusy); s.gens[pane] +%= 1; const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err); - const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial }; + const command = s.core.panes[pane].?.command != null; + const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; s.ptys[pane] = pt; var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; if (host_io.shellCwd(pt.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); @@ -4353,7 +4401,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const s = shellOf(ctx); - if (s.ptys[pane]) |pt| if (pt.fd >= 0) host_io.signalTty(pt.pid, pt.fd, sig); + // A command whose exit is recorded is reaped: its group may be another's. + if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { @@ -4491,7 +4540,7 @@ test "the headless grid host round-trips a yank back as a paste" { core.update(.{ .key = .{ .cp = ' ' } }); core.update(.{ .key = .{ .cp = 'y' } }); while (core.nextEffect()) |e| core.perform(e); - const yanked = core.yank orelse return error.MissingYank; + const yanked = core.registers.text(core.gpa, '+') orelse return error.MissingYank; try std.testing.expect(yanked.len > 0); const before = pane.file.?.content.len; diff --git a/src/host_io.zig b/src/host_io.zig index f1a4eb98..a019e15d 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -760,8 +760,13 @@ pub const Shell = struct { return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } }; } - fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 { + /// A shell by its path or its name in the usual directories, or null. + /// A path may be under a mount this editor serves: the turn goes out + /// with the lookups (pardes.Turn.yield). + pub fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 { if (bin.len == 0 or bin.len + 1 > buf.len) return null; + pardes.turn.yield(); + defer pardes.turn.back(); if (std.mem.indexOfScalar(u8, bin, '/') != null) { @memcpy(buf[0..bin.len], bin); buf[bin.len] = 0; @@ -966,6 +971,9 @@ test "the child environment replaces the launcher's terminal identity exactly on extern "c" fn unsetenv(name: [*:0]const u8) c_int; +/// The longest line a command pane runs (exec.zig refuses a longer one). +pub const exec_command_max = pardes.exec.command_max; + pub fn forkShell( core: ?*pardes.Pardes, pane: usize, @@ -979,6 +987,7 @@ pub fn forkShell( const native_cwd = filesystem.localPath(cwd) orelse cwd; if (std.mem.indexOfScalar(u8, native_cwd, 0) != null) return error.InvalidPath; var cwd_buf: [4096]u8 = undefined; + const serial = if (core) |c| if (c.panes[pane]) |pn| pn.serial else 0 else 0; const cwd_z: ?[:0]const u8 = if (native_cwd.len == 0) null else dir: { const path = std.fmt.bufPrintSentinel(&cwd_buf, "{s}", .{native_cwd}, 0) catch return error.NameTooLong; // A shell's directory may be inside a mount this editor serves. @@ -990,9 +999,32 @@ pub fn forkShell( }; var master: c_int = -1; var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const spawn = Shell.resolve(bin, &path_buf, prompt_rcs); + // A terminal opened on a shell of its own (`Tty fish`) runs that one, + // copied: looking it up gives the turn up, and the pane may go. + var own_buf: [256]u8 = undefined; + var own: ?[]const u8 = null; + if (core) |c| if (c.panes[pane]) |pn| if (pn.shell) |name| { + const n = @min(name.len, own_buf.len); + @memcpy(own_buf[0..n], name[0..n]); + own = own_buf[0..n]; + }; + var spawn = Shell.resolve(own orelse bin, &path_buf, prompt_rcs); + // The turn was given up for the stat and the lookup: a 9P client may + // have closed the pane, and another taken its slot, whose command this + // is not. + if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone; + // A command pane's child is the shell running its one line, which has + // no prompt to mark (exec.zig runCommand). + const one_line = if (core) |c| if (c.panes[pane]) |pn| pn.command else null else null; + var command_buf: [exec_command_max + 1]u8 = undefined; + if (one_line) |line| { + if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand; + @memcpy(command_buf[0..line.len], line); + command_buf[line.len] = 0; + spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null }; + } var helper_buf: [4096]u8 = undefined; - const mounted = if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false; + const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false; const helper: ?[:0]u8 = if (mounted) blk: { if (comptime builtin.os.tag != .linux) return error.LinuxRequired; if (fs == null) return error.SessionSocketRequired; @@ -1033,7 +1065,7 @@ pub fn forkShell( _exit(127); } ninep_io.setCloexec(master); - if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), spawn.argv[1] != null); + if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), one_line == null and spawn.argv[1] != null); return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid }; } @@ -1366,6 +1398,185 @@ pub fn retireShell(pid: libc.pid_t) void { thread.detach(); } +// ---- a command pane's child, watched to its exit ---- +// +// A command pane's command is done when its process exits, not when its pty +// reaches end of file: a job it put in the background can hold the pty open +// after it has gone, and a command can close its terminal and run on. A +// thread per command waits for the exit without reaping (WNOWAIT), so the +// pid is still the child's -- no Kill can reach another process that took +// it -- until the host, told through `takeExited`, reaps it in its own loop +// with `exitStatus` and tells the core. + +const waitid_p_pid: c_int = 1; +const waitid_wexited: c_int = 4; +const waitid_wnowait: c_int = if (builtin.os.tag == .linux) 0x01000000 else 0x20; +extern "c" fn waitid(idtype: c_int, id: c_uint, infop: *anyopaque, options: c_int) c_int; + +var exited: [pardes.MAX_PANES * 2]std.atomic.Value(libc.pid_t) = @splat(.init(0)); +var exit_lock: libc.pthread_mutex_t = .{}; +var exit_wake: ?struct { ctx: ?*anyopaque, f: *const fn (?*anyopaque) void } = null; + +/// Who wakes the host's loop when a watched child exits; null at shutdown, +/// after which a watcher still waiting tells nobody. +pub fn setExitWake(ctx: ?*anyopaque, f: ?*const fn (?*anyopaque) void) void { + _ = libc.pthread_mutex_lock(&exit_lock); + defer _ = libc.pthread_mutex_unlock(&exit_lock); + exit_wake = if (f) |wake| .{ .ctx = ctx, .f = wake } else null; +} + +/// Watches a command pane's child; false when no thread could be started, +/// and the host then reads its exit at end of file instead. +pub fn watchExit(pid: libc.pid_t) bool { + const thread = std.Thread.spawn(.{}, watchChild, .{pid}) catch return false; + thread.detach(); + return true; +} + +fn watchChild(pid: libc.pid_t) void { + var info: [128]u8 align(16) = undefined; + while (waitid(waitid_p_pid, @intCast(pid), &info, waitid_wexited | waitid_wnowait) < 0 and libc.errno(-1) == .INTR) {} + // A cell for it, waiting for one if the host is slow to take them. + place: while (true) { + for (&exited) |*cell| if (cell.cmpxchgStrong(0, pid, .acq_rel, .acquire) == null) break :place; + const ts: libc.timespec = .{ .sec = 0, .nsec = 5 * std.time.ns_per_ms }; + _ = libc.nanosleep(&ts, null); + } + wakeForExit(); +} + +fn wakeForExit() void { + _ = libc.pthread_mutex_lock(&exit_lock); + defer _ = libc.pthread_mutex_unlock(&exit_lock); + if (exit_wake) |wake| wake.f(wake.ctx); +} + +/// Where a host's command pane is between its child's exit and the end of +/// its output. The exit is told once the output before it is in: at the +/// pty's end of file; or, when something else still holds the pty (a job +/// the command left in the background), once nothing is waiting to be read +/// -- checked when the exit is seen and again after each chunk of output, +/// with no timer to miss. acme's waitthread tells an exit when it is +/// reaped (acme.c:587-700); a terminal also has output in flight. +pub const CommandWatch = struct { + watched: bool = false, + /// The master, to ask whether output is still waiting. + fd: c_int = -1, + eof: bool = false, + exited: bool = false, + told: bool = false, + status: ?u8 = null, + + /// Whether the exit is to be told now. + pub fn due(w: *const CommandWatch) bool { + if (!w.exited or w.told) return false; + if (w.eof) return true; + var pfd = [1]libc.pollfd{.{ .fd = w.fd, .events = libc.POLL.IN, .revents = 0 }}; + if (libc.poll(&pfd, 1, 0) < 0) return true; + // Every holder of its terminal gone: the end of file comes, after + // the rest of the output. + if (pfd[0].revents & libc.POLL.HUP != 0) return false; + return pfd[0].revents & libc.POLL.IN == 0; + } +}; + +/// A host's pty slot, whichever way it keeps it: an optional, or one whose +/// `fd` is -1 when empty. +fn SlotPty(comptime S: type) type { + return switch (@typeInfo(S)) { + .optional => |o| *o.child, + else => *S, + }; +} + +fn slotPty(slot: anytype) ?SlotPty(@TypeOf(slot.*)) { + return switch (@typeInfo(@TypeOf(slot.*))) { + .optional => if (slot.*) |*pt| pt else null, + else => if (slot.fd < 0) null else slot, + }; +} + +/// Each watched child that exited: reaped, and the core told once its +/// output is in, its pty closed (`close(ctx, id)`) if its end of file came +/// first. Every host runs this after an exit's wake and after each chunk +/// of a command's output; `ptys` is its slot array, each slot with a `pid` +/// and a `cmd`. +pub fn takeExits(core: *pardes.Pardes, ptys: anytype, ctx: anytype, comptime close: anytype) void { + while (takeExited()) |pid| for (ptys) |*slot| { + const pt = slotPty(slot) orelse continue; + if (!pt.cmd.watched or pt.pid != pid or pt.cmd.exited) continue; + pt.cmd.status = (reapExited(pid) orelse break).status; + pt.cmd.exited = true; + pt.pid = 0; // reaped: no signal or retire may reach whoever gets it next + break; + }; + for (ptys, 0..) |*slot, id| { + const pt = slotPty(slot) orelse continue; + if (!pt.cmd.watched or !pt.cmd.due()) continue; + pt.cmd.told = true; + const eof = pt.cmd.eof; + core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } }); + if (eof) close(ctx, id); + } +} + +/// A watched command's pty reached end of file: the core hears its output +/// is over; the pty is closed now if its exit was told, else when it is. +pub fn commandEof(core: *pardes.Pardes, ptys: anytype, id: usize, ctx: anytype, comptime close: anytype) void { + const pt = slotPty(&ptys[id]) orelse return; + pt.cmd.eof = true; + core.update(.{ .eof = .{ .pane = @intCast(id) } }); + if (pt.cmd.told) return close(ctx, id); + takeExits(core, ptys, ctx, close); +} + +/// Reaps `pid` if it has exited: its status (null: unknown, or not ours +/// to read), or null when it is still running -- a pid a watcher reported +/// may since have been reaped by another and taken by a new child. +pub fn reapExited(pid: libc.pid_t) ?struct { status: ?u8 } { + if (pid <= 0) return .{ .status = null }; + var status: c_int = 0; + const got = libc.waitpid(pid, &status, libc.W.NOHANG); + if (got == 0) return null; + if (got != pid) return .{ .status = null }; + const s: u32 = @bitCast(status); + if (libc.W.IFEXITED(s)) return .{ .status = @truncate(libc.W.EXITSTATUS(s)) }; + if (libc.W.IFSIGNALED(s)) return .{ .status = @truncate(128 + @as(u32, @intFromEnum(libc.W.TERMSIG(s)))) }; + return .{ .status = null }; +} + +/// A watched child that has exited, not yet reaped, or null. +pub fn takeExited() ?libc.pid_t { + for (&exited) |*cell| { + const pid = cell.swap(0, .acq_rel); + if (pid != 0) return pid; + } + return null; +} + +/// A pane's child at its pty's end of file: reaped, and how it exited +/// (its code, or 128 and the signal that ended it), if it is gone within +/// `wait_ms`; null if it is still running (a job it left holds nothing +/// open, but the child itself may be a moment behind its pty), which the +/// caller then retires as it would any shell. +pub fn exitStatus(pid: libc.pid_t, wait_ms: u32) ?u8 { + if (pid <= 0) return null; + var waited: u32 = 0; + while (true) : (waited += 2) { + var status: c_int = 0; + const got = libc.waitpid(pid, &status, libc.W.NOHANG); + if (got == pid) { + const s: u32 = @bitCast(status); + if (libc.W.IFEXITED(s)) return @truncate(libc.W.EXITSTATUS(s)); + if (libc.W.IFSIGNALED(s)) return @truncate(128 + @as(u32, @intFromEnum(libc.W.TERMSIG(s)))); + return null; + } + if (got < 0 or waited >= wait_ms) return null; + const ts: libc.timespec = .{ .sec = 0, .nsec = 2 * std.time.ns_per_ms }; + _ = libc.nanosleep(&ts, null); + } +} + fn reapShell(pid: libc.pid_t, grace_ms: u32) void { var waited: u32 = 0; while (waited < grace_ms) : (waited += 5) { @@ -1669,7 +1880,7 @@ fn haveFile(path: [*:0]const u8) bool { return true; } -fn nowMs() i64 { +pub fn nowMs() i64 { var ts: libc.timespec = undefined; _ = libc.clock_gettime(.MONOTONIC, &ts); return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000); @@ -1728,6 +1939,61 @@ test "Kill stops the foreground job, never the shell, and finds none with job co try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG)); } +test "a command pane's child is its shell running the one line, and its exit status is read at the end" { + if (comptime !tty_probe_platform) return error.SkipZigTest; + const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + const id = core.freeSlot().?; + _ = try core.newCommand(id, "", "printf 'said '; exit 7"); + while (core.nextEffect()) |_| {} + const rcs: Shell.PromptFiles = .{}; + const child = try forkShell(core, id, &rcs, "/bin/sh", "", 24, 80, null); + defer _ = libc.close(child.file.handle); + var got: [256]u8 = undefined; + var n: usize = 0; + while (n < got.len) { + const r = libc.read(child.file.handle, got[n..].ptr, got.len - n); + if (r <= 0) break; // EIO once the child is gone: the pty's end + n += @intCast(r); + } + try std.testing.expect(std.mem.indexOf(u8, got[0..n], "said") != null); + try std.testing.expectEqual(@as(?u8, 7), exitStatus(child.pid, 2000)); +} + +test "a command's exit is its process's, told by its watcher, whether or not its pty has ended" { + if (comptime !tty_probe_platform) return error.SkipZigTest; + const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + const rcs: Shell.PromptFiles = .{}; + const Case = struct { line: []const u8, status: u8 }; + // A job left in the background holds the pty open past the command's + // exit; a command that lets go of its terminal runs on past its end of + // file. Either way the exit is the process's. + for ([_]Case{ + .{ .line = "sleep 3 & exit 4", .status = 4 }, + .{ .line = "exec </dev/null >/dev/null 2>&1; sleep 0.3; exit 5", .status = 5 }, + }) |c| { + const id = core.freeSlot().?; + _ = try core.newCommand(id, "", c.line); + while (core.nextEffect()) |_| {} + const child = try forkShell(core, id, &rcs, "/bin/sh", "", 24, 80, null); + defer _ = libc.close(child.file.handle); + try std.testing.expect(watchExit(child.pid)); + var got: ?libc.pid_t = null; + var waited: i64 = 0; + while (got == null and waited < 5000) : (waited += 10) { + got = takeExited(); + if (got == null) sleepMs(10); + } + try std.testing.expectEqual(@as(?libc.pid_t, child.pid), got); + // Told before it is reaped: the pid is still the child's. + try std.testing.expectEqual(@as(?u8, c.status), reapExited(child.pid).?.status); + try core.removePane(id, null); + } +} + test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" { const pid = libc.fork(); if (pid == 0) { diff --git a/src/layout.zig b/src/layout.zig index b642c52c..f0e6fd16 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -297,7 +297,7 @@ pub fn removePane(p: *Pardes, id: usize) void { p.col_serial[j] = p.col_serial[j + 1]; } p.ncol -= 1; - p.col_tags[p.ncol] = .{ .what = .column }; + p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa }; p.col_serial[p.ncol] = 0; } } @@ -321,7 +321,7 @@ pub fn joinCol(p: *Pardes) void { p.col_serial[j] = p.col_serial[j + 1]; } p.ncol -= 1; - p.col_tags[p.ncol] = .{ .what = .column }; + p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa }; p.col_serial[p.ncol] = 0; } @@ -350,7 +350,7 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool { if (needs_rebase) old_weight *= 2; if (id == source_id) { if (p.col_n[source_col] <= 1) return false; - absorbVWeight(p, id, null); + _ = absorbVWeight(p, id, null); removePane(p, id); } else if (findPane(p, id) != null) return false; @@ -371,7 +371,7 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool { p.col_serial[j] = p.col_serial[j - 1]; } p.col_weight[c] = new_weight; - p.col_tags[c] = .{ .what = .column }; + p.col_tags[c] = .{ .what = .column, .gpa = p.gpa }; p.col_serial[c] = 0; p.col_panes[c][0] = id; p.col_n[c] = 1; @@ -391,9 +391,11 @@ pub fn snapColWeights(p: *Pardes, c: usize) void { /// Hand a closing pane's rows to `to`. With none named they go up: past any /// results listing to the nearest pane above that is not one, or to the /// pane below when the closing one is first in its column. -pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void { - const f = findPane(p, id) orelse return; - if (p.col_n[f.col] <= 1) return; +/// Give a closing pane's rows to `to`, or to the nearest document above it +/// (the pane below for the top one); the pane that got them is returned. +pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) ?usize { + const f = findPane(p, id) orelse return null; + if (p.col_n[f.col] <= 1) return null; snapColWeights(p, f.col); var sib = to orelse if (f.idx > 0) p.col_panes[f.col][f.idx - 1] else p.col_panes[f.col][f.idx + 1]; var k = f.idx; @@ -401,7 +403,9 @@ pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void { sib = p.col_panes[f.col][k - 1]; if (p.panes[sib]) |pp| if (if (pp.file) |ff| panes.Output.fileTraits(ff.output).doc else true) break; } - if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h))); + const s = p.panes[sib] orelse return null; + s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h))); + return sib; } pub fn splitParent(p: *Pardes, want: usize) usize { diff --git a/src/look.zig b/src/look.zig index 258cf8f5..2e27a001 100644 --- a/src/look.zig +++ b/src/look.zig @@ -503,7 +503,11 @@ const SearchStart = enum { top, cursor }; pub fn submitSearch(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; - if (normal.selRegexArmed(pane)) |a| return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split); + if (normal.selRegexArmed(pane)) |a| { + // helix keeps the pattern in `/` + if (a.pat.len > 0) p.registers.put(p.gpa, '/', a.pat, 0, 1, true); + return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.mode); + } const armed = pane.promptText() orelse return; const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return; if (std.mem.startsWith(u8, armed, config.rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]); @@ -1013,7 +1017,7 @@ pub fn clearNavigationSelection(p: *Pardes, pane: *Pane) void { pane.body.nsel = 0; pane.body.select = false; pane.body.normal.clear(); - pane.body.append_at = null; + pane.body.restore_cursor = false; pane.body.sticky_col = -1; for (0..pane.sel.len) |slot| pane.clearPointerSelection(slot); pane.sel = @splat(.{}); @@ -1163,6 +1167,13 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { pane.search_row = null; _ = searchStep(p, id, 1); } + // A path that is not there searches for its text, and a search + // that found nothing says so rather than leave an empty buffer. + const results = if (pane.search_pane) |rid| p.panes[rid] else null; + if (results) |r| if (r.file) |f| if (f.content.len == 0) { + var said: [96]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match"); + }; }, .pane => |t| { if (t.id >= MAX_PANES) return; diff --git a/src/macos.zig b/src/macos.zig index 6cd7838f..b472e6cc 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -125,8 +125,30 @@ const Pty = struct { pid: posix.pid_t, gen: u32, reader: std.Io.Future(anyerror!void), + /// A command pane's child, watched to its exit (host_io.watchExit); + /// the pty stays open until both its end of file and its exit. + cmd: host_io.CommandWatch = .{}, }; +/// A watched child exited: wake the host, which takes it (`takeExits`). +/// Called on a watcher thread holding host_io's exit lock, which +/// pardes_deinit takes to clear the wake: safe only because the runtime's +/// wakeup never waits for the main thread (AppDelegate's is a +/// DispatchQueue.main.async); one that did would deadlock the quit. +fn wakeForExit(ctx: ?*anyopaque) void { + const st: *State = @ptrCast(@alignCast(ctx orelse return)); + wake(st); +} + +/// The command panes' exits, told once their output is in (host_io). +fn takeExits(st: *State) void { + host_io.takeExits(st.core, &st.ptys, st, closeWatched); +} + +fn closeWatched(st: *State, id: usize) void { + reap(st, @intCast(id)); +} + const WatchedFile = struct { path: []u8, serial: u32, @@ -782,6 +804,7 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void { for (&st.ptys, 0..) |*slot, id| if (slot.*) |*pt| startReader(st, pt, @intCast(id)); pardes.lsp.setStatusSink(st, lspStatusSink); + host_io.setExitWake(st, wakeForExit); // From here the core is touched only inside the entry points above, // each of which takes the turn for its own duration; between them the // 9P connections have it. @@ -801,6 +824,7 @@ export fn pardes_deinit() void { st.ninep = null; } pardes.lsp.setStatusSink(null, null); + host_io.setExitWake(null, null); if (st.lsp_task) |*t| { t.future.cancel(st.io) catch {}; st.lsp_task = null; @@ -1268,6 +1292,8 @@ export fn pardes_watch_changed(pane: u8, generation: u32) void { fn drainInbox(st: *State) bool { var batch = st.inbox.take(st.io); + // After this batch's output, which an exit is told behind. + defer takeExits(st); var did = batch.len > 0; for (batch.slice()) |msg| { defer msg.free(st.gpa); @@ -1279,6 +1305,24 @@ fn drainInbox(st: *State) bool { }, .eof => |e| { if (st.gens[e.pane] != e.gen) continue; + if (st.ptys[e.pane]) |*pt| { + // A command's pty stays open until its child has exited: + // closing it would hang up one that runs on without it. + // Its exit, if it came first, is told now its output is in. + if (pt.cmd.watched) { + host_io.commandEof(st.core, &st.ptys, e.pane, st, closeWatched); + continue; + } + // Unwatched, a command's exit is read here, as its end. + if (st.core.panes[e.pane]) |pane| if (pane.command != null) { + const status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire + reap(st, e.pane); + st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); + st.core.update(.{ .eof = .{ .pane = e.pane } }); + continue; + }; + } reap(st, e.pane); st.core.update(.{ .eof = .{ .pane = e.pane } }); }, @@ -1344,7 +1388,7 @@ fn restoreCore(st: *State) bool { return false; }; defer st.gpa.free(bytes); - const replacement = dump.restore(st.core, bytes) catch |err| { + const replacement = dump.restore(st.core, bytes, path) catch |err| { st.core.reportError(st.core.active, "Restore", err); return false; }; @@ -2403,6 +2447,7 @@ fn spawnShell(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { .pid = child.pid, .gen = gen, .reader = .{ .any_future = null, .result = {} }, + .cmd = .{ .watched = (if (core.panes[pane]) |pn| pn.command != null else false) and host_io.watchExit(child.pid), .fd = child.file.handle }, }; var lbuf: [1024]u8 = undefined; if (host_io.shellCwd(child.pid, &lbuf)) |wd| core.setCwd(pane, wd); @@ -2423,7 +2468,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const st = hostState(ctx); - if (st.ptys[pane]) |pt| host_io.signalTty(pt.pid, pt.file.handle, sig); + // A command whose exit is recorded is reaped: its group may be another's. + if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { diff --git a/src/memory.zig b/src/memory.zig index 7b6ee2c0..abfd40f6 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -17,6 +17,9 @@ pub const limits = struct { pub const wrap_rows = if (board) 128 else 256; pub const undo_max = if (board) 16 else 256; pub const message_log = if (board) 16 else 128; + /// ranges one selection holds; helix has no limit, and a `%s` over a + /// file's words wants hundreds + pub const selections = if (board) 64 else 1024; /// /log's ring, which records whether or not anyone reads it. pub const log_bytes: usize = if (board) 4 * KiB else 64 * KiB; pub const max_tag_tail: usize = if (board) 512 else 4096; @@ -187,6 +190,7 @@ test "memory limits preserve desktop capacities" { try std.testing.expectEqual(256, limits.wrap_rows); try std.testing.expectEqual(256, limits.undo_max); try std.testing.expectEqual(128, limits.message_log); + try std.testing.expectEqual(1024, limits.selections); try std.testing.expectEqual(@as(usize, 4096), limits.max_tag_tail); try std.testing.expectEqual(@as(usize, 4095), limits.host_path_cap); try std.testing.expectEqual(config.embed_sources, limits.embedded_sources); diff --git a/src/modal.zig b/src/modal.zig index 3827d11f..cc7f1ebd 100644 --- a/src/modal.zig +++ b/src/modal.zig @@ -15,6 +15,8 @@ pub const Normal = struct { prefix_replace, prefix_next, prefix_prev, + prefix_register, + prefix_view_sticky, goto_file_start, goto_last_line, @@ -27,6 +29,8 @@ pub const Normal = struct { goto_view_top, goto_view_center, goto_view_bottom, + goto_last_modification, + goto_word, goto_definition, goto_declaration, goto_type_definition, @@ -36,6 +40,7 @@ pub const Normal = struct { view_top, view_center, view_bottom, + view_middle, view_scroll_down, view_scroll_up, @@ -49,6 +54,13 @@ pub const Normal = struct { add_newline, goto_diagnostic, goto_diagnostic_end, + goto_function, + goto_class, + goto_parameter, + goto_comment, + goto_test, + goto_entry, + goto_xml_element, move_left, move_right, @@ -96,6 +108,17 @@ pub const Normal = struct { trim_sels, select_regex, split_regex, + keep_regex, + remove_regex, + ensure_forward, + expand_node, + shrink_node, + next_sibling, + prev_sibling, + all_siblings, + all_children, + parent_node_end, + parent_node_start, delete, delete_noyank, @@ -108,6 +131,10 @@ pub const Normal = struct { to_lowercase, to_uppercase, join_lines, + join_select_space, + rotate_contents_fwd, + rotate_contents_back, + align_selections, indent, unindent, format, @@ -123,6 +150,10 @@ pub const Normal = struct { pipe_selection_to, insert_output, append_output, + keep_pipe, + macro_record, + macro_replay, + repeat_insert, search, search_next, search_prev, @@ -156,6 +187,7 @@ pub const Normal = struct { replace, next, prev, + register, }; pub const MatchSub = enum(u8) { @@ -172,9 +204,14 @@ pub const Normal = struct { prefix: Prefix = .none, match_sub: MatchSub = .none, held_char: u21 = 0, + /// `"<reg>`: the register the next command names, 0 for none + register: u21 = 0, + /// `Z`: the view keys stay armed until Esc (helix's sticky view) + sticky: bool = false, + /// Drop what is pending; sticky view mode stays armed through it. pub fn clear(state: *State) void { - state.* = .{}; + state.* = if (state.sticky) .{ .sticky = true, .prefix = .view } else .{}; } }; @@ -188,13 +225,22 @@ pub const Normal = struct { insert, /// `A-!` — no stdin, and the output is appended AFTER each selection. append, + /// `$` — stdin is the selection; the output is discarded, and only the + /// selections the command exits 0 on are kept. + keep, /// Do the selections become stdin? helix's `pipe` flag. pub fn pipes(b: PipeBehavior) bool { - return b == .replace or b == .ignore; + return b == .replace or b == .ignore or b == .keep; } }; + /// helix's tree-sitter selection keys: where a range goes in the tree + /// helix's tree-sitter textobjects, by what their queries call them + pub const Object = enum { function, class, parameter, comment, @"test", entry, xml_element }; + pub const NodeWalk = enum { expand, shrink, next_sibling, prev_sibling, all_siblings, all_children, parent_end, parent_start }; + /// what the s/S/K/Alt-K prompt does with its pattern's matches + pub const RegexMode = enum { select, split, keep, remove }; pub const Scope = enum { once, per_selection }; pub const Direction = enum { backward, forward }; pub const Motion = enum { @@ -221,8 +267,9 @@ pub const Normal = struct { view_top, view_center, view_bottom, + last_modification, }; - pub const View = enum { top, center, bottom, scroll_down, scroll_up }; + pub const View = enum { top, center, bottom, middle, scroll_down, scroll_up }; pub const Find = enum { forward, backward, till_forward, till_backward }; pub const Line = enum { start, end, first_nonws }; pub const Page = enum { half_down, half_up, down, up }; @@ -247,6 +294,7 @@ pub const Normal = struct { merge, merge_consecutive, trim, + ensure_forward, }; pub const Edit = enum { delete, @@ -260,6 +308,10 @@ pub const Normal = struct { lowercase, uppercase, join_lines, + join_select_space, + rotate_contents_forward, + rotate_contents_backward, + align_selections, indent, unindent, comment_toggle, @@ -295,7 +347,7 @@ pub const Normal = struct { insert: struct { kind: Insert, count: u32 }, select: struct { kind: Select, count: u32 }, multi: struct { kind: Multi, count: u32 }, - select_regex: bool, // false = select, true = split + select_regex: RegexMode, edit: struct { kind: Edit, count: u32 }, lsp: Lsp, adjust_number: i64, @@ -304,6 +356,12 @@ pub const Normal = struct { pipe_selection: PipeBehavior, search, search_step: Direction, + goto_word, + macro_record, + macro_replay: u32, + node: NodeWalk, + object_jump: struct { object: Object, direction: Direction, count: u32 }, + repeat_insert: u32, pub fn scope(value: Action) Scope { return switch (value) { @@ -315,9 +373,22 @@ pub const Normal = struct { .pipe_selection, .search, .search_step, + .macro_record, + .macro_replay, + .node, + .goto_word, + .repeat_insert, => .once, .edit => |edit| switch (edit.kind) { - .comment_toggle, .undo, .redo => .once, + .join_lines, + .join_select_space, + .rotate_contents_forward, + .rotate_contents_backward, + .align_selections, + .comment_toggle, + .undo, + .redo, + => .once, else => .per_selection, }, else => .per_selection, @@ -354,7 +425,10 @@ pub const Normal = struct { pub fn parse(state: *State, key: Input) Result { if (key.has(.escape)) { - state.clear(); + // Esc out of sticky view mode leaves that mode and nothing else + const sticky = state.sticky; + state.* = .{}; + if (sticky) return .ignored; return resultAction(.escape); } @@ -386,6 +460,8 @@ pub const Normal = struct { if (key.has(.goto_view_top)) return resultAction(.{ .goto = .{ .target = .view_top, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_view_center)) return resultAction(.{ .goto = .{ .target = .view_center, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_view_bottom)) return resultAction(.{ .goto = .{ .target = .view_bottom, .count = count, .explicit_count = counted.explicit } }); + if (key.has(.goto_word)) return resultAction(.goto_word); + if (key.has(.goto_last_modification)) return resultAction(.{ .goto = .{ .target = .last_modification, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_definition)) return resultAction(.{ .lsp = .definition }); if (key.has(.goto_declaration)) return resultAction(.{ .lsp = .declaration }); if (key.has(.goto_type_definition)) return resultAction(.{ .lsp = .type_definition }); @@ -394,8 +470,9 @@ pub const Normal = struct { return .ignored; }, .view => { - state.prefix = .none; + if (!state.sticky) state.prefix = .none; if (key.has(.view_top)) return resultAction(.{ .view = .top }); + if (key.has(.view_middle)) return resultAction(.{ .view = .middle }); if (key.has(.view_center)) return resultAction(.{ .view = .center }); if (key.has(.view_bottom)) return resultAction(.{ .view = .bottom }); if (key.has(.view_scroll_down)) return resultAction(.{ .view = .scroll_down }); @@ -473,14 +550,28 @@ pub const Normal = struct { if (key.has(.add_newline)) return resultAction(.{ .add_newline = .{ .direction = direction, .count = count } }); if (key.has(.goto_diagnostic)) return resultAction(.{ .diagnostic = .{ .direction = direction, .endpoint = false } }); if (key.has(.goto_diagnostic_end)) return resultAction(.{ .diagnostic = .{ .direction = direction, .endpoint = true } }); + inline for (.{ .{ .goto_function, .function }, .{ .goto_class, .class }, .{ .goto_parameter, .parameter }, .{ .goto_comment, .comment }, .{ .goto_test, .@"test" }, .{ .goto_entry, .entry }, .{ .goto_xml_element, .xml_element } }) |pair| + if (key.has(pair[0])) return resultAction(.{ .object_jump = .{ .object = pair[1], .direction = direction, .count = count } }); return .ignored; }, + .register => { + state.prefix = .none; + const char = key.literal() orelse return .ignored; + state.register = char; + // the count typed before `"` is still the command's + state.count = if (counted.explicit) count else 0; + return .pending; + }, .none => {}, } // Prefix setters retain the count for their continuation. if (key.has(.prefix_goto)) return armPrefix(state, .goto, if (counted.explicit) count else 0); if (key.has(.prefix_view)) return armPrefix(state, .view, if (counted.explicit) count else 0); + if (key.has(.prefix_view_sticky)) { + state.sticky = true; + return armPrefix(state, .view, 0); + } if (key.has(.prefix_find_fwd)) return armPrefix(state, .find_fwd, if (counted.explicit) count else 0); if (key.has(.prefix_find_back)) return armPrefix(state, .find_back, if (counted.explicit) count else 0); if (key.has(.prefix_till_fwd)) return armPrefix(state, .till_fwd, if (counted.explicit) count else 0); @@ -489,6 +580,7 @@ pub const Normal = struct { if (key.has(.prefix_next)) return armPrefix(state, .next, if (counted.explicit) count else 0); if (key.has(.prefix_prev)) return armPrefix(state, .prev, if (counted.explicit) count else 0); if (key.has(.prefix_match)) return armPrefix(state, .match, 0); + if (key.has(.prefix_register)) return armPrefix(state, .register, if (counted.explicit) count else 0); if (key.has(.move_left)) return resultAction(.{ .move = .{ .motion = .left, .count = count } }); if (key.has(.move_right)) return resultAction(.{ .move = .{ .motion = .right, .count = count } }); @@ -535,8 +627,19 @@ pub const Normal = struct { if (key.has(.merge_sels)) return resultAction(.{ .multi = .{ .kind = .merge, .count = count } }); if (key.has(.merge_consecutive_sels)) return resultAction(.{ .multi = .{ .kind = .merge_consecutive, .count = count } }); if (key.has(.trim_sels)) return resultAction(.{ .multi = .{ .kind = .trim, .count = count } }); - if (key.has(.select_regex)) return resultAction(.{ .select_regex = false }); - if (key.has(.split_regex)) return resultAction(.{ .select_regex = true }); + if (key.has(.select_regex)) return resultAction(.{ .select_regex = .select }); + if (key.has(.split_regex)) return resultAction(.{ .select_regex = .split }); + if (key.has(.keep_regex)) return resultAction(.{ .select_regex = .keep }); + if (key.has(.remove_regex)) return resultAction(.{ .select_regex = .remove }); + if (key.has(.ensure_forward)) return resultAction(.{ .multi = .{ .kind = .ensure_forward, .count = count } }); + if (key.has(.expand_node)) return resultAction(.{ .node = .expand }); + if (key.has(.shrink_node)) return resultAction(.{ .node = .shrink }); + if (key.has(.next_sibling)) return resultAction(.{ .node = .next_sibling }); + if (key.has(.prev_sibling)) return resultAction(.{ .node = .prev_sibling }); + if (key.has(.all_siblings)) return resultAction(.{ .node = .all_siblings }); + if (key.has(.all_children)) return resultAction(.{ .node = .all_children }); + if (key.has(.parent_node_end)) return resultAction(.{ .node = .parent_end }); + if (key.has(.parent_node_start)) return resultAction(.{ .node = .parent_start }); if (key.has(.delete)) return resultAction(.{ .edit = .{ .kind = .delete, .count = count } }); if (key.has(.delete_noyank)) return resultAction(.{ .edit = .{ .kind = .delete_noyank, .count = count } }); @@ -549,6 +652,10 @@ pub const Normal = struct { if (key.has(.to_lowercase)) return resultAction(.{ .edit = .{ .kind = .lowercase, .count = count } }); if (key.has(.to_uppercase)) return resultAction(.{ .edit = .{ .kind = .uppercase, .count = count } }); if (key.has(.join_lines)) return resultAction(.{ .edit = .{ .kind = .join_lines, .count = count } }); + if (key.has(.join_select_space)) return resultAction(.{ .edit = .{ .kind = .join_select_space, .count = count } }); + if (key.has(.rotate_contents_fwd)) return resultAction(.{ .edit = .{ .kind = .rotate_contents_forward, .count = count } }); + if (key.has(.rotate_contents_back)) return resultAction(.{ .edit = .{ .kind = .rotate_contents_backward, .count = count } }); + if (key.has(.align_selections)) return resultAction(.{ .edit = .{ .kind = .align_selections, .count = count } }); if (key.has(.indent)) return resultAction(.{ .edit = .{ .kind = .indent, .count = count } }); if (key.has(.unindent)) return resultAction(.{ .edit = .{ .kind = .unindent, .count = count } }); if (key.has(.format)) return resultAction(.{ .lsp = .format }); @@ -564,6 +671,10 @@ pub const Normal = struct { if (key.has(.pipe_selection_to)) return resultAction(.{ .pipe_selection = .ignore }); if (key.has(.insert_output)) return resultAction(.{ .pipe_selection = .insert }); if (key.has(.append_output)) return resultAction(.{ .pipe_selection = .append }); + if (key.has(.keep_pipe)) return resultAction(.{ .pipe_selection = .keep }); + if (key.has(.macro_record)) return resultAction(.macro_record); + if (key.has(.macro_replay)) return resultAction(.{ .macro_replay = count }); + if (key.has(.repeat_insert)) return resultAction(.{ .repeat_insert = count }); if (key.has(.search)) return resultAction(.search); if (key.has(.search_next)) return resultAction(.{ .search_step = .forward }); if (key.has(.search_prev)) return resultAction(.{ .search_step = .backward }); @@ -1287,6 +1398,53 @@ fn wmIsPrev(t: WordTarget) bool { } /// w/b/e/W/B/E: helix word_move — each step selects the traversed span. +/// helix jump_to_word's candidates: the words of two or more word +/// characters between `start` and `end`, nearest the cursor first, taking one +/// forward and one back in turn, each over its word pointing the way it was +/// found, as many as `out` holds. +pub fn jumpWords(text: []const u8, cursor: usize, start: usize, end: usize, out: []Selection) usize { + var fwd: Selection = .{ .anchor = cursor, .head = cursor }; + var rev = fwd; + if (cursor < text.len and !isWordWhitespace(wordClassAt(text, cursor))) { + const word_end = moveWord(text, fwd, 1, .next_word_end); + if (word_end.anchor == cursor) fwd = word_end; // a word of one grapheme + const word_start = moveWord(text, rev, 1, .prev_word_start); + if (word_start.anchor == nextGrapheme(text, cursor)) rev = word_start; + } + var n: usize = 0; + while (n < out.len) { + var changed = false; + while (fwd.head < end) { + fwd = moveWord(text, fwd, 1, .next_word_end); + // the two graphemes before the head are word characters + const one = prevGrapheme(text, fwd.head); + if (fwd.head == 0 or one == 0 or wordClassAt(text, one) != .word or wordClassAt(text, prevGrapheme(text, one)) != .word) continue; + changed = true; + var from = fwd.anchor; + while (from < fwd.head and wordClassAt(text, from) != .word) from = nextGrapheme(text, from); + fwd.anchor = from; + out[n] = fwd; + n += 1; + break; + } + if (n == out.len) break; + while (rev.head > start) { + rev = moveWord(text, rev, 1, .prev_word_start); + // the two graphemes from the head are word characters + if (wordClassAt(text, rev.head) != .word or wordClassAt(text, nextGrapheme(text, rev.head)) != .word) continue; + changed = true; + var to = rev.anchor; + while (to > rev.head and wordClassAt(text, prevGrapheme(text, to)) != .word) to = prevGrapheme(text, to); + rev.anchor = to; + out[n] = rev; + n += 1; + break; + } + if (!changed) break; + } + return n; +} + pub fn moveWord(text: []const u8, r0: Selection, count: usize, target: WordTarget) Selection { const is_prev = wmIsPrev(target); if ((is_prev and r0.head == 0) or (!is_prev and r0.head == text.len)) return r0; diff --git a/src/mouse.zig b/src/mouse.zig index 5ab6fe2b..22eb3270 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -192,7 +192,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { else => false, }) for (p.panes) |slot| { const pane = slot orelse continue; - if (pane.prompt == .del_side) pane.prompt = .none; + if (pane.prompt == .del_side or pane.prompt == .repl_choice) pane.prompt = .none; }; if (m.button == .back or m.button == .forward) { if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1); @@ -717,12 +717,12 @@ fn dispatchPointerBuiltin( const arg = p.chord_arg; p.chord_arg = null; defer if (arg) |a| p.gpa.free(a); - const operand = text orelse { - if (gesture) |g| _ = reportGesture(p, id, cmd, "", g.on_tag, g.operand, arg != null); - return; - }; + // A click that takes no text does nothing, and tells an event reader + // nothing, as acme's look3 and execute return on an empty expansion. + const operand = text orelse return; if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return; - exec.runBuiltin(p, cmd, id, "", exec.withArg(p, operand, arg)); + // A middle click on a body is the exec a bound REPL may take. + exec.runBuiltinFrom(p, cmd, id, "", exec.withArg(p, operand, arg), gesture != null and !gesture.?.on_tag); } fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { @@ -747,7 +747,7 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { pane.body.vsel.active = false; pane.body.nsel = 0; pane.body.select = false; - pane.body.append_at = null; + pane.body.restore_cursor = false; pane.body.sticky_col = -1; pane.body.normal.clear(); edit.setYank(p, text); @@ -859,7 +859,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { pane.body.vsel.active = false; pane.body.nsel = 0; // a click says WHERE the one cursor is pane.body.select = false; - pane.body.append_at = null; + pane.body.restore_cursor = false; pane.body.sticky_col = -1; if (s.ctrl) p.lspRequest(s.id, .definition, ""); } @@ -963,8 +963,8 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { if (cut) { if (t.vsel.active) edit.normalDelete(p, t, true); } else if (t.vsel.active) { - edit.normalReplaceYank(p, t); - } else edit.pasteText(p, t, p.yank orelse return, true, 1); + edit.chordReplace(p, t); + } else edit.pasteText(p, t, p.registers.text(p.gpa, pardes.Registers.default) orelse return, true, 1); return; } if (pane.body.mode == .tty) { @@ -973,7 +973,7 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { pane.sel[sel_slot].state = .none; // drop the sweep highlight } if (cut) return; - const y = p.yank orelse return; + const y = p.registers.text(p.gpa, pardes.Registers.default) orelse return; if (y.len == 0) return; if (panes.Terminal.reportsMouse(pane)) { const col: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].c1 + 1, 1, 9999)); @@ -1016,11 +1016,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { if (cut) { if (pane.body.vsel.active) edit.normalDelete(p, &pane.body, true); } else if (pane.body.vsel.active) { - edit.normalReplaceYank(p, &pane.body); + edit.chordReplace(p, &pane.body); } else if (pane.file) |*f| { // bare cursor: splice the register in literally (no linewise // magic) so a paste right after a cut restores the text exactly - const y = p.yank orelse return; + const y = p.registers.text(p.gpa, pardes.Registers.default) orelse return; if (y.len == 0) return; const at = modal.Cursor{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) }; edit.pushUndo(p, &pane.body); @@ -1039,7 +1039,7 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { pane.body.cur_pinned = true; pane.body.sticky_col = -1; pane.ensureCursorVisible(); - } else edit.pasteText(p, &pane.body, p.yank orelse return, true, 1); + } else edit.pasteText(p, &pane.body, p.registers.text(p.gpa, pardes.Registers.default) orelse return, true, 1); } test "plain left click clears explicit modal selection" { diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index cc7ca632..0537c17a 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -14,8 +14,11 @@ fn clip(n: usize) u32 { pub const e_no_match = "no match for regexp"; pub const e_range = "address out of range"; pub const e_regexp = "bad regular expression"; -pub const e_slow = "regular expression search took too long"; +/// Not "took too long": 9ns reads errors by their words, and that would be +/// ENAMETOOLONG. +pub const e_slow = "regular expression search gave up, backtracking past its step budget"; pub const e_syntax = "bad address syntax"; +pub const e_order = "addresses out of order"; pub const Addr = struct { text: []const u8, @@ -93,7 +96,9 @@ pub const Addr = struct { size = .line; }, '/', '?' => { - const back = c == '?'; + // `-/re/` searches back as `?re?` does (acme's address() + // takes the direction a `-` set, addr.c). + const back = c == '?' or dir == '-'; r = a.regexp(r, a.pattern(c), back) orelse return null; dir = 0; size = .line; @@ -243,6 +248,44 @@ const Node = tree.Node; const E = tree.E; const Status = tree.Status; +test "an address is evaluated from the current one, as acme's are" { + const p = try th.withFile(testing.allocator, "a foo b foo c foo\n"); + defer p.deinit(); + const addr = Node.of(th.serialOf(p), .addr); + const Step = struct { expr: []const u8, q0: u32, q1: u32 }; + _ = th.wr(p, addr, "#0"); + for ([_]Step{ + // each search starts where the last address ended + .{ .expr = "/foo/", .q0 = 2, .q1 = 5 }, + .{ .expr = "/foo/", .q0 = 8, .q1 = 11 }, + // `.` is that address, and `-` before a pattern searches back + .{ .expr = ".", .q0 = 8, .q1 = 11 }, + .{ .expr = "-/foo/", .q0 = 2, .q1 = 5 }, + .{ .expr = "./foo/", .q0 = 8, .q1 = 11 }, + .{ .expr = "+/foo/", .q0 = 14, .q1 = 17 }, + // a number then a pattern: the search starts at the number + .{ .expr = "#9/foo/", .q0 = 14, .q1 = 17 }, + }) |c| { + try testing.expectEqual(Status.ok, th.wr(p, addr, c.expr).reply.status); + try testing.expectEqual(c.q0, p.panes[0].?.fs.addr.q0); + try testing.expectEqual(c.q1, p.panes[0].?.fs.addr.q1); + } + // A range whose end comes before its start is sam's error. + try testing.expectEqualStrings(e_order, th.wr(p, addr, "#10,#5").reply.ename); + + // `;` evaluates its right side from its left (acme's addr.c:170-171, + // `ar = r`), `,` from the address the whole started at. + const q = try th.withFile(testing.allocator, "b a b\n"); + defer q.deinit(); + const qaddr = Node.of(th.serialOf(q), .addr); + _ = th.wr(q, qaddr, "#0"); + try testing.expectEqual(Status.ok, th.wr(q, qaddr, "/a/;/b/").reply.status); + try testing.expectEqual(@as(u32, 2), q.panes[0].?.fs.addr.q0); + try testing.expectEqual(@as(u32, 5), q.panes[0].?.fs.addr.q1); + _ = th.wr(q, qaddr, "#0"); + try testing.expectEqualStrings(e_order, th.wr(q, qaddr, "/a/,/b/").reply.ename); +} + test "regular expressions search lines as sam's do, and a search wraps" { const p = try th.withFile(testing.allocator, "alpha beta\nbeta gamma\ngamma\n"); defer p.deinit(); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index cd327d03..a64521e7 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -53,8 +53,26 @@ fn noteResult(p: *Pardes, serial: u32) void { /// the pane it acted on (even one it closed). fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { const context = p.panes[id].?.serial; + // A pane an event reader holds hears its clicks instead of acting on + // them (editors/acme/look.c:53 and exec.c:173 send them to winevent), + // and a line written to a look or exec is a click: on that pane, or for + // the root's, on the pane with the keyboard. The reader writes the + // record back to have it done. + if (p.panes[id].?.fs.readers > 0) { + const named = std.meta.stringToEnum(Builtin, exec_line.commandText(text)) != null; + var flag: u32 = pardes.ctlfs.events.flag_expansion; + if (named) flag |= pardes.ctlfs.events.flag_builtin; + if (!exec and !named and std.mem.indexOfAny(u8, text, "/.:") != null) flag |= pardes.ctlfs.events.flag_filename; + _ = pardes.ctlfs.events.noteAction(p, id, if (exec) .body_exec else .body_look, 0, 0, flag, text); + return noteResult(p, context); + } const before = Serials.snapshot(p); - if (exec) _ = pardes.exec.execute(p, id, text) else look.lookAt(p, id, text); + // An exec answers the pane it went to: a command pane it reused, or + // the terminal it typed into. + const went: ?usize = if (exec) pardes.exec.execute(p, id, text) else blk: { + look.lookAt(p, id, text); + break :blk null; + }; const after = Serials.snapshot(p); var created = false; for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) { @@ -62,6 +80,7 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { created = true; }; if (created) return; + if (went) |dst| if (p.panes[dst]) |pane| return noteResult(p, pane.serial); noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial); } @@ -144,7 +163,7 @@ fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void { try w.writeByte('\''); } -pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len; +pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len + 48; /// acme's winctlprint: serial, tag length, body length, a reserved zero, the /// dirty flag, then the width in cells, the font and the tab width. Then @@ -167,6 +186,10 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 { // While a column's or the workspace's tag has the keyboard, no pane does. const current = p.panes[p.active] == pane and !p.header_focus; w.print(" {d:>11} {s} ", .{ config.tab_width, if (current) "current" else "notcurrent" }) catch {}; + // A terminal bound as a REPL ends with its id, `python-a`. + var idbuf: [48]u8 = undefined; + const id = exec_line.replId(&idbuf, pane); + if (id.len > 0) w.print("{s} ", .{id}) catch {}; return w.buffered(); } @@ -177,7 +200,7 @@ const Builtin = builtins.registry.Builtin(); /// Refuses a control message, quoting it the way Plan 9's cmderror does /// (kernel/misc/parse.c:82): `unknown control message "Bogus 3"`. fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply { - const room = p.fs.ename.len - why.len - 3; + const room = p.fs.ename.len -| (why.len + 3); const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\"", .{ why, line[0..@min(line.len, room)] }) catch why; return tree.failText(req.tag, E.INVAL, text); } @@ -201,7 +224,12 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const takes = switch (setting.action) { .theme => for (pardes.themes) |t| { if (std.mem.eql(u8, t.name, arg)) break true; - } else false, + } else { + // `refuse` quotes the line after it, in what is left of 128. + var near: [40]u8 = undefined; + var why: [104]u8 = undefined; + return refuse(p, req, std.fmt.bufPrint(&why, "bad value in control message; like it: {s}; ThemeSel lists all", .{pardes.colors.themesNear(&near, arg)}) catch "bad value in control message", line); + }, .font => config.Runtime.FontSpec.parse(arg) != null, else => probe: { var probe = p.settings; @@ -220,7 +248,18 @@ fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply { p.fs.refused = false; p.fs.failure_len = 0; defer p.fs.no_prompt = false; + // What a ctl write touched reads back from /look and /exec as a click's + // does (`run`), so a script that made a pane with `Tty` finds it there. + const context = p.panes[id].?.serial; + const before = Serials.snapshot(p); _ = exec_line.executeBuiltinLine(p, id, line); + const after = Serials.snapshot(p); + var created = false; + for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) { + noteResult(p, serial); + created = true; + }; + if (!created) noteResult(p, context); if (p.fs.refused) return refuse(p, req, e_prompt, line); if (p.fs.failure_len == 0) return null; const refusal = refuse(p, req, p.fs.failure[0..p.fs.failure_len], line); @@ -292,6 +331,7 @@ pub fn readRoot(p: *Pardes, req: Req) Reply { /// runs, as the root's look and exec click there; all are checked first. pub fn writeRoot(p: *Pardes, req: Req) Reply { for ([2]bool{ false, true }) |apply| { + if (apply) p.fs.results_len = 0; var it = std.mem.splitScalar(u8, req.data, '\n'); while (it.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); @@ -361,7 +401,7 @@ pub fn writeFocus(p: *Pardes, req: Req) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } -const e_no_pane = "no such window"; +const e_no_pane = "no such pane"; pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply { var buf: [pane_text_cap]u8 = undefined; @@ -399,6 +439,7 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { // Checked whole before anything applies, so a write that must wait for // the lock has done nothing yet when it goes again. for ([2]bool{ false, true }) |apply| { + if (apply) p.fs.results_len = 0; var held = !other and pane.fs.lock != null; var it = std.mem.splitScalar(u8, req.data, '\n'); while (it.next()) |raw| { @@ -615,7 +656,9 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename); try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename); try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename); - try testing.expectEqualStrings("bad value in control message \"Theme no-such-theme\"", wr(p, root_ctl, "Theme no-such-theme").reply.ename); + const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename; + try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n"); + try testing.expect(std.mem.endsWith(u8, no_theme, "; ThemeSel lists all \"Theme no-such-theme\"")); try testing.expectEqualStrings("wrong #args in control message \"Newcol 2\"", wr(p, root_ctl, "Newcol 2").reply.ename); try testing.expectEqualStrings("wrong #args in control message \"Theme\"", wr(p, root_ctl, "Theme").reply.ename); // A bare required word fails the check, so the line before never runs. @@ -625,7 +668,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi // its line; the lines before it have taken effect, as in acme. const failed = wr(p, root_ctl, "Verbose on\nMount x\nVerbose off"); try testing.expectEqual(E.IO, failed.errno()); - try testing.expectEqualStrings("Mount name dial: MissingArgument \"Mount x\"", failed.reply.ename); + try testing.expectEqualStrings("Mount name dial: missing argument \"Mount x\"", failed.reply.ename); try testing.expect(p.settings.verbose); try testing.expect(!p.fs.no_prompt); try testing.expectEqual(Status.ok, wr(p, root_ctl, "Verbose off").reply.status); @@ -860,3 +903,116 @@ test "exec Save writes the named file through the host" { try testing.expect(!pane_files.dirtyOf(pane)); try testing.expectEqualStrings(pane.file.?.content, p.fallback.get("/tmp/renamed.txt").?); } + +test "a pane's own look and exec go to the event reader holding it, which writes them back to act" { + const gpa = testing.allocator; + const p = try withFile(gpa, "Msg routed\n"); + defer p.deinit(); + const serial = serialOf(p); + const event = Node.of(serial, .event); + const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle; + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .exec), "Msg routed\n").reply.status); + // Not done: the reader has it, as it would a middle click. + const pane = p.panes[0].?; + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "routed") == null); + const record = call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes; + try testing.expectEqualStrings("FX0 0 2 10 Msg routed\n", record); + // Written back, it is done. + try testing.expectEqual(tree.Status.ok, wr(p, event, record).reply.status); + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "routed") != null); + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .look), "one.txt:3\n").reply.status); + try testing.expectEqualStrings("FL0 0 6 9 one.txt:3\n", call(p, .{ .tag = 3, .op = .read, .node = event, .handle = h, .size = 256 }).bytes); + // The root's is a click at the pane with the keyboard, this one. + try testing.expectEqual(tree.Status.ok, wr(p, root_exec, "Msg rooted\n").reply.status); + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "rooted") == null); + try testing.expectEqualStrings("FX0 0 2 10 Msg rooted\n", call(p, .{ .tag = 5, .op = .read, .node = event, .handle = h, .size = 256 }).bytes); + _ = call(p, .{ .tag = 4, .op = .release, .node = event, .handle = h }); + // With no reader, the root's and the pane's act at once. + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .exec), "Msg direct\n").reply.status); + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "direct") != null); +} + +test "look and exec read back what the last command touched, a ctl write's too" { + const gpa = testing.allocator; + const p = try withFile(gpa, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + var want: [32]u8 = undefined; + _ = wr(p, root_exec, "Msg first\n"); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{serial}), rd(p, root_exec, 0, 64).bytes); + // A pane made through ctl is what both read next, not the Msg's pane. + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .ctl), "Tty\n").reply.status); + const made = p.panes[p.active].?.serial; + try testing.expect(made != serial); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, root_exec, 0, 64).bytes); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, @intFromEnum(tree.TopFile.look), 0, 64).bytes); + // A ctl write that makes nothing names the pane it ran at. + try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Verbose off\n").reply.status); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{made}), rd(p, root_exec, 0, 64).bytes); +} + +test "an error's words give the errno a mount reads: EINVAL for what is malformed, EIO for what failed" { + // 9ns (cloud9's 9ns/src/nine.zig) maps an error to an errno by its + // words, and EIO when none of them match: these words must not. + const addressing = @import("addr.zig"); + const einval = [_][]const u8{ "control message", "invalid", "bad " }; + const other = [_][]const u8{ "interrupt", "shut down", "not exist", "not found", "no such", "exists", "not empty", "not a dir", "is a dir", "permission", "denied", "read-only", "read only", "no space", "not allowed", "not permitted", "cannot", "fid", "busy", "in use", "too long", "not supported", "unsupported" }; + const Case = struct { text: []const u8, malformed: bool }; + for ([_]Case{ + .{ .text = addressing.e_syntax, .malformed = true }, + .{ .text = addressing.e_regexp, .malformed = true }, + .{ .text = e_prompt, .malformed = true }, + .{ .text = addressing.e_no_match, .malformed = false }, + .{ .text = addressing.e_range, .malformed = false }, + .{ .text = addressing.e_order, .malformed = false }, + .{ .text = addressing.e_slow, .malformed = false }, + .{ .text = "/tmp/x.txt: Modified (Exit again to discard)", .malformed = false }, + }) |c| { + const says_einval = for (einval) |w| { + if (std.ascii.findIgnoreCase(c.text, w) != null) break true; + } else false; + try testing.expectEqual(c.malformed, says_einval); + for (other) |w| try testing.expect(std.ascii.findIgnoreCase(c.text, w) == null); + } +} + +test "Joincol with no column to the right and Theme with no such theme say so" { + const gpa = testing.allocator; + const p = try withFile(gpa, "x\n"); + defer p.deinit(); + const root_ctl = @intFromEnum(tree.TopFile.ctl); + const joined = wr(p, root_ctl, "Joincol\n"); + try testing.expectEqual(tree.Status.err, joined.reply.status); + try testing.expectStringStartsWith(joined.reply.ename, "Joincol: no column to the right"); + const themed = wr(p, root_ctl, "Theme drak\n"); + try testing.expectEqual(E.INVAL, themed.errno()); + try testing.expectStringStartsWith(themed.reply.ename, "bad value in control message; like it: "); + try testing.expect(std.mem.indexOf(u8, themed.reply.ename, " dark") != null or std.mem.indexOf(u8, themed.reply.ename, ": dark") != null); + try testing.expect(std.mem.endsWith(u8, themed.reply.ename, "\"Theme drak\"")); + // A click on the word says it on the message row. + _ = wr(p, Node.of(serialOf(p), .exec), "Theme drak\n"); + const pane = p.panes[p.active].?; + try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null); +} + +test "a look that fills a column's stillborn scratch is answered with the scratch already gone" { + const gpa = testing.allocator; + const p = try withFile(gpa, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" }); + var dir: [4096]u8 = undefined; + const at = dir[0..try tmp.dir.realPath(testing.io, &dir)]; + var line: [4200]u8 = undefined; + // Newcol makes a column of one empty +New; a file looked from it + // joins that column and takes the scratch's place. + try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status); + const scratch = p.panes[p.active].?.serial; + try testing.expectEqual(tree.Status.ok, wr(p, Node.of(scratch, .look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status); + try testing.expect(p.paneBySerial(scratch) == null); + var want: [32]u8 = undefined; + const gone = try std.fmt.bufPrint(&want, "{d} ", .{scratch}); + var rows = std.mem.splitScalar(u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, '\n'); + while (rows.next()) |row| try testing.expect(!std.mem.startsWith(u8, row, gone)); +} diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 69dc271d..ffdea95b 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -7,6 +7,7 @@ const look = @import("../look.zig"); const cloud9 = @import("cloud9"); const tree = @import("tree.zig"); const pane_files = @import("pane.zig"); +const dump = @import("../dump.zig"); const Pardes = pardes.Pardes; const Pane = pardes.Pane; @@ -155,6 +156,13 @@ pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void { /// kernel mapped the reply to (`Invalid argument`), and here is the reason. /// The log is the one place for it, as acme's `errors` file takes text and /// answers nothing: a per-pane readable error file would be a second. +/// `dump <path>` when a Dump is written, `restore <path>` first in a +/// Restore's replacement. +pub fn notePath(p: *Pardes, what: []const u8, path: []const u8) void { + var buf: [pardes.memory.limits.host_path_cap + 16]u8 = undefined; + pushLog(p, std.fmt.bufPrint(&buf, "{s} {s}\n", .{ what, path }) catch return); +} + pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { const why = if (reply.ename.len > 0) reply.ename else cloud9.fs.errString(reply.errno); var name: [16]u8 = undefined; @@ -460,7 +468,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void { p.fs.news = true; } -const EventRecord = struct { action: Action, q0: u32, q1: u32 }; +const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null }; const EventReader = struct { data: []const u8, @@ -470,15 +478,27 @@ const EventReader = struct { if (er.i >= er.data.len) return null; var i = er.i; if (i + 2 > er.data.len) return null; + const origin = er.data[i]; i += 1; const action = Action.fromChar(er.data[i]) orelse return null; i += 1; const q0 = scanNumber(er.data, &i) orelse return null; const q1 = scanNumber(er.data, &i) orelse return null; while (i < er.data.len and er.data[i] == ' ') i += 1; + // acme takes only `origin action q0 q1`; the record whole, as read, + // is taken too, so a click with no place in the text (one written + // to a pane's own look or exec) can be written back and done. + var text: ?[]const u8 = null; + if (i < er.data.len and er.data[i] != '\n') { + _ = scanNumber(er.data, &i) orelse return null; + const n = scanNumber(er.data, &i) orelse return null; + if (i >= er.data.len or er.data[i] != ' ' or er.data.len - (i + 1) < n) return null; + text = er.data[i + 1 ..][0..n]; + i += 1 + n; + } if (i >= er.data.len or er.data[i] != '\n') return null; er.i = i + 1; - return .{ .action = action, .q0 = q0, .q1 = q1 }; + return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text }; } }; @@ -517,9 +537,15 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply { const whole = if (r.action.onTag()) pane_files.tagOf(p, pane) else pane_files.bodyOf(pane); const lo = @min(@as(usize, r.q0), whole.len); const hi = @max(lo, @min(@as(usize, r.q1), whole.len)); - const text = p.scratch.allocator().dupe(u8, whole[lo..hi]) catch continue; + const said = if (r.q0 == r.q1) r.text orelse whole[lo..hi] else whole[lo..hi]; + const text = p.scratch.allocator().dupe(u8, said) catch continue; switch (r.action) { - .body_exec, .tag_exec => _ = exec.execute(p, live, text), + // A body's click written back is done as the click would have + // been (acme's xfideventwrite, xfid.c:842-844): a REPL bound for + // it takes it. One that came from a write to exec (`F`) was + // never a click, and runs as the command it was. + .body_exec => _ = exec.executeFrom(p, live, text, r.origin != 'F'), + .tag_exec => _ = exec.execute(p, live, text), .body_look, .tag_look => look.lookAt(p, live, text), else => unreachable, } @@ -664,7 +690,8 @@ test "a pane deleted while its event file is open leaves no suppression behind" const b = call(p, .{ .tag = 19, .op = .open, .node = event, .omode = 1 }); try testing.expectEqual(@as(u16, 2), p.fs.listeners); - _ = wr(p, Node.of(serial, .exec), "Del\n"); + // Through ctl: a line written to its exec would go to the reader. + _ = wr(p, Node.of(serial, .ctl), "Del\n"); try testing.expect(p.paneBySerial(serial) == null); try testing.expectEqual(@as(u16, 0), p.fs.listeners); // The reader's next read hears what acme says of a window gone under it. @@ -850,7 +877,7 @@ test "a refused or failed write is an err record in the log, saying which file a _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus"); try testing.expectEqualStrings("err - ctl: unknown control message \"Bogus\"\n", next.record(p, f, frozen)); _ = wr(p, @intFromEnum(tree.TopFile.focus), "99999"); - try testing.expectEqualStrings("err - focus: no such window\n", next.record(p, f, frozen)); + try testing.expectEqualStrings("err - focus: no such pane\n", next.record(p, f, frozen)); // A write that succeeds says nothing. _ = wr(p, Node.of(serial, .addr), "#0"); try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status); @@ -870,6 +897,21 @@ test "a refused or failed write is an err record in the log, saying which file a _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = g }); } +test "a Dump written and a Restore made are in the log, with their files" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + p.setLastDump("/tmp/pardes.dump.zon"); + try dump.dumpState(p); + const restored = try dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon"); + defer restored.deinit(); + for ([_]*Pardes{ p, restored }, [_][]const u8{ "dump", "restore" }) |core, what| { + const log = try freezeLog(core); + defer core.gpa.free(log.bytes); + var want: [64]u8 = undefined; + try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what}))); + } +} + test "opens of the log share the open records, and a closed one frees its record" { const gpa = testing.allocator; const p = try withFile(gpa, "x\n"); @@ -885,3 +927,59 @@ test "opens of the log share the open records, and a closed one frees its record for (handles) |h| _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h }); for (p.fs.opens) |o| try testing.expect(o.node == 0); } + +test "a click in the body reports the offsets of the word it takes, so the record can be written back" { + const config = @import("../config.zig"); + const gpa = testing.allocator; + const text = "alpha qty beta\n qty\n\tqty\ndef f(qty): return qty.x\n" ++ "w" ** 100 ++ " qty\n"; + const p = try withFile(gpa, text); + defer p.deinit(); + // A scratch made over 9P, clicked while another pane has the keyboard. + const scratch = try th.newPane(p); + _ = wr(p, Node.of(scratch, .body), text); + for ([_]u32{ serialOf(p), scratch }) |serial| { + const id = p.paneBySerial(serial).?; + p.active = p.paneBySerial(if (serial == scratch) serialOf(p) else scratch).?; + const event = Node.of(serial, .event); + const h = call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle; + var frame = std.heap.ArenaAllocator.init(gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[id]; + const x0 = rect.x + config.GUTTER + config.PREFIX_W; + const y0 = if (p.settings.tag_bottom) rect.y else rect.y + p.panes[id].?.tag_rows; + const tab: u16 = config.tab_width; + const Click = struct { x: u16, y: u16, look: []const u8, exec: []const u8, scroll: i32 = 0 }; + // Where the wrapped line's `qty` landed, as drawn. + const surface = try p.render(frame.allocator()); + var wrap_col: u16 = 0; + while (wrap_col < rect.w and !std.mem.eql(u8, surface.at(x0 + wrap_col, y0 + 5).grapheme(), "q")) wrap_col += 1; + for ([_]Click{ + .{ .x = x0 + 7, .y = y0, .look = "ML6 9 0 3 qty\n", .exec = "MX6 9 0 3 qty\n" }, + .{ .x = x0 + 5, .y = y0 + 1, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n" }, + .{ .x = x0 + tab + 1, .y = y0 + 2, .look = "ML24 27 0 3 qty\n", .exec = "MX24 27 0 3 qty\n" }, + .{ .x = x0 + 7, .y = y0 + 3, .look = "ML34 37 0 3 qty\n", .exec = "MX34 37 0 3 qty\n" }, + // Scrolled a line, the first row shows the second. + .{ .x = x0 + 5, .y = y0, .look = "ML19 22 0 3 qty\n", .exec = "MX19 22 0 3 qty\n", .scroll = 1 }, + // Wrapped: the word on the continuation row. + .{ .x = x0 + wrap_col, .y = y0 + 5, .look = "ML154 157 0 3 qty\n", .exec = "MX154 157 0 3 qty\n" }, + }) |c| for ([_]pardes.Mouse.Button{ config.look_button, config.exec_button }, [_][]const u8{ c.look, c.exec }) |button, want| { + // The pointer rests on the word first, as a hand's does, and + // the hover preview comes up under it. + p.panes[id].?.scrollBy(c.scroll); + _ = try p.render(frame.allocator()); + p.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = c.x, .row = c.y } }); + for (0..200) |_| p.update(.tick); + p.update(.{ .mouse = .{ .button = button, .kind = .press, .col = c.x, .row = c.y } }); + p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = c.x, .row = c.y } }); + try testing.expectEqualStrings(want, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).bytes); + p.panes[id].?.scrollBy(-c.scroll); + }; + // A click on nothing expands to nothing, and acme then says + // nothing (look.c:37, exec.c:141). + p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = x0 + 60, .row = y0 + 1 } }); + p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = x0 + 60, .row = y0 + 1 } }); + try testing.expectEqual(Status.again, call(p, .{ .tag = 2, .op = .read, .node = event, .handle = h, .size = 256 }).reply.status); + _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h }); + } +} diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 0a511118..d2b2a791 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -194,6 +194,20 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const return take; } +test "a terminal's body stats as long as it reads" { + const gpa = testing.allocator; + const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const pane = p.panes[0].?; + try testing.expect(pane.isTerminal()); + p.update(.{ .output = .{ .pane = 0, .bytes = "$ ls\r\nalpha beta\r\n$ " } }); + const body = Node.of(pane.serial, .body); + const whole = rd(p, body, 0, 1 << 16); + try testing.expect(whole.bytes.len > 0); + try testing.expectEqual(@as(u64, whole.bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = body }).reply.attr.size); +} + // ---- what stat reports ---- /// A range file's text: acme's addr format, two right-aligned offsets, so @@ -204,7 +218,9 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 { const pane = p.panes[id] orelse return 0; const pf = &pane.fs; return switch (f) { - .body, .data, .xdata => bodyOf(pane).len, + // A terminal's body reads its screen and history, as text. + .body => if (pane.file == null and pane.isTerminal()) panes.Terminal.screenTextLen(pane) else bodyOf(pane).len, + .data, .xdata => bodyOf(pane).len, .tag => tagOf(p, pane).len, .name => nameOf(pane).len + 1, .ctl => ctl: { @@ -453,16 +469,22 @@ pub const e_addr_failed = "no address: the last one written to addr failed"; /// A range file takes an address expression, or that pair of offsets. One /// that does not evaluate says why: `bad address syntax`, `no match for -/// regexp`, `address out of range`, `bad regular expression`. +/// regexp`, `address out of range`, `bad regular expression`, `addresses +/// out of order`. fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { const pf = &pane.fs; const text = bodyOf(pane); clampAddr(pf, text.len); const expr = std.mem.trimEnd(u8, req.data, "\n"); var a: addressing.Addr = .{ .text = text, .lim = pf.limit, .expr = expr }; - const r = if (pairOf(expr)) |pair| + var r = if (pairOf(expr)) |pair| State.Range{ .q0 = @min(pair.q0, clip(text.len)), .q1 = @min(pair.q1, clip(text.len)) } else if (a.address(pf.addr)) |found| (if (a.i < expr.len) null else found) else null; + // sam's check, which acme leaves out: `#100,#50` names no range. + if (r) |found| if (found.q0 > found.q1) { + a.err = addressing.e_order; + r = null; + }; const range = r orelse { if (file == .addr) pf.addr_failed = true; return tree.failText(req.tag, E.INVAL, a.err); @@ -485,7 +507,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply { const word = std.mem.trim(u8, req.data, " \t\r\n"); - if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return Reply.fail(req.tag, E.INVAL); + if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "takes 1 or 0"); const on = word[0] == '1'; const pf = &pane.fs; switch (file) { @@ -851,8 +873,10 @@ test "the flag files read and write the state acme kept behind ctl verbs" { for ([_]u64{ dirty, mark, scroll }) |node| { try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size); - for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| + for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| { try testing.expectEqual(E.INVAL, wr(p, node, bad).errno()); + try testing.expectEqualStrings("takes 1 or 0", wr(p, node, bad).reply.ename); + } } try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes); diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index dcf9a8a7..3599b612 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -43,7 +43,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply { while (it.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; - if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); + if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, e_bad_pty_ctl); } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; @@ -65,13 +65,13 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { const which = signalNamed(words.next() orelse return false) orelse return false; if (words.next() != null) return false; if (!apply) return true; - p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .sig = which } }); + p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .serial = pane.serial, .sig = which } }); }, .exec => { if (words.next() != null) return false; if (pane.cwdSlice().len > pardes.effect_path_cap) return false; if (!apply) return true; - p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(pane.cwdSlice()) } }); + p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); }, } return true; @@ -92,7 +92,8 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { // Busy is what a run written now would be told: a command is running // or a line is typed at the prompt. Before the shell's first prompt a // run waits for it rather than answer busy, so that is not busy. - const marks = if (pane.terminal) |state| &state.stream.handler else null; + // A build with no terminal panes (the board) has no marks to read. + const marks = if (comptime !pardes.panes.Terminal.enabled) null else if (pane.terminal) |state| &state.stream.handler else null; const busy = p.hostTtyTaken(id) or (!pane.fs.unmarked and (waitingRun(p, pane) != null or (if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane)) else false))); const out = p.fs.stage(p.gpa); @@ -162,10 +163,14 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { if (std.mem.trim(u8, line, " \t").len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line); if (line.len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line); for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, e_bad_line); + if (comptime !pardes.panes.Terminal.enabled) return tree.failText(req.tag, E.INVAL, e_bad_line); const pf = &pane.fs; const state = pane.terminal orelse return tree.failText(req.tag, E.INVAL, e_bad_line); const marks = &state.stream.handler; - if (pf.unmarked) { + if (pane.command != null) { + // Its child is the command, and no shell is there to take a line. + answer(p, slot, "error {s}", .{if (pane.command_done) "command done; not a shell" else "a command runs here, not a shell"}); + } else if (pf.unmarked) { answer(p, slot, "error no prompt marks", .{}); } else if (waitingRun(p, pane) != null or (marks.prompts > 0 and (marks.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)))) @@ -335,6 +340,7 @@ fn waitingRun(p: *Pardes, pane: *Pane) ?*Run { } const e_bad_line = "bad command line"; +const e_bad_pty_ctl = tree.e_bad_ctl ++ "; takes winsize COLS ROWS, sig INT|TERM|HUP|QUIT|KILL, or exec"; pub fn readData(p: *Pardes, req: Req, pf: *pane_files.State) Reply { if (pf.pty_out.empty()) return .{ .tag = req.tag, .status = .again }; @@ -505,6 +511,7 @@ test "every pty/ctl verb, and every refusal" { }) |bad| { const a = wr(p, ctl, bad); try testing.expectEqual(E.INVAL, a.errno()); + try testing.expectEqualStrings(e_bad_pty_ctl, a.reply.ename); // names what it takes try testing.expect(a.winsize == null); try testing.expect(a.signal == null); try testing.expect(!a.spawned); @@ -743,6 +750,25 @@ test "run answers how its command ended once the shell is back at a prompt" { for (p.fs.opens) |o| try testing.expect(o.node == 0); } +test "run on a command pane says no shell is there" { + const gpa = testing.allocator; + const p = try withTerm(gpa); + defer p.deinit(); + const node = Node.of(serialOf(p), .pty_run); + const pane = p.panes[0].?; + pane.command = try gpa.dupe(u8, "make"); + for ([_]struct { bool, []const u8 }{ + .{ false, "error a command runs here, not a shell\n" }, + .{ true, "error command done; not a shell\n" }, + }) |case| { + pane.command_done = case[0]; + const o = call(p, .{ .tag = 1, .op = .open, .node = node }); + _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = o.reply.handle, .data = "ls\n" }); + try testing.expectEqualStrings(case[1], call(p, .{ .tag = 3, .op = .read, .node = node, .handle = o.reply.handle, .size = 64 }).bytes); + _ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = o.reply.handle }); + } +} + test "a run's answer says cut when its output's start is gone, and reads a bounded tail" { const gpa = testing.allocator; const p = try withTerm(gpa); diff --git a/src/normal.zig b/src/normal.zig index da707e26..17ebaac6 100644 --- a/src/normal.zig +++ b/src/normal.zig @@ -7,6 +7,7 @@ const exec = @import("exec.zig"); const look = @import("look.zig"); const std = @import("std"); const regexp = @import("regexp.zig"); +const syntax = @import("syntax.zig"); const modal = @import("modal.zig"); const panes = @import("panes.zig"); const edit = @import("edit.zig"); @@ -18,6 +19,7 @@ const Key = pardes.Key; const hit = pardes.hit; const sel_slot = pardes.sel_slot; const Pardes = pardes.Pardes; +const Macro = pardes.Macro; fn isPrefix(key: Key, cp: u21) bool { return key.cp == cp and !key.ctrl and !key.alt; @@ -27,6 +29,7 @@ fn roleBindingName(comptime role: modal.Normal.Role) []const u8 { return switch (role) { .prefix_goto => "goto_prefix", .prefix_view => "view_prefix", + .prefix_view_sticky => "view_prefix_sticky", .prefix_match => "match_prefix", .prefix_find_fwd => "find_char_fwd", .prefix_find_back => "find_char_back", @@ -35,6 +38,7 @@ fn roleBindingName(comptime role: modal.Normal.Role) []const u8 { .prefix_replace => "replace_prefix", .prefix_next => "next_prefix", .prefix_prev => "prev_prefix", + .prefix_register => "register_prefix", else => @tagName(role), }; } @@ -67,27 +71,38 @@ pub fn multiOnce(p: *Pardes) bool { return p.multi_first; } -fn startSelRegex(p: *Pardes, pane: *Pane, t: *Text, split: bool) void { +fn startSelRegex(p: *Pardes, pane: *Pane, t: *Text, mode: modal.Normal.RegexMode) void { const text = edit.flatSurface(p, t) catch return; const got = t.ranges(text, 0, &pane.sel_snap); pane.nsel_snap = @intCast(got.n); pane.sel_snap_pri = @intCast(got.pri); pane.sel_snap_expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; - p.startPrompt(pane, t, .{ .search = if (split) config.split_marker else config.select_marker }); + p.startPrompt(pane, t, .{ .search = regexMarker(mode) }); } -pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } { +fn regexMarker(mode: modal.Normal.RegexMode) []const u8 { + return switch (mode) { + .select => config.select_marker, + .split => config.split_marker, + .keep => config.keep_marker, + .remove => config.remove_marker, + }; +} + +pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, mode: modal.Normal.RegexMode } { if (pane.prompt != .search or pane.nsel_snap == 0) return null; const armed = pane.input.own orelse ""; - const split = std.mem.startsWith(u8, armed, config.split_marker); - if (!split and !std.mem.startsWith(u8, armed, config.select_marker)) return null; + const mode: modal.Normal.RegexMode = for (std.enums.values(modal.Normal.RegexMode)) |m| { + if (std.mem.startsWith(u8, armed, regexMarker(m))) break m; + } else return null; const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return null; - return .{ .pat = armed[slash + 1 ..], .split = split }; + return .{ .pat = armed[slash + 1 ..], .mode = mode }; } -/// Select (or split on) `pat` inside the selections `t` had when s/S was -/// pressed; the prompt's snapshot of them lives on `pane`. -pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: bool) void { +/// Select (or split on, keep or remove by) `pat` inside the selections `t` +/// had when s/S/K/Alt-K was pressed; the prompt's snapshot of them lives on +/// `pane`. +pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, mode: modal.Normal.RegexMode) void { const text = edit.flatSurface(p, t) catch return; const snap = pane.sel_snap[0..pane.nsel_snap]; var out: [Text.max_selections]modal.Selection = undefined; @@ -106,6 +121,17 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: const from = @min(r.anchor, r.head); const to = @min(@max(r.anchor, r.head), text.len); if (from >= to) continue; + if (mode == .keep or mode == .remove) { + // helix keep_or_remove_matches: the range stays when a match + // starts inside it (K), or when none does (Alt-K) + const hit_at = re.find(hay_all, from, to, to) catch null; + const found = if (hit_at) |h| h.start < to else false; + if (found == (mode == .keep) and m < Text.max_selections) { + out[m] = r; + m += 1; + } + continue; + } // Searched as sam searches, the way addr does (regexp.zig): the // text around the selection still says where its lines begin. var at = from; @@ -113,7 +139,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: while (at < to and m < Text.max_selections) { // A search too slow to finish keeps what it found so far. const hit_at = (re.find(hay_all, at, to, to) catch break) orelse break; - if (split) { + if (mode == .split) { out[m] = .{ .anchor = piece, .head = hit_at.start }; m += 1; piece = hit_at.end; @@ -124,7 +150,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: // an empty match would otherwise never advance at = if (hit_at.end > hit_at.start) hit_at.end else hit_at.end + 1; } - if (split and piece < to and m < Text.max_selections) { + if (mode == .split and piece < to and m < Text.max_selections) { out[m] = .{ .anchor = piece, .head = to }; m += 1; } @@ -142,13 +168,158 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: t.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO) } +/// The file's parse, made on the first key that walks it after an edit and +/// kept until the next: never made again for a key that changed nothing. +pub fn fileTree(p: *Pardes, f: *panes.File.State) ?*anyopaque { + if (f.node_tree == null or f.node_tree_revision != f.revision) { + if (f.node_tree) |old| syntax.freeTree(old); + f.node_tree = syntax.parseTree(f.path, f.content); + f.node_tree_revision = f.revision; + for (f.node_history.items) |step| p.gpa.free(step.ranges); + f.node_history.clearRetainingCapacity(); + } + return f.node_tree; +} + +/// `]f`/`[f` and kin (helix goto_ts_object): to the next object starting +/// after the cursor or the previous one ending before it, count times; select +/// mode stretches the range to it. +fn objectJump(p: *Pardes, pane: *Pane, t: *Text, text: []const u8, range: modal.Selection, object: modal.Normal.Object, forward: bool, count: u32) void { + if (t.what != .body) return; + const f = if (pane.file) |*file| file else return; + const tree = fileTree(p, f) orelse return; + var last = range; + for (0..count) |_| { + const s = syntax.objectNext(f.path, tree, text, object, forward, modal.selectionCursor(text, last)) orelse break; + const next: modal.Selection = .{ .anchor = s.from, .head = s.to }; + if (std.meta.eql(next, last)) break; + last = next; + } + if (std.meta.eql(last, range)) return; + if (t.select) { + const head = if (last.head < range.anchor) last.anchor else last.head; + return t.setRange(text, 0, .{ .anchor = range.anchor, .head = head }, false); + } + const out: modal.Selection = if (forward) last else .{ .anchor = last.head, .head = last.anchor }; + t.setRange(text, 0, out, false); +} + +/// helix's tree-sitter selection keys: every range walks the file's parse, +/// which is made on the first of these keys after an edit and kept, never +/// made again for a key that changed nothing. A range the tree has nothing +/// to say about stays as it was; a text without a grammar, all of them. +pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk) void { + if (t.what != .body) return; + const f = if (pane.file) |*file| file else return; + const tree = fileTree(p, f) orelse return; + const text = f.content; + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + const arena = p.scratch.allocator(); + const rs = arena.alloc(modal.Selection, Text.max_selections) catch return; + const got = t.ranges(text, 0, rs[0..Text.max_selections]); + if (walk == .shrink) if (f.node_history.pop()) |prev| { + defer p.gpa.free(prev.ranges); + // back to what Alt-o grew from, while the selection still holds it + const holds = for (prev.ranges) |pr| { + const pf = @min(pr.anchor, pr.head); + const pt = @max(pr.anchor, pr.head); + if (for (rs[0..got.n]) |r| { + if (@min(r.anchor, r.head) <= pf and pt <= @max(r.anchor, r.head)) break true; + } else false) continue; + break false; + } else true; + if (holds) return t.setRanges(text, prev.ranges, &.{}, prev.primary, expl); + for (f.node_history.items) |step| p.gpa.free(step.ranges); + f.node_history.clearRetainingCapacity(); + }; + const out = arena.alloc(modal.Selection, Text.max_selections) catch return; + const spans = arena.alloc(syntax.Span, Text.max_selections) catch return; + var m: usize = 0; + for (rs[0..got.n]) |r| { + if (m == out.len) break; + const from = @min(r.anchor, r.head); + const to = @max(r.anchor, r.head); + const back = r.head < r.anchor; + const n = syntax.walkNodes(tree, walk, from, to, modal.selectionCursor(text, r), spans[0 .. out.len - m]); + if (n == 0) { + out[m] = r; + m += 1; + continue; + } + for (spans[0..n]) |s| { + out[m] = switch (walk) { + .next_sibling => .{ .anchor = s.from, .head = s.to }, + .prev_sibling => .{ .anchor = s.to, .head = s.from }, + // helix move_parent_node_end: a one-character range on the + // bound, or stretched to it in select mode + .parent_end, .parent_start => bound: { + const head = s.from; + if (t.select) { + const anchor = r.anchor; + break :bound .{ .anchor = anchor, .head = if (head >= anchor) modal.nextGrapheme(text, head) else head }; + } + const next = modal.nextGrapheme(text, head); + break :bound if (back) .{ .anchor = next, .head = head } else .{ .anchor = head, .head = next }; + }, + else => if (back) .{ .anchor = s.to, .head = s.from } else .{ .anchor = s.from, .head = s.to }, + }; + m += 1; + } + } + const same = m == got.n and std.mem.eql(u8, std.mem.sliceAsBytes(out[0..m]), std.mem.sliceAsBytes(rs[0..m])); + if (walk == .expand and !same) remember: { + const ranges = p.gpa.dupe(modal.Selection, rs[0..got.n]) catch break :remember; + f.node_history.append(p.gpa, .{ .ranges = ranges, .primary = got.pri }) catch p.gpa.free(ranges); + } + t.setRanges(text, out[0..m], &.{}, @min(got.pri, m - 1), expl); +} + +/// `gw` (helix goto_word): label the words in view, nearest the cursor first, +/// with two letters each; the next two keys choose one (`jumpKey`). +fn startJump(p: *Pardes, pane: *Pane, t: *Text) void { + if (t.what != .body) return; + const text = edit.flatSurface(p, t) catch return; + const cursor = modal.selectionCursor(text, t.primaryRange(text, 0)); + const top: usize = @intCast(@max(0, pane.scroll())); + const start = @min(modal.lineStartOffset(text, top), text.len); + const end = @min(modal.lineStartOffset(text, top + pane.rows), text.len); + const letters = config.jump_label_alphabet.len; + pane.jump.resize(p.gpa, letters * letters) catch return; + pane.jump.shrinkRetainingCapacity(modal.jumpWords(text, cursor, start, end, pane.jump.items)); + pane.jump_first = null; +} + +/// A key while `gw`'s labels are up: a label's first letter, then its +/// second, which selects that word; anything else takes the labels away. +pub fn jumpKey(p: *Pardes, pane: *Pane, t: *Text, key: Key) void { + const letters = config.jump_label_alphabet; + const at = if (key.ctrl or key.alt or key.cp > 0x7f) null else std.mem.indexOfScalar(u8, letters, @intCast(key.cp)); + const i = at orelse return pane.jump.clearRetainingCapacity(); + const first = pane.jump_first orelse { + if (i * letters.len > pane.jump.items.len) return pane.jump.clearRetainingCapacity(); + pane.jump_first = @intCast(i); + return; + }; + const chosen = first * letters.len + i; + defer pane.jump.clearRetainingCapacity(); + if (chosen >= pane.jump.items.len) return; + const r = pane.jump.items[chosen]; + const text = edit.flatSurface(p, t) catch return; + if (!t.select) return t.setRange(text, 0, .{ .anchor = @min(r.anchor, r.head), .head = @max(r.anchor, r.head) }, true); + // select mode stretches the primary to the word (helix Movement::Extend) + const pr = t.primaryRange(text, 0); + const anchor = if (r.anchor < r.head) @min(r.anchor, @min(pr.anchor, pr.head)) else @max(r.anchor, @max(pr.anchor, pr.head)); + t.setRange(text, 0, .{ .anchor = anchor, .head = r.head }, true); +} + const KeyState = struct { mode: Text.Mode, select: bool, normal: modal.Normal.State, find_op: u8, find_ch: u21, - append_at: @FieldType(Text, "append_at"), + restore_cursor: bool, + reg_wait: bool, fn of(t: *Text) KeyState { return .{ @@ -157,7 +328,8 @@ const KeyState = struct { .normal = t.normal, .find_op = t.find_op, .find_ch = t.find_ch, - .append_at = t.append_at, + .restore_cursor = t.restore_cursor, + .reg_wait = t.reg_wait, }; } @@ -167,7 +339,8 @@ const KeyState = struct { t.normal = s.normal; t.find_op = s.find_op; t.find_ch = s.find_ch; - t.append_at = s.append_at; + t.restore_cursor = s.restore_cursor; + t.reg_wait = s.reg_wait; } }; @@ -227,6 +400,8 @@ pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void { while (i > 0) { i -= 1; p.multi_first = passes == 0; + p.multi_index = i; + p.multi_count = n; passes += 1; saved.into(t); t.nsel = 0; // the handler underneath sees ONE selection @@ -284,11 +459,16 @@ pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void { } pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void { + // `"<reg>` is held by the parser until the command it names arrives + const named = t.normal.register; const parsed = modal.Normal.parse(&t.normal, normalInput(key)); const semantic = switch (parsed) { .pending, .ignored, .unbound => return, .action => |value| value, }; + t.normal.register = 0; + p.register = named; + defer p.register = 0; if (t.nsel == 0 or semantic.scope() == .once) return executeNormalAction(p, t, semantic); replaySels(p, t, .{ .normal = semantic }); @@ -313,6 +493,8 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void switch (semantic) { .escape => { t.select = false; + // typed again by a macro, Esc is only helix's + if (p.macro.replaying > 0) return; return exec.runBuiltin(p, .Last, p.active, "", null); }, .goto => |go| switch (go.target) { @@ -346,6 +528,10 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void .view_top => return t.gotoWindow(text, range, .top, go.count), .view_center => return t.gotoWindow(text, range, .center, go.count), .view_bottom => return t.gotoWindow(text, range, .bottom, go.count), + .last_modification => { + const at = t.last_edit orelse return; + return t.pointMove(text, range, modal.offsetAt(text, .{ .row = @intCast(@max(0, at.row)), .col = @intCast(@max(0, at.col)) })); + }, }, .view => |view| if (t.what == .body) switch (view) { .top => { @@ -360,6 +546,15 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void pane.scrollBy(t.cur_row - (pane.scroll() + @as(i32, pane.rows) - 1)); pane.ensureCursorVisible(); }, + .middle => { + // helix align_view_middle: the cursor's column to the middle + // of an unwrapped view + if (pane.body_wrapped) return; + const line = text[modal.lineStartOffset(text, modal.lineAtOffset(text, cur))..modal.lineEndOffset(text, modal.lineAtOffset(text, cur))]; + const w: usize = @intCast(@max(1, @as(i32, pane.cols) - @as(i32, panes.File.gutterWidth(pane)))); + const col = panes.File.rawDisplayCol(line, @intCast(@max(0, t.cur_col))); + pane.hscroll = @intCast(panes.File.rawAtDisplay(line, col -| w / 2)); + }, .scroll_down => return t.scrollViewMove(text, range, 1), .scroll_up => return t.scrollViewMove(text, range, -1), }, @@ -520,27 +715,28 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void .all => return t.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false), }, .multi => |multi| { - if (multi.kind == .keep_primary) { - t.nsel = 0; - return; - } + if (multi.kind == .keep_primary) return t.dropSels(); return t.multiSelAction(text, multi.kind, multi.count); }, // s, S and | answer for the text they are pressed in; `/` searches // the body wherever it is pressed, as acme's Look from a tag does. - .select_regex => |split| return startSelRegex(p, pane, t, split), + .select_regex => |mode| return startSelRegex(p, pane, t, mode), .edit => |op| switch (op.kind) { .delete => return edit.normalDelete(p, t, true), .delete_noyank => return edit.normalDelete(p, t, false), .change => return edit.normalChange(p, t), .yank => return edit.normalYank(p, t), - .replace_with_yank => return edit.normalReplaceYank(p, t), - .paste_after => return edit.pasteText(p, t, p.yank orelse return, false, op.count), - .paste_before => return edit.pasteText(p, t, p.yank orelse return, true, op.count), + .replace_with_yank => return edit.pasteRegister(p, t, .replace, op.count), + .paste_after => return edit.pasteRegister(p, t, .after, op.count), + .paste_before => return edit.pasteRegister(p, t, .before, op.count), .switch_case => return edit.normalCase(p, t, .toggle), .lowercase => return edit.normalCase(p, t, .lower), .uppercase => return edit.normalCase(p, t, .upper), - .join_lines => return edit.normalJoin(p, t), + .join_lines => return edit.normalJoin(p, t, false), + .join_select_space => return edit.normalJoin(p, t, true), + .rotate_contents_forward => return edit.normalRotateContents(p, t, true, op.count), + .rotate_contents_backward => return edit.normalRotateContents(p, t, false, op.count), + .align_selections => return edit.normalAlign(p, t), .indent => return edit.normalIndent(p, t, op.count, true), .unindent => return edit.normalIndent(p, t, op.count, false), .comment_toggle => return edit.normalToggleComment(p, t), @@ -556,7 +752,7 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void .format => .format, }, ""), .adjust_number => |delta| return edit.normalAdjustNumber(p, t, delta), - .leader => { + .leader => if (p.macro.replaying == 0) { p.leader_on = true; p.leader_n = 0; }, @@ -573,6 +769,46 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void p, if (direction == .forward) @as(i32, 1) else -1, ), + .node => |walk| return nodeSelect(p, pane, t, walk), + .goto_word => return startJump(p, pane, t), + .object_jump => |jump| return objectJump(p, pane, t, text, range, jump.object, jump.direction == .forward, jump.count), + .macro_record => { + const m = &p.macro; + if (m.recording == 0) { + m.recording = if (p.register != 0) p.register else '@'; + m.recorded.clearRetainingCapacity(); + return; + } + // the keys of this command (a count, `"<reg>`, the `Q`) are not + // the macro's + m.recorded.shrinkRetainingCapacity(m.recorded.items.len -| (m.command.items.len + 1)); + const keys = Macro.encode(p.gpa, m.recorded.items) catch return; + defer p.gpa.free(keys); + p.registers.put(p.gpa, m.recording, keys, 0, 1, true); + m.recording = 0; + }, + .macro_replay => |count| { + const name: u21 = if (p.register != 0) p.register else '@'; + const notation = p.registers.value(name, 0) orelse return; + const keys = Macro.decode(p.scratch.allocator(), notation) catch return; + p.register = 0; + for (0..count) |_| p.replayKeys(keys); + }, + .repeat_insert => |count| { + const m = &p.macro; + if (m.last_insert.items.len == 0 or m.repeating) return; + if (m.inserting) return; // the session is still open + const keys = p.scratch.allocator().dupe(Macro.Stored, m.last_insert.items) catch return; + m.repeating = true; + defer m.repeating = false; + // helix repeat_last_insert: the command once, what was typed + // count times, and out of insert mode the way it was left + const typed = @min(m.typed_from, keys.len); + p.replayKeys(keys[0..typed]); + if (keys.len == typed) return; + for (0..count) |_| p.replayKeys(keys[typed .. keys.len - 1]); + p.replayKeys(keys[keys.len - 1 ..]); + }, } } @@ -713,10 +949,11 @@ test "flat text movement and selection replay need no scratch rows" { pane.body.cur_row = 1024; pane.body.cur_col = 0; pane.body.nsel = 1; + _ = pane.body.reserveSels(); pane.body.sels[0] = .{ .row = 3072, .col = 0, .arow = 3072, .acol = 0 }; handleNormal(p, &pane.body, .{ .cp = 'l' }); handleNormal(p, &pane.body, .{ .cp = 'j' }); - try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 1), pane.body.nsel); try std.testing.expectEqual(@as(i32, 1025), pane.body.cur_row); try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expectEqual(@as(i32, 3073), pane.body.sels[0].row); @@ -740,20 +977,20 @@ test "s and S search a multi-line selection as addr does: ^ and $ per line, . wi defer p.deinit(); const text = "alpha x\nbeta alpha\nalphabet\n"; const pane = try p.setTestFile(text); - const Case = struct { pat: []const u8, split: bool, want: []const [2]usize }; + const Case = struct { pat: []const u8, mode: modal.Normal.RegexMode, want: []const [2]usize }; for ([_]Case{ // ^ at each line's start, not only the selection's - .{ .pat = "^alpha", .split = false, .want = &.{ .{ 0, 5 }, .{ 19, 24 } } }, + .{ .pat = "^alpha", .mode = .select, .want = &.{ .{ 0, 5 }, .{ 19, 24 } } }, // $ at each line's end - .{ .pat = "alpha$", .split = false, .want = &.{.{ 13, 18 }} }, + .{ .pat = "alpha$", .mode = .select, .want = &.{.{ 13, 18 }} }, // . stops at the newline: one selection per line, not one for all - .{ .pat = "a.*", .split = false, .want = &.{ .{ 0, 7 }, .{ 11, 18 }, .{ 19, 27 } } }, + .{ .pat = "a.*", .mode = .select, .want = &.{ .{ 0, 7 }, .{ 11, 18 }, .{ 19, 27 } } }, // S splits on a line's end - .{ .pat = "$", .split = true, .want = &.{ .{ 0, 7 }, .{ 7, 18 }, .{ 18, 27 }, .{ 27, 28 } } }, + .{ .pat = "$", .mode = .split, .want = &.{ .{ 0, 7 }, .{ 7, 18 }, .{ 18, 27 }, .{ 27, 28 } } }, }) |c| { pane.sel_snap[0] = .{ .anchor = 0, .head = text.len }; pane.nsel_snap = 1; - applySelRegex(p, pane, &pane.body, c.pat, c.split); + applySelRegex(p, pane, &pane.body, c.pat, c.mode); var out: [Text.max_selections]modal.Selection = undefined; const got = pane.body.ranges(text, 0, &out); try std.testing.expectEqual(c.want.len, got.n); @@ -763,3 +1000,132 @@ test "s and S search a multi-line selection as addr does: ^ and $ per line, . wi } } } + +test "tree-sitter selection keys do in a JSON file what hx 25.07.1 does" { + if (!syntax.supportsPath("a.json")) return; + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + const text = "{\"a\": [1, 2, 3], \"b\": true}\n"; + // each checked against the installed hx through a pty: the keys, then + // the text it saved + const Case = struct { keys: []const u8, want: []const u8 }; + for ([_]Case{ + .{ .keys = "10l\x1bo" ++ "d", .want = "{\"a\": , \"b\": true}\n" }, + .{ .keys = "10l\x1bo\x1bod", .want = "{, \"b\": true}\n" }, + .{ .keys = "10l\x1bo\x1bo\x1bod", .want = "\n" }, + .{ .keys = "10l\x1bo\x1bIix\x1b", .want = "{\"a\": [x1, x2, x3], \"b\": true}\n" }, + .{ .keys = "10l\x1bo\x1bo\x1b>d", .want = "{\"a\": [1, 2, 3] \"b\": true}\n" }, + .{ .keys = "10l\x1bpd", .want = "{\"a\": [1 2, 3], \"b\": true}\n" }, + .{ .keys = "10l\x1baix\x1b", .want = "{\"a\": [x1, x2, x3], \"b\": true}\n" }, + .{ .keys = "10l\x1bo\x1bo\x1bid", .want = "{\"a\": , \"b\": true}\n" }, + .{ .keys = "6lx\x1bid", .want = "\n" }, + .{ .keys = "10l\x1beiX\x1b", .want = "{\"a\": [1, 2X, 3], \"b\": true}\n" }, + .{ .keys = "10l\x1bb\x1bbiX\x1b", .want = "{X\"a\": [1, 2, 3], \"b\": true}\n" }, + // insert Tab after text leaves the node (smart tab) + .{ .keys = "8li\tX\x1b", .want = "{\"a\": [1, 2, 3]X, \"b\": true}\n" }, + }) |c| { + const pane = try p.setTestFile(text); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try p.gpa.dupe(u8, "/test.json"); + // ESC before a key is Alt on it; a lone ESC is Esc; `>` is Alt-right + var i: usize = 0; + while (i < c.keys.len) : (i += 1) { + const ch = c.keys[i]; + if (ch == 0x1b and i + 1 < c.keys.len) { + i += 1; + const k = c.keys[i]; + p.update(.{ .key = if (k == '>') .{ .cp = Key.right, .alt = true } else .{ .cp = k, .alt = true } }); + } else if (ch == 0x1b) { + p.update(.{ .key = .{ .cp = Key.escape } }); + } else if (ch == '\t') { + p.update(.{ .key = .{ .cp = Key.tab } }); + } else p.update(.{ .key = .{ .cp = ch, .text = c.keys[i..][0..1] } }); + while (p.nextEffect()) |_| {} + } + std.testing.expectEqualStrings(c.want, pane.file.?.content) catch |err| { + std.debug.print("keys {s}\n", .{c.keys}); + return err; + }; + } +} + +test "one cursor holds no room for other ranges; a second range makes it and one again gives it back" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("alpha beta\nalpha beta\n"); + for ("wxe%;gg") |cp| handleNormal(p, &pane.body, .{ .cp = cp }); + try std.testing.expectEqual(@as(usize, 0), pane.body.sels.len); + handleNormal(p, &pane.body, .{ .cp = 'C' }); + try std.testing.expectEqual(@as(u16, 1), pane.body.nsel); + try std.testing.expect(pane.body.sels.len > 0); + handleNormal(p, &pane.body, .{ .cp = 'l' }); // replayed at both, still two + try std.testing.expectEqual(@as(u16, 1), pane.body.nsel); + handleNormal(p, &pane.body, .{ .cp = ',' }); + try std.testing.expectEqual(@as(usize, 0), pane.body.sels.len); + handleNormal(p, &pane.body, .{ .cp = 'C' }); + handleNormal(p, &pane.body, .{ .cp = ';' }); + handleNormal(p, &pane.body, .{ .cp = 'x' }); // x at both: still two lines + handleNormal(p, &pane.body, .{ .cp = '_', .alt = true }); // merged to one + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); + try std.testing.expectEqual(@as(usize, 0), pane.body.sels.len); +} + +test "tree-sitter textobject keys do in Python, JSON and Zig what hx 25.07.1 does" { + if (!syntax.supportsPath("a.py") or !syntax.supportsPath("a.json") or !syntax.supportsPath("a.zig")) return; + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + // each checked against the installed hx through a pty: the keys, then + // the text it saved + const Case = struct { path: []const u8, text: []const u8, keys: []const u8, want: []const u8 }; + for ([_]Case{ + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]fd", .want = "def alpha(a, b):\n return a + b\n\n\n\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]f]fd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "2]fd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "Gk[fd", .want = "ef alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "jmifd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "jmafd", .want = "\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "10lmiad", .want = "def alpha(, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "10lmaad", .want = "def alpha( b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]Td", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\n\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]cd", .want = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n \n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "v]fd", .want = "\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]td", .want = "ef alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.py", .text = "def alpha(a, b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n", .keys = "]ad", .want = "def alpha( b):\n return a + b\n\n\ndef beta(c):\n # note\n return c\n\n\ndef test_one():\n assert beta(1) == 1\n" }, + .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "]ed", .want = "{, \"b\": [1, 2]}\n" }, + .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "]e]ed", .want = "{\"a\": 1, }\n" }, + .{ .path = "/test.json", .text = "{\"a\": 1, \"b\": [1, 2]}\n", .keys = "8lmaed", .want = "{\"a\": 1,\"b\": [1, 2]}\n" }, + // Zig: the query is helix's, ported to pardes's zig grammar. No case + // walks call arguments: the reference helix counts them as + // parameters and the installed 25.07.1's query does not. + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "]fd", .want = "const std = @import(\"std\");\n\n/// adds\n\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "5jmifd", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 \n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "5jmafd", .want = "const std = @import(\"std\");\n\n/// adds\n\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "]td", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\n\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "9jmitd", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "9jmatd", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\n\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "]Td", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\n\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "16jmiTd", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" \n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "3j11lmiad", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "3j11lmaad", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add( b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "]cd", .want = "const std = @import(\"std\");\n\n\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "4jmacd", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "16jf2[ad", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, ) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "16j[fd", .want = "const std = @import(\"std\");\n\n/// adds\n\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + .{ .path = "/test.zig", .text = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\nconst Color = enum { red, green };\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n", .keys = "]t]td", .want = "const std = @import(\"std\");\n\n/// adds\npub fn add(a: u32, b: u32) u32 {\n // sum\n return a + b;\n}\n\nconst Point = struct {\n x: i32,\n y: i32,\n};\n\n\n\ntest \"add works\" {\n try std.testing.expectEqual(@as(u32, 3), add(1, 2));\n}\n" }, + }) |c| { + const pane = try p.setTestFile(c.text); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try p.gpa.dupe(u8, c.path); + for (c.keys, 0..) |ch, i| { + p.update(.{ .key = .{ .cp = ch, .text = c.keys[i..][0..1] } }); + while (p.nextEffect()) |_| {} + } + std.testing.expectEqualStrings(c.want, pane.file.?.content) catch |err| { + std.debug.print("keys {s} on {s}\n", .{ c.keys, c.path }); + return err; + }; + } +} diff --git a/src/panes.zig b/src/panes.zig index d4b336d7..8049da4e 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -24,6 +24,9 @@ const Key = pardes.Key; pub const EditText = struct { text: []u8, row0: i32 }; pub const Pane = struct { + /// The words that throw unsaved text away (builtins.zig warnModified). + pub const Discarding = enum { Exit, Restore }; + /// One mouse selection (block-shaped), per button. c/r are text-area relative; /// r counts from the tag's first row (the body starts at Pane.tag_rows). pub const Sel = struct { @@ -69,6 +72,10 @@ pub const Pane = struct { /// Del from the keyboard with open panes both above and below: which /// one takes the rows. Nothing is typed; one key answers. del_side, + /// An exec from a file's body with several REPLs bound for its + /// language: which one takes it (exec.zig routeToRepl). One key + /// answers, the question being in `question`. + repl_choice, /// A new name for the file, drafted in place of the path on the /// tagline after a click there; Enter commits it, as a buffer name /// and never a disk rename. @@ -99,9 +106,39 @@ pub const Pane = struct { /// middle click, a pty/run): its first word, which Kill matches, and /// the shell's command count its start (C mark) reaches. sent_command: ?struct { word: [64]u8 = undefined, len: u8 = 0, seq: u32 = 0 } = null, + /// A command pane (exec.zig runCommand): the line its child runs as + /// `$SHELL -c`, owned, where an interactive terminal has null. + command: ?[]u8 = null, + /// The command's child has exited, and with what status (null: unknown, + /// shown `exit ?`). + command_done: bool = false, + command_status: ?u8 = null, + /// Its pty is still open: a job the command left in the background may + /// be printing to it, and a reuse would hang it up. + command_pty: bool = false, + /// The shell a terminal runs when it is not the configured one (`Tty + /// fish`), owned. + shell: ?[]u8 = null, + /// An interactive terminal's tag text naming its shell, ` Tty+fish ...` + /// (Pardes.acknowledgeShell); empty until the host says which it ran. + shell_tail: [96]u8 = undefined, + shell_tail_len: u8 = 0, + /// A terminal bound as a language's REPL (`Repl python`): an exec from + /// the body of a file in that language is sent to it (exec.zig + /// routeToRepl). `letter` makes its id, `python-a`, with the language's + /// name from grammar_manifest. + repl: ?struct { lang: u8, letter: u8 } = null, + /// It has been told that its program takes no bracketed paste. + repl_warned: bool = false, + /// A question with its answers in it (`repl_choice`). + question: [96]u8 = undefined, + question_len: u8 = 0, + /// The newest message says where an exec was sent: it is tinted. + msg_sent: bool = false, /// The revision an Exit or a Restore last warned this pane was modified - /// at (builtins.zig warnModified). - discard_warned: ?u32 = null, + /// at, and which of them: only that word goes ahead on the warning + /// (builtins.zig warnModified). + discard_warned: ?struct { revision: u32, by: Discarding } = null, /// Native host queues the Linux v9fs launcher in this pane's initial shell. v9fs_on_spawn: bool = false, file: ?File.State = null, @@ -120,9 +157,13 @@ pub const Pane = struct { search_pane: ?usize = null, search_row: ?usize = null, look_at: ?LookSpot = null, + /// `gw`'s words, labelled in order while it waits for a label to be + /// typed (gpa-owned); `jump_first` is the label's first letter once typed. + jump: std.ArrayList(modal.Selection) = .empty, + jump_first: ?u8 = null, sel_snap: [Text.max_selections]modal.Selection = undefined, - nsel_snap: u8 = 0, - sel_snap_pri: u8 = 0, + nsel_snap: u16 = 0, + sel_snap_pri: u16 = 0, sel_snap_expl: bool = false, /// The tag: what the user keeps after the computed prefix (the path, the /// dirty marker, a PDF's page), its cursor, selections and undo. The @@ -168,6 +209,8 @@ pub const Pane = struct { msg_len: u16 = 0, /// Where the newest message line is in its life. msg_life: MessageLife = .{}, + /// When it was said, in monotonic ms (Messages.expireUnattended). + msg_said_ms: i64 = 0, /// The row (among the notice rows) the newest line holds for its life. msg_slot: u8 = 0, /// Earlier message lines still on screen, oldest first, each running its @@ -340,6 +383,7 @@ pub const Pane = struct { return switch (p.prompt) { .none, .name => null, .del_side => config.del_question, + .repl_choice => p.question[0..p.question_len], else => p.input.own orelse "", }; } diff --git a/src/pardes.zig b/src/pardes.zig index 5fa3307b..f559b125 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -20,6 +20,8 @@ pub const edit = @import("edit.zig"); pub const body_layer = @import("body_layer.zig"); pub const draw = @import("draw.zig"); pub const Messages = @import("Messages.zig"); +pub const Registers = @import("Registers.zig"); +pub const Macro = @import("Macro.zig"); pub const Pipe = @import("Pipe.zig"); pub const colors = @import("colors.zig"); pub const Theme = colors.Theme; @@ -455,6 +457,8 @@ test { _ = @import("regexp.zig"); _ = @import("edit.zig"); _ = @import("Messages.zig"); + _ = @import("Registers.zig"); + _ = @import("Macro.zig"); _ = @import("selection_pipe.zig"); _ = @import("Pipe.zig"); _ = @import("colors.zig"); @@ -555,7 +559,7 @@ test "Detach asks the frontend to leave, and says so when there is nothing to le p.perform(.{ .detach = asked }); const pane = p.panes[0].?; - try std.testing.expectEqualStrings("detach: NotAttached", pane.msg[0..pane.msg_len]); + try std.testing.expectEqualStrings("detach: not attached", pane.msg[0..pane.msg_len]); try std.testing.expect(!p.quit); } @@ -626,8 +630,9 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" { while (p.nextEffect()) |_| {} // initial shell spawn/geometry p.acknowledgeShell(0, "/bin/sh", false); + // The Tty word names the shell the host ran, and opens another of it. const pane = p.panes[0].?; - try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(pane)); + try std.testing.expectEqualStrings(" Tty+sh" ++ tagline.terminal_pane_tail[" Tty".len..], tagline.curTail(pane)); const tag_x = p.rects[0].x + TAG_TEXT_INSET; const tag_y = p.rects[0].y; @@ -649,6 +654,7 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" { }; try std.testing.expectEqual(panes_before + 1, panes_after); try std.testing.expect(p.panes[p.active].?.isTerminal()); + try std.testing.expectEqualStrings("sh", p.panes[p.active].?.shell.?); // Argument-taking builtins use the same gesture path: a tag sweep is the // complete command line, and Theme consumes its tail without a PTY write. @@ -1186,7 +1192,7 @@ test "first document fallback splits like Tty from the originating pane" { const free = p.freeSlot().?; const doc = try p.openTutorView(free); exec.placeDoc(p, from, free, doc); - exec.spawnTty(tty, from); + _ = exec.spawnTty(tty, from); p.sync(); tty.sync(); try std.testing.expectEqual(@as(usize, 2), p.ncol); @@ -1277,34 +1283,35 @@ test "an unsaved file marker sits between its path and builtins until Save" { try std.testing.expect(std.mem.indexOf(u8, output, "/test.txt *") == null); } -test "unknown Exec from an image writes to a terminal in the image directory" { +test "unknown Exec from a document runs as a command pane in its directory, typed into no terminal" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{}); defer p.deinit(); while (p.nextEffect()) |_| {} p.acknowledgeShell(0, "/bin/sh", false); + // A terminal free at a prompt in the same directory is not borrowed. p.setCwd(0, "/tmp/pardes-image-dir"); const image_doc = try p.newDocPane(1); image_doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pardes-image-dir/pic.ppm") }; try std.testing.expectEqualStrings("/tmp/pardes-image-dir", Pardes.paneDir(image_doc)); - try std.testing.expectEqual(@as(?usize, 0), exec.execute(p, 1, "echo image-fallback")); + const dst = exec.execute(p, 1, "echo image-fallback") orelse return error.ExecFoundNowhereToRun; + try std.testing.expect(dst != 0 and dst != 1); + try std.testing.expectEqualStrings("echo image-fallback", p.panes[dst].?.command.?); - var sent: [256]u8 = undefined; - var sent_len: usize = 0; + var spawned = false; while (p.nextEffect()) |effect| switch (effect) { - .write => |w| { - try std.testing.expectEqual(@as(u8, 0), w.pane); - try std.testing.expect(w.bytes.slice().len <= sent.len - sent_len); - @memcpy(sent[sent_len..][0..w.bytes.slice().len], w.bytes.slice()); - sent_len += w.bytes.slice().len; + .write => return error.TypedIntoATerminal, + .spawn => |sp| if (sp.pane == dst) { + try std.testing.expectEqualStrings("/tmp/pardes-image-dir", sp.cwd.slice()); + spawned = true; }, - .spawn => return error.ImageExecSpawnedDespiteMatchingTerminal, else => {}, }; - try std.testing.expectEqualStrings("echo image-fallback\r", sent[0..sent_len]); + try std.testing.expect(spawned); } + const FakeTtyQuery = struct { taken: [MAX_PANES]bool = @splat(false), asked: usize = 0, @@ -1322,7 +1329,7 @@ const FakeTtyQuery = struct { } }; -test "Exec in a terminal whose tty is taken spawns a shell instead of typing at the program" { +test "Exec in a terminal whose tty is taken runs as a command pane instead of typing at the program" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{}); defer p.deinit(); @@ -1335,42 +1342,384 @@ test "Exec in a terminal whose tty is taken spawns a shell instead of typing at host.install(p); host.taken[0] = true; const dst = exec.execute(p, 0, "echo taken-fallback") orelse return error.ExecFoundNowhereToRun; - // somewhere ELSE — and still without moving focus, which is execute's + // somewhere ELSE -- and still without moving focus, which is execute's // contract and the whole difference between it and a look try std.testing.expect(dst != 0); try std.testing.expectEqual(@as(usize, 0), p.active); - - var spawned: ?Effect = null; - var sent: [256]u8 = undefined; - var sent_len: usize = 0; + try std.testing.expectEqualStrings("echo taken-fallback", p.panes[dst].?.command.?); + var spawned = false; while (p.nextEffect()) |effect| switch (effect) { - .write => |w| { - if (w.pane == 0) return error.WroteACommandLineIntoTheProgramOnTheTty; - try std.testing.expectEqual(@as(u8, @intCast(dst)), w.pane); - try std.testing.expect(w.bytes.slice().len <= sent.len - sent_len); - @memcpy(sent[sent_len..][0..w.bytes.slice().len], w.bytes.slice()); - sent_len += w.bytes.slice().len; + .write => return error.WroteACommandLineIntoATerminal, + .spawn => |sp| if (sp.pane == dst) { + // ...in the directory the command was about, the taken pane's own + try std.testing.expectEqualStrings("/tmp/pardes-taken", sp.cwd.slice()); + spawned = true; }, - .spawn => spawned = effect, else => {}, }; - const sp = (spawned orelse return error.NoShellForTheOccupiedTerminal).spawn; - try std.testing.expectEqual(@as(u8, @intCast(dst)), sp.pane); - // ...in the directory the command was about, which is the taken pane's own - try std.testing.expectEqualStrings("/tmp/pardes-taken", sp.cwd.slice()); - try std.testing.expectEqual(@as(usize, 0), sent_len); - p.acknowledgeShell(dst, "/bin/sh", false); - while (p.nextEffect()) |effect| switch (effect) { - .write => |w| { - try std.testing.expectEqual(@as(u8, @intCast(dst)), w.pane); - @memcpy(sent[sent_len..][0..w.bytes.slice().len], w.bytes.slice()); - sent_len += w.bytes.slice().len; - }, - else => {}, + try std.testing.expect(spawned); +} + +test "a command of several lines keeps its tag one row, and its echo does not staircase" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const doc = try p.newDocPane(1); + doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pic.ppm") }; + const two = "def greet(name):\n return name\n\nprint(greet(\"x\"))\n"; + const dst = exec.execute(p, 1, two) orelse return error.NoCommandPane; + while (p.nextEffect()) |_| {} + const pane = p.panes[dst].?; + const prefix = try tagline.tagPrefix(p, pane); + try std.testing.expect(std.mem.indexOfScalar(u8, prefix, '\n') == null); + try std.testing.expect(std.mem.indexOf(u8, prefix, "(def greet(name):...) running") != null); + // Rendered without a panic, however the tag wraps. + var frame = std.heap.ArenaAllocator.init(gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + p.update(.{ .exited = .{ .pane = @intCast(dst), .status = 0 } }); + _ = exec.execute(p, 1, two); + while (p.nextEffect()) |_| {} + _ = try p.render(frame.allocator()); + const body = try panes.Terminal.screenTextAlloc(pane, gpa); + defer gpa.free(body); + try std.testing.expect(std.mem.indexOf(u8, body, "% def greet(name):\n return name") != null); +} + +test "a spawn is for the pane it was made for, not whatever took the slot since" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const Count = struct { + var spawns: usize = 0; + var command: [32]u8 = undefined; + var core: *Pardes = undefined; + const vtable: Host.VTable = .{ .spawn = spawn }; + fn spawn(_: ?*anyopaque, pane: u8, _: []const u8) void { + spawns += 1; + const line = core.panes[pane].?.command.?; + @memcpy(command[0..line.len], line); + } + }; + Count.core = p; + p.host = .{ .vtable = &Count.vtable }; + // A command pane closed and its slot taken, in one step, before the + // effects run: only the second's spawn is performed. + const slot = p.freeSlot().?; + _ = try p.newCommand(slot, "", "first"); + try p.removePane(slot, null); + try std.testing.expectEqual(slot, p.freeSlot().?); + _ = try p.newCommand(slot, "", "second"); + while (p.nextEffect()) |effect| p.perform(effect); + try std.testing.expectEqual(@as(usize, 1), Count.spawns); + try std.testing.expectEqualStrings("second", Count.command[0.."second".len]); +} + +test "a REPL bound for a language takes the exec made on a body in it; tags, Exec by name and command words still run" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .shells = 3, .cols = 100, .rows = 30 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const file = try p.setTestFile("print(1)\nls\n"); + gpa.free(file.file.?.path); + file.file.?.path = try gpa.dupe(u8, "/x.py"); + const Sent = struct { + var to: [MAX_PANES][128]u8 = undefined; + var len: [MAX_PANES]usize = @splat(0); + var spawned: bool = false; + fn drain(pp: *Pardes) void { + len = @splat(0); + spawned = false; + while (pp.nextEffect()) |effect| switch (effect) { + .write => |w| { + @memcpy(to[w.pane][len[w.pane]..][0..w.bytes.slice().len], w.bytes.slice()); + len[w.pane] += w.bytes.slice().len; + }, + .spawn => spawned = true, + else => {}, + }; + } + fn said(pane: usize) []const u8 { + return to[pane][0..len[pane]]; + } + }; + // The programs the REPLs run hold their terminals' ttys. + var host: FakeTtyQuery = .{}; + host.install(p); + host.taken[1] = true; + host.taken[2] = true; + // Only a terminal is a REPL; a language's name is the syntax table's. + try std.testing.expect(p.executeBuiltinLine(0, "Repl python")); + try std.testing.expect(file.repl == null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl pithon")); + try std.testing.expect(p.panes[1].?.repl == null); + try std.testing.expect(std.mem.indexOf(u8, p.panes[1].?.msg[0..p.panes[1].?.msg_len], "python") != null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); + // again, as a code fence may name it: no change + try std.testing.expect(p.executeBuiltinLine(1, "Repl PY")); + var idbuf: [48]u8 = undefined; + try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?)); + try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, p.panes[1].?), " python-a")); + Sent.drain(p); + + // A click on the file's body, a single word: typed into the REPL. + try std.testing.expectEqual(@as(?usize, 1), exec.executeFrom(p, 0, "ls", true)); + Sent.drain(p); + try std.testing.expectEqualStrings("ls\r", Sent.said(1)); + try std.testing.expect(!Sent.spawned); + try std.testing.expect(file.msg_sent); + try std.testing.expectEqualStrings("\u{2192} python-a", file.msg[0..file.msg_len]); + // A body's click written back to event is done as the click: sent. A + // line written to exec came back as F and runs as the command it was. + { + const th = @import("ninep/testing.zig"); + const event = ctlfs.Node.of(file.serial, .event); + const h = th.call(p, .{ .tag = 1, .op = .open, .node = event }).reply.handle; + defer _ = th.call(p, .{ .tag = 2, .op = .release, .node = event, .handle = h }); + // The 9P harness performs the effects: what it wrote is its answer's. + const sent = th.wr(p, event, "MX0 8\n"); + try std.testing.expectEqualStrings("print(1)\r", sent.pty_buf[0..sent.pty_len]); + const ran = th.wr(p, event, "FX0 0 2 2 ls\n"); + try std.testing.expect(ran.pty_len == 0 and ran.spawned); + // The pane's ctl line ends with the id it is bound as. + var line: [ctlfs.ctl.pane_text_cap]u8 = undefined; + try std.testing.expect(std.mem.endsWith(u8, ctlfs.ctl.paneText(p, p.panes[1].?, &line), " python-a ")); + } + // What is sent is plain text: CRLF is LF, no ESC or other control. + _ = exec.executeFrom(p, 0, "a = 1\r\nb = \x1b[201~2", true); + Sent.drain(p); + try std.testing.expectEqualStrings("a = 1\rb = [201~2\r", Sent.said(1)); + // Several selections executed at once: each is sent, not the first. + { + const text = try edit.flatSurface(p, &file.body); + file.body.setRanges(text, &.{ .{ .anchor = 0, .head = 8 }, .{ .anchor = 9, .head = 11 } }, &.{}, 0, true); + try std.testing.expect(exec.chordEachSel(p, file, .Exec)); + Sent.drain(p); + try std.testing.expectEqualStrings("print(1)\rls\r", Sent.said(1)); + } + // Its own body too. + try std.testing.expectEqual(@as(?usize, 1), exec.executeFrom(p, 1, "x = 1", true)); + Sent.drain(p); + try std.testing.expectEqualStrings("x = 1\r", Sent.said(1)); + + // Escapes: a tag's word, Exec by name, a command word. Each a command. + _ = exec.execute(p, 0, "ls"); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + try std.testing.expect(p.executeBuiltinLine(0, "Exec ls")); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + _ = exec.executeFrom(p, 0, "@`ls`", true); + Sent.drain(p); + try std.testing.expect(Sent.spawned and Sent.len[1] == 0); + + // Two bound: it asks, one key answers, Esc sends nowhere. + try std.testing.expect(p.executeBuiltinLine(2, "Repl python")); + try std.testing.expectEqualStrings("python-b", exec.replId(&idbuf, p.panes[2].?)); + Sent.drain(p); + try std.testing.expectEqual(@as(?usize, null), exec.executeFrom(p, 0, "print(2)", true)); + try std.testing.expectEqual(Pane.Prompt.repl_choice, file.prompt); + try std.testing.expect(std.mem.indexOf(u8, file.promptText().?, "a b") != null); + p.update(.{ .key = .{ .cp = 'b' } }); + Sent.drain(p); + try std.testing.expectEqualStrings("print(2)\r", Sent.said(2)); + try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none); + _ = exec.executeFrom(p, 0, "print(3)", true); + p.update(.{ .key = .{ .cp = Key.escape } }); + Sent.drain(p); + try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0); + // The pane a letter offered is the one it answers for: unbound while + // the question stood, it is sent nothing, and that is said. + _ = exec.executeFrom(p, 0, "print(4)", true); + try std.testing.expect(p.executeBuiltinLine(2, "Repl -")); + p.update(.{ .key = .{ .cp = 'b' } }); + Sent.drain(p); + try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0); + try std.testing.expectEqualStrings("Repl: the REPL chosen is gone; nothing sent", file.msg[0..file.msg_len]); + try std.testing.expect(p.executeBuiltinLine(2, "Repl python")); + try std.testing.expectEqualStrings("python-b", exec.replId(&idbuf, p.panes[2].?)); + + // A REPL whose program has left (Ctrl-D: the shell has the tty) is + // sent nothing, and nothing runs: it says so. + host.taken[1] = false; + host.taken[2] = false; + _ = exec.executeFrom(p, 0, "import os", true); + Sent.drain(p); + try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0 and !Sent.spawned); + try std.testing.expect(std.mem.indexOf(u8, file.msg[0..file.msg_len], "no program running") != null); + host.taken[1] = true; + host.taken[2] = true; + + // Unbound, its letter is free again; bare, it says what it is. + try std.testing.expect(p.executeBuiltinLine(1, "Repl -")); + try std.testing.expect(p.panes[1].?.repl == null); + try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); + try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?)); + try std.testing.expect(p.executeBuiltinLine(2, "Repl")); + try std.testing.expectEqualStrings("Repl: python-b", p.panes[2].?.msg[0..p.panes[2].?.msg_len]); + + // A command pane is a REPL while its command runs: done, it is none, + // and a done one cannot be bound. A document pane is no terminal. + const cmd = p.freeSlot().?; + _ = try p.newCommand(cmd, "", "python3"); + try std.testing.expect(p.executeBuiltinLine(cmd, "Repl python")); + try std.testing.expect(p.panes[cmd].?.repl != null); + p.update(.{ .exited = .{ .pane = @intCast(cmd), .status = 0 } }); + try std.testing.expect(p.panes[cmd].?.repl == null); + try std.testing.expect(p.executeBuiltinLine(cmd, "Repl python")); + try std.testing.expect(p.panes[cmd].?.repl == null); + const doc_slot = p.freeSlot().?; + const doc = try p.newDocPane(doc_slot); + try std.testing.expect(p.executeBuiltinLine(doc_slot, "Repl python")); + try std.testing.expect(doc.repl == null); +} + +test "a restored terminal's tag names neither the REPL it was nor the shell it had" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + p.acknowledgeShell(0, "/usr/bin/fish", true); + var host: FakeTtyQuery = .{}; + host.install(p); + try std.testing.expect(p.executeBuiltinLine(0, "Repl python")); + try dump.dumpState(p); + const restored = try dump.restore(p, p.dump_out.?, "test"); + defer restored.deinit(); + const pane = restored.panes[0].?; + try std.testing.expect(pane.repl == null and pane.tag.own == null); + const shown = try tagline.tagText(restored, restored.scratch.allocator(), pane); + try std.testing.expect(std.mem.indexOf(u8, shown, "python-a") == null); + try std.testing.expect(std.mem.indexOf(u8, shown, "Tty+fish") == null); +} + +test "Tty+fish, one word a tag can hold, opens a terminal on that shell" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + // sh, the one shell every host has. + try std.testing.expect(p.executeBuiltinLine(0, "Tty+sh")); + try std.testing.expect(p.active != 0); + try std.testing.expectEqualStrings("sh", p.panes[p.active].?.shell.?); + p.acknowledgeShell(p.active, "/bin/sh", false); + try std.testing.expect(std.mem.startsWith(u8, tagline.curTail(p.panes[p.active].?), " Tty+sh ")); + p.active = 0; + // A shell that is not there is said, not quietly replaced, and the + // pane that asked gets no shell of its own. + const before = p.active; + const caller = p.panes[0].?; + try std.testing.expect(p.executeBuiltinLine(0, "Tty /nonexistent")); + try std.testing.expect(p.active == before and caller.shell == null); + try std.testing.expect(std.mem.indexOf(u8, caller.msg[0..caller.msg_len], "no shell \"/nonexistent\"") != null); + try std.testing.expect(p.executeBuiltinLine(0, "Tty fsh-not-a-shell")); + try std.testing.expect(caller.shell == null); + // Only a word that says so splits at `+`: `Dump+x.zon` is no Dump. + try std.testing.expect(!p.executeBuiltinLine(0, "Dump+x.zon")); + try std.testing.expect(!p.executeBuiltinLine(0, "Msg+hello")); +} + +test "a command pane shows how its command ended, and the next command there runs below it" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{}); + defer p.deinit(); + while (p.nextEffect()) |_| {} + p.setCwd(0, "/tmp/pardes-cmd"); + const doc = try p.newDocPane(1); + doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pardes-cmd/pic.ppm") }; + + const dst = exec.execute(p, 1, "make -j8") orelse return error.NoCommandPane; + while (p.nextEffect()) |_| {} + const pane = p.panes[dst].?; + // Its first command is echoed as every later one is. + { + const first = try panes.Terminal.screenTextAlloc(pane, gpa); + defer gpa.free(first); + try std.testing.expect(std.mem.startsWith(u8, first, "% make -j8")); + } + var frame = std.heap.ArenaAllocator.init(gpa); + defer frame.deinit(); + try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, pane), "(make -j8) running")); + // Running, a second command goes to a pane of its own. + const other = exec.execute(p, 1, "ls") orelse return error.NoCommandPane; + try std.testing.expect(other != dst); + while (p.nextEffect()) |_| {} + p.update(.{ .output = .{ .pane = @intCast(dst), .bytes = "compiled\r\n" } }); + // Its exit comes first, a job it left still printing: done, and not + // the next command's until its pty closes. + p.update(.{ .exited = .{ .pane = @intCast(dst), .status = 2 } }); + try std.testing.expect(pane.command_done and pane.command_pty); + const busy = exec.execute(p, 1, "true") orelse return error.NoCommandPane; + try std.testing.expect(busy != dst); + while (p.nextEffect()) |_| {} + // Its output ending: now it may be reused. + p.update(.{ .eof = .{ .pane = @intCast(dst) } }); + try std.testing.expect(p.panes[dst] == pane and !pane.command_pty); + try std.testing.expect(p.panes[dst] == pane); + try std.testing.expect(pane.command_done); + try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, pane), "(make -j8) exit 2")); + // Kill leaves a finished one be. + try std.testing.expect(p.executeBuiltinLine(0, "Kill make")); + while (p.nextEffect()) |effect| if (effect == .signal_pty and effect.signal_pty.pane == dst) return error.SignalledAFinishedCommand; + // A program that crashed out of its full screen leaves the emulator + // in its modes; the next command starts clear of them. + p.update(.{ .output = .{ .pane = @intCast(dst), .bytes = "\x1b[?1049h\x1b[?2004h\x1b[?1000h" } }); + try std.testing.expect(panes.Terminal.bracketedPaste(pane)); + // Done, the directory's next command runs there, below what it showed. + try std.testing.expectEqual(@as(?usize, dst), exec.execute(p, 1, "make test")); + try std.testing.expect(!panes.Terminal.bracketedPaste(pane)); + var respawned = false; + while (p.nextEffect()) |effect| if (effect == .spawn and effect.spawn.pane == dst) { + respawned = true; + }; + try std.testing.expect(respawned); + const body = try panes.Terminal.screenTextAlloc(pane, gpa); + defer gpa.free(body); + const said = std.mem.indexOf(u8, body, "compiled") orelse return error.OutputLost; + const ended = std.mem.indexOf(u8, body, "exit 2") orelse return error.ExitLost; + const next = std.mem.indexOf(u8, body, "% make test") orelse return error.NoSeparator; + try std.testing.expect(said < ended and ended < next); + try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, pane), "(make test) running")); + // Kill stops a running one: its whole process group, SIGTERM. + try std.testing.expect(p.executeBuiltinLine(0, "Kill make")); + var signalled = false; + while (p.nextEffect()) |effect| if (effect == .signal_pty and effect.signal_pty.pane == dst) { + try std.testing.expectEqual(PtySignal.term, effect.signal_pty.sig); + signalled = true; }; - try std.testing.expectEqualStrings("echo taken-fallback\r", sent[0..sent_len]); + try std.testing.expect(signalled); + // The log says what ran and how it ended. + const log = try ctlfs.events.freezeLog(p); + defer gpa.free(log.bytes); + var want: [64]u8 = undefined; + // The whole line, after the `new` of the pane it runs in. + const ran = std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "run {d} make -j8\n", .{pane.serial})) orelse return error.NoRunRecord; + const made = std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "new {d} ", .{pane.serial})) orelse return error.NoNewRecord; + try std.testing.expect(made < ran); + try std.testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "exit {d} 2\n", .{pane.serial})) != null); + // A signal for a pane is not performed on whoever takes its slot. + const Count = struct { + var signals: usize = 0; + const vtable: Host.VTable = .{ .pty_signal = signal }; + fn signal(_: ?*anyopaque, _: u8, _: PtySignal) void { + signals += 1; + } + }; + const host_before = p.host; + p.host = .{ .vtable = &Count.vtable }; + try std.testing.expect(p.executeBuiltinLine(0, "Kill make")); + try p.removePane(dst, null); + _ = try p.newCommand(dst, "", "other"); + while (p.nextEffect()) |effect| if (effect == .signal_pty) p.perform(effect); + try std.testing.expectEqual(@as(usize, 0), Count.signals); + p.host = host_before; + // A command line longer than a command pane takes is refused. + const long = "x" ** (exec.command_max + 1); + try std.testing.expectEqual(@as(?usize, null), exec.execute(p, 1, long)); } + test "image dump restores source bytes renderer choices and exact custom tail" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .tty_only = true }); @@ -1423,50 +1772,6 @@ test "image dump restores source bytes renderer choices and exact custom tail" { try std.testing.expectEqualStrings(" Keep Del", redump.panes[0].tag_tail.?); } -test "Exec from a document pane skips an occupied terminal in its directory and spawns" { - const gpa = std.testing.allocator; - const p = try Pardes.init(gpa, .{}); - defer p.deinit(); - while (p.nextEffect()) |_| {} - - p.setCwd(0, "/tmp/pardes-image-dir"); - const image_doc = try p.newDocPane(1); - image_doc.image = .{ .path = try gpa.dupe(u8, "/tmp/pardes-image-dir/pic.ppm") }; - var host: FakeTtyQuery = .{}; - host.install(p); - host.taken[0] = true; - - const dst = exec.execute(p, 1, "echo image-fallback") orelse return error.ExecFoundNowhereToRun; - try std.testing.expect(dst != 0 and dst != 1); - - var spawned: ?Effect = null; - var sent: [256]u8 = undefined; - var sent_len: usize = 0; - while (p.nextEffect()) |effect| switch (effect) { - .write => |w| { - if (w.pane == 0) return error.WroteACommandLineIntoTheProgramOnTheTty; - try std.testing.expectEqual(@as(u8, @intCast(dst)), w.pane); - try std.testing.expect(w.bytes.slice().len <= sent.len - sent_len); - @memcpy(sent[sent_len..][0..w.bytes.slice().len], w.bytes.slice()); - sent_len += w.bytes.slice().len; - }, - .spawn => spawned = effect, - else => {}, - }; - const sp = (spawned orelse return error.NoShellForTheOccupiedTerminal).spawn; - try std.testing.expectEqualStrings("/tmp/pardes-image-dir", sp.cwd.slice()); - try std.testing.expectEqual(@as(usize, 0), sent_len); - p.acknowledgeShell(dst, "/bin/sh", false); - while (p.nextEffect()) |effect| switch (effect) { - .write => |w| { - try std.testing.expectEqual(@as(u8, @intCast(dst)), w.pane); - @memcpy(sent[sent_len..][0..w.bytes.slice().len], w.bytes.slice()); - sent_len += w.bytes.slice().len; - }, - else => {}, - }; - try std.testing.expectEqualStrings("echo image-fallback\r", sent[0..sent_len]); -} test "a Look on a directory does not type ls into an occupied terminal" { const gpa = std.testing.allocator; @@ -1569,11 +1874,13 @@ test "the host is asked about a tty only where a command line is about to go" { while (p.nextEffect()) |_| {} try std.testing.expectEqual(@as(usize, 1), host.asked); + // ...and a taken one is asked once too: the line then runs as a command + // pane, with no other terminal to ask about. host.taken[0] = true; host.asked = 0; _ = exec.execute(p, 0, "echo lazy-again"); while (p.nextEffect()) |_| {} - try std.testing.expectEqual(@as(usize, 2), host.asked); + try std.testing.expectEqual(@as(usize, 1), host.asked); } test "New opens an empty scratch below the caller, inheriting its directory" { @@ -1626,14 +1933,14 @@ test "owned cwd preserves long paths aliases and failed updates" { try std.testing.expect(p.panes[free] == null); exec.saveTo(p, 0, "file.txt"); try std.testing.expectEqual(effects, p.effects_len); - try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "PathTooLong") != null); + try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "path too long") != null); const current = pane.cwdSlice(); allocator.fail_index = allocator.alloc_index; p.setCwd(0, "/replacement"); try std.testing.expectEqual(current.ptr, pane.cwdSlice().ptr); try std.testing.expectEqualStrings(&path, pane.cwdSlice()); - try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "OutOfMemory") != null); + try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "out of memory") != null); allocator.fail_index = std.math.maxInt(usize); p.setCwd(0, "/replacement"); try std.testing.expectEqualStrings("/replacement", pane.cwdSlice()); @@ -1765,7 +2072,7 @@ test "owned cwd restore either copies the directory or preserves the old core" { for (0..256) |failure| { allocator.has_induced_failure = false; allocator.fail_index = allocator.alloc_index + failure; - const replacement = dump.restore(p, p.dump_out.?) catch { + const replacement = dump.restore(p, p.dump_out.?, "test") catch { try std.testing.expect(allocator.has_induced_failure); try std.testing.expectEqual(original, p.panes[0].?); try std.testing.expectEqualStrings("/retained/terminal/directory", original.cwdSlice()); @@ -2155,7 +2462,7 @@ test "a host with no methods at all is a complete in-process pardes" { try std.testing.expectEqual(before, p.panes[0].?.terminal.?.vt.screens.active.cursor.y); // the clipboard round-trips through the in-process one - p.yank = try gpa.dupe(u8, "copied"); + p.registers.put(gpa, '+', "copied", 0, 1, true); p.perform(.set_clipboard); try std.testing.expectEqualStrings("copied", p.fallback.clipboard.items); @@ -2184,7 +2491,7 @@ test "a builtin that needs a pane reports capacity failure when every slot is fu }; const help = std.meta.stringToEnum(Builtin, "Help") orelse return error.MissingHelpBuiltin; exec.runBuiltin(p, help, 0, "", null); - try std.testing.expect(std.mem.indexOf(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "help: NoPaneSlots") != null); + try std.testing.expect(std.mem.indexOf(u8, p.panes[0].?.msg[0..p.panes[0].?.msg_len], "help: no pane slots") != null); } test "Font keeps requested and acknowledged faces as separate plain state" { @@ -2717,14 +3024,14 @@ test "editable workspace and column tags are typed into and persist" { try std.testing.expect(std.mem.startsWith(u8, tagline.columnTag(p, 0), "Grep New")); try std.testing.expectEqualStrings("untouched\n", p.panes[0].?.file.?.content); try dump.dumpState(p); - const restored = try dump.restore(p, p.dump_out.?); + const restored = try dump.restore(p, p.dump_out.?, "test"); defer restored.deinit(); try std.testing.expectEqualStrings(p.global_tag.own.?, restored.global_tag.own.?); try std.testing.expectEqualStrings(tagline.columnTag(p, 0), tagline.columnTag(restored, 0)); p.gpa.free(p.col_tags[0].own.?); p.col_tags[0].own = try p.gpa.dupe(u8, ""); try dump.dumpState(p); - const empty = try dump.restore(p, p.dump_out.?); + const empty = try dump.restore(p, p.dump_out.?, "test"); defer empty.deinit(); try std.testing.expectEqualStrings("", tagline.columnTag(empty, 0)); } @@ -2948,7 +3255,7 @@ test "s in a header selects in the header's own text" { const text = try edit.flatSurface(p, &p.global_tag); const got = p.global_tag.ranges(text, 0, &ranges); try std.testing.expectEqual(@as(usize, 3), got.n); // Newcol, Joincol, NextColor - try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); } test "Enter in a header runs the word under its cursor, as the execute key does" { @@ -3076,7 +3383,13 @@ pub const Event = union(enum) { row_metrics: ?RowMetrics = null, }, output: struct { pane: u8, bytes: []const u8 }, - eof: struct { pane: u8 }, + /// The pane's pty reached end of file: its output is over. An + /// interactive terminal closes; a command pane waits for `exited`. + eof: struct { pane: u8, status: ?u8 = null }, + /// A command pane's child exited, with this status (null: unknown). It + /// is the command's end, whatever still holds its pty (a job it left in + /// the background prints on until end of file). + exited: struct { pane: u8, status: ?u8 }, lsp_resp: struct { id: u32, rows: ?[]const u8 }, pipe_resp: struct { id: u32, @@ -3114,10 +3427,14 @@ pub const WatchMode = enum { reconcile, baseline_disk }; pub const effect_path_cap = 256; pub const Effect = union(enum) { - spawn: struct { pane: u8, cwd: Buf(effect_path_cap) }, + /// A child for the pane in slot `pane` -- the one whose serial is + /// `serial`: a pane closed and its slot taken before the effect is + /// performed is not the one it was for (perform). + spawn: struct { pane: u8, serial: u32, cwd: Buf(effect_path_cap) }, write: struct { pane: u8, bytes: Buf(64) }, resize_pty: struct { pane: u8, cols: u16, rows: u16 }, - signal_pty: struct { pane: u8, sig: PtySignal }, + /// For the pane whose serial is `serial`, as `spawn` is (perform). + signal_pty: struct { pane: u8, serial: u32, sig: PtySignal }, /// The terminal pane in this slot is gone: the shell hangs up its pty /// and ends the shell, rather than leave it running with nobody to read. close_pty: struct { pane: u8 }, @@ -3801,6 +4118,9 @@ pub const Pardes = struct { /// it is dragged instead of once on release. native_image_patches: bool = false, quit: bool = false, + /// A detached session with no frontend attached: nothing renders, so + /// no input ever dismisses a message (Messages.expireUnattended). + unattended: bool = false, haptic: HapticSlot = if (platform == .macos) .none else {}, drag: mouse.Drag = .none, hover_col: u16 = 0, @@ -3819,6 +4139,14 @@ pub const Pardes = struct { multi_on: bool = false, multi_first: bool = false, multi_stop: bool = false, + /// While a key is replayed across the ranges: which range this pass is, + /// in document order, and how many there are. A register value belongs + /// to its range by this index. + multi_index: usize = 0, + multi_count: usize = 1, + /// The register the running normal command names (`"<reg>`); 0 for the + /// default one. + register: u21 = 0, leader_on: bool = false, leader_keys: [4]u8 = undefined, leader_n: u8 = 0, @@ -3839,6 +4167,11 @@ pub const Pardes = struct { chord_arg: ?[]u8 = null, /// how many `execute` calls are on the stack — see max_exec_depth exec_depth: u8 = 0, + /// An exec waiting on the REPL question (`Pane.Prompt.repl_choice`): + /// the pane it came from, the language, and the text, owned. + /// `offered` holds, by letter, the serial of the pane each answer named + /// when asked: a letter answered later is that pane or nothing. + repl_ask: ?struct { from: u32, lang: u8, text: []u8, offered: [26]u32 = @splat(0) } = null, /// A key press is being handled: someone is at the keyboard to answer, /// so a builtin may ask rather than guess. Nothing else sets it -- not /// a click, a 9P write, a startup line or a pane's shell exiting. @@ -3901,7 +4234,10 @@ pub const Pardes = struct { in_head: usize = 0, in_len: usize = 0, - yank: ?[]u8 = null, + registers: Registers = .{}, + macro: Macro = .{}, + /// an edited text nothing kept (a tag's shown text): see edit.retireEditText + retired_edit: ?[]u8 = null, clip_pending: ?edit.ClipRequest = null, /// the last serialized dump (gpa-owned), read by the write_dump effect dump_out: ?[]u8 = null, @@ -3947,6 +4283,8 @@ pub const Pardes = struct { .pdf_gpa = pdf_gpa, .tree_sitter_gpa = tree_sitter_gpa, .opts = opts, + .col_tags = @splat(.{ .what = .column, .gpa = gpa }), + .global_tag = .{ .what = .workspace, .gpa = gpa }, .fs = .{}, .screen_w = opts.cols, .screen_h = opts.rows, @@ -4055,6 +4393,7 @@ pub const Pardes = struct { pub fn deinit(p: *Pardes) void { look.cancelLookHover(p); + if (p.repl_ask) |ask| p.gpa.free(ask.text); for (0..MAX_PANES) |id| p.dropPendingWrite(id); for (&p.panes) |*slot| if (slot.*) |pane| { p.teardownPane(pane); @@ -4071,7 +4410,9 @@ pub const Pardes = struct { const gpa = p.gpa; for (&p.col_tags) |*t| t.deinit(gpa); p.global_tag.deinit(gpa); - if (p.yank) |y| gpa.free(y); + p.registers.deinit(gpa); + p.macro.deinit(gpa); + if (p.retired_edit) |r| gpa.free(r); if (p.dump_out) |d| gpa.free(d); p.fallback.deinit(); if (p.last_dump) |d| gpa.free(d); @@ -4097,6 +4438,7 @@ pub const Pardes = struct { const copy = p.gpa.dupe(u8, path) catch return; if (p.last_dump) |old| p.gpa.free(old); p.last_dump = copy; + ctlfs.events.notePath(p, "dump", path); } /// the shell polls this each frame: a pending Restore's dump path, or null @@ -4181,6 +4523,8 @@ pub const Pardes = struct { p.shell_rows.dropPane(pane); for (0..pane.pointer_selections.len) |slot| pane.clearPointerSelection(slot); panes.Terminal.deinitPendingCommand(pane); + if (pane.command) |line| p.gpa.free(line); + if (pane.shell) |bin| p.gpa.free(bin); if (pane.image) |*iv| { iv.deinit(p.image_gpa); } @@ -4190,6 +4534,7 @@ pub const Pardes = struct { pane.body.deinit(p.gpa); pane.tag.deinit(p.gpa); pane.input.deinit(p.gpa); + pane.jump.deinit(p.gpa); panes.Terminal.deinitEmulator(pane, p.gpa); pane.clearCwd(); pane.fs.deinit(p.gpa); @@ -4222,7 +4567,27 @@ pub const Pardes = struct { const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h); panes.Terminal.armShellSpawn(pane); p.installPane(id, pane); - p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(cwd) } }); + p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } }); + return pane; + } + + /// A command pane: a terminal whose child is `$SHELL -c line` rather + /// than an interactive shell, left showing its output and `exit N` when + /// it is done (exec.zig runCommand). + pub fn newCommand(p: *Pardes, id: usize, cwd: []const u8, line: []const u8) !*Pane { + std.debug.assert(p.panes[id] == null); + if (cwd.len > effect_path_cap) return error.PathTooLong; + const owned = try p.gpa.dupe(u8, line); + errdefer p.gpa.free(owned); + const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h); + pane.command = owned; + pane.command_pty = true; + pane.body.mode = .tty; + // Its directory is the one it was run for, and says which of the + // directory's commands reuse it; the child's own cd does not move it. + pane.setOwnedCwd(cwd) catch {}; + p.installPane(id, pane); + p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } }); return pane; } @@ -4300,6 +4665,14 @@ pub const Pardes = struct { if (off < bytes.len) p.parkPendingWrite(id, bytes[off..]); } + /// How many bytes `emitWrite` can take for pane `id` now without + /// dropping any: the effects left, then what the pending buffer holds. + pub fn writeRoom(p: *const Pardes, id: usize) usize { + const pending = limits.pending_write_cap -| p.pending_write_bytes; + if (p.pending_write[id] != null) return pending; + return (p.effects.len - p.effects_len) * 64 + pending; + } + fn parkPendingWrite(p: *Pardes, id: usize, bytes: []const u8) void { if (comptime limits.pending_write_cap == 0) return; if (p.pending_write_bytes + bytes.len > limits.pending_write_cap) return; @@ -4322,6 +4695,8 @@ pub const Pardes = struct { /// changes it observes, e.g. via /proc/<pid>/cwd before each frame). pub fn setCwd(p: *Pardes, id: usize, cwd: []const u8) void { const pane = p.panes[id] orelse return; + // A command pane stays the directory's it was run for (newCommand). + if (pane.command != null) return; const cur = switch (pane.cwd) { .owned => |dir| dir, else => "", @@ -4343,6 +4718,15 @@ pub const Pardes = struct { if (id < MAX_PANES) if (p.panes[id]) |pane| { ctlfs.pty.shellGone(p, pane, true); // a respawn replaced whatever ran pane.fs.unmarked = !prompt_marks; + // The tag's Tty word names the shell the host ran, and a click + // on it (`Tty+fish`) opens another of the same. + if (pane.command == null) { + const base = std.fs.path.basename(executable); + const rest = tagline.terminal_pane_tail[" Tty".len..]; + if (std.fmt.bufPrint(&pane.shell_tail, " Tty+{s}{s}", .{ base, rest })) |tail| { + pane.shell_tail_len = @intCast(tail.len); + } else |_| {} + } }; if (id < MAX_PANES) panes.Terminal.shellSpawned(p, id, prompt_marks); if (p.settings.shell.effective.set(executable)) @@ -4557,14 +4941,18 @@ pub const Pardes = struct { pub fn perform(p: *Pardes, e: Effect) void { const v = p.host.vtable; switch (e) { - .spawn => |s| if (v.spawn) |f| f(p.host.ctx, s.pane, s.cwd.slice()) else { + // The host reads the pane (its command, its shell) as it forks, + // so the slot must still hold the pane the spawn was for. + .spawn => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.spawn) |f| f(p.host.ctx, s.pane, s.cwd.slice()) else { p.fallback.spawned[s.pane] = true; }, // A pane with no child is silent: nothing invents output on its // screen, and the bytes are dropped rather than transcribed. .write => |w| if (v.pty_write) |f| f(p.host.ctx, w.pane, w.bytes.slice()), .resize_pty => |r| if (v.pty_resize) |f| f(p.host.ctx, r.pane, r.cols, r.rows), - .signal_pty => |s| if (v.pty_signal) |f| f(p.host.ctx, s.pane, s.sig), + // A signal for a pane that closed, its slot retaken in the same + // step, would reach another pane's child. + .signal_pty => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.pty_signal) |f| f(p.host.ctx, s.pane, s.sig), .close_pty => |c| if (v.close_pty) |f| f(p.host.ctx, c.pane), .open_link => |u| if (v.open_link) |f| f(p.host.ctx, u.slice()) @@ -4604,7 +4992,7 @@ pub const Pardes = struct { } }, .set_clipboard => { - const text = p.yank orelse ""; + const text = p.registers.text(p.gpa, '+') orelse ""; if (v.set_clipboard) |f| f(p.host.ctx, text) else p.fallback.setClipboard(text); }, // No desktop to ask: answer from the in-process clipboard at once, @@ -4691,6 +5079,10 @@ pub const Pardes = struct { look.cancelLookHover(p); const reply = ctlfs.handle(p, req); if (reply.status == .err and (req.op == .write or req.op == .setattr)) ctlfs.events.noteError(p, req, reply); + // A pane that joined a column holding only a stillborn scratch takes + // it now, before the answer: one read of index after the look shows + // the replacement done, and the log has its del with the new. + exec.replaceStillborn(p); // The request was a whole step of its own, so it settles the way a // step does: the cursor and scroll reconciled, the scripted panes // told, and the panes it made announced to /log now rather than at @@ -4707,6 +5099,7 @@ pub const Pardes = struct { // the screen. p.needs_frame = p.needs_frame or ev != .tick; p.shell_rows.sweep(p.gpa); + Messages.expireUnattended(p); // An announcement stands in only until its own step says more. for (p.panes) |slot| if (slot) |pane| { pane.msg_announcement = false; @@ -4786,8 +5179,14 @@ pub const Pardes = struct { panes.Terminal.feedOutput(p, pane, o.bytes); ctlfs.pty.noteMarks(p, o.pane, pane); }, - .eof => |e| p.removePane(e.pane, null) catch |err| { - if (p.panes[e.pane]) |pane| pane.body.mode = .normal; + .exited => |e| if (p.panes[e.pane]) |pane| if (pane.command != null and !pane.command_done) { + exec.commandDone(p, e.pane, e.status); + }, + .eof => |e| if (p.panes[e.pane]) |pane| if (pane.command != null) { + // Output is over; the command is over when its child exits. + pane.command_pty = false; + } else p.removePane(e.pane, null) catch |err| { + pane.body.mode = .normal; p.reportError(e.pane, "terminal exited; Del retries close", err); }, .lsp_resp => |r| p.lspResponse(r.id, r.rows), @@ -4896,11 +5295,42 @@ pub const Pardes = struct { } else panes.Terminal.enterTty(p, id); } + /// Type `keys` again (a macro, `.`): through the modal handling as + /// typed, except that no key leaves the text for pardes's own acts -- + /// Esc does not hop panes, and Enter and Tab neither look nor execute. + pub fn replayKeys(p: *Pardes, keys: []const Macro.Stored) void { + if (p.macro.replaying >= 8) return; // a macro typing itself + p.macro.replaying += 1; + defer p.macro.replaying -= 1; + for (keys) |stored| { + var buf: [4]u8 = undefined; + p.handleKey(stored.typed(&buf)); + } + } + fn handleKey(p: *Pardes, key: Key) void { + if (p.macro.recording != 0 and p.macro.replaying == 0) Macro.append(&p.macro.recorded, p.gpa, key); // A question waiting on a pane takes the next key, whatever mode the // pane or a header edit is in: that key is its answer. for (p.panes, 0..) |slot, id| { const asked = slot orelse continue; + if (asked.prompt == .repl_choice) { + asked.prompt = .none; + const ask = p.repl_ask orelse return; + p.repl_ask = null; + defer p.gpa.free(ask.text); + // Its letter sends it there; anything else takes it back. + if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return; + const serial = ask.offered[key.cp - 'a']; + if (serial == 0) return; + // The pane offered, still that REPL and still running: it + // may have closed or been rebound while the question stood. + if (p.paneBySerial(serial)) |to| if (p.panes[to].?.repl) |r| + if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(p, to)) + return exec.sendToRepl(p, id, to, ask.text); + p.setMessage(id, "Repl: the REPL chosen is gone; nothing sent"); + return; + } if (asked.prompt != .del_side) continue; asked.prompt = .none; if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k"); @@ -4934,7 +5364,7 @@ pub const Pardes = struct { // in it the chord is the program's -- vim's visual block, // readline's quoted-insert -- and swallowing it would make // Ctrl-V a black hole in every full-screen application. - if (p.yank) |text| return edit.typeToTty(p, p.active, pane, text); + if (p.registers.text(p.gpa, Registers.default)) |text| return edit.typeToTty(p, p.active, pane, text); } return panes.Terminal.forwardKey(p, p.active, key); } @@ -5011,8 +5441,8 @@ pub const Pardes = struct { .search => if (submit) look.submitSearch(p, p.active) else if (normal.selRegexArmed(pane)) |_| if (target) |answered| - normal.applySelRegex(p, pane, answered, "", false), - .none, .del_side, .name => unreachable, + normal.applySelRegex(p, pane, answered, "", .select), + .none, .del_side, .repl_choice, .name => unreachable, } pane.closePrompt(); if (target) |answered| { @@ -5022,10 +5452,11 @@ pub const Pardes = struct { return; } edit.handleInsert(p, &pane.input, key); - if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.split); + if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.mode); return; } - if (t.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) { + if (t == &pane.body and pane.jump.items.len > 0) return normal.jumpKey(p, pane, t, key); + if (t.mode == .normal and p.macro.replaying == 0 and (hit(key, config.look_key) or hit(key, config.exec_key))) { // A header's words are the workspace's and column's commands, so // Enter runs them there, as the execute key does. const header_word = t.what == .column or t.what == .workspace; @@ -5058,32 +5489,59 @@ pub const Pardes = struct { pane.body.vsel.active = false; pane.body.msel.active = false; pane.body.select = false; - exec.runBuiltin(p, cmd, p.active, "", txt); + // The execute key on a body: text a bound REPL may take. + exec.runBuiltinFrom(p, cmd, p.active, "", txt, true); return; } } const sel = look.expandedSel(p, pane, look.cursorWordSel(p, pane)) orelse return; const word = edit.selectionText(p, pane, sel) catch return; - exec.runBuiltin(p, cmd, p.active, "", word); + exec.runBuiltinFrom(p, cmd, p.active, "", word, true); return; } if (t.mode == .normal and t.what == .body and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key); if (t.what == .tag and t.mode == .insert and tagline.typedIntoName(p, pane, key)) return; // A tag's prefix is read-only: a key whose edit reached into it // changes nothing, not even where the cursor is. - const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel }; + const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel }; + var kept_sels: [Text.max_selections - 1]Text.SelRange = undefined; + const kept_n = t.nsel; + @memcpy(kept_sels[0..kept_n], t.sels[0..kept_n]); t.refused = false; defer if (t.refused) { - t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel = kept; + t.cur_row, t.cur_col, t.vsel, t.msel = kept; + t.restoreSels(kept_sels[0..kept_n]); }; + const was = t.mode; switch (t.mode) { .normal => normal.handleNormal(p, t, key), - .insert => { - if (hit(key, config.escape)) return edit.exitInsert(p, t); - edit.handleInsert(p, t, key); - }, + .insert => if (hit(key, config.escape) and !t.reg_wait) + edit.exitInsert(p, t) + else + edit.handleInsert(p, t, key), .tty => panes.Terminal.forwardKey(p, p.active, key), } + // `.`'s log: the keys of the normal command that entered insert + // mode, then everything typed until it is left + if (p.macro.repeating) return; + const m = &p.macro; + switch (was) { + .normal => { + Macro.append(&m.command, p.gpa, key); + if (t.mode == .insert) { + m.last_insert.clearRetainingCapacity(); + m.last_insert.appendSlice(p.gpa, m.command.items) catch {}; + m.typed_from = m.last_insert.items.len; + m.inserting = true; + m.command.clearRetainingCapacity(); + } else if (std.meta.eql(t.normal, modal.Normal.State{})) m.command.clearRetainingCapacity(); + }, + .insert => if (m.inserting) { + Macro.append(&m.last_insert, p.gpa, key); + if (t.mode != .insert) m.inserting = false; + }, + .tty => {}, + } } fn leaderKey(p: *Pardes, key: Key) void { @@ -5480,7 +5938,10 @@ pub const Pardes = struct { // ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ---- /// Close a pane; its rows go to `to`, or where layout.absorbVWeight - /// sends them when that is null. + /// sends them when that is null. Focus on it follows its rows: to the + /// pane that grew into them, or to the scratch left standing in an + /// emptied column. Every route that closes a pane comes through here or + /// removeColumn, so none picks focus for itself, and none by recency. pub fn removePane(p: *Pardes, id: usize, to: ?usize) !void { const pane = p.panes[id] orelse return; // A column outlives its panes. Closing the last one leaves an empty @@ -5507,10 +5968,11 @@ pub const Pardes = struct { layout.insert(p, f.col, f.idx + 1, free); break :heir free; }; - layout.absorbVWeight(p, id, to); + const gainer = layout.absorbVWeight(p, id, to); + const beside = if (last_in_column) p.besideColumn(place.?.col, id) else null; layout.removePane(p, id); p.panes[id] = null; - if (p.active == id) p.active = heir orelse p.prevFocus(id) orelse { + if (p.active == id) p.active = heir orelse gainer orelse beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5519,6 +5981,11 @@ pub const Pardes = struct { pub fn removeColumn(p: *Pardes, id: usize) !void { const place = layout.findPane(p, id) orelse return; + const had_focus = layout.findPane(p, p.active); + const beside = if (had_focus != null and had_focus.?.col == place.col) + p.besideColumn(place.col, p.active) + else + null; var ids: [MAX_PANES]usize = undefined; var parents: [MAX_PANES]*Pane = undefined; const count = p.col_n[place.col]; @@ -5532,7 +5999,7 @@ pub const Pardes = struct { layout.removePane(p, closed); p.panes[closed] = null; } - if (p.panes[p.active] == null) p.active = p.prevFocus(p.active) orelse { + if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5555,16 +6022,26 @@ pub const Pardes = struct { return null; } - pub fn prevFocus(p: *Pardes, closing: usize) ?usize { - var i = p.njumps; - while (i > 0) { - i -= 1; - const id = p.jumps[i].pane; - if (id != closing and p.panes[id] != null) return id; - } - for (p.panes, 0..) |slot, k| { - if (slot != null and k != closing) return k; + /// The pane that takes over `id`'s place when column `col` goes: in the + /// column layout.removePane gives the width to, the one level with the + /// middle of `id`. Asked before the column goes, while rects still hold. + fn besideColumn(p: *const Pardes, col: usize, id: usize) ?usize { + if (p.ncol < 2) return null; + const next = if (col > 0) col - 1 else col + 1; + const r = p.rects[id]; + const mid = r.y + r.h / 2; + var pick: ?usize = null; + for (p.col_panes[next][0..p.col_n[next]]) |k| { + if (p.panes[k] == null) continue; + pick = k; + if (p.rects[k].y +| p.rects[k].h > mid) break; } + return pick; + } + + /// Any live pane: the last resort when nothing took the closed one's place. + fn anyPane(p: *const Pardes) ?usize { + for (p.panes, 0..) |slot, k| if (slot != null) return k; return null; } @@ -6251,6 +6728,7 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { // A word that is not a side closes nothing. try std.testing.expect(p.executeBuiltinLine(again, "Del x")); + try std.testing.expectEqualStrings("Del: takes k, j or nothing", p.panes[again].?.msg[0..p.panes[again].?.msg_len]); try std.testing.expect(p.panes[again] != null); // DelAbove is Del k, and a line that is not a key never asks. try std.testing.expect(p.executeBuiltinLine(again, "DelAbove")); @@ -6278,6 +6756,59 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expect(p.panes[low] == null); } +test "a Look that finds nothing says so" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + p.newScratchBelow(p.active); + const id = p.active; + p.sync(); + look.lookAt(p, id, "/no/such/zzqq"); + try std.testing.expectEqualStrings("Look: no match for \"/no/such/zzqq\"", p.panes[id].?.msg[0..p.panes[id].?.msg_len]); +} + +test "focus on a closed pane goes to the pane that takes its place, not the one focused before" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 }); + defer p.deinit(); + const shell = p.active; + p.newScratchColumn(shell); + const right = p.active; + p.sync(); + p.newScratchBelow(shell); + const low = p.active; + p.sync(); + // The dogfood case: `right` was focused just before `low`, but `shell` + // grows into `low`'s rows, so `shell` gets the focus. + try std.testing.expect(p.executeBuiltinLine(low, "Del")); + p.sync(); + try std.testing.expect(p.panes[low] == null); + try std.testing.expectEqual(shell, p.active); + + // Closing a pane that has no focus leaves the focus where it is. + p.newScratchBelow(shell); + const other = p.active; + p.sync(); + p.active = right; + p.sync(); + try std.testing.expect(p.executeBuiltinLine(other, "Del")); + try std.testing.expectEqual(right, p.active); + + // Delcol: the column that takes the width holds, level with the closed + // pane, the pane that takes its place; `right` was focused just before. + p.newScratchBelow(right); + const right_low = p.active; + p.newScratchBelow(shell); + const left_low = p.active; + p.active = right; + p.sync(); + p.active = left_low; + p.sync(); + try p.removeColumn(left_low); + p.sync(); + try std.testing.expectEqual(right_low, p.active); +} + test "closing the last pane in a column leaves an empty one in its place" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 }); @@ -6431,7 +6962,7 @@ test "raw tty Escape only leaves at a shell prompt" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24, .file = "mise.toml" }); defer p.deinit(); - exec.spawnTty(p, 0); + _ = exec.spawnTty(p, 0); const shell = p.active; const pane = p.panes[shell].?; var buf: [256]u8 = undefined; @@ -6462,7 +6993,7 @@ test "raw tty Ctrl-B switches to editor mode without sending child input" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .file = "mise.toml" }); defer p.deinit(); - exec.spawnTty(p, 0); + _ = exec.spawnTty(p, 0); const shell = p.active; p.sync(); var buf: [256]u8 = undefined; @@ -6902,7 +7433,7 @@ test "jump history clears selections and lands at the recorded cursor" { try std.testing.expectEqual(@as(usize, 2), p.njumps); try std.testing.expectEqual(@as(u32, 3), p.jumps[0].line); try std.testing.expect(!pane.body.vsel.active and !pane.body.msel.active and !pane.body.select); - try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); try std.testing.expectEqual(.none, pane.sel[0].state); try std.testing.expect(pane.pointer_selections[0] == null); try std.testing.expectEqual(@as(i32, if (index == 0) 2 else 0), pane.body.cur_row); diff --git a/src/syntax.zig b/src/syntax.zig index 9c1a4f4b..8ed00bfa 100644 --- a/src/syntax.zig +++ b/src/syntax.zig @@ -3,6 +3,7 @@ const config = @import("pardes_config"); const tracy = @import("tracy.zig"); const grammar_manifest = @import("grammar_manifest.zig"); const look = @import("look.zig"); +const regexp = @import("regexp.zig"); const c_heap = @import("c_heap"); pub const enabled = config.syntax_highlighting; @@ -27,6 +28,10 @@ const Spec = struct { exts: []const []const u8, language: *const fn () callconv(.c) *const ts.Language, query_src: []const u8, + /// helix's textobjects.scm for the grammar (vendor/queries), "" for none + objects_src: []const u8 = "", + objects: ?*ts.Query = null, + objects_failed: bool = false, selected: ?Selected = null, capture_styles: [256]u8 = undefined, }; @@ -131,6 +136,7 @@ fn initSpecs() [specCount()]Spec { .exts = g.exts, .language = @extern(LanguageFn, .{ .name = "tree_sitter_" ++ g.name }), .query_src = querySrc(g), + .objects_src = @field(ts_queries, g.name ++ "_textobjects"), }; i += 1; } @@ -173,6 +179,9 @@ pub fn stop() void { selected.query.destroy(); } spec.selected = null; + if (spec.objects) |q| q.destroy(); + spec.objects = null; + spec.objects_failed = false; } ts_set_allocator(null, null, null, null); syntax_started = false; @@ -226,35 +235,8 @@ fn forExt(ext: []const u8) !?Selected { return null; } -const LangAlias = struct { []const u8, []const u8 }; -const lang_aliases = [_]LangAlias{ - .{ "js", "javascript" }, - .{ "jsx", "javascript" }, - .{ "mjs", "javascript" }, - .{ "py", "python" }, - .{ "sh", "bash" }, - .{ "shell", "bash" }, - .{ "zsh", "bash" }, - .{ "bash", "bash" }, - .{ "rs", "rust" }, - .{ "c++", "cpp" }, - .{ "cxx", "cpp" }, - .{ "cc", "cpp" }, - .{ "cs", "c_sharp" }, - .{ "kt", "kotlin" }, - .{ "rb", "ruby" }, - .{ "ml", "ocaml" }, - .{ "hs", "haskell" }, -}; - fn forLang(name: []const u8) !?Selected { - var canonical = name; - for (lang_aliases) |a| { - if (std.ascii.eqlIgnoreCase(name, a[0])) { - canonical = a[1]; - break; - } - } + const canonical = if (grammar_manifest.byName(name)) |i| grammar_manifest.all[i].name else name; for (&specs) |*spec| { if (std.ascii.eqlIgnoreCase(canonical, spec.name)) return try ensure(spec); } @@ -337,6 +319,234 @@ pub fn supportsPath(path: []const u8) bool { return false; } +/// A whole file's parse, kept by the file for the keys that walk its nodes +/// (helix's Alt-o and kin). Opaque outside this file; null without a grammar. +pub fn parseTree(path: []const u8, content: []const u8) ?*anyopaque { + if (!enabled) return null; + const selected = (forExt(std.fs.path.extension(path)) catch return null) orelse return null; + const tree = selected.parser.parseString(content, null) orelse return null; + return @ptrCast(tree); +} + +pub fn freeTree(tree: *anyopaque) void { + if (!enabled) return; + const t: *ts.Tree = @ptrCast(@alignCast(tree)); + t.destroy(); +} + +/// Where a node walk goes from a range (helix object.rs and movement.rs). +pub const NodeWalk = @import("modal.zig").Normal.NodeWalk; +pub const Span = struct { from: usize, to: usize }; + +/// The spans `walk` takes the byte range [from, to) to, into `out`; `at` is +/// the range's cursor. None when the tree has nothing to say there. +pub fn walkNodes(tree_ptr: *anyopaque, walk: NodeWalk, from: usize, to: usize, at: usize, out: []Span) usize { + if (!enabled or out.len == 0) return 0; + const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr)); + const lo: u32 = @intCast(@min(from, std.math.maxInt(u32))); + const hi: u32 = @intCast(@min(to, std.math.maxInt(u32))); + if (walk == .parent_end or walk == .parent_start) { + var node = tree.rootNode().namedDescendantForByteRange(lo, hi) orelse return 0; + if (walk == .parent_end) { + out[0] = .{ .from = node.endByte(), .to = node.endByte() }; + return 1; + } + // already at the node's lo: its first ancestor that starts earlier + if (node.startByte() == at) { + const first = node.startByte(); + while (node.startByte() >= first or !node.isNamed()) node = node.parent() orelse break; + } + out[0] = .{ .from = node.startByte(), .to = node.startByte() }; + return 1; + } + var cursor = tree.walk(); + defer cursor.destroy(); + // helix TreeCursor.reset_to_byte_range: the smallest node holding the range + while (true) { + const node = cursor.node(); + if (lo < node.startByte() or hi > node.endByte()) { + _ = cursor.gotoParent(); + break; + } + if (cursor.gotoFirstChildForByte(lo) == null) break; + } + switch (walk) { + .expand => while (cursor.node().startByte() == lo and cursor.node().endByte() == hi) { + if (!cursor.gotoParent()) break; + }, + .shrink => _ = cursor.gotoFirstChild(), + .next_sibling => while (!cursor.gotoNextSibling()) { + if (!cursor.gotoParent()) break; + }, + .prev_sibling => while (!cursor.gotoPreviousSibling()) { + if (!cursor.gotoParent()) break; + }, + .all_siblings, .all_children => { + if (walk == .all_siblings) { + while (true) { + if (!cursor.gotoParent()) return 0; + if (cursor.node().childCount() > 1) break; + } + } + // the named children, or nothing to say + var n: usize = 0; + if (!cursor.gotoFirstChild()) return 0; + while (true) { + const child = cursor.node(); + if (child.isNamed() and n < out.len) { + out[n] = .{ .from = child.startByte(), .to = child.endByte() }; + n += 1; + } + if (!cursor.gotoNextSibling()) break; + } + return n; + }, + .parent_end, .parent_start => unreachable, + } + const node = cursor.node(); + out[0] = .{ .from = node.startByte(), .to = node.endByte() }; + return 1; +} + + +/// helix's textobjects, which its `]f`, `mif` and kin select: the name a +/// query's captures begin with (`function.around`, `test.inside`). +pub const Object = @import("modal.zig").Normal.Object; + +fn objectName(o: Object) []const u8 { + return if (o == .xml_element) "xml-element" else @tagName(o); +} + +/// The grammar's textobject query for `path`, compiled the first time; null +/// when the grammar has none. +fn objectQuery(path: []const u8) ?*ts.Query { + const ext = std.fs.path.extension(path); + for (&specs) |*spec| for (spec.exts) |choice| { + if (!std.ascii.eqlIgnoreCase(ext, choice)) continue; + if (spec.objects) |q| return q; + if (spec.objects_src.len == 0 or spec.objects_failed) return null; + var error_offset: u32 = 0; + spec.objects = ts.Query.create(spec.language(), spec.objects_src, &error_offset) catch { + spec.objects_failed = true; + return null; + }; + return spec.objects; + }; + return null; +} + +fn captureId(q: *const ts.Query, name: []const u8) ?u32 { + for (0..q.captureCount()) |i| if (std.mem.eql(u8, q.captureNameForId(@intCast(i)) orelse "", name)) return @intCast(i); + return null; +} + +/// A match's `capture`, as one span over all its nodes (helix's grouped +/// capture), when the match's `#eq?` and `#match?` hold. +fn matchSpan(q: *const ts.Query, match: anytype, capture: u32, text: []const u8) ?Span { + var span: ?Span = null; + for (match.captures) |c| if (c.index == capture) { + const s: Span = .{ .from = c.node.startByte(), .to = c.node.endByte() }; + span = if (span) |old| .{ .from = @min(old.from, s.from), .to = @max(old.to, s.to) } else s; + }; + var found = span orelse return null; + // a node that takes its line's newline (this zig grammar's comments) + // is the line without it, as helix's grammars have it + if (found.to > found.from + 1 and found.to <= text.len and text[found.to - 1] == '\n') found.to -= 1; + const steps = q.predicatesForPattern(match.pattern_index); + var i: usize = 0; + while (i < steps.len) { + var end = i; + while (end < steps.len and steps[end].type != .done) end += 1; + const pred = steps[i..end]; + i = end + 1; + if (pred.len != 3 or pred[0].type != .string or pred[1].type != .capture) continue; + const op = q.stringValueForId(pred[0].value_id) orelse continue; + const subject = for (match.captures) |c| { + if (c.index == pred[1].value_id) break text[@min(c.node.startByte(), text.len)..@min(c.node.endByte(), text.len)]; + } else continue; + const want = if (pred[2].type == .string) q.stringValueForId(pred[2].value_id) orelse continue else for (match.captures) |c| { + if (c.index == pred[2].value_id) break text[@min(c.node.startByte(), text.len)..@min(c.node.endByte(), text.len)]; + } else continue; + if (std.mem.eql(u8, op, "eq?")) { + if (!std.mem.eql(u8, subject, want)) return null; + } else if (std.mem.eql(u8, op, "match?")) { + var re = regexp.Regex.compile(want) catch continue; + if ((re.find(subject, 0, subject.len, subject.len) catch null) == null) return null; + } + } + return found; +} + +/// `mi`/`ma`: the smallest `object.inside` (or `.around`) holding byte `at` +/// (helix textobject_treesitter). None without a query or such an object. +pub fn objectAt(path: []const u8, tree_ptr: *anyopaque, text: []const u8, object: Object, around: bool, at: usize) ?Span { + if (!enabled) return null; + const q = objectQuery(path) orelse return null; + var buf: [32]u8 = undefined; + const want = std.fmt.bufPrint(&buf, "{s}.{s}", .{ objectName(object), if (around) "around" else "inside" }) catch return null; + const capture = captureId(q, want) orelse return null; + const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr)); + const cursor = ts.QueryCursor.create(); + defer cursor.destroy(); + cursor.exec(q, tree.rootNode()); + var best: ?Span = null; + while (cursor.nextMatch()) |match| { + const s = matchSpan(q, match, capture, text) orelse continue; + if (!(s.from <= at and at < s.to)) continue; + if (best == null or s.to - s.from < best.?.to - best.?.from) best = s; + } + const s = best orelse return null; + if (s.from >= text.len or s.to >= text.len) return null; + return s; +} + +/// `]f`/`[f` and kin: the next object starting after byte `at`, or the +/// previous one ending before it (helix goto_treesitter_object), from the +/// first of `.movement`, `.around`, `.inside` the query names. +pub fn objectNext(path: []const u8, tree_ptr: *anyopaque, text: []const u8, object: Object, forward: bool, at: usize) ?Span { + if (!enabled) return null; + const q = objectQuery(path) orelse return null; + var buf: [32]u8 = undefined; + const capture = for ([_][]const u8{ "movement", "around", "inside" }) |kind| { + const want = std.fmt.bufPrint(&buf, "{s}.{s}", .{ objectName(object), kind }) catch return null; + if (captureId(q, want)) |id| break id; + } else return null; + const tree: *const ts.Tree = @ptrCast(@alignCast(tree_ptr)); + const cursor = ts.QueryCursor.create(); + defer cursor.destroy(); + cursor.exec(q, tree.rootNode()); + var best: ?Span = null; + while (cursor.nextMatch()) |match| { + const s = matchSpan(q, match, capture, text) orelse continue; + if (forward) { + if (s.from <= at) continue; + if (best == null or s.from < best.?.from or (s.from == best.?.from and s.to > best.?.to)) best = s; + } else { + if (s.to >= at) continue; + if (best == null or s.to > best.?.to or (s.to == best.?.to and s.from < best.?.from)) best = s; + } + } + const s = best orelse return null; + if (s.from >= text.len or s.to >= text.len) return null; + return s; +} + +test "every vendored textobject query compiles against its grammar" { + if (!enabled) return; + var failed: usize = 0; + for (&specs) |*spec| { + if (spec.objects_src.len == 0) continue; + var error_offset: u32 = 0; + const q = ts.Query.create(spec.language(), spec.objects_src, &error_offset) catch { + std.debug.print("textobjects for {s} do not compile (offset {d})\n", .{ spec.name, error_offset }); + failed += 1; + continue; + }; + q.destroy(); + } + try std.testing.expectEqual(@as(usize, 0), failed); +} + /// Owned source analysis. Both slices use the allocator passed to analyzeSource. /// The parsed tree is released before returning; callers can cache this result. pub const SourceAnalysis = struct { diff --git a/src/tagline.zig b/src/tagline.zig index 92c18510..59c51b4d 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -7,6 +7,7 @@ const modal = @import("modal.zig"); const ctlfs = @import("ninep/tree.zig"); const panes = @import("panes.zig"); const edit = @import("edit.zig"); +const exec = @import("exec.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const dump = @import("dump.zig"); @@ -21,6 +22,7 @@ pub const file_pane_tail = " " ++ config.file_pane_builtins_str; pub const context_pane_tail = file_pane_tail ++ " TreeContext"; pub const locations_pane_tail = file_pane_tail ++ " LocationsConfig"; pub const terminal_pane_tail = " " ++ config.terminal_pane_builtins_str; +pub const command_pane_tail = " " ++ config.command_pane_builtins_str; const dirty_marker = " *"; pub const legacy_pane_tail = " Del"; pub const legacy_file_pane_tail = " Save Del"; @@ -109,6 +111,29 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { if (comptime pdf_enabled) if (pane.pdf) |pv| return std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count }); if (pane.image) |*state| return panes.Image.tagPrefix(arena, state); + // A command pane: its directory, the line it runs, and how that went. + // Its first line only, cut to 48 bytes at a character's start: a tag's + // first row is where the prefix ends, and a line that ran over to a + // second (acme runs a multi-line selection as one command) would move + // it (body_layer.zig tagTintRange). + if (pane.command) |line| { + const first = line[0 .. std.mem.indexOfAny(u8, line, "\r\n") orelse line.len]; + var cut = @min(first.len, 48); + while (cut < first.len and cut > 0 and first[cut] & 0xC0 == 0x80) cut -= 1; + const shown = first[0..cut]; + const more = if (shown.len < line.len) "..." else ""; + var idbuf: [48]u8 = undefined; + const id = exec.replId(&idbuf, pane); + const gap = if (id.len > 0) " " else ""; + if (!pane.command_done) return std.fmt.allocPrint(arena, "{s} ({s}{s}) running{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id }); + if (pane.command_status) |n| return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit {d}{s}{s}", .{ pane.cwdSlice(), shown, more, n, gap, id }); + return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit ?{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id }); + } + // A bound REPL says what it is bound as, beside its Tty word. + if (pane.repl != null) { + var idbuf: [48]u8 = undefined; + return std.fmt.allocPrint(arena, "{s} {s}", .{ pane.cwdSlice(), exec.replId(&idbuf, pane) }); + } if (pane.file) |f| { const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision) @@ -148,7 +173,8 @@ pub fn defaultTail(pane: *const Pane) []const u8 { if (panes.Output.usesLocationsConfig(out.from)) return locations_pane_tail; if (panes.File.supportsContext(pane)) return context_pane_tail; if (pane.file != null) return file_pane_tail; - if (pane.isTerminal()) return terminal_pane_tail; + if (pane.command != null) return command_pane_tail; + if (pane.isTerminal()) return if (pane.shell_tail_len > 0) pane.shell_tail[0..pane.shell_tail_len] else terminal_pane_tail; return pane_tail; } @@ -200,7 +226,7 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { t.msel.active = false; t.nsel = 0; t.select = false; - t.append_at = null; + t.restore_cursor = false; t.sticky_col = -1; t.mode = .normal; t.normal.clear(); @@ -306,7 +332,7 @@ pub fn enterHeader(p: *Pardes, column: ?usize) void { t.msel.active = false; t.nsel = 0; t.select = false; - t.append_at = null; + t.restore_cursor = false; t.mode = .normal; t.normal.clear(); } @@ -390,8 +416,26 @@ pub fn restoreTailAt( }, .terminal => &.{ terminal_pane_tail, pre_close_last_terminal_pane_tail, pre_close_last_pane_tail, pre_mode_terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail, pre_column_terminal_pane_tail, pre_tty_terminal_pane_tail, prev_terminal_pane_tail, legacy_terminal_pane_tail, pane_tail, pre_collapse_pane_tail, pre_column_pane_tail, prev_pane_tail, legacy_pane_tail }, }; + // A terminal's saved tag carries what was live about it -- the REPL id + // after its directory, the shell in its Tty word -- which is neither + // dumped nor true of the new shell: its tail is still the default. + var plain = said; + var plain_buf: [256]u8 = undefined; + if (class == .terminal) { + const first = plain[0 .. std.mem.indexOfScalar(u8, plain, ' ') orelse plain.len]; + if (std.mem.lastIndexOfScalar(u8, first, '-')) |dash| if (dash + 2 == first.len and first[dash + 1] >= 'a' and first[dash + 1] <= 'z') { + for (@import("grammar_manifest.zig").all) |g| if (std.mem.eql(u8, g.name, first[0..dash])) { + plain = std.mem.trimStart(u8, plain[first.len..], " "); + break; + }; + }; + if (std.mem.startsWith(u8, plain, "Tty+")) { + const word_end = std.mem.indexOfScalar(u8, plain, ' ') orelse plain.len; + plain = std.fmt.bufPrint(&plain_buf, "Tty{s}", .{plain[word_end..]}) catch plain; + } + } for (defaults) |default| - if (std.mem.eql(u8, said, std.mem.trimStart(u8, default, " "))) return; + if (std.mem.eql(u8, plain, std.mem.trimStart(u8, default, " "))) return; pane.tag.own = p.gpa.dupe(u8, rest) catch return; } diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 6083f609..822c2d7f 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -318,8 +318,17 @@ const Pty = struct { file: std.Io.File, pid: posix.pid_t, reader: std.Io.Future(anyerror!void), + /// A command pane's child, watched to its exit (host_io.watchExit); + /// the pty stays open until both its end of file and its exit. + cmd: host_io.CommandWatch = .{}, }; +/// A watched child exited: wake the loop, which takes it (`takeExits`). +fn wakeForExit(ctx: ?*anyopaque) void { + const loop: *Loop = @ptrCast(@alignCast(ctx.?)); + _ = loop.tryPostEvent(.nop) catch {}; +} + const KittyPlacement = struct { cell_x: u16, cell_y: u16, @@ -732,7 +741,9 @@ fn localSession( .test_clock = host_io.testClock(), }; pardes.lsp.setStatusSink(&sh, lspStatusSink); + host_io.setExitWake(loop, wakeForExit); defer { + host_io.setExitWake(null, null); pardes.lsp.setStatusSink(null, null); for (&sh.ptys) |*slot| if (slot.*) |*pt| { pt.reader.cancel(io) catch {}; @@ -805,7 +816,7 @@ fn localSession( break :blk; }; defer gpa.free(bytes); - const nc = dump.restore(core, bytes) catch |err| { + const nc = dump.restore(core, bytes, rp) catch |err| { core.reportError(core.active, "Restore", err); break :blk; }; @@ -818,6 +829,7 @@ fn localSession( for (&sh.ptys) |*slot| if (slot.*) |*pt| { pt.reader.cancel(io) catch {}; _ = libc.close(pt.file.handle); + host_io.retireShell(pt.pid); slot.* = null; }; for (0..pardes.MAX_PANES) |wid| file_watch.watchPane( @@ -991,6 +1003,9 @@ const Shell = struct { batch += 1; } if (motion) |m| _ = s.apply(m); + // An exit's wake (a .nop) or output an exit waited behind: told + // here, inside the step, so the frame after it shows it. + s.takeExits(); s.reloadWatched(); } @@ -1020,14 +1035,27 @@ const Shell = struct { if (s.gens[pr.id] == pr.gen) core.update(.{ .output = .{ .pane = @intCast(pr.id), .bytes = pr.bytes } }); s.gpa.free(pr.bytes); + // An exit waiting behind this output may be told now. + s.takeExits(); return true; }, .pty_eof => |e| if (s.gens[e.id] == e.gen) { if (s.ptys[e.id]) |*pt| { pt.reader.await(s.io) catch {}; // reader just finished; join it or its future leaks + // A command's pty stays open until its child has exited: + // closing it would hang up one that runs on without it. + // Its exit, if it came first, is told now its output is in. + if (pt.cmd.watched) { + host_io.commandEof(core, &s.ptys, e.id, s, closeWatched); + return true; + } _ = libc.close(pt.file.handle); - host_io.retireShell(pt.pid); + // Unwatched, a command's exit is read here, as its end. + const command = if (core.panes[e.id]) |pn| pn.command != null else false; + const status = if (command) host_io.exitStatus(pt.pid, 100) else null; + if (status == null) host_io.retireShell(pt.pid); s.ptys[e.id] = null; + if (command) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } }); } core.update(.{ .eof = .{ .pane = @intCast(e.id) } }); }, @@ -1216,7 +1244,8 @@ const Shell = struct { closePty(ctx, pane); // a shell still in the slot goes first, reaped s.gens[pane] +%= 1; const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err); - s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} } }; + const command = if (s.core.panes[pane]) |pn| pn.command != null else false; + s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); if (s.threads_ok) { @@ -1241,7 +1270,9 @@ const Shell = struct { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const s = of(ctx); - if (s.ptys[pane]) |pt| host_io.signalTty(pt.pid, pt.file.handle, sig); + // A command whose exit is recorded is reaped: its group may be + // another's by now. + if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig); } /// The pane is gone: hang its pty up, which the kernel passes on to the @@ -1263,6 +1294,17 @@ const Shell = struct { return host_io.ttyTaken(pt.pid, pt.file.handle); } + /// The command panes' exits, told once their output is in (host_io). + fn takeExits(s: *@This()) void { + host_io.takeExits(s.core, &s.ptys, s, closeWatched); + } + + fn closeWatched(s: *@This(), id: usize) void { + const pt = &(s.ptys[id] orelse return); + _ = libc.close(pt.file.handle); + s.ptys[id] = null; + } + fn killJob(ctx: ?*anyopaque, pane: u8) bool { const s = of(ctx); const pt = s.ptys[pane] orelse return false; diff --git a/src/tutor.txt b/src/tutor.txt index 507ce40e..0893415e 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -461,7 +461,7 @@ typed N frontends share ONE screen at the smallest common grid the default 9P socket works in a daemon too - KEYS h j k l w b e 0 $ ^ gg ge f F t T v x d c y p + KEYS h j k l w b e 0 gl ^ gg ge f F t T v x d c y p i a I A o O u undo U redo motions SELECT, so `i` types at the selection's start and `;` collapses first @@ -3,6 +3,7 @@ import contextlib import fcntl import json import os +import signal from pathlib import Path import subprocess import struct @@ -245,7 +246,7 @@ def discovery(binary, embedded=False): client.write('/focus', b'99999\n') raise AssertionError('focus took a serial no pane has') except OSError as refused: - assert 'no such window' in str(refused), refused + assert 'no such pane' in str(refused), refused # /ctl reads the settings as a write takes them and takes the # session's builtins; a pane's ctl takes the pane's. settings = client.read('/ctl') @@ -424,10 +425,27 @@ def exit_answers(binary): raise AssertionError("Exit did not quit the editor") +def signal_cleans_up(binary): + """A detached session told to stop by a signal unlinks its sockets.""" + for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]: + with tempfile.TemporaryDirectory(prefix='pardes-sig-') as directory: + root = Path(directory) + seen = {} + with session(binary, root, 'sig', terminal=seen) as (client, address): + detached = root / 'pardes-detached-sig.sock' + assert address.exists() and detached.exists() + os.kill(seen['pid'], sig) + deadline = time.monotonic() + 5 + while (address.exists() or detached.exists()) and time.monotonic() < deadline: + time.sleep(.01) + assert not address.exists() and not detached.exists(), (sig, list(root.iterdir())) + + def test(binary, quic=False): started = time.monotonic() run_file(binary) exit_answers(binary) + signal_cleans_up(binary) for options, message in [ (['--fs'], b'no such option'), (['--fs9'], b'no such option'), @@ -602,23 +620,23 @@ def test(binary, quic=False): assert old.read_fid(held_screen, count=1) == b'{' old.write('/pane/1/body', b'changed after dump\n', truncate=True) # Unsaved text is asked about once, as Exit asks; the second - # Restore goes ahead. + # Restore goes ahead, and is answered before the hang-up. try: execute(old, control, 'Restore ' + str(saved)) except OSError as refused: assert 'Modified' in str(refused), refused assert old.read('/pane/1/body') == b'changed after dump\n' - try: - execute(old, control, 'Restore ' + str(saved)) - except (EOFError, ConnectionResetError, BrokenPipeError): - pass + execute(old, control, 'Restore ' + str(saved)) + # Refused while the connections are cut, or cut already. try: old.read_fid(held_body) - except (EOFError, ConnectionResetError, BrokenPipeError): + except (EOFError, OSError): pass else: raise AssertionError('Restore left an old body fid usable') with Client(address) as restored: + # The new log names its panes, then where they came from. + assert restored.read('/log').endswith(b'\nrestore ' + str(saved).encode() + b'\n'), restored.read('/log') restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()] assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids file_id, control_id = restored_ids diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig index 822577d8..2f78ca1a 100644 --- a/test/fs_namespace.zig +++ b/test/fs_namespace.zig @@ -88,7 +88,7 @@ test "transactional restore preserves host capabilities and advances pane identi p.fs.tcp_address = .{ .ip4 = .loopback(5640) }; p.fs.quic_address = .{ .ip6 = .loopback(5641) }; try pardes.dump.dumpState(p); - const restored = try pardes.dump.restore(p, p.dump_out.?); + const restored = try pardes.dump.restore(p, p.dump_out.?, "test"); defer restored.deinit(); try std.testing.expectEqual(@as(u32, 41), restored.lsp_seq); try std.testing.expectEqual(@as(u32, 73), restored.pipe.seq); @@ -103,6 +103,7 @@ test "transactional restore preserves host capabilities and advances pane identi try std.testing.expectError(error.BadDumpMagic, pardes.dump.restore( p, ".{ .magic = \"wrong\", .screen = .{.cols = 80, .rows = 24} }", + "test", )); try std.testing.expect(p.panes[0] == pane); try std.testing.expectEqualStrings("before\n", pane.file.?.content); diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt index 27c39150..38ca89e5 100644 --- a/test/gui-goldens.txt +++ b/test/gui-goldens.txt @@ -14,5 +14,5 @@ 14-theme-ink cc2ea256ddda24e1c97871f75b71ddf62fa825bd02f5d1432592d05e478c707a 15-theme-acme-light 2bde8a2bdfada7f061217f52cb2272913fc0657dbfa1d7448e1852f321f8e3bf 16-debug 129ad6f5f85b0e60e81bc6e8e78434629ee751f49731cf2eb28fd0553efd914a -17-terminal a1305fd755b6bc957228bbf28833f972c2355af9abf40b77f76b30741109574f -18-mid-transition d4f353ef5fda80411cd9de9729e5b23f59ff7511c10774e69214152ee8070725 +17-terminal 45a3d9f823ccf28a336ef6687c9c210f9fb05cc426c0fdee98bb0ebb2a957d42 +18-mid-transition 79f90fb247f75ab284fca147028ee18ea78e77a02a57203699fb423f4bcc02fb diff --git a/test/hxcases/cases.jsonl b/test/hxcases/cases.jsonl index b0a3aad4..f36afe11 100644 --- a/test/hxcases/cases.jsonl +++ b/test/hxcases/cases.jsonl @@ -252,6 +252,9 @@ {"name": "add-nl-below", "pane": "file", "text": "aa\nbb\n", "keys": "]<space>"} {"name": "add-nl-above", "pane": "file", "text": "aa\nbb\n", "keys": "j[<space>"} {"name": "add-nl-count", "pane": "file", "text": "aa\nbb\n", "keys": "2]<space>"} +{"name": "add-nl-above-sel", "pane": "file", "text": "aa\nbb\ncc\n", "keys": "vj[<space>"} +{"name": "add-nl-below-sel", "pane": "file", "text": "aa\nbb\ncc\n", "keys": "jvk]<space>"} +{"name": "add-nl-above-lines", "pane": "file", "text": "aa\nbb\ncc\n", "keys": "jxx[<space>"} {"name": "d-word", "pane": "file", "text": "alpha beta\n", "keys": "wd"} {"name": "d-bare-char", "pane": "file", "text": "abc\n", "keys": "d"} {"name": "d-on-nl-joins", "pane": "file", "text": "ab\ncd\n", "keys": "lld"} @@ -287,6 +290,8 @@ {"name": "r-cancel-esc", "pane": "file", "text": "abc\n", "keys": "r<esc>l"} {"name": "R-inline", "pane": "file", "text": "alpha beta\n", "keys": "wyggR"} {"name": "R-linewise", "pane": "file", "text": "one\ntwo\n", "keys": "xyjR"} +{"name": "R-backward", "pane": "file", "text": "alpha beta\n", "keys": "wyebR"} +{"name": "R-backward-linewise", "pane": "file", "text": "one\ntwo three\n", "keys": "xyjxbR"} {"name": "tilde-sel-keeps", "pane": "file", "text": "abc\n", "keys": "vll~"} {"name": "tilde-bare", "pane": "file", "text": "aBc\n", "keys": "~"} {"name": "backtick-lower", "pane": "file", "text": "ABC\n", "keys": "vll`"} @@ -297,6 +302,12 @@ {"name": "J-empty-next-nospace", "pane": "file", "text": "aa\n\nbb\n", "keys": "J"} {"name": "J-last-line-eats-nl", "pane": "file", "text": "aa\nbb\n", "keys": "jJ"} {"name": "J-count-ignored", "pane": "file", "text": "a\nb\nc\nd\n", "keys": "3J"} +{"name": "msel-J-same-line", "pane": "file", "text": "aa bb\ncc\ndd\n", "keys": "wCJ"} +{"name": "msel-J-shared-line", "pane": "file", "text": "ab\ncd\nef\n", "keys": "%s\\w<ret>J"} +{"name": "alt-J-selects-space", "pane": "file", "text": "aa\n bb\ncc\n", "keys": "xx<A-J>"} +{"name": "alt-J-each-range", "pane": "file", "text": "a\nb\nc\nd\n", "keys": "%<A-s><A-J>"} +{"name": "alt-J-blank-no-space", "pane": "file", "text": "aa\n\nbb\n", "keys": "<A-J>"} +{"name": "msel-J-two-lines", "pane": "file", "text": "aa bb\ncc\ndd\nee\n", "keys": "Cs\\w<ret>J"} {"name": "indent-basic", "pane": "file", "text": "foo\nbar\n", "keys": "<gt>"} {"name": "indent-realign", "pane": "file", "text": " x\n", "keys": "gs<gt>"} {"name": "indent-cursor-maps", "pane": "file", "text": "foo\n", "keys": "ll<gt>"} @@ -316,6 +327,9 @@ {"name": "ms-sel", "pane": "file", "text": "hello\n", "keys": "vllms("} {"name": "ms-bare", "pane": "file", "text": "ab\n", "keys": "ms("} {"name": "ms-quote-self", "pane": "file", "text": "ab cd\n", "keys": "wems'"} +{"name": "ms-backward", "pane": "file", "text": "ab cd\n", "keys": "wbms("} +{"name": "ms-backward-lines", "pane": "file", "text": "ab\ncd\nef\n", "keys": "jjlvkkms["} +{"name": "msel-ms-backward", "pane": "file", "text": "id 1,x 2\n", "keys": "%s\\d<ret>dhbms`"} {"name": "md-basic", "pane": "file", "text": "a(bcd)e\n", "keys": "3lmd("} {"name": "md-quote", "pane": "file", "text": "a 'bc' d\n", "keys": "3lmd'"} {"name": "mr-basic", "pane": "file", "text": "a(bcd)e\n", "keys": "3lmr(["} @@ -330,6 +344,21 @@ {"name": "i-keeps-sel-start", "pane": "file", "text": "alpha beta\n", "keys": "wi"} {"name": "wa-sel-end", "pane": "file", "text": "alpha beta\n", "keys": "wa"} {"name": "wiX-edit-drops-sel", "pane": "file", "text": "alpha beta\n", "keys": "wiX<esc>"} +{"name": "reg-selection-insert", "pane": "file", "text": "ab\n", "keys": "vli<C-r>.<esc>"} +{"name": "ins-sel-backspace", "pane": "file", "text": "alpha beta\n", "keys": "wi<backspace><esc>"} +{"name": "ins-append-enter", "pane": "file", "text": "ab cd\n", "keys": "wa<ret>x<esc>"} +{"name": "ins-append-backspace", "pane": "file", "text": "ab cd\n", "keys": "ea<backspace><backspace><esc>"} +{"name": "ins-sel-delete", "pane": "file", "text": "ab cd\n", "keys": "wi<del><esc>"} +{"name": "ins-sel-arrow-collapses", "pane": "file", "text": "ab cd\n", "keys": "wi<right><esc>"} +{"name": "msel-insert-keeps-ranges", "pane": "file", "text": "ab cd\nab cd\n", "keys": "wCiX<esc>"} +{"name": "msel-append-lines", "pane": "file", "text": "aa\nbb\n", "keys": "%<A-s>a!<esc>"} +{"name": "ins-append-bare", "pane": "file", "text": "ab\n", "keys": "axy<esc>"} +{"name": "ins-change-then-type", "pane": "file", "text": "ab cd\n", "keys": "wcX<esc>"} +{"name": "ins-open-below-sel", "pane": "file", "text": "ab\ncd\n", "keys": "xoX<esc>"} +{"name": "ins-enter-trims-blanks", "pane": "file", "text": "ab cd\n", "keys": "5li<ret><esc>"} +{"name": "ins-enter-in-indent", "pane": "file", "text": " ab\n", "keys": "2li<ret><esc>"} +{"name": "ins-enter-after-open", "pane": "file", "text": " ab\n", "keys": "o<ret>x<esc>"} +{"name": "msel-ins-enter", "pane": "file", "text": "ab cd\nab cd\n", "keys": "wCa<ret><esc>"} {"name": "a-esc-restore", "pane": "file", "text": "abc\n", "keys": "a<esc>"} {"name": "ax-restore", "pane": "file", "text": "abc\n", "keys": "ax<esc>"} {"name": "lax-restore", "pane": "file", "text": "abc\n", "keys": "lax<esc>"} @@ -341,6 +370,11 @@ {"name": "ins-enter-indent", "pane": "file", "text": " foo\n", "keys": "A<ret>x<esc>"} {"name": "ins-backspace", "pane": "file", "text": "abc\n", "keys": "lli<backspace><esc>"} {"name": "ins-backspace-join", "pane": "file", "text": "ab\ncd\n", "keys": "ji<backspace><esc>"} +{"name": "ins-backspace-indent-unit", "pane": "file", "text": " x\n", "keys": "8li<backspace>"} +{"name": "ins-backspace-indent-partial", "pane": "file", "text": " x\n", "keys": "6li<backspace>"} +{"name": "ins-backspace-indent-tab", "pane": "file", "text": "\t\tx\n", "keys": "2li<backspace>"} +{"name": "ins-backspace-open-indent", "pane": "file", "text": " ab\n", "keys": "O<backspace>"} +{"name": "ins-backspace-after-text", "pane": "file", "text": " a x\n", "keys": "7li<backspace>"} {"name": "ins-ctrl-w", "pane": "file", "text": "one two\n", "keys": "A<C-w><esc>"} {"name": "ins-ctrl-w-col0-join", "pane": "file", "text": "ab\ncd\n", "keys": "ji<C-w><esc>"} {"name": "ins-alt-bs", "pane": "file", "text": "one two\n", "keys": "A<A-backspace><esc>"} @@ -390,6 +424,56 @@ {"name": "msel-trim", "pane": "file", "text": " ab \n", "keys": "%_"} {"name": "msel-trim-all-ws", "pane": "file", "text": " \n", "keys": "%_"} {"name": "msel-trim-multi", "pane": "file", "text": " ab \n cd \n", "keys": "x<A-x>C_"} +{"name": "msel-rotate-contents-fwd", "pane": "file", "text": "a b c\n", "keys": "%s\\w<ret><A-)>"} +{"name": "msel-rotate-contents-back", "pane": "file", "text": "a b c\n", "keys": "%s\\w<ret><A-(>"} +{"name": "msel-rotate-contents-count", "pane": "file", "text": "a b c d\n", "keys": "%s\\w<ret>2<A-)>"} +{"name": "msel-rotate-contents-lengths", "pane": "file", "text": "one to three\n", "keys": "%s\\w+<ret>)<A-(>"} +{"name": "msel-rotate-contents-backward", "pane": "file", "text": "ab cde\n", "keys": "%s\\w+<ret><A-;><A-)>"} +{"name": "msel-rotate-contents-lines", "pane": "file", "text": "aa\nbbb\n", "keys": "%<A-s><A-)>"} +{"name": "sel-many-ranges", "pane": "file", "text": "w w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\n", "keys": "%sw<ret>"} +{"name": "sel-many-ranges-edit", "pane": "file", "text": "w w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\n", "keys": "%sw<ret>cxy<esc>"} +{"name": "keep-regex", "pane": "file", "text": "foo bar baz\n", "keys": "%s\\w+<ret>Kba<ret>"} +{"name": "remove-regex", "pane": "file", "text": "foo bar baz\n", "keys": "%s\\w+<ret><A-K>ba<ret>"} +{"name": "keep-regex-none", "pane": "file", "text": "foo bar\n", "keys": "%s\\w+<ret>Kzz<ret>"} +{"name": "keep-regex-primary", "pane": "file", "text": "a1 b2 a3\n", "keys": "%s\\w+<ret>))Ka<ret>"} +{"name": "keep-regex-esc", "pane": "file", "text": "foo bar\n", "keys": "%s\\w+<ret>Kfo<esc>"} +{"name": "keep-regex-lines", "pane": "file", "text": "foo\nbar\nbaz\n", "keys": "%<A-s>K^b<ret>"} +{"name": "ensure-forward", "pane": "file", "text": "ab cd\n", "keys": "%s\\w+<ret><A-;><A-:>"} +{"name": "ensure-forward-one", "pane": "file", "text": "abc\n", "keys": "vll<A-;><A-:>"} +{"name": "node-keys-plain-text", "pane": "file", "text": "ab cd\n", "keys": "w<A-o><A-i><A-p><A-right><A-a><A-I><A-e><A-b>"} +{"name": "goto-last-mod", "pane": "file", "text": "ab\ncd\nef\n", "keys": "jliXY<esc>gg"} +{"name": "goto-last-mod-go", "pane": "file", "text": "ab\ncd\nef\n", "keys": "jliXY<esc>ggg."} +{"name": "goto-last-mod-delete", "pane": "file", "text": "abc\ndef\n", "keys": "jlxdgg"} +{"name": "goto-last-mod-delete-go", "pane": "file", "text": "abc\ndef\n", "keys": "jldggg."} +{"name": "goto-last-mod-select", "pane": "file", "text": "ab\ncd\nef\n", "keys": "jjiX<esc>ggvg."} +{"name": "alt-u-undo", "pane": "file", "text": "ab\n", "keys": "iX<esc><A-u>"} +{"name": "alt-U-redo", "pane": "file", "text": "ab\n", "keys": "iX<esc>u<A-U>"} +{"name": "ins-checkpoint-undo", "pane": "file", "text": "ab\n", "keys": "iX<C-s>Y<esc>u"} +{"name": "ins-checkpoint-undo-twice", "pane": "file", "text": "ab\n", "keys": "iX<C-s>Y<esc>uu"} +{"name": "ins-checkpoint-redo", "pane": "file", "text": "ab\n", "keys": "iX<C-s>Y<esc>uuU"} +{"name": "msel-ins-checkpoint", "pane": "file", "text": "ab\nab\n", "keys": "CiX<C-s>Y<esc>u"} +{"name": "ins-tab-after-text-plain", "pane": "file", "text": "ab cd\n", "keys": "2li<tab>X<esc>"} +{"name": "ins-shift-tab", "pane": "file", "text": "ab cd\n", "keys": "2li<S-tab>X<esc>"} +{"name": "sticky-view-scroll", "pane": "file", "text": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n", "keys": "Zjjj<esc>"} +{"name": "sticky-view-then-move", "pane": "file", "text": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n", "keys": "Zjj<esc>j"} +{"name": "sticky-view-ignores", "pane": "file", "text": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n", "keys": "Zjxj<esc>"} +{"name": "view-middle", "pane": "file", "text": "abcdef\n", "keys": "3lzm"} +{"name": "view-z-not-sticky", "pane": "file", "text": "line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n", "keys": "zjj"} +{"name": "goto-word-first", "pane": "file", "text": "alpha beta gamma\ndelta x\n", "keys": "gwaa"} +{"name": "goto-word-second", "pane": "file", "text": "alpha beta gamma\ndelta x\n", "keys": "gwab"} +{"name": "goto-word-back", "pane": "file", "text": "alpha beta gamma\ndelta x\n", "keys": "jwgwab"} +{"name": "goto-word-cancel", "pane": "file", "text": "alpha beta gamma\n", "keys": "gwzl"} +{"name": "goto-word-bad-second", "pane": "file", "text": "alpha beta\n", "keys": "gwa9l"} +{"name": "goto-word-extend", "pane": "file", "text": "alpha beta gamma\n", "keys": "wvgwab"} +{"name": "goto-word-mid", "pane": "file", "text": "one two three four five\n", "keys": "2wgwaa"} +{"name": "msel-align-basic", "pane": "file", "text": "a=1\nbbb=2\n", "keys": "%s=<ret>&"} +{"name": "msel-align-columns", "pane": "file", "text": "a,b,c\nccc,d,e\n", "keys": "%s,<ret>&"} +{"name": "msel-align-uneven", "pane": "file", "text": "a,b\nccc,d,eee,f\n", "keys": "%s,<ret>&"} +{"name": "msel-align-words", "pane": "file", "text": "x = 1\nlonger = 22\n", "keys": "%s\\w+<ret>&"} +{"name": "msel-align-backward", "pane": "file", "text": "x = 11\nlonger = 222\n", "keys": "%s\\d+<ret><A-;>&"} +{"name": "align-multiline-refused", "pane": "file", "text": "ab\ncd\n", "keys": "vj&"} +{"name": "msel-align-select-mode", "pane": "file", "text": "a=1\nbbb=2\n", "keys": "%s=<ret>v&"} +{"name": "rotate-contents-one", "pane": "file", "text": "ab\n", "keys": "vl<A-)>"} {"name": "msel-insert", "pane": "file", "text": "aa\nbb\n", "keys": "Cix<esc>"} {"name": "msel-insert-tty", "pane": "tty", "text": "aa\nbb\n", "keys": "Cix<esc>"} {"name": "msel-append", "pane": "file", "text": "aa\nbb\n", "keys": "Caz<esc>"} @@ -409,6 +493,35 @@ {"name": "msel-collapse", "pane": "file", "text": "abcd\nabcd\n", "keys": "vllC;"} {"name": "msel-flip", "pane": "file", "text": "abcd\nabcd\n", "keys": "vllC<A-;>"} {"name": "msel-yank-paste", "pane": "file", "text": "ab\ncd\n", "keys": "Cyp"} +{"name": "reg-named-yank-paste", "pane": "file", "text": "ab cd\n", "keys": "\"ayw\"ap"} +{"name": "reg-named-keeps-default", "pane": "file", "text": "ab cd\n", "keys": "yw\"ay\"_dp"} +{"name": "reg-blackhole-delete", "pane": "file", "text": "ab cd\n", "keys": "yw\"_dP"} +{"name": "msel-reg-paste-per-range", "pane": "file", "text": "ab\ncd\n", "keys": "Cyl2Pp"} +{"name": "msel-reg-fewer-values", "pane": "file", "text": "ab\ncd\nef\n", "keys": "yCCp"} +{"name": "msel-reg-replace-per-range", "pane": "file", "text": "ab\ncd\n", "keys": "Cy<A-(>Ry"} +{"name": "msel-reg-more-values-one-range", "pane": "file", "text": "ab\ncd\n", "keys": "Cy,p"} +{"name": "reg-index-insert", "pane": "file", "text": "x\nx\nx\n", "keys": "%<A-s>i<C-r>#<esc>"} +{"name": "reg-search-paste", "pane": "file", "text": "ab\n", "keys": "%sb<ret>\"/p"} +{"name": "reg-count-before-quote", "pane": "file", "text": "ab\n", "keys": "y2\"ap"} +{"name": "reg-count-after-quote", "pane": "file", "text": "ab\n", "keys": "\"ay\"a3p"} +{"name": "reg-ctrl-r-named", "pane": "file", "text": "ab\n", "keys": "\"ayli<C-r>a<esc>"} +{"name": "reg-ctrl-r-esc", "pane": "file", "text": "ab\n", "keys": "i<C-r><esc>x"} +{"name": "macro-record-replay", "pane": "file", "text": "a\nb\nc\n", "keys": "QA!<esc>jQq"} +{"name": "macro-count", "pane": "file", "text": "a\nb\nc\nd\n", "keys": "QA!<esc>jQ2q"} +{"name": "macro-named", "pane": "file", "text": "a\nb\nc\n", "keys": "\"aQA!<esc>jQ\"aq"} +{"name": "macro-register-text", "pane": "file", "text": "a\nb\n", "keys": "QA!<esc>jQ\"@P"} +{"name": "macro-with-prompt", "pane": "file", "text": "a,b\nc,d\n", "keys": "QxS,<ret>d<esc>jQq"} +{"name": "macro-esc-in-normal", "pane": "file", "text": "ab\ncd\n", "keys": "Qvl<esc>jQq"} +{"name": "macro-empty", "pane": "file", "text": "ab\n", "keys": "QQq"} +{"name": "dot-insert", "pane": "file", "text": "ab\ncd\n", "keys": "iX<esc>j.."} +{"name": "dot-append-count", "pane": "file", "text": "ab\ncd\n", "keys": "aYZ<esc>j3."} +{"name": "dot-change", "pane": "file", "text": "ab cd\nef gh\n", "keys": "wcX<esc>jb."} +{"name": "dot-open-below", "pane": "file", "text": "a\nb\n", "keys": "oX<esc>j."} +{"name": "dot-counted-command", "pane": "file", "text": "abcd\n", "keys": "2lixy<esc>."} +{"name": "dot-multi", "pane": "file", "text": "ab\nab\n", "keys": "CiX<esc>."} +{"name": "dot-nothing", "pane": "file", "text": "ab\n", "keys": ".x"} +{"name": "append-at-end-adds-newline", "pane": "file", "text": "ab\n", "keys": "jaX<esc>"} +{"name": "append-at-end-no-newline", "pane": "file", "text": "ab", "keys": "laX<esc>"} {"name": "msel-esc-keeps", "pane": "file", "text": "aa\nbb\n", "keys": "C<esc>"} {"name": "msel-select-mode", "pane": "file", "text": "abcd\nabcd\n", "keys": "Cvl"} {"name": "msel-open-count", "pane": "file", "text": "ab\n", "keys": "2O"} diff --git a/test/hxcases/goldens.jsonl b/test/hxcases/goldens.jsonl index eee7294b..2dab15c0 100644 --- a/test/hxcases/goldens.jsonl +++ b/test/hxcases/goldens.jsonl @@ -252,6 +252,9 @@ {"name":"add-nl-below","text":"aa\n\nbb\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"add-nl-above","text":"aa\n\nbb\n","mode":"normal","cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}} {"name":"add-nl-count","text":"aa\n\n\nbb\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"add-nl-above-sel","text":"\naa\nbb\ncc\n","mode":"select","cursor":{"row":2,"col":0},"anchor":{"row":1,"col":0}} +{"name":"add-nl-below-sel","text":"aa\nbb\n\ncc\n","mode":"select","cursor":{"row":0,"col":0},"anchor":{"row":1,"col":0}} +{"name":"add-nl-above-lines","text":"aa\n\nbb\ncc\n","mode":"normal","cursor":{"row":3,"col":2},"anchor":{"row":2,"col":0}} {"name":"d-word","text":"beta\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"d-bare-char","text":"bc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"d-on-nl-joins","text":"abcd\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} @@ -287,6 +290,8 @@ {"name":"r-cancel-esc","text":"abc\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} {"name":"R-inline","text":"alpha lpha beta\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":0}} {"name":"R-linewise","text":"one\ntwoone\n","mode":"normal","cursor":{"row":1,"col":6},"anchor":{"row":1,"col":3}} +{"name":"R-backward","text":"alpha alpha \n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":11}} +{"name":"R-backward-linewise","text":"one\ntwo one\n\n","mode":"normal","cursor":{"row":1,"col":4},"anchor":{"row":1,"col":7}} {"name":"tilde-sel-keeps","text":"ABC\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} {"name":"tilde-bare","text":"ABc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"backtick-lower","text":"abc\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} @@ -297,6 +302,12 @@ {"name":"J-empty-next-nospace","text":"aa\nbb\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"J-last-line-eats-nl","text":"aa\nbb","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}} {"name":"J-count-ignored","text":"a b\nc\nd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"msel-J-same-line","text":"aa bb cc dd\n","mode":"normal","cursor":{"row":0,"col":8},"anchor":{"row":0,"col":6},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":6}}],"primary":1} +{"name":"msel-J-shared-line","text":"ab cd ef","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}},{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7}}],"primary":0} +{"name":"alt-J-selects-space","text":"aa bb\ncc\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} +{"name":"alt-J-each-range","text":"a b c d","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}}],"primary":0} +{"name":"alt-J-blank-no-space","text":"aa\nbb\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"msel-J-two-lines","text":"aa bb cc dd\nee\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}}],"primary":0} {"name":"indent-basic","text":" foo\nbar\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}} {"name":"indent-realign","text":" x\n","mode":"normal","cursor":{"row":0,"col":8},"anchor":{"row":0,"col":8}} {"name":"indent-cursor-maps","text":" foo\n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}} @@ -316,6 +327,9 @@ {"name":"ms-sel","text":"(hel)lo\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":0}} {"name":"ms-bare","text":"(a)b\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} {"name":"ms-quote-self","text":"ab' cd'\n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":2}} +{"name":"ms-backward","text":"(ab )cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":4}} +{"name":"ms-backward-lines","text":"a[b\ncd\nef]\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":2,"col":2}} +{"name":"msel-ms-backward","text":"`id `,`x `\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":9}}],"primary":0} {"name":"md-basic","text":"abcde\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} {"name":"md-quote","text":"a bc d\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} {"name":"mr-basic","text":"a[bcd]e\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}} @@ -330,6 +344,21 @@ {"name":"i-keeps-sel-start","text":"alpha beta\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":5}} {"name":"wa-sel-end","text":"alpha beta\n","mode":"insert","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":0}} {"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":6}} +{"name":"reg-selection-insert","text":"abab\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":3}} +{"name":"ins-sel-backspace","text":"alpha beta\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":5}} +{"name":"ins-append-enter","text":"ab\nxcd\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":0,"col":0}} +{"name":"ins-append-backspace","text":" cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"ins-sel-delete","text":"b cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":1}} +{"name":"ins-sel-arrow-collapses","text":"ab cd\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"msel-insert-keeps-ranges","text":"Xab cd\nXab cd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":3},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":3}}],"primary":1} +{"name":"msel-append-lines","text":"aa!\nbb!\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":0}}],"primary":0} +{"name":"ins-append-bare","text":"axyb\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} +{"name":"ins-change-then-type","text":"Xcd\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"ins-open-below-sel","text":"ab\nX\ncd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}} +{"name":"ins-enter-trims-blanks","text":"ab\ncd\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}} +{"name":"ins-enter-in-indent","text":"\n ab\n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}} +{"name":"ins-enter-after-open","text":" ab\n\n x\n","mode":"normal","cursor":{"row":2,"col":5},"anchor":{"row":2,"col":5}} +{"name":"msel-ins-enter","text":"ab\ncd\nab\ncd\n","mode":"normal","cursor":{"row":2,"col":2},"anchor":{"row":2,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":0}}],"primary":1} {"name":"a-esc-restore","text":"abc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"ax-restore","text":"axbc\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}} {"name":"lax-restore","text":"abxc\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":1}} @@ -341,6 +370,11 @@ {"name":"ins-enter-indent","text":" foo\n x\n","mode":"normal","cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}} {"name":"ins-backspace","text":"ac\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} {"name":"ins-backspace-join","text":"abcd\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} +{"name":"ins-backspace-indent-unit","text":" x\n","mode":"insert","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}} +{"name":"ins-backspace-indent-partial","text":" x\n","mode":"insert","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}} +{"name":"ins-backspace-indent-tab","text":"\tx\n","mode":"insert","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"ins-backspace-open-indent","text":"\n ab\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"ins-backspace-after-text","text":" a x\n","mode":"insert","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}} {"name":"ins-ctrl-w","text":"one \n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}} {"name":"ins-ctrl-w-col0-join","text":"cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"ins-alt-bs","text":"one \n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}} @@ -390,6 +424,56 @@ {"name":"msel-trim","text":" ab \n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":2}} {"name":"msel-trim-all-ws","text":" \n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}} {"name":"msel-trim-multi","text":" ab \n cd \n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":1}}],"primary":1} +{"name":"msel-rotate-contents-fwd","text":"c a b\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}],"primary":1} +{"name":"msel-rotate-contents-back","text":"b c a\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}],"primary":2} +{"name":"msel-rotate-contents-count","text":"c d a b\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}}],"primary":2} +{"name":"msel-rotate-contents-lengths","text":"to three one\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":11},"anchor":{"row":0,"col":9}}],"primary":0} +{"name":"msel-rotate-contents-backward","text":"cde ab\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":5},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":5}}],"primary":1} +{"name":"msel-rotate-contents-lines","text":"bbb\naa\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1} +{"name":"sel-many-ranges","text":"w w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\nw w w w w w w w w w w w w w w w w w w w\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}},{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":8}},{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":0,"col":12},"anchor":{"row":0,"col":12}},{"cursor":{"row":0,"col":14},"anchor":{"row":0,"col":14}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":16}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":18}},{"cursor":{"row":0,"col":20},"anchor":{"row":0,"col":20}},{"cursor":{"row":0,"col":22},"anchor":{"row":0,"col":22}},{"cursor":{"row":0,"col":24},"anchor":{"row":0,"col":24}},{"cursor":{"row":0,"col":26},"anchor":{"row":0,"col":26}},{"cursor":{"row":0,"col":28},"anchor":{"row":0,"col":28}},{"cursor":{"row":0,"col":30},"anchor":{"row":0,"col":30}},{"cursor":{"row":0,"col":32},"anchor":{"row":0,"col":32}},{"cursor":{"row":0,"col":34},"anchor":{"row":0,"col":34}},{"cursor":{"row":0,"col":36},"anchor":{"row":0,"col":36}},{"cursor":{"row":0,"col":38},"anchor":{"row":0,"col":38}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}},{"cursor":{"row":1,"col":4},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":6},"anchor":{"row":1,"col":6}},{"cursor":{"row":1,"col":8},"anchor":{"row":1,"col":8}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":10}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":12}},{"cursor":{"row":1,"col":14},"anchor":{"row":1,"col":14}},{"cursor":{"row":1,"col":16},"anchor":{"row":1,"col":16}},{"cursor":{"row":1,"col":18},"anchor":{"row":1,"col":18}},{"cursor":{"row":1,"col":20},"anchor":{"row":1,"col":20}},{"cursor":{"row":1,"col":22},"anchor":{"row":1,"col":22}},{"cursor":{"row":1,"col":24},"anchor":{"row":1,"col":24}},{"cursor":{"row":1,"col":26},"anchor":{"row":1,"col":26}},{"cursor":{"row":1,"col":28},"anchor":{"row":1,"col":28}},{"cursor":{"row":1,"col":30},"anchor":{"row":1,"col":30}},{"cursor":{"row":1,"col":32},"anchor":{"row":1,"col":32}},{"cursor":{"row":1,"col":34},"anchor":{"row":1,"col":34}},{"cursor":{"row":1,"col":36},"anchor":{"row":1,"col":36}},{"cursor":{"row":1,"col":38},"anchor":{"row":1,"col":38}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":2}},{"cursor":{"row":2,"col":4},"anchor":{"row":2,"col":4}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":2,"col":8},"anchor":{"row":2,"col":8}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":10}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":12}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":14}},{"cursor":{"row":2,"col":16},"anchor":{"row":2,"col":16}},{"cursor":{"row":2,"col":18},"anchor":{"row":2,"col":18}},{"cursor":{"row":2,"col":20},"anchor":{"row":2,"col":20}},{"cursor":{"row":2,"col":22},"anchor":{"row":2,"col":22}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":24}},{"cursor":{"row":2,"col":26},"anchor":{"row":2,"col":26}},{"cursor":{"row":2,"col":28},"anchor":{"row":2,"col":28}},{"cursor":{"row":2,"col":30},"anchor":{"row":2,"col":30}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":32}},{"cursor":{"row":2,"col":34},"anchor":{"row":2,"col":34}},{"cursor":{"row":2,"col":36},"anchor":{"row":2,"col":36}},{"cursor":{"row":2,"col":38},"anchor":{"row":2,"col":38}},{"cursor":{"row":3,"col":0},"anchor":{"row":3,"col":0}},{"cursor":{"row":3,"col":2},"anchor":{"row":3,"col":2}},{"cursor":{"row":3,"col":4},"anchor":{"row":3,"col":4}},{"cursor":{"row":3,"col":6},"anchor":{"row":3,"col":6}},{"cursor":{"row":3,"col":8},"anchor":{"row":3,"col":8}},{"cursor":{"row":3,"col":10},"anchor":{"row":3,"col":10}},{"cursor":{"row":3,"col":12},"anchor":{"row":3,"col":12}},{"cursor":{"row":3,"col":14},"anchor":{"row":3,"col":14}},{"cursor":{"row":3,"col":16},"anchor":{"row":3,"col":16}},{"cursor":{"row":3,"col":18},"anchor":{"row":3,"col":18}},{"cursor":{"row":3,"col":20},"anchor":{"row":3,"col":20}},{"cursor":{"row":3,"col":22},"anchor":{"row":3,"col":22}},{"cursor":{"row":3,"col":24},"anchor":{"row":3,"col":24}},{"cursor":{"row":3,"col":26},"anchor":{"row":3,"col":26}},{"cursor":{"row":3,"col":28},"anchor":{"row":3,"col":28}},{"cursor":{"row":3,"col":30},"anchor":{"row":3,"col":30}},{"cursor":{"row":3,"col":32},"anchor":{"row":3,"col":32}},{"cursor":{"row":3,"col":34},"anchor":{"row":3,"col":34}},{"cursor":{"row":3,"col":36},"anchor":{"row":3,"col":36}},{"cursor":{"row":3,"col":38},"anchor":{"row":3,"col":38}},{"cursor":{"row":4,"col":0},"anchor":{"row":4,"col":0}},{"cursor":{"row":4,"col":2},"anchor":{"row":4,"col":2}},{"cursor":{"row":4,"col":4},"anchor":{"row":4,"col":4}},{"cursor":{"row":4,"col":6},"anchor":{"row":4,"col":6}},{"cursor":{"row":4,"col":8},"anchor":{"row":4,"col":8}},{"cursor":{"row":4,"col":10},"anchor":{"row":4,"col":10}},{"cursor":{"row":4,"col":12},"anchor":{"row":4,"col":12}},{"cursor":{"row":4,"col":14},"anchor":{"row":4,"col":14}},{"cursor":{"row":4,"col":16},"anchor":{"row":4,"col":16}},{"cursor":{"row":4,"col":18},"anchor":{"row":4,"col":18}},{"cursor":{"row":4,"col":20},"anchor":{"row":4,"col":20}},{"cursor":{"row":4,"col":22},"anchor":{"row":4,"col":22}},{"cursor":{"row":4,"col":24},"anchor":{"row":4,"col":24}},{"cursor":{"row":4,"col":26},"anchor":{"row":4,"col":26}},{"cursor":{"row":4,"col":28},"anchor":{"row":4,"col":28}},{"cursor":{"row":4,"col":30},"anchor":{"row":4,"col":30}},{"cursor":{"row":4,"col":32},"anchor":{"row":4,"col":32}},{"cursor":{"row":4,"col":34},"anchor":{"row":4,"col":34}},{"cursor":{"row":4,"col":36},"anchor":{"row":4,"col":36}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":38}},{"cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},{"cursor":{"row":5,"col":2},"anchor":{"row":5,"col":2}},{"cursor":{"row":5,"col":4},"anchor":{"row":5,"col":4}},{"cursor":{"row":5,"col":6},"anchor":{"row":5,"col":6}},{"cursor":{"row":5,"col":8},"anchor":{"row":5,"col":8}},{"cursor":{"row":5,"col":10},"anchor":{"row":5,"col":10}},{"cursor":{"row":5,"col":12},"anchor":{"row":5,"col":12}},{"cursor":{"row":5,"col":14},"anchor":{"row":5,"col":14}},{"cursor":{"row":5,"col":16},"anchor":{"row":5,"col":16}},{"cursor":{"row":5,"col":18},"anchor":{"row":5,"col":18}},{"cursor":{"row":5,"col":20},"anchor":{"row":5,"col":20}},{"cursor":{"row":5,"col":22},"anchor":{"row":5,"col":22}},{"cursor":{"row":5,"col":24},"anchor":{"row":5,"col":24}},{"cursor":{"row":5,"col":26},"anchor":{"row":5,"col":26}},{"cursor":{"row":5,"col":28},"anchor":{"row":5,"col":28}},{"cursor":{"row":5,"col":30},"anchor":{"row":5,"col":30}},{"cursor":{"row":5,"col":32},"anchor":{"row":5,"col":32}},{"cursor":{"row":5,"col":34},"anchor":{"row":5,"col":34}},{"cursor":{"row":5,"col":36},"anchor":{"row":5,"col":36}},{"cursor":{"row":5,"col":38},"anchor":{"row":5,"col":38}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":6,"col":2},"anchor":{"row":6,"col":2}},{"cursor":{"row":6,"col":4},"anchor":{"row":6,"col":4}},{"cursor":{"row":6,"col":6},"anchor":{"row":6,"col":6}},{"cursor":{"row":6,"col":8},"anchor":{"row":6,"col":8}},{"cursor":{"row":6,"col":10},"anchor":{"row":6,"col":10}},{"cursor":{"row":6,"col":12},"anchor":{"row":6,"col":12}},{"cursor":{"row":6,"col":14},"anchor":{"row":6,"col":14}},{"cursor":{"row":6,"col":16},"anchor":{"row":6,"col":16}},{"cursor":{"row":6,"col":18},"anchor":{"row":6,"col":18}},{"cursor":{"row":6,"col":20},"anchor":{"row":6,"col":20}},{"cursor":{"row":6,"col":22},"anchor":{"row":6,"col":22}},{"cursor":{"row":6,"col":24},"anchor":{"row":6,"col":24}},{"cursor":{"row":6,"col":26},"anchor":{"row":6,"col":26}},{"cursor":{"row":6,"col":28},"anchor":{"row":6,"col":28}},{"cursor":{"row":6,"col":30},"anchor":{"row":6,"col":30}},{"cursor":{"row":6,"col":32},"anchor":{"row":6,"col":32}},{"cursor":{"row":6,"col":34},"anchor":{"row":6,"col":34}},{"cursor":{"row":6,"col":36},"anchor":{"row":6,"col":36}},{"cursor":{"row":6,"col":38},"anchor":{"row":6,"col":38}},{"cursor":{"row":7,"col":0},"anchor":{"row":7,"col":0}},{"cursor":{"row":7,"col":2},"anchor":{"row":7,"col":2}},{"cursor":{"row":7,"col":4},"anchor":{"row":7,"col":4}},{"cursor":{"row":7,"col":6},"anchor":{"row":7,"col":6}},{"cursor":{"row":7,"col":8},"anchor":{"row":7,"col":8}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":7,"col":12},"anchor":{"row":7,"col":12}},{"cursor":{"row":7,"col":14},"anchor":{"row":7,"col":14}},{"cursor":{"row":7,"col":16},"anchor":{"row":7,"col":16}},{"cursor":{"row":7,"col":18},"anchor":{"row":7,"col":18}},{"cursor":{"row":7,"col":20},"anchor":{"row":7,"col":20}},{"cursor":{"row":7,"col":22},"anchor":{"row":7,"col":22}},{"cursor":{"row":7,"col":24},"anchor":{"row":7,"col":24}},{"cursor":{"row":7,"col":26},"anchor":{"row":7,"col":26}},{"cursor":{"row":7,"col":28},"anchor":{"row":7,"col":28}},{"cursor":{"row":7,"col":30},"anchor":{"row":7,"col":30}},{"cursor":{"row":7,"col":32},"anchor":{"row":7,"col":32}},{"cursor":{"row":7,"col":34},"anchor":{"row":7,"col":34}},{"cursor":{"row":7,"col":36},"anchor":{"row":7,"col":36}},{"cursor":{"row":7,"col":38},"anchor":{"row":7,"col":38}},{"cursor":{"row":8,"col":0},"anchor":{"row":8,"col":0}},{"cursor":{"row":8,"col":2},"anchor":{"row":8,"col":2}},{"cursor":{"row":8,"col":4},"anchor":{"row":8,"col":4}},{"cursor":{"row":8,"col":6},"anchor":{"row":8,"col":6}},{"cursor":{"row":8,"col":8},"anchor":{"row":8,"col":8}},{"cursor":{"row":8,"col":10},"anchor":{"row":8,"col":10}},{"cursor":{"row":8,"col":12},"anchor":{"row":8,"col":12}},{"cursor":{"row":8,"col":14},"anchor":{"row":8,"col":14}},{"cursor":{"row":8,"col":16},"anchor":{"row":8,"col":16}},{"cursor":{"row":8,"col":18},"anchor":{"row":8,"col":18}},{"cursor":{"row":8,"col":20},"anchor":{"row":8,"col":20}},{"cursor":{"row":8,"col":22},"anchor":{"row":8,"col":22}},{"cursor":{"row":8,"col":24},"anchor":{"row":8,"col":24}},{"cursor":{"row":8,"col":26},"anchor":{"row":8,"col":26}},{"cursor":{"row":8,"col":28},"anchor":{"row":8,"col":28}},{"cursor":{"row":8,"col":30},"anchor":{"row":8,"col":30}},{"cursor":{"row":8,"col":32},"anchor":{"row":8,"col":32}},{"cursor":{"row":8,"col":34},"anchor":{"row":8,"col":34}},{"cursor":{"row":8,"col":36},"anchor":{"row":8,"col":36}},{"cursor":{"row":8,"col":38},"anchor":{"row":8,"col":38}},{"cursor":{"row":9,"col":0},"anchor":{"row":9,"col":0}},{"cursor":{"row":9,"col":2},"anchor":{"row":9,"col":2}},{"cursor":{"row":9,"col":4},"anchor":{"row":9,"col":4}},{"cursor":{"row":9,"col":6},"anchor":{"row":9,"col":6}},{"cursor":{"row":9,"col":8},"anchor":{"row":9,"col":8}},{"cursor":{"row":9,"col":10},"anchor":{"row":9,"col":10}},{"cursor":{"row":9,"col":12},"anchor":{"row":9,"col":12}},{"cursor":{"row":9,"col":14},"anchor":{"row":9,"col":14}},{"cursor":{"row":9,"col":16},"anchor":{"row":9,"col":16}},{"cursor":{"row":9,"col":18},"anchor":{"row":9,"col":18}},{"cursor":{"row":9,"col":20},"anchor":{"row":9,"col":20}},{"cursor":{"row":9,"col":22},"anchor":{"row":9,"col":22}},{"cursor":{"row":9,"col":24},"anchor":{"row":9,"col":24}},{"cursor":{"row":9,"col":26},"anchor":{"row":9,"col":26}},{"cursor":{"row":9,"col":28},"anchor":{"row":9,"col":28}},{"cursor":{"row":9,"col":30},"anchor":{"row":9,"col":30}},{"cursor":{"row":9,"col":32},"anchor":{"row":9,"col":32}},{"cursor":{"row":9,"col":34},"anchor":{"row":9,"col":34}},{"cursor":{"row":9,"col":36},"anchor":{"row":9,"col":36}},{"cursor":{"row":9,"col":38},"anchor":{"row":9,"col":38}}],"primary":0} +{"name":"sel-many-ranges-edit","text":"xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\nxy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy xy\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}},{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":8}},{"cursor":{"row":0,"col":11},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":14},"anchor":{"row":0,"col":14}},{"cursor":{"row":0,"col":17},"anchor":{"row":0,"col":17}},{"cursor":{"row":0,"col":20},"anchor":{"row":0,"col":20}},{"cursor":{"row":0,"col":23},"anchor":{"row":0,"col":23}},{"cursor":{"row":0,"col":26},"anchor":{"row":0,"col":26}},{"cursor":{"row":0,"col":29},"anchor":{"row":0,"col":29}},{"cursor":{"row":0,"col":32},"anchor":{"row":0,"col":32}},{"cursor":{"row":0,"col":35},"anchor":{"row":0,"col":35}},{"cursor":{"row":0,"col":38},"anchor":{"row":0,"col":38}},{"cursor":{"row":0,"col":41},"anchor":{"row":0,"col":41}},{"cursor":{"row":0,"col":44},"anchor":{"row":0,"col":44}},{"cursor":{"row":0,"col":47},"anchor":{"row":0,"col":47}},{"cursor":{"row":0,"col":50},"anchor":{"row":0,"col":50}},{"cursor":{"row":0,"col":53},"anchor":{"row":0,"col":53}},{"cursor":{"row":0,"col":56},"anchor":{"row":0,"col":56}},{"cursor":{"row":0,"col":59},"anchor":{"row":0,"col":59}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}},{"cursor":{"row":1,"col":8},"anchor":{"row":1,"col":8}},{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":11}},{"cursor":{"row":1,"col":14},"anchor":{"row":1,"col":14}},{"cursor":{"row":1,"col":17},"anchor":{"row":1,"col":17}},{"cursor":{"row":1,"col":20},"anchor":{"row":1,"col":20}},{"cursor":{"row":1,"col":23},"anchor":{"row":1,"col":23}},{"cursor":{"row":1,"col":26},"anchor":{"row":1,"col":26}},{"cursor":{"row":1,"col":29},"anchor":{"row":1,"col":29}},{"cursor":{"row":1,"col":32},"anchor":{"row":1,"col":32}},{"cursor":{"row":1,"col":35},"anchor":{"row":1,"col":35}},{"cursor":{"row":1,"col":38},"anchor":{"row":1,"col":38}},{"cursor":{"row":1,"col":41},"anchor":{"row":1,"col":41}},{"cursor":{"row":1,"col":44},"anchor":{"row":1,"col":44}},{"cursor":{"row":1,"col":47},"anchor":{"row":1,"col":47}},{"cursor":{"row":1,"col":50},"anchor":{"row":1,"col":50}},{"cursor":{"row":1,"col":53},"anchor":{"row":1,"col":53}},{"cursor":{"row":1,"col":56},"anchor":{"row":1,"col":56}},{"cursor":{"row":1,"col":59},"anchor":{"row":1,"col":59}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":2}},{"cursor":{"row":2,"col":5},"anchor":{"row":2,"col":5}},{"cursor":{"row":2,"col":8},"anchor":{"row":2,"col":8}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":14}},{"cursor":{"row":2,"col":17},"anchor":{"row":2,"col":17}},{"cursor":{"row":2,"col":20},"anchor":{"row":2,"col":20}},{"cursor":{"row":2,"col":23},"anchor":{"row":2,"col":23}},{"cursor":{"row":2,"col":26},"anchor":{"row":2,"col":26}},{"cursor":{"row":2,"col":29},"anchor":{"row":2,"col":29}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":32}},{"cursor":{"row":2,"col":35},"anchor":{"row":2,"col":35}},{"cursor":{"row":2,"col":38},"anchor":{"row":2,"col":38}},{"cursor":{"row":2,"col":41},"anchor":{"row":2,"col":41}},{"cursor":{"row":2,"col":44},"anchor":{"row":2,"col":44}},{"cursor":{"row":2,"col":47},"anchor":{"row":2,"col":47}},{"cursor":{"row":2,"col":50},"anchor":{"row":2,"col":50}},{"cursor":{"row":2,"col":53},"anchor":{"row":2,"col":53}},{"cursor":{"row":2,"col":56},"anchor":{"row":2,"col":56}},{"cursor":{"row":2,"col":59},"anchor":{"row":2,"col":59}},{"cursor":{"row":3,"col":2},"anchor":{"row":3,"col":2}},{"cursor":{"row":3,"col":5},"anchor":{"row":3,"col":5}},{"cursor":{"row":3,"col":8},"anchor":{"row":3,"col":8}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":11}},{"cursor":{"row":3,"col":14},"anchor":{"row":3,"col":14}},{"cursor":{"row":3,"col":17},"anchor":{"row":3,"col":17}},{"cursor":{"row":3,"col":20},"anchor":{"row":3,"col":20}},{"cursor":{"row":3,"col":23},"anchor":{"row":3,"col":23}},{"cursor":{"row":3,"col":26},"anchor":{"row":3,"col":26}},{"cursor":{"row":3,"col":29},"anchor":{"row":3,"col":29}},{"cursor":{"row":3,"col":32},"anchor":{"row":3,"col":32}},{"cursor":{"row":3,"col":35},"anchor":{"row":3,"col":35}},{"cursor":{"row":3,"col":38},"anchor":{"row":3,"col":38}},{"cursor":{"row":3,"col":41},"anchor":{"row":3,"col":41}},{"cursor":{"row":3,"col":44},"anchor":{"row":3,"col":44}},{"cursor":{"row":3,"col":47},"anchor":{"row":3,"col":47}},{"cursor":{"row":3,"col":50},"anchor":{"row":3,"col":50}},{"cursor":{"row":3,"col":53},"anchor":{"row":3,"col":53}},{"cursor":{"row":3,"col":56},"anchor":{"row":3,"col":56}},{"cursor":{"row":3,"col":59},"anchor":{"row":3,"col":59}},{"cursor":{"row":4,"col":2},"anchor":{"row":4,"col":2}},{"cursor":{"row":4,"col":5},"anchor":{"row":4,"col":5}},{"cursor":{"row":4,"col":8},"anchor":{"row":4,"col":8}},{"cursor":{"row":4,"col":11},"anchor":{"row":4,"col":11}},{"cursor":{"row":4,"col":14},"anchor":{"row":4,"col":14}},{"cursor":{"row":4,"col":17},"anchor":{"row":4,"col":17}},{"cursor":{"row":4,"col":20},"anchor":{"row":4,"col":20}},{"cursor":{"row":4,"col":23},"anchor":{"row":4,"col":23}},{"cursor":{"row":4,"col":26},"anchor":{"row":4,"col":26}},{"cursor":{"row":4,"col":29},"anchor":{"row":4,"col":29}},{"cursor":{"row":4,"col":32},"anchor":{"row":4,"col":32}},{"cursor":{"row":4,"col":35},"anchor":{"row":4,"col":35}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":38}},{"cursor":{"row":4,"col":41},"anchor":{"row":4,"col":41}},{"cursor":{"row":4,"col":44},"anchor":{"row":4,"col":44}},{"cursor":{"row":4,"col":47},"anchor":{"row":4,"col":47}},{"cursor":{"row":4,"col":50},"anchor":{"row":4,"col":50}},{"cursor":{"row":4,"col":53},"anchor":{"row":4,"col":53}},{"cursor":{"row":4,"col":56},"anchor":{"row":4,"col":56}},{"cursor":{"row":4,"col":59},"anchor":{"row":4,"col":59}},{"cursor":{"row":5,"col":2},"anchor":{"row":5,"col":2}},{"cursor":{"row":5,"col":5},"anchor":{"row":5,"col":5}},{"cursor":{"row":5,"col":8},"anchor":{"row":5,"col":8}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":11}},{"cursor":{"row":5,"col":14},"anchor":{"row":5,"col":14}},{"cursor":{"row":5,"col":17},"anchor":{"row":5,"col":17}},{"cursor":{"row":5,"col":20},"anchor":{"row":5,"col":20}},{"cursor":{"row":5,"col":23},"anchor":{"row":5,"col":23}},{"cursor":{"row":5,"col":26},"anchor":{"row":5,"col":26}},{"cursor":{"row":5,"col":29},"anchor":{"row":5,"col":29}},{"cursor":{"row":5,"col":32},"anchor":{"row":5,"col":32}},{"cursor":{"row":5,"col":35},"anchor":{"row":5,"col":35}},{"cursor":{"row":5,"col":38},"anchor":{"row":5,"col":38}},{"cursor":{"row":5,"col":41},"anchor":{"row":5,"col":41}},{"cursor":{"row":5,"col":44},"anchor":{"row":5,"col":44}},{"cursor":{"row":5,"col":47},"anchor":{"row":5,"col":47}},{"cursor":{"row":5,"col":50},"anchor":{"row":5,"col":50}},{"cursor":{"row":5,"col":53},"anchor":{"row":5,"col":53}},{"cursor":{"row":5,"col":56},"anchor":{"row":5,"col":56}},{"cursor":{"row":5,"col":59},"anchor":{"row":5,"col":59}},{"cursor":{"row":6,"col":2},"anchor":{"row":6,"col":2}},{"cursor":{"row":6,"col":5},"anchor":{"row":6,"col":5}},{"cursor":{"row":6,"col":8},"anchor":{"row":6,"col":8}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":11}},{"cursor":{"row":6,"col":14},"anchor":{"row":6,"col":14}},{"cursor":{"row":6,"col":17},"anchor":{"row":6,"col":17}},{"cursor":{"row":6,"col":20},"anchor":{"row":6,"col":20}},{"cursor":{"row":6,"col":23},"anchor":{"row":6,"col":23}},{"cursor":{"row":6,"col":26},"anchor":{"row":6,"col":26}},{"cursor":{"row":6,"col":29},"anchor":{"row":6,"col":29}},{"cursor":{"row":6,"col":32},"anchor":{"row":6,"col":32}},{"cursor":{"row":6,"col":35},"anchor":{"row":6,"col":35}},{"cursor":{"row":6,"col":38},"anchor":{"row":6,"col":38}},{"cursor":{"row":6,"col":41},"anchor":{"row":6,"col":41}},{"cursor":{"row":6,"col":44},"anchor":{"row":6,"col":44}},{"cursor":{"row":6,"col":47},"anchor":{"row":6,"col":47}},{"cursor":{"row":6,"col":50},"anchor":{"row":6,"col":50}},{"cursor":{"row":6,"col":53},"anchor":{"row":6,"col":53}},{"cursor":{"row":6,"col":56},"anchor":{"row":6,"col":56}},{"cursor":{"row":6,"col":59},"anchor":{"row":6,"col":59}},{"cursor":{"row":7,"col":2},"anchor":{"row":7,"col":2}},{"cursor":{"row":7,"col":5},"anchor":{"row":7,"col":5}},{"cursor":{"row":7,"col":8},"anchor":{"row":7,"col":8}},{"cursor":{"row":7,"col":11},"anchor":{"row":7,"col":11}},{"cursor":{"row":7,"col":14},"anchor":{"row":7,"col":14}},{"cursor":{"row":7,"col":17},"anchor":{"row":7,"col":17}},{"cursor":{"row":7,"col":20},"anchor":{"row":7,"col":20}},{"cursor":{"row":7,"col":23},"anchor":{"row":7,"col":23}},{"cursor":{"row":7,"col":26},"anchor":{"row":7,"col":26}},{"cursor":{"row":7,"col":29},"anchor":{"row":7,"col":29}},{"cursor":{"row":7,"col":32},"anchor":{"row":7,"col":32}},{"cursor":{"row":7,"col":35},"anchor":{"row":7,"col":35}},{"cursor":{"row":7,"col":38},"anchor":{"row":7,"col":38}},{"cursor":{"row":7,"col":41},"anchor":{"row":7,"col":41}},{"cursor":{"row":7,"col":44},"anchor":{"row":7,"col":44}},{"cursor":{"row":7,"col":47},"anchor":{"row":7,"col":47}},{"cursor":{"row":7,"col":50},"anchor":{"row":7,"col":50}},{"cursor":{"row":7,"col":53},"anchor":{"row":7,"col":53}},{"cursor":{"row":7,"col":56},"anchor":{"row":7,"col":56}},{"cursor":{"row":7,"col":59},"anchor":{"row":7,"col":59}},{"cursor":{"row":8,"col":2},"anchor":{"row":8,"col":2}},{"cursor":{"row":8,"col":5},"anchor":{"row":8,"col":5}},{"cursor":{"row":8,"col":8},"anchor":{"row":8,"col":8}},{"cursor":{"row":8,"col":11},"anchor":{"row":8,"col":11}},{"cursor":{"row":8,"col":14},"anchor":{"row":8,"col":14}},{"cursor":{"row":8,"col":17},"anchor":{"row":8,"col":17}},{"cursor":{"row":8,"col":20},"anchor":{"row":8,"col":20}},{"cursor":{"row":8,"col":23},"anchor":{"row":8,"col":23}},{"cursor":{"row":8,"col":26},"anchor":{"row":8,"col":26}},{"cursor":{"row":8,"col":29},"anchor":{"row":8,"col":29}},{"cursor":{"row":8,"col":32},"anchor":{"row":8,"col":32}},{"cursor":{"row":8,"col":35},"anchor":{"row":8,"col":35}},{"cursor":{"row":8,"col":38},"anchor":{"row":8,"col":38}},{"cursor":{"row":8,"col":41},"anchor":{"row":8,"col":41}},{"cursor":{"row":8,"col":44},"anchor":{"row":8,"col":44}},{"cursor":{"row":8,"col":47},"anchor":{"row":8,"col":47}},{"cursor":{"row":8,"col":50},"anchor":{"row":8,"col":50}},{"cursor":{"row":8,"col":53},"anchor":{"row":8,"col":53}},{"cursor":{"row":8,"col":56},"anchor":{"row":8,"col":56}},{"cursor":{"row":8,"col":59},"anchor":{"row":8,"col":59}},{"cursor":{"row":9,"col":2},"anchor":{"row":9,"col":2}},{"cursor":{"row":9,"col":5},"anchor":{"row":9,"col":5}},{"cursor":{"row":9,"col":8},"anchor":{"row":9,"col":8}},{"cursor":{"row":9,"col":11},"anchor":{"row":9,"col":11}},{"cursor":{"row":9,"col":14},"anchor":{"row":9,"col":14}},{"cursor":{"row":9,"col":17},"anchor":{"row":9,"col":17}},{"cursor":{"row":9,"col":20},"anchor":{"row":9,"col":20}},{"cursor":{"row":9,"col":23},"anchor":{"row":9,"col":23}},{"cursor":{"row":9,"col":26},"anchor":{"row":9,"col":26}},{"cursor":{"row":9,"col":29},"anchor":{"row":9,"col":29}},{"cursor":{"row":9,"col":32},"anchor":{"row":9,"col":32}},{"cursor":{"row":9,"col":35},"anchor":{"row":9,"col":35}},{"cursor":{"row":9,"col":38},"anchor":{"row":9,"col":38}},{"cursor":{"row":9,"col":41},"anchor":{"row":9,"col":41}},{"cursor":{"row":9,"col":44},"anchor":{"row":9,"col":44}},{"cursor":{"row":9,"col":47},"anchor":{"row":9,"col":47}},{"cursor":{"row":9,"col":50},"anchor":{"row":9,"col":50}},{"cursor":{"row":9,"col":53},"anchor":{"row":9,"col":53}},{"cursor":{"row":9,"col":56},"anchor":{"row":9,"col":56}},{"cursor":{"row":9,"col":59},"anchor":{"row":9,"col":59}}],"primary":0} +{"name":"keep-regex","text":"foo bar baz\n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":8}}],"primary":0} +{"name":"remove-regex","text":"foo bar baz\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} +{"name":"keep-regex-none","text":"foo bar\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":4}}],"primary":0} +{"name":"keep-regex-primary","text":"a1 b2 a3\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":6}}],"primary":0} +{"name":"keep-regex-esc","text":"foo bar\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":4}}],"primary":0} +{"name":"keep-regex-lines","text":"foo\nbar\nbaz\n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"ensure-forward","text":"ab cd\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":3}}],"primary":0} +{"name":"ensure-forward-one","text":"abc\n","mode":"select","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} +{"name":"node-keys-plain-text","text":"ab cd\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} +{"name":"goto-last-mod","text":"ab\ncXYd\nef\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"goto-last-mod-go","text":"ab\ncXYd\nef\n","mode":"normal","cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}} +{"name":"goto-last-mod-delete","text":"abc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"goto-last-mod-delete-go","text":"abc\ndf\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}} +{"name":"goto-last-mod-select","text":"ab\ncd\nXef\n","mode":"select","cursor":{"row":2,"col":1},"anchor":{"row":0,"col":0}} +{"name":"alt-u-undo","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"alt-U-redo","text":"Xab\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"ins-checkpoint-undo","text":"Xab\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"ins-checkpoint-undo-twice","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"ins-checkpoint-redo","text":"Xab\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"msel-ins-checkpoint","text":"Xab\nXab\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1} +{"name":"ins-tab-after-text-plain","text":"ab X cd\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}} +{"name":"ins-shift-tab","text":"ab X cd\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}} +{"name":"sticky-view-scroll","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}} +{"name":"sticky-view-then-move","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}} +{"name":"sticky-view-ignores","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}} +{"name":"view-middle","text":"abcdef\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}} +{"name":"view-z-not-sticky","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}} +{"name":"goto-word-first","text":"alpha beta gamma\ndelta x\n","mode":"normal","cursor":{"row":0,"col":9},"anchor":{"row":0,"col":6}} +{"name":"goto-word-second","text":"alpha beta gamma\ndelta x\n","mode":"normal","cursor":{"row":0,"col":15},"anchor":{"row":0,"col":11}} +{"name":"goto-word-back","text":"alpha beta gamma\ndelta x\n","mode":"normal","cursor":{"row":0,"col":15},"anchor":{"row":0,"col":11}} +{"name":"goto-word-cancel","text":"alpha beta gamma\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"goto-word-bad-second","text":"alpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"goto-word-extend","text":"alpha beta gamma\n","mode":"select","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":5}} +{"name":"goto-word-mid","text":"one two three four five\n","mode":"normal","cursor":{"row":0,"col":12},"anchor":{"row":0,"col":8}} +{"name":"msel-align-basic","text":"a =1\nbbb=2\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}}],"primary":0} +{"name":"msel-align-columns","text":"a ,b,c\nccc,d,e\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}}],"primary":0} +{"name":"msel-align-uneven","text":"a ,b\nccc,d,eee,f\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}}],"primary":0} +{"name":"msel-align-words","text":" x = 1\nlonger = 22\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5},"sels":[{"cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}},{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":9}}],"primary":0} +{"name":"msel-align-backward","text":"x = 11\nlonger = 222\n","mode":"normal","cursor":{"row":0,"col":9},"anchor":{"row":0,"col":10},"sels":[{"cursor":{"row":0,"col":9},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":11}}],"primary":0} +{"name":"align-multiline-refused","text":"ab\ncd\n","mode":"select","cursor":{"row":1,"col":0},"anchor":{"row":0,"col":0}} +{"name":"msel-align-select-mode","text":"a =1\nbbb=2\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}}],"primary":0} +{"name":"rotate-contents-one","text":"ab\n","mode":"select","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}} {"name":"msel-insert","text":"xaa\nxbb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1} {"name":"msel-insert-tty","text":"xaa\nxbb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1} {"name":"msel-append","text":"aza\nbzb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1} @@ -409,6 +493,35 @@ {"name":"msel-collapse","text":"abcd\nabcd\n","mode":"select","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}}],"primary":1} {"name":"msel-flip","text":"abcd\nabcd\n","mode":"select","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":2}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":2}}],"primary":1} {"name":"msel-yank-paste","text":"aab\nccd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1} +{"name":"reg-named-yank-paste","text":"ab acd\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}} +{"name":"reg-named-keeps-default","text":"cad\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"reg-blackhole-delete","text":"acd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"msel-reg-paste-per-range","text":"aaaab\nccccd\n","mode":"normal","cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}}],"primary":1} +{"name":"msel-reg-fewer-values","text":"aab\ncad\neaf\n","mode":"normal","cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}}],"primary":2} +{"name":"msel-reg-replace-per-range","text":"ab\ncd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}}],"primary":0} +{"name":"msel-reg-more-values-one-range","text":"ab\ncad\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}} +{"name":"reg-index-insert","text":"1x\n2x\n3x\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}}],"primary":0} +{"name":"reg-search-paste","text":"abb\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} +{"name":"reg-count-before-quote","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"reg-count-after-quote","text":"aaaab\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":1}} +{"name":"reg-ctrl-r-named","text":"aab\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}} +{"name":"reg-ctrl-r-esc","text":"xab\n","mode":"insert","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} +{"name":"macro-record-replay","text":"a!\nb!\nc\n","mode":"normal","cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}} +{"name":"macro-count","text":"a!\nb!\nc!\nd\n","mode":"normal","cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}} +{"name":"macro-named","text":"a!\nb!\nc\n","mode":"normal","cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}} +{"name":"macro-register-text","text":"a!\nbA!<esc>j\n","mode":"normal","cursor":{"row":1,"col":8},"anchor":{"row":1,"col":1}} +{"name":"macro-with-prompt","text":",c,d\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}} +{"name":"macro-esc-in-normal","text":"ab\ncd\n","mode":"normal","cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}} +{"name":"macro-empty","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"dot-insert","text":"Xab\ncXXd\n","mode":"normal","cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}} +{"name":"dot-append-count","text":"aYZb\ncd\nYZYZYZ\n","mode":"normal","cursor":{"row":2,"col":5},"anchor":{"row":1,"col":2}} +{"name":"dot-change","text":"Xcd\nX gh\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}} +{"name":"dot-open-below","text":"a\nX\nb\nX\n","mode":"normal","cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}} +{"name":"dot-counted-command","text":"abxyxycd\n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}} +{"name":"dot-multi","text":"XXab\nXXab\n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}}],"primary":1} +{"name":"dot-nothing","text":"ab\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}} +{"name":"append-at-end-adds-newline","text":"ab\nX","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}} +{"name":"append-at-end-no-newline","text":"abX\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":1}} {"name":"msel-esc-keeps","text":"aa\nbb\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}}],"primary":1} {"name":"msel-select-mode","text":"abcd\nabcd\n","mode":"select","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1} {"name":"msel-open-count","text":"\n\nab\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}}],"primary":0} diff --git a/test/hxcases/golf-goldens.jsonl b/test/hxcases/golf-goldens.jsonl new file mode 100644 index 00000000..d2d77302 --- /dev/null +++ b/test/hxcases/golf-goldens.jsonl @@ -0,0 +1,124 @@ +{"name":"golf-csv-to-sql-01","text":"id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":2,"col":33},"anchor":{"row":0,"col":0}} +{"name":"golf-csv-to-sql-02","text":"id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":0,"col":28},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":28},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":28},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"golf-csv-to-sql-03","text":"id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":0,"col":28},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":28},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":28},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"golf-csv-to-sql-04","text":"id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":0,"col":17},"anchor":{"row":0,"col":17}},{"cursor":{"row":0,"col":28},"anchor":{"row":0,"col":28}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":10}},{"cursor":{"row":1,"col":17},"anchor":{"row":1,"col":17}},{"cursor":{"row":1,"col":28},"anchor":{"row":1,"col":28}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":4},"anchor":{"row":2,"col":4}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":12}},{"cursor":{"row":2,"col":19},"anchor":{"row":2,"col":19}},{"cursor":{"row":2,"col":20},"anchor":{"row":2,"col":20}},{"cursor":{"row":2,"col":31},"anchor":{"row":2,"col":31}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":32}}],"primary":0} +{"name":"golf-csv-to-sql-05","text":"id ,Item ,cost ,location \nid ,Item ,cost ,location \nid ,Item ,cost ,location \n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":9},"anchor":{"row":0,"col":9}},{"cursor":{"row":0,"col":15},"anchor":{"row":0,"col":15}},{"cursor":{"row":0,"col":25},"anchor":{"row":0,"col":25}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":1,"col":15},"anchor":{"row":1,"col":15}},{"cursor":{"row":1,"col":25},"anchor":{"row":1,"col":25}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":9},"anchor":{"row":2,"col":9}},{"cursor":{"row":2,"col":15},"anchor":{"row":2,"col":15}},{"cursor":{"row":2,"col":25},"anchor":{"row":2,"col":25}}],"primary":0} +{"name":"golf-csv-to-sql-06","text":"id ,Item ,cost ,location \nid ,Item ,cost ,location \nid ,Item ,cost ,location \n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7}},{"cursor":{"row":0,"col":13},"anchor":{"row":0,"col":13}},{"cursor":{"row":0,"col":23},"anchor":{"row":0,"col":23}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":1,"col":13},"anchor":{"row":1,"col":13}},{"cursor":{"row":1,"col":23},"anchor":{"row":1,"col":23}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":7},"anchor":{"row":2,"col":7}},{"cursor":{"row":2,"col":13},"anchor":{"row":2,"col":13}},{"cursor":{"row":2,"col":23},"anchor":{"row":2,"col":23}}],"primary":0} +{"name":"golf-csv-to-sql-07","text":"id ,Item ,cost ,location \nid ,Item ,cost ,location \nid ,Item ,cost ,location \n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":1},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":1}},{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":7}},{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":13}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":23}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":4},"anchor":{"row":1,"col":7}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":13}},{"cursor":{"row":1,"col":16},"anchor":{"row":1,"col":23}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":4},"anchor":{"row":2,"col":7}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":13}},{"cursor":{"row":2,"col":16},"anchor":{"row":2,"col":23}}],"primary":0} +{"name":"golf-csv-to-sql-08","text":"`id` ,`Item` ,`cost` ,`location` \n`id` ,`Item` ,`cost` ,`location` \n`id` ,`Item` ,`cost` ,`location` \n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":14},"anchor":{"row":0,"col":19}},{"cursor":{"row":0,"col":22},"anchor":{"row":0,"col":31}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":6},"anchor":{"row":1,"col":11}},{"cursor":{"row":1,"col":14},"anchor":{"row":1,"col":19}},{"cursor":{"row":1,"col":22},"anchor":{"row":1,"col":31}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":19}},{"cursor":{"row":2,"col":22},"anchor":{"row":2,"col":31}}],"primary":0} +{"name":"golf-csv-to-sql-09","text":"`id` ,`item` ,`cost` ,`location` \n`id` ,`item` ,`cost` ,`location` \n`id` ,`item` ,`cost` ,`location` \n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":14},"anchor":{"row":0,"col":19}},{"cursor":{"row":0,"col":22},"anchor":{"row":0,"col":31}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":6},"anchor":{"row":1,"col":11}},{"cursor":{"row":1,"col":14},"anchor":{"row":1,"col":19}},{"cursor":{"row":1,"col":22},"anchor":{"row":1,"col":31}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":19}},{"cursor":{"row":2,"col":22},"anchor":{"row":2,"col":31}}],"primary":0} +{"name":"golf-csv-to-sql-10","text":"`id` ,`item` ,`cost` ,`location` \n`id` ,`item` ,`cost` ,`location` \n`id` ,`item` ,`cost` ,`location` \n","mode":"normal","cursor":{"row":0,"col":31},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":31},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":31},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":31},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"golf-csv-to-sql-11","text":"(`id` ,`item` ,`cost` ,`location`) \n(`id` ,`item` ,`cost` ,`location`) \n(`id` ,`item` ,`cost` ,`location`) \n","mode":"normal","cursor":{"row":0,"col":33},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":33},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":33},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":33},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"golf-csv-to-sql-12","text":"(`id` ,`item` ,`cost` ,`location`) \n(`id` ,`item` ,`cost` ,`location`) \n(`id` ,`item` ,`cost` ,`location`) \n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}}],"primary":0} +{"name":"golf-csv-to-sql-13","text":"INSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \n","mode":"insert","cursor":{"row":0,"col":21},"anchor":{"row":0,"col":21},"sels":[{"cursor":{"row":0,"col":21},"anchor":{"row":0,"col":21}},{"cursor":{"row":1,"col":21},"anchor":{"row":1,"col":21}},{"cursor":{"row":2,"col":21},"anchor":{"row":2,"col":21}}],"primary":0} +{"name":"golf-csv-to-sql-14","text":"INSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(`id` ,`item` ,`cost` ,`location`) \n","mode":"normal","cursor":{"row":0,"col":21},"anchor":{"row":0,"col":21},"sels":[{"cursor":{"row":0,"col":21},"anchor":{"row":0,"col":21}},{"cursor":{"row":1,"col":21},"anchor":{"row":1,"col":21}},{"cursor":{"row":2,"col":21},"anchor":{"row":2,"col":21}}],"primary":0} +{"name":"golf-csv-to-sql-15","text":"INSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \n","mode":"insert","cursor":{"row":0,"col":29},"anchor":{"row":0,"col":21},"sels":[{"cursor":{"row":0,"col":29},"anchor":{"row":0,"col":21}},{"cursor":{"row":1,"col":29},"anchor":{"row":1,"col":21}},{"cursor":{"row":2,"col":29},"anchor":{"row":2,"col":21}}],"primary":0} +{"name":"golf-csv-to-sql-16","text":"INSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table`id` ,`item` ,`cost` ,`location`) \n","mode":"insert","cursor":{"row":0,"col":30},"anchor":{"row":0,"col":29},"sels":[{"cursor":{"row":0,"col":30},"anchor":{"row":0,"col":29}},{"cursor":{"row":1,"col":30},"anchor":{"row":1,"col":29}},{"cursor":{"row":2,"col":30},"anchor":{"row":2,"col":29}}],"primary":0} +{"name":"golf-csv-to-sql-17","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) \nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) \n","mode":"normal","cursor":{"row":0,"col":30},"anchor":{"row":0,"col":29},"sels":[{"cursor":{"row":0,"col":30},"anchor":{"row":0,"col":29}},{"cursor":{"row":1,"col":30},"anchor":{"row":1,"col":29}},{"cursor":{"row":2,"col":30},"anchor":{"row":2,"col":29}}],"primary":0} +{"name":"golf-csv-to-sql-18","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\n","mode":"insert","cursor":{"row":0,"col":72},"anchor":{"row":0,"col":72},"sels":[{"cursor":{"row":0,"col":72},"anchor":{"row":0,"col":72}},{"cursor":{"row":1,"col":72},"anchor":{"row":1,"col":72}},{"cursor":{"row":2,"col":72},"anchor":{"row":2,"col":72}}],"primary":0} +{"name":"golf-csv-to-sql-19","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (\n","mode":"normal","cursor":{"row":0,"col":72},"anchor":{"row":0,"col":72},"sels":[{"cursor":{"row":0,"col":72},"anchor":{"row":0,"col":72}},{"cursor":{"row":1,"col":72},"anchor":{"row":1,"col":72}},{"cursor":{"row":2,"col":72},"anchor":{"row":2,"col":72}}],"primary":0} +{"name":"golf-csv-to-sql-20","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 1,Item 1,cost 1,location 1\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 2,Item 2,cost 2,location 2\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":0,"col":100},"anchor":{"row":0,"col":72},"sels":[{"cursor":{"row":0,"col":100},"anchor":{"row":0,"col":72}},{"cursor":{"row":1,"col":100},"anchor":{"row":1,"col":72}},{"cursor":{"row":2,"col":104},"anchor":{"row":2,"col":72}}],"primary":0} +{"name":"golf-csv-to-sql-21","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 1,Item 1,cost 1,location 1\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 2,Item 2,cost 2,location 2\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES (id 10,Item 10,cost 10,location 10\n","mode":"normal","cursor":{"row":0,"col":75},"anchor":{"row":0,"col":72},"sels":[{"cursor":{"row":0,"col":75},"anchor":{"row":0,"col":72}},{"cursor":{"row":0,"col":82},"anchor":{"row":0,"col":77}},{"cursor":{"row":0,"col":89},"anchor":{"row":0,"col":84}},{"cursor":{"row":0,"col":100},"anchor":{"row":0,"col":91}},{"cursor":{"row":1,"col":75},"anchor":{"row":1,"col":72}},{"cursor":{"row":1,"col":82},"anchor":{"row":1,"col":77}},{"cursor":{"row":1,"col":89},"anchor":{"row":1,"col":84}},{"cursor":{"row":1,"col":100},"anchor":{"row":1,"col":91}},{"cursor":{"row":2,"col":76},"anchor":{"row":2,"col":72}},{"cursor":{"row":2,"col":84},"anchor":{"row":2,"col":78}},{"cursor":{"row":2,"col":92},"anchor":{"row":2,"col":86}},{"cursor":{"row":2,"col":104},"anchor":{"row":2,"col":94}}],"primary":0} +{"name":"golf-csv-to-sql-22","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 1','Item 1','cost 1','location 1'\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 2','Item 2','cost 2','location 2'\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 10','Item 10','cost 10','location 10'\n","mode":"normal","cursor":{"row":0,"col":77},"anchor":{"row":0,"col":72},"sels":[{"cursor":{"row":0,"col":77},"anchor":{"row":0,"col":72}},{"cursor":{"row":0,"col":86},"anchor":{"row":0,"col":79}},{"cursor":{"row":0,"col":95},"anchor":{"row":0,"col":88}},{"cursor":{"row":0,"col":108},"anchor":{"row":0,"col":97}},{"cursor":{"row":1,"col":77},"anchor":{"row":1,"col":72}},{"cursor":{"row":1,"col":86},"anchor":{"row":1,"col":79}},{"cursor":{"row":1,"col":95},"anchor":{"row":1,"col":88}},{"cursor":{"row":1,"col":108},"anchor":{"row":1,"col":97}},{"cursor":{"row":2,"col":78},"anchor":{"row":2,"col":72}},{"cursor":{"row":2,"col":88},"anchor":{"row":2,"col":80}},{"cursor":{"row":2,"col":98},"anchor":{"row":2,"col":90}},{"cursor":{"row":2,"col":112},"anchor":{"row":2,"col":100}}],"primary":0} +{"name":"golf-csv-to-sql-23","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 1','Item 1','cost 1','location 1';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 2','Item 2','cost 2','location 2';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 10','Item 10','cost 10','location 10';\n","mode":"insert","cursor":{"row":0,"col":110},"anchor":{"row":0,"col":110},"sels":[{"cursor":{"row":0,"col":110},"anchor":{"row":0,"col":110}},{"cursor":{"row":1,"col":110},"anchor":{"row":1,"col":110}},{"cursor":{"row":2,"col":114},"anchor":{"row":2,"col":114}}],"primary":0} +{"name":"golf-csv-to-sql-24","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 1','Item 1','cost 1','location 1';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 2','Item 2','cost 2','location 2';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 10','Item 10','cost 10','location 10';\n","mode":"normal","cursor":{"row":0,"col":98},"anchor":{"row":0,"col":110},"sels":[{"cursor":{"row":0,"col":98},"anchor":{"row":0,"col":110}},{"cursor":{"row":1,"col":98},"anchor":{"row":1,"col":110}},{"cursor":{"row":2,"col":101},"anchor":{"row":2,"col":114}}],"primary":0} +{"name":"golf-csv-to-sql-25","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 1','Item 1','cost 1','location 1';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 2','Item 2','cost 2','location 2';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 10','Item 10','cost 10','location 10';\n","mode":"normal","cursor":{"row":0,"col":98},"anchor":{"row":0,"col":98},"sels":[{"cursor":{"row":0,"col":98},"anchor":{"row":0,"col":98}},{"cursor":{"row":1,"col":98},"anchor":{"row":1,"col":98}},{"cursor":{"row":2,"col":101},"anchor":{"row":2,"col":101}}],"primary":0} +{"name":"golf-csv-to-sql","text":"INSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 1','Item 1','cost 1','Location 1';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 2','Item 2','cost 2','Location 2';\nINSERT INTO `database(.`table` id` ,`item` ,`cost` ,`location`) VALUES ('id 10','Item 10','cost 10','Location 10';\n","mode":"normal","cursor":{"row":0,"col":98},"anchor":{"row":0,"col":98},"sels":[{"cursor":{"row":0,"col":98},"anchor":{"row":0,"col":98}},{"cursor":{"row":1,"col":98},"anchor":{"row":1,"col":98}},{"cursor":{"row":2,"col":101},"anchor":{"row":2,"col":101}}],"primary":0} +{"name":"golf-enumerate-and-align-01","text":"[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":12,"col":2},"anchor":{"row":0,"col":0}} +{"name":"golf-enumerate-and-align-02","text":"[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":12,"col":2},"anchor":{"row":0,"col":0}} +{"name":"golf-enumerate-and-align-03","text":"[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":2},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":2},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":2},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":2},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":2},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":2},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":2},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":2},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":2},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":2},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":2},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-04","text":"[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n","mode":"insert","cursor":{"row":1,"col":4},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":4},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":4},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":4},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":4},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":4},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":4},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":4},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":4},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":4},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":4},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":4},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-05","text":"[\n { rank: word: \"a\", count: 2565 },\n { rank: word: \"and\", count: 1777 },\n { rank: word: \"of\", count: 1331 },\n { rank: word: \"that\", count: 1263 },\n { rank: word: \"to\", count: 1030 },\n { rank: word: \"in\", count: 1027 },\n { rank: word: \"it\", count: 754 },\n { rank: word: \"as\", count: 730 },\n { rank: word: \"was\", count: 687 },\n { rank: word: \"you\", count: 652 },\n { rank: word: \"for\", count: 630 },\n];\n","mode":"insert","cursor":{"row":1,"col":10},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":10},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":10},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":10},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":10},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":10},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":10},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":10},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":10},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-06","text":"[\n { rank: 1 word: \"a\", count: 2565 },\n { rank: 2 word: \"and\", count: 1777 },\n { rank: 3 word: \"of\", count: 1331 },\n { rank: 4 word: \"that\", count: 1263 },\n { rank: 5 word: \"to\", count: 1030 },\n { rank: 6 word: \"in\", count: 1027 },\n { rank: 7 word: \"it\", count: 754 },\n { rank: 8 word: \"as\", count: 730 },\n { rank: 9 word: \"was\", count: 687 },\n { rank: 10 word: \"you\", count: 652 },\n { rank: 11 word: \"for\", count: 630 },\n];\n","mode":"insert","cursor":{"row":1,"col":11},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":11},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":11},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":11},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":11},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":12},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":12},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-07","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"insert","cursor":{"row":1,"col":12},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":12},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":12},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":12},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":12},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":12},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":12},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":12},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":13},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-08","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":11},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":2}},{"cursor":{"row":4,"col":11},"anchor":{"row":4,"col":2}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":2}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":2}},{"cursor":{"row":7,"col":11},"anchor":{"row":7,"col":2}},{"cursor":{"row":8,"col":11},"anchor":{"row":8,"col":2}},{"cursor":{"row":9,"col":11},"anchor":{"row":9,"col":2}},{"cursor":{"row":10,"col":12},"anchor":{"row":10,"col":2}},{"cursor":{"row":11,"col":12},"anchor":{"row":11,"col":2}}],"primary":0} +{"name":"golf-enumerate-and-align-09","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":12,"col":2},"anchor":{"row":0,"col":0}} +{"name":"golf-enumerate-and-align-10","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":10}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":12}},{"cursor":{"row":1,"col":18},"anchor":{"row":1,"col":18}},{"cursor":{"row":1,"col":23},"anchor":{"row":1,"col":23}},{"cursor":{"row":1,"col":30},"anchor":{"row":1,"col":30}},{"cursor":{"row":1,"col":34},"anchor":{"row":1,"col":31}},{"cursor":{"row":1,"col":35},"anchor":{"row":1,"col":35}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":9},"anchor":{"row":2,"col":9}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":10}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":12}},{"cursor":{"row":2,"col":18},"anchor":{"row":2,"col":18}},{"cursor":{"row":2,"col":25},"anchor":{"row":2,"col":25}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":32}},{"cursor":{"row":2,"col":36},"anchor":{"row":2,"col":33}},{"cursor":{"row":2,"col":37},"anchor":{"row":2,"col":37}},{"cursor":{"row":3,"col":0},"anchor":{"row":3,"col":0}},{"cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}},{"cursor":{"row":3,"col":3},"anchor":{"row":3,"col":3}},{"cursor":{"row":3,"col":9},"anchor":{"row":3,"col":9}},{"cursor":{"row":3,"col":10},"anchor":{"row":3,"col":10}},{"cursor":{"row":3,"col":12},"anchor":{"row":3,"col":12}},{"cursor":{"row":3,"col":18},"anchor":{"row":3,"col":18}},{"cursor":{"row":3,"col":24},"anchor":{"row":3,"col":24}},{"cursor":{"row":3,"col":31},"anchor":{"row":3,"col":31}},{"cursor":{"row":3,"col":35},"anchor":{"row":3,"col":32}},{"cursor":{"row":3,"col":36},"anchor":{"row":3,"col":36}},{"cursor":{"row":4,"col":0},"anchor":{"row":4,"col":0}},{"cursor":{"row":4,"col":1},"anchor":{"row":4,"col":1}},{"cursor":{"row":4,"col":3},"anchor":{"row":4,"col":3}},{"cursor":{"row":4,"col":9},"anchor":{"row":4,"col":9}},{"cursor":{"row":4,"col":10},"anchor":{"row":4,"col":10}},{"cursor":{"row":4,"col":12},"anchor":{"row":4,"col":12}},{"cursor":{"row":4,"col":18},"anchor":{"row":4,"col":18}},{"cursor":{"row":4,"col":26},"anchor":{"row":4,"col":26}},{"cursor":{"row":4,"col":33},"anchor":{"row":4,"col":33}},{"cursor":{"row":4,"col":37},"anchor":{"row":4,"col":34}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":38}},{"cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},{"cursor":{"row":5,"col":1},"anchor":{"row":5,"col":1}},{"cursor":{"row":5,"col":3},"anchor":{"row":5,"col":3}},{"cursor":{"row":5,"col":9},"anchor":{"row":5,"col":9}},{"cursor":{"row":5,"col":10},"anchor":{"row":5,"col":10}},{"cursor":{"row":5,"col":12},"anchor":{"row":5,"col":12}},{"cursor":{"row":5,"col":18},"anchor":{"row":5,"col":18}},{"cursor":{"row":5,"col":24},"anchor":{"row":5,"col":24}},{"cursor":{"row":5,"col":31},"anchor":{"row":5,"col":31}},{"cursor":{"row":5,"col":35},"anchor":{"row":5,"col":32}},{"cursor":{"row":5,"col":36},"anchor":{"row":5,"col":36}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":6,"col":1},"anchor":{"row":6,"col":1}},{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":3}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":6,"col":10},"anchor":{"row":6,"col":10}},{"cursor":{"row":6,"col":12},"anchor":{"row":6,"col":12}},{"cursor":{"row":6,"col":18},"anchor":{"row":6,"col":18}},{"cursor":{"row":6,"col":24},"anchor":{"row":6,"col":24}},{"cursor":{"row":6,"col":31},"anchor":{"row":6,"col":31}},{"cursor":{"row":6,"col":35},"anchor":{"row":6,"col":32}},{"cursor":{"row":6,"col":36},"anchor":{"row":6,"col":36}},{"cursor":{"row":7,"col":0},"anchor":{"row":7,"col":0}},{"cursor":{"row":7,"col":1},"anchor":{"row":7,"col":1}},{"cursor":{"row":7,"col":3},"anchor":{"row":7,"col":3}},{"cursor":{"row":7,"col":9},"anchor":{"row":7,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":7,"col":12},"anchor":{"row":7,"col":12}},{"cursor":{"row":7,"col":18},"anchor":{"row":7,"col":18}},{"cursor":{"row":7,"col":24},"anchor":{"row":7,"col":24}},{"cursor":{"row":7,"col":31},"anchor":{"row":7,"col":31}},{"cursor":{"row":7,"col":34},"anchor":{"row":7,"col":32}},{"cursor":{"row":7,"col":35},"anchor":{"row":7,"col":35}},{"cursor":{"row":8,"col":0},"anchor":{"row":8,"col":0}},{"cursor":{"row":8,"col":1},"anchor":{"row":8,"col":1}},{"cursor":{"row":8,"col":3},"anchor":{"row":8,"col":3}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":8,"col":10},"anchor":{"row":8,"col":10}},{"cursor":{"row":8,"col":12},"anchor":{"row":8,"col":12}},{"cursor":{"row":8,"col":18},"anchor":{"row":8,"col":18}},{"cursor":{"row":8,"col":24},"anchor":{"row":8,"col":24}},{"cursor":{"row":8,"col":31},"anchor":{"row":8,"col":31}},{"cursor":{"row":8,"col":34},"anchor":{"row":8,"col":32}},{"cursor":{"row":8,"col":35},"anchor":{"row":8,"col":35}},{"cursor":{"row":9,"col":0},"anchor":{"row":9,"col":0}},{"cursor":{"row":9,"col":1},"anchor":{"row":9,"col":1}},{"cursor":{"row":9,"col":3},"anchor":{"row":9,"col":3}},{"cursor":{"row":9,"col":9},"anchor":{"row":9,"col":9}},{"cursor":{"row":9,"col":10},"anchor":{"row":9,"col":10}},{"cursor":{"row":9,"col":12},"anchor":{"row":9,"col":12}},{"cursor":{"row":9,"col":18},"anchor":{"row":9,"col":18}},{"cursor":{"row":9,"col":25},"anchor":{"row":9,"col":25}},{"cursor":{"row":9,"col":32},"anchor":{"row":9,"col":32}},{"cursor":{"row":9,"col":35},"anchor":{"row":9,"col":33}},{"cursor":{"row":9,"col":36},"anchor":{"row":9,"col":36}},{"cursor":{"row":10,"col":0},"anchor":{"row":10,"col":0}},{"cursor":{"row":10,"col":1},"anchor":{"row":10,"col":1}},{"cursor":{"row":10,"col":3},"anchor":{"row":10,"col":3}},{"cursor":{"row":10,"col":9},"anchor":{"row":10,"col":9}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":10}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}},{"cursor":{"row":10,"col":19},"anchor":{"row":10,"col":19}},{"cursor":{"row":10,"col":26},"anchor":{"row":10,"col":26}},{"cursor":{"row":10,"col":33},"anchor":{"row":10,"col":33}},{"cursor":{"row":10,"col":36},"anchor":{"row":10,"col":34}},{"cursor":{"row":10,"col":37},"anchor":{"row":10,"col":37}},{"cursor":{"row":11,"col":0},"anchor":{"row":11,"col":0}},{"cursor":{"row":11,"col":1},"anchor":{"row":11,"col":1}},{"cursor":{"row":11,"col":3},"anchor":{"row":11,"col":3}},{"cursor":{"row":11,"col":9},"anchor":{"row":11,"col":9}},{"cursor":{"row":11,"col":11},"anchor":{"row":11,"col":10}},{"cursor":{"row":11,"col":13},"anchor":{"row":11,"col":13}},{"cursor":{"row":11,"col":19},"anchor":{"row":11,"col":19}},{"cursor":{"row":11,"col":26},"anchor":{"row":11,"col":26}},{"cursor":{"row":11,"col":33},"anchor":{"row":11,"col":33}},{"cursor":{"row":11,"col":36},"anchor":{"row":11,"col":34}},{"cursor":{"row":11,"col":37},"anchor":{"row":11,"col":37}}],"primary":0} +{"name":"golf-enumerate-and-align","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":11}},{"cursor":{"row":1,"col":13},"anchor":{"row":1,"col":13}},{"cursor":{"row":1,"col":19},"anchor":{"row":1,"col":19}},{"cursor":{"row":1,"col":27},"anchor":{"row":1,"col":27}},{"cursor":{"row":1,"col":34},"anchor":{"row":1,"col":34}},{"cursor":{"row":1,"col":38},"anchor":{"row":1,"col":35}},{"cursor":{"row":1,"col":39},"anchor":{"row":1,"col":39}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":9},"anchor":{"row":2,"col":9}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":13},"anchor":{"row":2,"col":13}},{"cursor":{"row":2,"col":19},"anchor":{"row":2,"col":19}},{"cursor":{"row":2,"col":27},"anchor":{"row":2,"col":27}},{"cursor":{"row":2,"col":34},"anchor":{"row":2,"col":34}},{"cursor":{"row":2,"col":38},"anchor":{"row":2,"col":35}},{"cursor":{"row":2,"col":39},"anchor":{"row":2,"col":39}},{"cursor":{"row":3,"col":0},"anchor":{"row":3,"col":0}},{"cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}},{"cursor":{"row":3,"col":3},"anchor":{"row":3,"col":3}},{"cursor":{"row":3,"col":9},"anchor":{"row":3,"col":9}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":11}},{"cursor":{"row":3,"col":13},"anchor":{"row":3,"col":13}},{"cursor":{"row":3,"col":19},"anchor":{"row":3,"col":19}},{"cursor":{"row":3,"col":27},"anchor":{"row":3,"col":27}},{"cursor":{"row":3,"col":34},"anchor":{"row":3,"col":34}},{"cursor":{"row":3,"col":38},"anchor":{"row":3,"col":35}},{"cursor":{"row":3,"col":39},"anchor":{"row":3,"col":39}},{"cursor":{"row":4,"col":0},"anchor":{"row":4,"col":0}},{"cursor":{"row":4,"col":1},"anchor":{"row":4,"col":1}},{"cursor":{"row":4,"col":3},"anchor":{"row":4,"col":3}},{"cursor":{"row":4,"col":9},"anchor":{"row":4,"col":9}},{"cursor":{"row":4,"col":11},"anchor":{"row":4,"col":11}},{"cursor":{"row":4,"col":13},"anchor":{"row":4,"col":13}},{"cursor":{"row":4,"col":19},"anchor":{"row":4,"col":19}},{"cursor":{"row":4,"col":27},"anchor":{"row":4,"col":27}},{"cursor":{"row":4,"col":34},"anchor":{"row":4,"col":34}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":35}},{"cursor":{"row":4,"col":39},"anchor":{"row":4,"col":39}},{"cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},{"cursor":{"row":5,"col":1},"anchor":{"row":5,"col":1}},{"cursor":{"row":5,"col":3},"anchor":{"row":5,"col":3}},{"cursor":{"row":5,"col":9},"anchor":{"row":5,"col":9}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":11}},{"cursor":{"row":5,"col":13},"anchor":{"row":5,"col":13}},{"cursor":{"row":5,"col":19},"anchor":{"row":5,"col":19}},{"cursor":{"row":5,"col":27},"anchor":{"row":5,"col":27}},{"cursor":{"row":5,"col":34},"anchor":{"row":5,"col":34}},{"cursor":{"row":5,"col":38},"anchor":{"row":5,"col":35}},{"cursor":{"row":5,"col":39},"anchor":{"row":5,"col":39}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":6,"col":1},"anchor":{"row":6,"col":1}},{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":3}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":11}},{"cursor":{"row":6,"col":13},"anchor":{"row":6,"col":13}},{"cursor":{"row":6,"col":19},"anchor":{"row":6,"col":19}},{"cursor":{"row":6,"col":27},"anchor":{"row":6,"col":27}},{"cursor":{"row":6,"col":34},"anchor":{"row":6,"col":34}},{"cursor":{"row":6,"col":38},"anchor":{"row":6,"col":35}},{"cursor":{"row":6,"col":39},"anchor":{"row":6,"col":39}},{"cursor":{"row":7,"col":0},"anchor":{"row":7,"col":0}},{"cursor":{"row":7,"col":1},"anchor":{"row":7,"col":1}},{"cursor":{"row":7,"col":3},"anchor":{"row":7,"col":3}},{"cursor":{"row":7,"col":9},"anchor":{"row":7,"col":9}},{"cursor":{"row":7,"col":11},"anchor":{"row":7,"col":11}},{"cursor":{"row":7,"col":13},"anchor":{"row":7,"col":13}},{"cursor":{"row":7,"col":19},"anchor":{"row":7,"col":19}},{"cursor":{"row":7,"col":27},"anchor":{"row":7,"col":27}},{"cursor":{"row":7,"col":34},"anchor":{"row":7,"col":34}},{"cursor":{"row":7,"col":38},"anchor":{"row":7,"col":36}},{"cursor":{"row":7,"col":39},"anchor":{"row":7,"col":39}},{"cursor":{"row":8,"col":0},"anchor":{"row":8,"col":0}},{"cursor":{"row":8,"col":1},"anchor":{"row":8,"col":1}},{"cursor":{"row":8,"col":3},"anchor":{"row":8,"col":3}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":8,"col":11},"anchor":{"row":8,"col":11}},{"cursor":{"row":8,"col":13},"anchor":{"row":8,"col":13}},{"cursor":{"row":8,"col":19},"anchor":{"row":8,"col":19}},{"cursor":{"row":8,"col":27},"anchor":{"row":8,"col":27}},{"cursor":{"row":8,"col":34},"anchor":{"row":8,"col":34}},{"cursor":{"row":8,"col":38},"anchor":{"row":8,"col":36}},{"cursor":{"row":8,"col":39},"anchor":{"row":8,"col":39}},{"cursor":{"row":9,"col":0},"anchor":{"row":9,"col":0}},{"cursor":{"row":9,"col":1},"anchor":{"row":9,"col":1}},{"cursor":{"row":9,"col":3},"anchor":{"row":9,"col":3}},{"cursor":{"row":9,"col":9},"anchor":{"row":9,"col":9}},{"cursor":{"row":9,"col":11},"anchor":{"row":9,"col":11}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":9,"col":19},"anchor":{"row":9,"col":19}},{"cursor":{"row":9,"col":27},"anchor":{"row":9,"col":27}},{"cursor":{"row":9,"col":34},"anchor":{"row":9,"col":34}},{"cursor":{"row":9,"col":38},"anchor":{"row":9,"col":36}},{"cursor":{"row":9,"col":39},"anchor":{"row":9,"col":39}},{"cursor":{"row":10,"col":0},"anchor":{"row":10,"col":0}},{"cursor":{"row":10,"col":1},"anchor":{"row":10,"col":1}},{"cursor":{"row":10,"col":3},"anchor":{"row":10,"col":3}},{"cursor":{"row":10,"col":9},"anchor":{"row":10,"col":9}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":10}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}},{"cursor":{"row":10,"col":19},"anchor":{"row":10,"col":19}},{"cursor":{"row":10,"col":27},"anchor":{"row":10,"col":27}},{"cursor":{"row":10,"col":34},"anchor":{"row":10,"col":34}},{"cursor":{"row":10,"col":38},"anchor":{"row":10,"col":36}},{"cursor":{"row":10,"col":39},"anchor":{"row":10,"col":39}},{"cursor":{"row":11,"col":0},"anchor":{"row":11,"col":0}},{"cursor":{"row":11,"col":1},"anchor":{"row":11,"col":1}},{"cursor":{"row":11,"col":3},"anchor":{"row":11,"col":3}},{"cursor":{"row":11,"col":9},"anchor":{"row":11,"col":9}},{"cursor":{"row":11,"col":11},"anchor":{"row":11,"col":10}},{"cursor":{"row":11,"col":13},"anchor":{"row":11,"col":13}},{"cursor":{"row":11,"col":19},"anchor":{"row":11,"col":19}},{"cursor":{"row":11,"col":27},"anchor":{"row":11,"col":27}},{"cursor":{"row":11,"col":34},"anchor":{"row":11,"col":34}},{"cursor":{"row":11,"col":38},"anchor":{"row":11,"col":36}},{"cursor":{"row":11,"col":39},"anchor":{"row":11,"col":39}}],"primary":0} +{"name":"golf-enumerate-and-align-from09-09","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":12,"col":2},"anchor":{"row":0,"col":0}} +{"name":"golf-enumerate-and-align-from09-10","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":10}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":12}},{"cursor":{"row":1,"col":18},"anchor":{"row":1,"col":18}},{"cursor":{"row":1,"col":23},"anchor":{"row":1,"col":23}},{"cursor":{"row":1,"col":30},"anchor":{"row":1,"col":30}},{"cursor":{"row":1,"col":34},"anchor":{"row":1,"col":31}},{"cursor":{"row":1,"col":35},"anchor":{"row":1,"col":35}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":9},"anchor":{"row":2,"col":9}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":10}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":12}},{"cursor":{"row":2,"col":18},"anchor":{"row":2,"col":18}},{"cursor":{"row":2,"col":25},"anchor":{"row":2,"col":25}},{"cursor":{"row":2,"col":32},"anchor":{"row":2,"col":32}},{"cursor":{"row":2,"col":36},"anchor":{"row":2,"col":33}},{"cursor":{"row":2,"col":37},"anchor":{"row":2,"col":37}},{"cursor":{"row":3,"col":0},"anchor":{"row":3,"col":0}},{"cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}},{"cursor":{"row":3,"col":3},"anchor":{"row":3,"col":3}},{"cursor":{"row":3,"col":9},"anchor":{"row":3,"col":9}},{"cursor":{"row":3,"col":10},"anchor":{"row":3,"col":10}},{"cursor":{"row":3,"col":12},"anchor":{"row":3,"col":12}},{"cursor":{"row":3,"col":18},"anchor":{"row":3,"col":18}},{"cursor":{"row":3,"col":24},"anchor":{"row":3,"col":24}},{"cursor":{"row":3,"col":31},"anchor":{"row":3,"col":31}},{"cursor":{"row":3,"col":35},"anchor":{"row":3,"col":32}},{"cursor":{"row":3,"col":36},"anchor":{"row":3,"col":36}},{"cursor":{"row":4,"col":0},"anchor":{"row":4,"col":0}},{"cursor":{"row":4,"col":1},"anchor":{"row":4,"col":1}},{"cursor":{"row":4,"col":3},"anchor":{"row":4,"col":3}},{"cursor":{"row":4,"col":9},"anchor":{"row":4,"col":9}},{"cursor":{"row":4,"col":10},"anchor":{"row":4,"col":10}},{"cursor":{"row":4,"col":12},"anchor":{"row":4,"col":12}},{"cursor":{"row":4,"col":18},"anchor":{"row":4,"col":18}},{"cursor":{"row":4,"col":26},"anchor":{"row":4,"col":26}},{"cursor":{"row":4,"col":33},"anchor":{"row":4,"col":33}},{"cursor":{"row":4,"col":37},"anchor":{"row":4,"col":34}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":38}},{"cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},{"cursor":{"row":5,"col":1},"anchor":{"row":5,"col":1}},{"cursor":{"row":5,"col":3},"anchor":{"row":5,"col":3}},{"cursor":{"row":5,"col":9},"anchor":{"row":5,"col":9}},{"cursor":{"row":5,"col":10},"anchor":{"row":5,"col":10}},{"cursor":{"row":5,"col":12},"anchor":{"row":5,"col":12}},{"cursor":{"row":5,"col":18},"anchor":{"row":5,"col":18}},{"cursor":{"row":5,"col":24},"anchor":{"row":5,"col":24}},{"cursor":{"row":5,"col":31},"anchor":{"row":5,"col":31}},{"cursor":{"row":5,"col":35},"anchor":{"row":5,"col":32}},{"cursor":{"row":5,"col":36},"anchor":{"row":5,"col":36}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":6,"col":1},"anchor":{"row":6,"col":1}},{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":3}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":6,"col":10},"anchor":{"row":6,"col":10}},{"cursor":{"row":6,"col":12},"anchor":{"row":6,"col":12}},{"cursor":{"row":6,"col":18},"anchor":{"row":6,"col":18}},{"cursor":{"row":6,"col":24},"anchor":{"row":6,"col":24}},{"cursor":{"row":6,"col":31},"anchor":{"row":6,"col":31}},{"cursor":{"row":6,"col":35},"anchor":{"row":6,"col":32}},{"cursor":{"row":6,"col":36},"anchor":{"row":6,"col":36}},{"cursor":{"row":7,"col":0},"anchor":{"row":7,"col":0}},{"cursor":{"row":7,"col":1},"anchor":{"row":7,"col":1}},{"cursor":{"row":7,"col":3},"anchor":{"row":7,"col":3}},{"cursor":{"row":7,"col":9},"anchor":{"row":7,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":7,"col":12},"anchor":{"row":7,"col":12}},{"cursor":{"row":7,"col":18},"anchor":{"row":7,"col":18}},{"cursor":{"row":7,"col":24},"anchor":{"row":7,"col":24}},{"cursor":{"row":7,"col":31},"anchor":{"row":7,"col":31}},{"cursor":{"row":7,"col":34},"anchor":{"row":7,"col":32}},{"cursor":{"row":7,"col":35},"anchor":{"row":7,"col":35}},{"cursor":{"row":8,"col":0},"anchor":{"row":8,"col":0}},{"cursor":{"row":8,"col":1},"anchor":{"row":8,"col":1}},{"cursor":{"row":8,"col":3},"anchor":{"row":8,"col":3}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":8,"col":10},"anchor":{"row":8,"col":10}},{"cursor":{"row":8,"col":12},"anchor":{"row":8,"col":12}},{"cursor":{"row":8,"col":18},"anchor":{"row":8,"col":18}},{"cursor":{"row":8,"col":24},"anchor":{"row":8,"col":24}},{"cursor":{"row":8,"col":31},"anchor":{"row":8,"col":31}},{"cursor":{"row":8,"col":34},"anchor":{"row":8,"col":32}},{"cursor":{"row":8,"col":35},"anchor":{"row":8,"col":35}},{"cursor":{"row":9,"col":0},"anchor":{"row":9,"col":0}},{"cursor":{"row":9,"col":1},"anchor":{"row":9,"col":1}},{"cursor":{"row":9,"col":3},"anchor":{"row":9,"col":3}},{"cursor":{"row":9,"col":9},"anchor":{"row":9,"col":9}},{"cursor":{"row":9,"col":10},"anchor":{"row":9,"col":10}},{"cursor":{"row":9,"col":12},"anchor":{"row":9,"col":12}},{"cursor":{"row":9,"col":18},"anchor":{"row":9,"col":18}},{"cursor":{"row":9,"col":25},"anchor":{"row":9,"col":25}},{"cursor":{"row":9,"col":32},"anchor":{"row":9,"col":32}},{"cursor":{"row":9,"col":35},"anchor":{"row":9,"col":33}},{"cursor":{"row":9,"col":36},"anchor":{"row":9,"col":36}},{"cursor":{"row":10,"col":0},"anchor":{"row":10,"col":0}},{"cursor":{"row":10,"col":1},"anchor":{"row":10,"col":1}},{"cursor":{"row":10,"col":3},"anchor":{"row":10,"col":3}},{"cursor":{"row":10,"col":9},"anchor":{"row":10,"col":9}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":10}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}},{"cursor":{"row":10,"col":19},"anchor":{"row":10,"col":19}},{"cursor":{"row":10,"col":26},"anchor":{"row":10,"col":26}},{"cursor":{"row":10,"col":33},"anchor":{"row":10,"col":33}},{"cursor":{"row":10,"col":36},"anchor":{"row":10,"col":34}},{"cursor":{"row":10,"col":37},"anchor":{"row":10,"col":37}},{"cursor":{"row":11,"col":0},"anchor":{"row":11,"col":0}},{"cursor":{"row":11,"col":1},"anchor":{"row":11,"col":1}},{"cursor":{"row":11,"col":3},"anchor":{"row":11,"col":3}},{"cursor":{"row":11,"col":9},"anchor":{"row":11,"col":9}},{"cursor":{"row":11,"col":11},"anchor":{"row":11,"col":10}},{"cursor":{"row":11,"col":13},"anchor":{"row":11,"col":13}},{"cursor":{"row":11,"col":19},"anchor":{"row":11,"col":19}},{"cursor":{"row":11,"col":26},"anchor":{"row":11,"col":26}},{"cursor":{"row":11,"col":33},"anchor":{"row":11,"col":33}},{"cursor":{"row":11,"col":36},"anchor":{"row":11,"col":34}},{"cursor":{"row":11,"col":37},"anchor":{"row":11,"col":37}}],"primary":0} +{"name":"golf-enumerate-and-align-from09","text":"[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":11}},{"cursor":{"row":1,"col":13},"anchor":{"row":1,"col":13}},{"cursor":{"row":1,"col":19},"anchor":{"row":1,"col":19}},{"cursor":{"row":1,"col":27},"anchor":{"row":1,"col":27}},{"cursor":{"row":1,"col":34},"anchor":{"row":1,"col":34}},{"cursor":{"row":1,"col":38},"anchor":{"row":1,"col":35}},{"cursor":{"row":1,"col":39},"anchor":{"row":1,"col":39}},{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":1}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":3}},{"cursor":{"row":2,"col":9},"anchor":{"row":2,"col":9}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":13},"anchor":{"row":2,"col":13}},{"cursor":{"row":2,"col":19},"anchor":{"row":2,"col":19}},{"cursor":{"row":2,"col":27},"anchor":{"row":2,"col":27}},{"cursor":{"row":2,"col":34},"anchor":{"row":2,"col":34}},{"cursor":{"row":2,"col":38},"anchor":{"row":2,"col":35}},{"cursor":{"row":2,"col":39},"anchor":{"row":2,"col":39}},{"cursor":{"row":3,"col":0},"anchor":{"row":3,"col":0}},{"cursor":{"row":3,"col":1},"anchor":{"row":3,"col":1}},{"cursor":{"row":3,"col":3},"anchor":{"row":3,"col":3}},{"cursor":{"row":3,"col":9},"anchor":{"row":3,"col":9}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":11}},{"cursor":{"row":3,"col":13},"anchor":{"row":3,"col":13}},{"cursor":{"row":3,"col":19},"anchor":{"row":3,"col":19}},{"cursor":{"row":3,"col":27},"anchor":{"row":3,"col":27}},{"cursor":{"row":3,"col":34},"anchor":{"row":3,"col":34}},{"cursor":{"row":3,"col":38},"anchor":{"row":3,"col":35}},{"cursor":{"row":3,"col":39},"anchor":{"row":3,"col":39}},{"cursor":{"row":4,"col":0},"anchor":{"row":4,"col":0}},{"cursor":{"row":4,"col":1},"anchor":{"row":4,"col":1}},{"cursor":{"row":4,"col":3},"anchor":{"row":4,"col":3}},{"cursor":{"row":4,"col":9},"anchor":{"row":4,"col":9}},{"cursor":{"row":4,"col":11},"anchor":{"row":4,"col":11}},{"cursor":{"row":4,"col":13},"anchor":{"row":4,"col":13}},{"cursor":{"row":4,"col":19},"anchor":{"row":4,"col":19}},{"cursor":{"row":4,"col":27},"anchor":{"row":4,"col":27}},{"cursor":{"row":4,"col":34},"anchor":{"row":4,"col":34}},{"cursor":{"row":4,"col":38},"anchor":{"row":4,"col":35}},{"cursor":{"row":4,"col":39},"anchor":{"row":4,"col":39}},{"cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},{"cursor":{"row":5,"col":1},"anchor":{"row":5,"col":1}},{"cursor":{"row":5,"col":3},"anchor":{"row":5,"col":3}},{"cursor":{"row":5,"col":9},"anchor":{"row":5,"col":9}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":11}},{"cursor":{"row":5,"col":13},"anchor":{"row":5,"col":13}},{"cursor":{"row":5,"col":19},"anchor":{"row":5,"col":19}},{"cursor":{"row":5,"col":27},"anchor":{"row":5,"col":27}},{"cursor":{"row":5,"col":34},"anchor":{"row":5,"col":34}},{"cursor":{"row":5,"col":38},"anchor":{"row":5,"col":35}},{"cursor":{"row":5,"col":39},"anchor":{"row":5,"col":39}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":6,"col":1},"anchor":{"row":6,"col":1}},{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":3}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":11}},{"cursor":{"row":6,"col":13},"anchor":{"row":6,"col":13}},{"cursor":{"row":6,"col":19},"anchor":{"row":6,"col":19}},{"cursor":{"row":6,"col":27},"anchor":{"row":6,"col":27}},{"cursor":{"row":6,"col":34},"anchor":{"row":6,"col":34}},{"cursor":{"row":6,"col":38},"anchor":{"row":6,"col":35}},{"cursor":{"row":6,"col":39},"anchor":{"row":6,"col":39}},{"cursor":{"row":7,"col":0},"anchor":{"row":7,"col":0}},{"cursor":{"row":7,"col":1},"anchor":{"row":7,"col":1}},{"cursor":{"row":7,"col":3},"anchor":{"row":7,"col":3}},{"cursor":{"row":7,"col":9},"anchor":{"row":7,"col":9}},{"cursor":{"row":7,"col":11},"anchor":{"row":7,"col":11}},{"cursor":{"row":7,"col":13},"anchor":{"row":7,"col":13}},{"cursor":{"row":7,"col":19},"anchor":{"row":7,"col":19}},{"cursor":{"row":7,"col":27},"anchor":{"row":7,"col":27}},{"cursor":{"row":7,"col":34},"anchor":{"row":7,"col":34}},{"cursor":{"row":7,"col":38},"anchor":{"row":7,"col":36}},{"cursor":{"row":7,"col":39},"anchor":{"row":7,"col":39}},{"cursor":{"row":8,"col":0},"anchor":{"row":8,"col":0}},{"cursor":{"row":8,"col":1},"anchor":{"row":8,"col":1}},{"cursor":{"row":8,"col":3},"anchor":{"row":8,"col":3}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":8,"col":11},"anchor":{"row":8,"col":11}},{"cursor":{"row":8,"col":13},"anchor":{"row":8,"col":13}},{"cursor":{"row":8,"col":19},"anchor":{"row":8,"col":19}},{"cursor":{"row":8,"col":27},"anchor":{"row":8,"col":27}},{"cursor":{"row":8,"col":34},"anchor":{"row":8,"col":34}},{"cursor":{"row":8,"col":38},"anchor":{"row":8,"col":36}},{"cursor":{"row":8,"col":39},"anchor":{"row":8,"col":39}},{"cursor":{"row":9,"col":0},"anchor":{"row":9,"col":0}},{"cursor":{"row":9,"col":1},"anchor":{"row":9,"col":1}},{"cursor":{"row":9,"col":3},"anchor":{"row":9,"col":3}},{"cursor":{"row":9,"col":9},"anchor":{"row":9,"col":9}},{"cursor":{"row":9,"col":11},"anchor":{"row":9,"col":11}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":9,"col":19},"anchor":{"row":9,"col":19}},{"cursor":{"row":9,"col":27},"anchor":{"row":9,"col":27}},{"cursor":{"row":9,"col":34},"anchor":{"row":9,"col":34}},{"cursor":{"row":9,"col":38},"anchor":{"row":9,"col":36}},{"cursor":{"row":9,"col":39},"anchor":{"row":9,"col":39}},{"cursor":{"row":10,"col":0},"anchor":{"row":10,"col":0}},{"cursor":{"row":10,"col":1},"anchor":{"row":10,"col":1}},{"cursor":{"row":10,"col":3},"anchor":{"row":10,"col":3}},{"cursor":{"row":10,"col":9},"anchor":{"row":10,"col":9}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":10}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}},{"cursor":{"row":10,"col":19},"anchor":{"row":10,"col":19}},{"cursor":{"row":10,"col":27},"anchor":{"row":10,"col":27}},{"cursor":{"row":10,"col":34},"anchor":{"row":10,"col":34}},{"cursor":{"row":10,"col":38},"anchor":{"row":10,"col":36}},{"cursor":{"row":10,"col":39},"anchor":{"row":10,"col":39}},{"cursor":{"row":11,"col":0},"anchor":{"row":11,"col":0}},{"cursor":{"row":11,"col":1},"anchor":{"row":11,"col":1}},{"cursor":{"row":11,"col":3},"anchor":{"row":11,"col":3}},{"cursor":{"row":11,"col":9},"anchor":{"row":11,"col":9}},{"cursor":{"row":11,"col":11},"anchor":{"row":11,"col":10}},{"cursor":{"row":11,"col":13},"anchor":{"row":11,"col":13}},{"cursor":{"row":11,"col":19},"anchor":{"row":11,"col":19}},{"cursor":{"row":11,"col":27},"anchor":{"row":11,"col":27}},{"cursor":{"row":11,"col":34},"anchor":{"row":11,"col":34}},{"cursor":{"row":11,"col":38},"anchor":{"row":11,"col":36}},{"cursor":{"row":11,"col":39},"anchor":{"row":11,"col":39}}],"primary":0} +{"name":"golf-export-from-mod-01","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n","mode":"normal","cursor":{"row":2,"col":13},"anchor":{"row":0,"col":0}} +{"name":"golf-export-from-mod-02","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\nmod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n","mode":"normal","cursor":{"row":5,"col":13},"anchor":{"row":3,"col":0}} +{"name":"golf-export-from-mod-03","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\nmod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n","mode":"normal","cursor":{"row":6,"col":13},"anchor":{"row":4,"col":0}} +{"name":"golf-export-from-mod-04","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\nmod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n","mode":"normal","cursor":{"row":4,"col":2},"anchor":{"row":4,"col":0},"sels":[{"cursor":{"row":4,"col":2},"anchor":{"row":4,"col":0}},{"cursor":{"row":5,"col":2},"anchor":{"row":5,"col":0}},{"cursor":{"row":6,"col":2},"anchor":{"row":6,"col":0}}],"primary":0} +{"name":"golf-export-from-mod-05","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\npub use generate_demos;\npub use mdbook_preprocessor;\npub use validate;\n","mode":"normal","cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7},"sels":[{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":7},"anchor":{"row":6,"col":7}}],"primary":0} +{"name":"golf-export-from-mod-06","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\npub use generate_demos;\npub use mdbook_preprocessor;\npub use validate;\n","mode":"normal","cursor":{"row":4,"col":21},"anchor":{"row":4,"col":8},"sels":[{"cursor":{"row":4,"col":21},"anchor":{"row":4,"col":8}},{"cursor":{"row":5,"col":26},"anchor":{"row":5,"col":8}},{"cursor":{"row":6,"col":15},"anchor":{"row":6,"col":8}}],"primary":0} +{"name":"golf-export-from-mod-07","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\npub use generate_demosgenerate_demos;\npub use mdbook_preprocessormdbook_preprocessor;\npub use validatevalidate;\n","mode":"normal","cursor":{"row":4,"col":35},"anchor":{"row":4,"col":22},"sels":[{"cursor":{"row":4,"col":35},"anchor":{"row":4,"col":22}},{"cursor":{"row":5,"col":45},"anchor":{"row":5,"col":27}},{"cursor":{"row":6,"col":23},"anchor":{"row":6,"col":16}}],"primary":0} +{"name":"golf-export-from-mod","text":"mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n\npub use generate_demos::generate_demos;\npub use mdbook_preprocessor::mdbook_preprocessor;\npub use validate::validate;\n","mode":"insert","cursor":{"row":4,"col":24},"anchor":{"row":4,"col":37},"sels":[{"cursor":{"row":4,"col":24},"anchor":{"row":4,"col":37}},{"cursor":{"row":5,"col":29},"anchor":{"row":5,"col":47}},{"cursor":{"row":6,"col":18},"anchor":{"row":6,"col":25}}],"primary":0} +{"name":"golf-function-into-class-01","text":"def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":10,"col":17},"anchor":{"row":0,"col":0}} +{"name":"golf-function-into-class-02","text":"def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":0,"col":12},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":12},"anchor":{"row":0,"col":4}},{"cursor":{"row":4,"col":12},"anchor":{"row":4,"col":4}},{"cursor":{"row":8,"col":12},"anchor":{"row":8,"col":4}}],"primary":0} +{"name":"golf-function-into-class-03","text":"def get_area(length, width):\n result = length * width\n return result\n\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"insert","cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7},"sels":[{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":8,"col":7},"anchor":{"row":8,"col":7}}],"primary":0} +{"name":"golf-function-into-class-04","text":"def get_area(length, width):\n result = length * width\n return result\n\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7},"sels":[{"cursor":{"row":0,"col":7},"anchor":{"row":0,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":8,"col":7},"anchor":{"row":8,"col":7}}],"primary":0} +{"name":"golf-function-into-class-05","text":"@staticmethod\ndef get_area(length, width):\n result = length * width\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"insert","cursor":{"row":0,"col":13},"anchor":{"row":0,"col":13},"sels":[{"cursor":{"row":0,"col":13},"anchor":{"row":0,"col":13}},{"cursor":{"row":5,"col":13},"anchor":{"row":5,"col":13}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}}],"primary":0} +{"name":"golf-function-into-class-06","text":"@staticmethod\ndef get_area(length, width):\n result = length * width\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":0,"col":13},"anchor":{"row":0,"col":13},"sels":[{"cursor":{"row":0,"col":13},"anchor":{"row":0,"col":13}},{"cursor":{"row":5,"col":13},"anchor":{"row":5,"col":13}},{"cursor":{"row":10,"col":13},"anchor":{"row":10,"col":13}}],"primary":0} +{"name":"golf-function-into-class-07","text":"@staticmethod\ndef get_area(length, width):\n result = length * width\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":2,"col":13},"anchor":{"row":2,"col":13},"sels":[{"cursor":{"row":2,"col":13},"anchor":{"row":2,"col":13}},{"cursor":{"row":7,"col":13},"anchor":{"row":7,"col":13}},{"cursor":{"row":12,"col":13},"anchor":{"row":12,"col":13}}],"primary":0} +{"name":"golf-function-into-class-08","text":"@staticmethod\ndef get_area(length, width):\n result = length * width\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"select","cursor":{"row":2,"col":26},"anchor":{"row":2,"col":13},"sels":[{"cursor":{"row":2,"col":26},"anchor":{"row":2,"col":13}},{"cursor":{"row":7,"col":32},"anchor":{"row":7,"col":13}},{"cursor":{"row":12,"col":35},"anchor":{"row":12,"col":13}}],"primary":0} +{"name":"golf-function-into-class-09","text":"@staticmethod\ndef get_area(length, width):\n result = length * width\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n result = length * width * height\n return result\n","mode":"normal","cursor":{"row":2,"col":26},"anchor":{"row":2,"col":13},"sels":[{"cursor":{"row":2,"col":26},"anchor":{"row":2,"col":13}},{"cursor":{"row":7,"col":32},"anchor":{"row":7,"col":13}},{"cursor":{"row":12,"col":35},"anchor":{"row":12,"col":13}}],"primary":0} +{"name":"golf-function-into-class-10","text":"@staticmethod\ndef get_area(length, width):\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n return result\n","mode":"normal","cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0},"sels":[{"cursor":{"row":2,"col":0},"anchor":{"row":2,"col":0}},{"cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},{"cursor":{"row":10,"col":0},"anchor":{"row":10,"col":0}}],"primary":0} +{"name":"golf-function-into-class-11","text":"@staticmethod\ndef get_area(length, width):\n return result\n\n@staticmethod\ndef get_perimiter(length, width):\n return result\n\n@staticmethod\ndef get_volume(length, width, height):\n return result\n","mode":"normal","cursor":{"row":2,"col":11},"anchor":{"row":2,"col":16},"sels":[{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":16}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":16}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":16}}],"primary":0} +{"name":"golf-function-into-class-12","text":"@staticmethod\ndef get_area(length, width):\n return length * width\n\n@staticmethod\ndef get_perimiter(length, width):\n return 2 * (length + width)\n\n@staticmethod\ndef get_volume(length, width, height):\n return length * width * height\n","mode":"normal","cursor":{"row":2,"col":11},"anchor":{"row":2,"col":24},"sels":[{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":24}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":30}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":33}}],"primary":0} +{"name":"golf-function-into-class-13","text":"@staticmethod\ndef get_area(length, width):\n return length * width\n\n@staticmethod\ndef get_perimiter(length, width):\n return 2 * (length + width)\n\n@staticmethod\ndef get_volume(length, width, height):\n return length * width * height\n","mode":"normal","cursor":{"row":2,"col":25},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":2,"col":25},"anchor":{"row":1,"col":0}},{"cursor":{"row":6,"col":31},"anchor":{"row":5,"col":0}},{"cursor":{"row":10,"col":34},"anchor":{"row":9,"col":0}}],"primary":0} +{"name":"golf-function-into-class-14","text":"@staticmethod\ndef get_area(length, width):\n return length * width\n\n@staticmethod\ndef get_perimiter(length, width):\n return 2 * (length + width)\n\n@staticmethod\ndef get_volume(length, width, height):\n return length * width * height\n","mode":"normal","cursor":{"row":1,"col":18},"anchor":{"row":1,"col":13},"sels":[{"cursor":{"row":1,"col":18},"anchor":{"row":1,"col":13}},{"cursor":{"row":1,"col":25},"anchor":{"row":1,"col":21}},{"cursor":{"row":2,"col":16},"anchor":{"row":2,"col":11}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":20}},{"cursor":{"row":5,"col":23},"anchor":{"row":5,"col":18}},{"cursor":{"row":5,"col":30},"anchor":{"row":5,"col":26}},{"cursor":{"row":6,"col":21},"anchor":{"row":6,"col":16}},{"cursor":{"row":6,"col":29},"anchor":{"row":6,"col":25}},{"cursor":{"row":9,"col":20},"anchor":{"row":9,"col":15}},{"cursor":{"row":9,"col":27},"anchor":{"row":9,"col":23}},{"cursor":{"row":9,"col":35},"anchor":{"row":9,"col":30}},{"cursor":{"row":10,"col":16},"anchor":{"row":10,"col":11}},{"cursor":{"row":10,"col":24},"anchor":{"row":10,"col":20}},{"cursor":{"row":10,"col":33},"anchor":{"row":10,"col":28}}],"primary":0} +{"name":"golf-function-into-class-15","text":"@staticmethod\ndef get_area(length, width):\n return length * width\n\n@staticmethod\ndef get_perimiter(length, width):\n return 2 * (length + width)\n\n@staticmethod\ndef get_volume(length, width, height):\n return length * width * height\n","mode":"normal","cursor":{"row":1,"col":13},"anchor":{"row":1,"col":18},"sels":[{"cursor":{"row":1,"col":13},"anchor":{"row":1,"col":18}},{"cursor":{"row":1,"col":21},"anchor":{"row":1,"col":25}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":16}},{"cursor":{"row":2,"col":20},"anchor":{"row":2,"col":24}},{"cursor":{"row":5,"col":18},"anchor":{"row":5,"col":23}},{"cursor":{"row":5,"col":26},"anchor":{"row":5,"col":30}},{"cursor":{"row":6,"col":16},"anchor":{"row":6,"col":21}},{"cursor":{"row":6,"col":25},"anchor":{"row":6,"col":29}},{"cursor":{"row":9,"col":15},"anchor":{"row":9,"col":20}},{"cursor":{"row":9,"col":23},"anchor":{"row":9,"col":27}},{"cursor":{"row":9,"col":30},"anchor":{"row":9,"col":35}},{"cursor":{"row":10,"col":11},"anchor":{"row":10,"col":16}},{"cursor":{"row":10,"col":20},"anchor":{"row":10,"col":24}},{"cursor":{"row":10,"col":28},"anchor":{"row":10,"col":33}}],"primary":0} +{"name":"golf-function-into-class-16","text":"@staticmethod\ndef get_area(length, width):\n return length * width\n\n@staticmethod\ndef get_perimiter(length, width):\n return 2 * (length + width)\n\n@staticmethod\ndef get_volume(length, width, height):\n return length * width * height\n","mode":"normal","cursor":{"row":1,"col":16},"anchor":{"row":1,"col":16},"sels":[{"cursor":{"row":1,"col":16},"anchor":{"row":1,"col":16}},{"cursor":{"row":1,"col":24},"anchor":{"row":1,"col":24}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":14}},{"cursor":{"row":2,"col":23},"anchor":{"row":2,"col":23}},{"cursor":{"row":5,"col":21},"anchor":{"row":5,"col":21}},{"cursor":{"row":5,"col":29},"anchor":{"row":5,"col":29}},{"cursor":{"row":6,"col":19},"anchor":{"row":6,"col":19}},{"cursor":{"row":6,"col":28},"anchor":{"row":6,"col":28}},{"cursor":{"row":9,"col":18},"anchor":{"row":9,"col":18}},{"cursor":{"row":9,"col":26},"anchor":{"row":9,"col":26}},{"cursor":{"row":9,"col":33},"anchor":{"row":9,"col":33}},{"cursor":{"row":10,"col":14},"anchor":{"row":10,"col":14}},{"cursor":{"row":10,"col":23},"anchor":{"row":10,"col":23}},{"cursor":{"row":10,"col":31},"anchor":{"row":10,"col":31}}],"primary":0} +{"name":"golf-function-into-class-17","text":"@staticmethod\ndef get_area(len, wid):\n return len * wid\n\n@staticmethod\ndef get_perimiter(len, wid):\n return 2 * (len + wid)\n\n@staticmethod\ndef get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"normal","cursor":{"row":1,"col":16},"anchor":{"row":1,"col":16},"sels":[{"cursor":{"row":1,"col":16},"anchor":{"row":1,"col":16}},{"cursor":{"row":1,"col":21},"anchor":{"row":1,"col":21}},{"cursor":{"row":2,"col":14},"anchor":{"row":2,"col":14}},{"cursor":{"row":2,"col":20},"anchor":{"row":2,"col":20}},{"cursor":{"row":5,"col":21},"anchor":{"row":5,"col":21}},{"cursor":{"row":5,"col":26},"anchor":{"row":5,"col":26}},{"cursor":{"row":6,"col":19},"anchor":{"row":6,"col":19}},{"cursor":{"row":6,"col":25},"anchor":{"row":6,"col":25}},{"cursor":{"row":9,"col":18},"anchor":{"row":9,"col":18}},{"cursor":{"row":9,"col":23},"anchor":{"row":9,"col":23}},{"cursor":{"row":9,"col":28},"anchor":{"row":9,"col":28}},{"cursor":{"row":10,"col":14},"anchor":{"row":10,"col":14}},{"cursor":{"row":10,"col":20},"anchor":{"row":10,"col":20}},{"cursor":{"row":10,"col":26},"anchor":{"row":10,"col":26}}],"primary":0} +{"name":"golf-function-into-class-18","text":" @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"normal","cursor":{"row":10,"col":30},"anchor":{"row":0,"col":4}} +{"name":"golf-function-into-class-19","text":"\n @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"golf-function-into-class","text":"class Calculator:\n @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"insert","cursor":{"row":0,"col":17},"anchor":{"row":0,"col":17}} +{"name":"golf-function-into-class-from18-18","text":" @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"normal","cursor":{"row":10,"col":30},"anchor":{"row":0,"col":4}} +{"name":"golf-function-into-class-from18-19","text":"\n @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} +{"name":"golf-function-into-class-from18","text":"class Calculator:\n @staticmethod\n def get_area(len, wid):\n return len * wid\n\n @staticmethod\n def get_perimiter(len, wid):\n return 2 * (len + wid)\n\n @staticmethod\n def get_volume(len, wid, hei):\n return len * wid * hei\n","mode":"insert","cursor":{"row":0,"col":17},"anchor":{"row":0,"col":17}} +{"name":"golf-invert-dictionary-01","text":"var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":18}} +{"name":"golf-invert-dictionary-02","text":"var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":8},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":13}}],"primary":0} +{"name":"golf-invert-dictionary-03","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14},"sels":[{"cursor":{"row":0,"col":9},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14}}],"primary":1} +{"name":"golf-invert-dictionary-04","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14},"sels":[{"cursor":{"row":0,"col":9},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14}}],"primary":1} +{"name":"golf-invert-dictionary-05","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":23},"anchor":{"row":0,"col":0}} +{"name":"golf-invert-dictionary-06","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":12},"anchor":{"row":0,"col":23}} +{"name":"golf-invert-dictionary-07","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":9},"anchor":{"row":0,"col":23},"sels":[{"cursor":{"row":1,"col":9},"anchor":{"row":0,"col":23}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":11}}],"primary":0} +{"name":"golf-invert-dictionary-08","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":8},"anchor":{"row":1,"col":4},"sels":[{"cursor":{"row":1,"col":8},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":12}}],"primary":0} +{"name":"golf-invert-dictionary-09","text":"var points_to_color = {\n 0 = \"red\",\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":12},"anchor":{"row":1,"col":8},"sels":[{"cursor":{"row":1,"col":4},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":8}}],"primary":1} +{"name":"golf-invert-dictionary-10","text":"var points_to_color = {\n 0 = \"red\",\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":12},"anchor":{"row":1,"col":8},"sels":[{"cursor":{"row":1,"col":4},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":8}}],"primary":1} +{"name":"golf-invert-dictionary","text":"var points_to_color = {\n 0 = \"red\",\n 5 = \"orange\",\n 10 = \"yellow\",\n 15 = \"green\",\n 20 = \"blue\",\n 30 = \"purple\",\n 50 = \"black\",\n}\n","mode":"normal","cursor":{"row":7,"col":15},"anchor":{"row":7,"col":9},"sels":[{"cursor":{"row":7,"col":5},"anchor":{"row":7,"col":4}},{"cursor":{"row":7,"col":15},"anchor":{"row":7,"col":9}}],"primary":1} +{"name":"golf-invert-dictionary-2-01","text":"var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":18}} +{"name":"golf-invert-dictionary-2-02","text":"var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":8},"anchor":{"row":0,"col":4},"sels":[{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":13}}],"primary":0} +{"name":"golf-invert-dictionary-2-03","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14},"sels":[{"cursor":{"row":0,"col":9},"anchor":{"row":0,"col":4}},{"cursor":{"row":0,"col":18},"anchor":{"row":0,"col":14}}],"primary":1} +{"name":"golf-invert-dictionary-2-04","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":0,"col":23},"anchor":{"row":0,"col":0}} +{"name":"golf-invert-dictionary-2-05","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":7,"col":17},"anchor":{"row":0,"col":23}} +{"name":"golf-invert-dictionary-2-06","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":9},"anchor":{"row":0,"col":23},"sels":[{"cursor":{"row":1,"col":9},"anchor":{"row":0,"col":23}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":11}},{"cursor":{"row":2,"col":12},"anchor":{"row":1,"col":14}},{"cursor":{"row":2,"col":15},"anchor":{"row":2,"col":14}},{"cursor":{"row":3,"col":12},"anchor":{"row":2,"col":17}},{"cursor":{"row":3,"col":16},"anchor":{"row":3,"col":14}},{"cursor":{"row":4,"col":11},"anchor":{"row":3,"col":18}},{"cursor":{"row":4,"col":15},"anchor":{"row":4,"col":13}},{"cursor":{"row":5,"col":10},"anchor":{"row":4,"col":17}},{"cursor":{"row":5,"col":14},"anchor":{"row":5,"col":12}},{"cursor":{"row":6,"col":12},"anchor":{"row":5,"col":16}},{"cursor":{"row":6,"col":16},"anchor":{"row":6,"col":14}},{"cursor":{"row":7,"col":11},"anchor":{"row":6,"col":18}},{"cursor":{"row":7,"col":15},"anchor":{"row":7,"col":13}},{"cursor":{"row":7,"col":17},"anchor":{"row":7,"col":17}}],"primary":0} +{"name":"golf-invert-dictionary-2-07","text":"var points_to_color = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n","mode":"normal","cursor":{"row":1,"col":8},"anchor":{"row":1,"col":4},"sels":[{"cursor":{"row":1,"col":8},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":12},"anchor":{"row":1,"col":12}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":4}},{"cursor":{"row":2,"col":15},"anchor":{"row":2,"col":15}},{"cursor":{"row":3,"col":11},"anchor":{"row":3,"col":4}},{"cursor":{"row":3,"col":16},"anchor":{"row":3,"col":15}},{"cursor":{"row":4,"col":10},"anchor":{"row":4,"col":4}},{"cursor":{"row":4,"col":15},"anchor":{"row":4,"col":14}},{"cursor":{"row":5,"col":9},"anchor":{"row":5,"col":4}},{"cursor":{"row":5,"col":14},"anchor":{"row":5,"col":13}},{"cursor":{"row":6,"col":11},"anchor":{"row":6,"col":4}},{"cursor":{"row":6,"col":16},"anchor":{"row":6,"col":15}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":4}},{"cursor":{"row":7,"col":15},"anchor":{"row":7,"col":14}}],"primary":0} +{"name":"golf-invert-dictionary-2","text":"var points_to_color = {\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n \"red\" = 0,\n}\n","mode":"normal","cursor":{"row":7,"col":8},"anchor":{"row":7,"col":4},"sels":[{"cursor":{"row":1,"col":11},"anchor":{"row":1,"col":4}},{"cursor":{"row":1,"col":15},"anchor":{"row":1,"col":15}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":4}},{"cursor":{"row":2,"col":16},"anchor":{"row":2,"col":15}},{"cursor":{"row":3,"col":10},"anchor":{"row":3,"col":4}},{"cursor":{"row":3,"col":15},"anchor":{"row":3,"col":14}},{"cursor":{"row":4,"col":9},"anchor":{"row":4,"col":4}},{"cursor":{"row":4,"col":14},"anchor":{"row":4,"col":13}},{"cursor":{"row":5,"col":11},"anchor":{"row":5,"col":4}},{"cursor":{"row":5,"col":16},"anchor":{"row":5,"col":15}},{"cursor":{"row":6,"col":10},"anchor":{"row":6,"col":4}},{"cursor":{"row":6,"col":15},"anchor":{"row":6,"col":14}},{"cursor":{"row":7,"col":8},"anchor":{"row":7,"col":4}},{"cursor":{"row":7,"col":12},"anchor":{"row":7,"col":12}}],"primary":12} +{"name":"golf-object-into-array-01","text":"const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}} +{"name":"golf-object-into-array-02","text":"const palette = [\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}} +{"name":"golf-object-into-array-03","text":"const palette = [\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":3,"col":18},"anchor":{"row":0,"col":17}} +{"name":"golf-object-into-array-04","text":"const palette = [\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":3,"col":18},"anchor":{"row":0,"col":17}} +{"name":"golf-object-into-array-05","text":"const palette = [\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9},"sels":[{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}}],"primary":0} +{"name":"golf-object-into-array-06","text":"const palette = [\n apricot, \"#f47868\",\n lightning, \"#ffcd1c\",\n delta, \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9},"sels":[{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}},{"cursor":{"row":2,"col":11},"anchor":{"row":2,"col":11}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}}],"primary":0} +{"name":"golf-object-into-array-07","text":"const palette = [\n apricot, \"#f47868\",\n lightning, \"#ffcd1c\",\n delta, \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":1,"col":8},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":8},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":10},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":6},"anchor":{"row":3,"col":2}}],"primary":0} +{"name":"golf-object-into-array-08","text":"const palette = [\n \"apricot\", \"#f47868\",\n \"lightning\", \"#ffcd1c\",\n \"delta\", \"6f44f0\",\n];\n","mode":"normal","cursor":{"row":1,"col":10},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":10},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":12},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":8},"anchor":{"row":3,"col":2}}],"primary":0} +{"name":"golf-object-into-array-09","text":"const palette = [\n \"apricot\", \"#f47868\",\n \"lightning\", \"#ffcd1c\",\n \"delta\", \"6f44f0\",\n];\n","mode":"select","cursor":{"row":1,"col":21},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":21},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":23},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":18},"anchor":{"row":3,"col":2}}],"primary":0} +{"name":"golf-object-into-array","text":"const palette = [\n [\"apricot\", \"#f47868\"],\n [\"lightning\", \"#ffcd1c\"],\n [\"delta\", \"6f44f0\"],\n];\n","mode":"normal","cursor":{"row":1,"col":23},"anchor":{"row":1,"col":2},"sels":[{"cursor":{"row":1,"col":23},"anchor":{"row":1,"col":2}},{"cursor":{"row":2,"col":25},"anchor":{"row":2,"col":2}},{"cursor":{"row":3,"col":20},"anchor":{"row":3,"col":2}}],"primary":0} +{"name":"golf-reverse-golf-example-01","text":"# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}} +{"name":"golf-reverse-golf-example-02","text":"# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}} +{"name":"golf-reverse-golf-example-03","text":"# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"normal","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":24}} +{"name":"golf-reverse-golf-example-04","text":"# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"normal","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":24}} +{"name":"golf-reverse-golf-example-05","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"normal","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}} +{"name":"golf-reverse-golf-example-06","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}} +{"name":"golf-reverse-golf-example-07","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}}],"primary":0} +{"name":"golf-reverse-golf-example-08","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":2,"col":24},"anchor":{"row":2,"col":32},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":32}}],"primary":2} +{"name":"golf-reverse-golf-example-09","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":10},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":10}},{"cursor":{"row":0,"col":15},"anchor":{"row":0,"col":24}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":33}}],"primary":0} +{"name":"golf-reverse-golf-example-10","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":14,"col":3},"anchor":{"row":0,"col":0}} +{"name":"golf-reverse-golf-example-11","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0}},{"cursor":{"row":12,"col":3},"anchor":{"row":12,"col":0}}],"primary":0} +{"name":"golf-reverse-golf-example-12","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":39},"anchor":{"row":12,"col":0}}],"primary":0} +{"name":"golf-reverse-golf-example","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n\n## After\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n","mode":"select","cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0},"sels":[{"cursor":{"row":7,"col":39},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0}}],"primary":1} +{"name":"golf-snake-case-to-camel-case-01","text":"const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":13,"col":2},"anchor":{"row":0,"col":0}} +{"name":"golf-snake-case-to-camel-case-02","text":"const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10},"sels":[{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":4,"col":22},"anchor":{"row":4,"col":22}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":10,"col":7},"anchor":{"row":10,"col":7}},{"cursor":{"row":11,"col":8},"anchor":{"row":11,"col":8}}],"primary":0} +{"name":"golf-snake-case-to-camel-case-03","text":"const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":4,"col":22},"anchor":{"row":4,"col":22},"sels":[{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":4,"col":22},"anchor":{"row":4,"col":22}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":10,"col":7},"anchor":{"row":10,"col":7}},{"cursor":{"row":11,"col":8},"anchor":{"row":11,"col":8}}],"primary":5} +{"name":"golf-snake-case-to-camel-case-04","text":"const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7},"sels":[{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":10,"col":7},"anchor":{"row":10,"col":7}},{"cursor":{"row":11,"col":8},"anchor":{"row":11,"col":8}}],"primary":5} +{"name":"golf-snake-case-to-camel-case-05","text":"const userprofile = {\n firstname: \"John\",\n lastname: \"Doe\",\n birthdate: \"1990-05-15\",\n emailaddress: \"[email protected]\",\n phonenumber: \"555-123-4567\",\n mailingaddress: {\n streetname: \"Main Street\",\n housenumber: 123,\n apartmentunit: \"4B\",\n zipcode: \"10001\",\n cityname: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7},"sels":[{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":10,"col":7},"anchor":{"row":10,"col":7}},{"cursor":{"row":11,"col":8},"anchor":{"row":11,"col":8}}],"primary":5} +{"name":"golf-snake-case-to-camel-case","text":"const userProfile = {\n firstName: \"John\",\n lastName: \"Doe\",\n birthDate: \"1990-05-15\",\n emailAddress: \"[email protected]\",\n phoneNumber: \"555-123-4567\",\n mailingAddress: {\n streetName: \"Main Street\",\n houseNumber: 123,\n apartmentUnit: \"4B\",\n zipCode: \"10001\",\n cityName: \"New York\",\n },\n};\n","mode":"normal","cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7},"sels":[{"cursor":{"row":0,"col":10},"anchor":{"row":0,"col":10}},{"cursor":{"row":1,"col":7},"anchor":{"row":1,"col":7}},{"cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}},{"cursor":{"row":3,"col":7},"anchor":{"row":3,"col":7}},{"cursor":{"row":4,"col":7},"anchor":{"row":4,"col":7}},{"cursor":{"row":5,"col":7},"anchor":{"row":5,"col":7}},{"cursor":{"row":6,"col":9},"anchor":{"row":6,"col":9}},{"cursor":{"row":7,"col":10},"anchor":{"row":7,"col":10}},{"cursor":{"row":8,"col":9},"anchor":{"row":8,"col":9}},{"cursor":{"row":9,"col":13},"anchor":{"row":9,"col":13}},{"cursor":{"row":10,"col":7},"anchor":{"row":10,"col":7}},{"cursor":{"row":11,"col":8},"anchor":{"row":11,"col":8}}],"primary":5} +{"name":"golf-text-into-array-01","text":"Hello\nThis\nIs\nHelix\n","mode":"normal","cursor":{"row":3,"col":5},"anchor":{"row":0,"col":0}} +{"name":"golf-text-into-array-02","text":"Hello\nThis\nIs\nHelix\n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":4},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":1},"anchor":{"row":2,"col":0}},{"cursor":{"row":3,"col":4},"anchor":{"row":3,"col":0}}],"primary":0} +{"name":"golf-text-into-array-03","text":"\"Hello\"\n\"This\"\n\"Is\"\n\"Helix\"\n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":6},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":0}},{"cursor":{"row":2,"col":3},"anchor":{"row":2,"col":0}},{"cursor":{"row":3,"col":6},"anchor":{"row":3,"col":0}}],"primary":0} +{"name":"golf-text-into-array-04","text":"\"Hello\", \"This\", \"Is\", \"Helix\"","mode":"insert","cursor":{"row":0,"col":8},"anchor":{"row":0,"col":8},"sels":[{"cursor":{"row":0,"col":8},"anchor":{"row":0,"col":8}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":16}},{"cursor":{"row":0,"col":22},"anchor":{"row":0,"col":22}}],"primary":0} +{"name":"golf-text-into-array","text":"[\"Hello\", \"This\", \"Is\", \"Helix\"]","mode":"normal","cursor":{"row":0,"col":31},"anchor":{"row":0,"col":0}} diff --git a/test/hxcases/golf-waivers.jsonl b/test/hxcases/golf-waivers.jsonl new file mode 100644 index 00000000..eb1f824b --- /dev/null +++ b/test/hxcases/golf-waivers.jsonl @@ -0,0 +1,7 @@ +{"name":"golf-reverse-golf-example-07","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-07","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}}],"primary":0},"expected":{"name":"golf-reverse-golf-example-07","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}}} +{"name":"golf-reverse-golf-example-08","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-08","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":2,"col":24},"anchor":{"row":2,"col":32},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":11}},{"cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":32}}],"primary":2},"expected":{"name":"golf-reverse-golf-example-08","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}}} +{"name":"golf-reverse-golf-example-09","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-09","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":10},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":10}},{"cursor":{"row":0,"col":15},"anchor":{"row":0,"col":24}},{"cursor":{"row":2,"col":24},"anchor":{"row":2,"col":33}}],"primary":0},"expected":{"name":"golf-reverse-golf-example-09","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":0,"col":16},"anchor":{"row":0,"col":25}}} +{"name":"golf-reverse-golf-example-10","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-10","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":14,"col":3},"anchor":{"row":0,"col":0}},"expected":{"name":"golf-reverse-golf-example-10","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":14,"col":3},"anchor":{"row":0,"col":0}}} +{"name":"golf-reverse-golf-example-11","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-11","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0}},{"cursor":{"row":12,"col":3},"anchor":{"row":12,"col":0}}],"primary":0},"expected":{"name":"golf-reverse-golf-example-11","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":6,"col":3},"anchor":{"row":6,"col":0}},{"cursor":{"row":12,"col":3},"anchor":{"row":12,"col":0}}],"primary":0}} +{"name":"golf-reverse-golf-example-12","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example-12","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":39},"anchor":{"row":12,"col":0}}],"primary":0},"expected":{"name":"golf-reverse-golf-example-12","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n","mode":"select","cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0},"sels":[{"cursor":{"row":7,"col":41},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":39},"anchor":{"row":12,"col":0}}],"primary":0}} +{"name":"golf-reverse-golf-example","reason":"pardes n/N/* are the acme look ring, by decision: \"\"N, n and * do not walk the search register","reference":{"name":"golf-reverse-golf-example","text":"# camelCase to snake_case\n\nRename all fields to be snake_case.\n\n## Before\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n\n## After\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n","mode":"select","cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0},"sels":[{"cursor":{"row":7,"col":39},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0}}],"primary":1},"expected":{"name":"golf-reverse-golf-example","text":"# snake_case to snake_case\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n\n## After\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n","mode":"select","cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0},"sels":[{"cursor":{"row":7,"col":39},"anchor":{"row":6,"col":0}},{"cursor":{"row":13,"col":41},"anchor":{"row":12,"col":0}}],"primary":1}} diff --git a/test/hxcases/golf.jsonl b/test/hxcases/golf.jsonl new file mode 100644 index 00000000..f327eaf1 --- /dev/null +++ b/test/hxcases/golf.jsonl @@ -0,0 +1,124 @@ +{"name": "golf-csv-to-sql-01", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%"} +{"name": "golf-csv-to-sql-02", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>"} +{"name": "golf-csv-to-sql-03", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yy"} +{"name": "golf-csv-to-sql-04", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>"} +{"name": "golf-csv-to-sql-05", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>d"} +{"name": "golf-csv-to-sql-06", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhh"} +{"name": "golf-csv-to-sql-07", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhb"} +{"name": "golf-csv-to-sql-08", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms`"} +{"name": "golf-csv-to-sql-09", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``"} +{"name": "golf-csv-to-sql-10", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_"} +{"name": "golf-csv-to-sql-11", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms("} +{"name": "golf-csv-to-sql-12", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(I"} +{"name": "golf-csv-to-sql-13", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database"} +{"name": "golf-csv-to-sql-14", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>"} +{"name": "golf-csv-to-sql-15", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table"} +{"name": "golf-csv-to-sql-16", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la"} +{"name": "golf-csv-to-sql-17", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>"} +{"name": "golf-csv-to-sql-18", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES ("} +{"name": "golf-csv-to-sql-19", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>"} +{"name": "golf-csv-to-sql-20", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yP"} +{"name": "golf-csv-to-sql-21", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>"} +{"name": "golf-csv-to-sql-22", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>ms'"} +{"name": "golf-csv-to-sql-23", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>ms'A;"} +{"name": "golf-csv-to-sql-24", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>ms'A;<esc>Fl"} +{"name": "golf-csv-to-sql-25", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>ms'A;<esc>Fl;"} +{"name": "golf-csv-to-sql", "pane": "file", "text": "id 1,Item 1,cost 1,location 1\nid 2,Item 2,cost 2,location 2\nid 10,Item 10,cost 10,location 10\n", "keys": "%<A-s>\"yys\\d<ret>dhhbms``x_ms(IINSERT INTO `database<esc>a.`table<esc>la <esc>AVALUES (<esc>\"yPS,<ret>ms'A;<esc>Fl;~"} +{"name": "golf-enumerate-and-align-01", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%"} +{"name": "golf-enumerate-and-align-02", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s"} +{"name": "golf-enumerate-and-align-03", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>"} +{"name": "golf-enumerate-and-align-04", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a "} +{"name": "golf-enumerate-and-align-05", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: "} +{"name": "golf-enumerate-and-align-06", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#"} +{"name": "golf-enumerate-and-align-07", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#,"} +{"name": "golf-enumerate-and-align-08", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#,<esc>"} +{"name": "golf-enumerate-and-align-09", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#,<esc>%s"} +{"name": "golf-enumerate-and-align-10", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#,<esc>%s |\\d+<ret>"} +{"name": "golf-enumerate-and-align", "pane": "file", "text": "[\n { word: \"a\", count: 2565 },\n { word: \"and\", count: 1777 },\n { word: \"of\", count: 1331 },\n { word: \"that\", count: 1263 },\n { word: \"to\", count: 1030 },\n { word: \"in\", count: 1027 },\n { word: \"it\", count: 754 },\n { word: \"as\", count: 730 },\n { word: \"was\", count: 687 },\n { word: \"you\", count: 652 },\n { word: \"for\", count: 630 },\n];\n", "keys": "%s\\{<ret>a rank: <C-r>#,<esc>%s |\\d+<ret>&"} +{"name": "golf-enumerate-and-align-from09-09", "pane": "file", "text": "[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n", "keys": "%s"} +{"name": "golf-enumerate-and-align-from09-10", "pane": "file", "text": "[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n", "keys": "%s |\\d+<ret>"} +{"name": "golf-enumerate-and-align-from09", "pane": "file", "text": "[\n { rank: 1, word: \"a\", count: 2565 },\n { rank: 2, word: \"and\", count: 1777 },\n { rank: 3, word: \"of\", count: 1331 },\n { rank: 4, word: \"that\", count: 1263 },\n { rank: 5, word: \"to\", count: 1030 },\n { rank: 6, word: \"in\", count: 1027 },\n { rank: 7, word: \"it\", count: 754 },\n { rank: 8, word: \"as\", count: 730 },\n { rank: 9, word: \"was\", count: 687 },\n { rank: 10, word: \"you\", count: 652 },\n { rank: 11, word: \"for\", count: 630 },\n];\n", "keys": "%s |\\d+<ret>&"} +{"name": "golf-export-from-mod-01", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%"} +{"name": "golf-export-from-mod-02", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp"} +{"name": "golf-export-from-mod-03", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space>"} +{"name": "golf-export-from-mod-04", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space><A-s>gse"} +{"name": "golf-export-from-mod-05", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space><A-s>gsecpub use<esc>"} +{"name": "golf-export-from-mod-06", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space><A-s>gsecpub use<esc>ley"} +{"name": "golf-export-from-mod-07", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space><A-s>gsecpub use<esc>leyp"} +{"name": "golf-export-from-mod", "pane": "file", "text": "mod generate_demos;\nmod mdbook_preprocessor;\nmod validate;\n", "keys": "%yp[<space><A-s>gsecpub use<esc>leypi::"} +{"name": "golf-function-into-class-01", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%"} +{"name": "golf-function-into-class-02", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>"} +{"name": "golf-function-into-class-03", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget"} +{"name": "golf-function-into-class-04", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>"} +{"name": "golf-function-into-class-05", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod"} +{"name": "golf-function-into-class-06", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>"} +{"name": "golf-function-into-class-07", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jj"} +{"name": "golf-function-into-class-08", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvgl"} +{"name": "golf-function-into-class-09", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvgly"} +{"name": "golf-function-into-class-10", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>"} +{"name": "golf-function-into-class-11", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xb"} +{"name": "golf-function-into-class-12", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbR"} +{"name": "golf-function-into-class-13", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxx"} +{"name": "golf-function-into-class-14", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>"} +{"name": "golf-function-into-class-15", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>b"} +{"name": "golf-function-into-class-16", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>blll"} +{"name": "golf-function-into-class-17", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>bllled"} +{"name": "golf-function-into-class-18", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>bllled%<gt>"} +{"name": "golf-function-into-class-19", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>bllled%<gt>O<backspace>"} +{"name": "golf-function-into-class", "pane": "file", "text": "def calculate_area(length, width):\n result = length * width\n return result\n\ndef calculate_perimiter(length, width):\n result = 2 * (length + width)\n return result\n\ndef calculate_volume(length, width, height):\n result = length * width * height\n return result\n", "keys": "%scalculate<ret>cget<esc>O@staticmethod<esc>jjvglyx<A-d>xbRkxxslength|width|height<ret>bllled%<gt>O<backspace>class Calculator:"} +{"name": "golf-function-into-class-from18-18", "pane": "file", "text": "@staticmethod\ndef get_area(len, wid):\n return len * wid\n\n@staticmethod\ndef get_perimiter(len, wid):\n return 2 * (len + wid)\n\n@staticmethod\ndef get_volume(len, wid, hei):\n return len * wid * hei\n", "keys": "%<gt>"} +{"name": "golf-function-into-class-from18-19", "pane": "file", "text": "@staticmethod\ndef get_area(len, wid):\n return len * wid\n\n@staticmethod\ndef get_perimiter(len, wid):\n return 2 * (len + wid)\n\n@staticmethod\ndef get_volume(len, wid, hei):\n return len * wid * hei\n", "keys": "%<gt>O<backspace>"} +{"name": "golf-function-into-class-from18", "pane": "file", "text": "@staticmethod\ndef get_area(len, wid):\n return len * wid\n\n@staticmethod\ndef get_perimiter(len, wid):\n return 2 * (len + wid)\n\n@staticmethod\ndef get_volume(len, wid, hei):\n return len * wid * hei\n", "keys": "%<gt>O<backspace>class Calculator:"} +{"name": "golf-invert-dictionary-01", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "web"} +{"name": "golf-invert-dictionary-02", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret>"} +{"name": "golf-invert-dictionary-03", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>"} +{"name": "golf-invert-dictionary-04", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Q"} +{"name": "golf-invert-dictionary-05", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qx"} +{"name": "golf-invert-dictionary-06", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,"} +{"name": "golf-invert-dictionary-07", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,S=<ret>"} +{"name": "golf-invert-dictionary-08", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,S=<ret>_"} +{"name": "golf-invert-dictionary-09", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,S=<ret>_<A-(>"} +{"name": "golf-invert-dictionary-10", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,S=<ret>_<A-(>Q"} +{"name": "golf-invert-dictionary", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>Qxt,S=<ret>_<A-(>Q6q"} +{"name": "golf-invert-dictionary-2-01", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "web"} +{"name": "golf-invert-dictionary-2-02", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret>"} +{"name": "golf-invert-dictionary-2-03", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>"} +{"name": "golf-invert-dictionary-2-04", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>x"} +{"name": "golf-invert-dictionary-2-05", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>xt}"} +{"name": "golf-invert-dictionary-2-06", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>xt}S,|=<ret>"} +{"name": "golf-invert-dictionary-2-07", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>xt}S,|=<ret>_"} +{"name": "golf-invert-dictionary-2", "pane": "file", "text": "var color_to_points = {\n \"red\" = 0,\n \"orange\" = 5,\n \"yellow\" = 10,\n \"green\" = 15,\n \"blue\" = 20,\n \"purple\" = 30,\n \"black\" = 50,\n}\n", "keys": "webS_to_<ret><A-(>xt}S,|=<ret>_2<A-(>"} +{"name": "golf-object-into-array-01", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "j"} +{"name": "golf-object-into-array-02", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{["} +{"name": "golf-object-into-array-03", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi["} +{"name": "golf-object-into-array-04", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s"} +{"name": "golf-object-into-array-05", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>"} +{"name": "golf-object-into-array-06", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>r,"} +{"name": "golf-object-into-array-07", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>r,be"} +{"name": "golf-object-into-array-08", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>r,bems\""} +{"name": "golf-object-into-array-09", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>r,bems\"vt,"} +{"name": "golf-object-into-array", "pane": "file", "text": "const palette = {\n apricot: \"#f47868\",\n lightning: \"#ffcd1c\",\n delta: \"6f44f0\",\n};\n", "keys": "jmr{[mi[s:<ret>r,bems\"vt,ms["} +{"name": "golf-reverse-golf-example-01", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "eb"} +{"name": "golf-reverse-golf-example-02", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "eby"} +{"name": "golf-reverse-golf-example-03", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb"} +{"name": "golf-reverse-golf-example-04", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*"} +{"name": "golf-reverse-golf-example-05", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*R"} +{"name": "golf-reverse-golf-example-06", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv"} +{"name": "golf-reverse-golf-example-07", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"N"} +{"name": "golf-reverse-golf-example-08", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn"} +{"name": "golf-reverse-golf-example-09", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn<A-)>"} +{"name": "golf-reverse-golf-example-10", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn<A-)>%"} +{"name": "golf-reverse-golf-example-11", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn<A-)>%s`+j<ret>"} +{"name": "golf-reverse-golf-example-12", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn<A-)>%s`+j<ret>f;"} +{"name": "golf-reverse-golf-example", "pane": "file", "text": "# snake_case to camelCase\n\nRename all fields to be camelCase.\n\n## Before\n\n```js\nconst user_profile = {first_name: \"John\"};\n```\n\n## After\n\n```js\nconst userProfile = {firstName: \"John\"};\n```\n", "keys": "ebyxb*Rv\"\"Nn<A-)>%s`+j<ret>f;<A-(>"} +{"name": "golf-snake-case-to-camel-case-01", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%"} +{"name": "golf-snake-case-to-camel-case-02", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%s_<ret>"} +{"name": "golf-snake-case-to-camel-case-03", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%s_<ret>5)"} +{"name": "golf-snake-case-to-camel-case-04", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%s_<ret>5)<A-,>"} +{"name": "golf-snake-case-to-camel-case-05", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%s_<ret>5)<A-,>d"} +{"name": "golf-snake-case-to-camel-case", "pane": "file", "text": "const user_profile = {\n first_name: \"John\",\n last_name: \"Doe\",\n birth_date: \"1990-05-15\",\n email_address: \"[email protected]\",\n phone_number: \"555-123-4567\",\n mailing_address: {\n street_name: \"Main Street\",\n house_number: 123,\n apartment_unit: \"4B\",\n zip_code: \"10001\",\n city_name: \"New York\",\n },\n};\n", "keys": "%s_<ret>5)<A-,>d~"} +{"name": "golf-text-into-array-01", "pane": "file", "text": "Hello\nThis\nIs\nHelix\n", "keys": "%"} +{"name": "golf-text-into-array-02", "pane": "file", "text": "Hello\nThis\nIs\nHelix\n", "keys": "%<A-s>"} +{"name": "golf-text-into-array-03", "pane": "file", "text": "Hello\nThis\nIs\nHelix\n", "keys": "%<A-s>ms\""} +{"name": "golf-text-into-array-04", "pane": "file", "text": "Hello\nThis\nIs\nHelix\n", "keys": "%<A-s>ms\"<A-J>i,"} +{"name": "golf-text-into-array", "pane": "file", "text": "Hello\nThis\nIs\nHelix\n", "keys": "%<A-s>ms\"<A-J>i,<esc>xms["} diff --git a/test/hxcases/parity-waivers.jsonl b/test/hxcases/parity-waivers.jsonl index 39a6a7c9..e28f3ff5 100644 --- a/test/hxcases/parity-waivers.jsonl +++ b/test/hxcases/parity-waivers.jsonl @@ -11,3 +11,9 @@ {"name":"comment-tab-indent","reason":"The terminal expands literal tabs to spaces before editing.","reference":{"name":"comment-tab-indent","text":"\t# alpha\n\t# beta\n","mode":"normal","cursor":{"row":1,"col":7},"anchor":{"row":0,"col":0}},"expected":{"name":"comment-tab-indent","text":" # alpha\n # beta\n","mode":"normal","cursor":{"row":1,"col":14},"anchor":{"row":0,"col":0}}} {"name":"comment-noeol","reason":"Terminal capture restores a trailing newline that the source omits.","reference":{"name":"comment-noeol","text":"# alpha","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},"expected":{"name":"comment-noeol","text":"# alpha\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}}} {"name":"comment-only-blank","reason":"Terminal capture expands tabs and trims trailing blank rows.","reference":{"name":"comment-only-blank","text":" \n\t\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":0,"col":0}},"expected":{"name":"comment-only-blank","text":" \n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":0}}} +{"name":"ins-backspace-indent-tab","reason":"The terminal expands literal tabs to spaces before editing.","reference":{"name":"ins-backspace-indent-tab","text":"\tx\n","mode":"insert","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},"expected":{"name":"ins-backspace-indent-tab","text":" x\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}} +{"name":"dot-append-count","reason":"A terminal's text is not the editor's to grow: a at its end adds no newline to append before, where a file gets one (helix append_mode).","reference":{"name":"dot-append-count","text":"aYZb\ncd\nYZYZYZ\n","mode":"normal","cursor":{"row":2,"col":5},"anchor":{"row":1,"col":2}},"expected":{"name":"dot-append-count","text":"aYZb\ncd\nYZYZYZ","mode":"normal","cursor":{"row":2,"col":6},"anchor":{"row":2,"col":6}}} +{"name":"sticky-view-scroll","reason":"Terminal scroll limits change where scrolloff places the cursor.","reference":{"name":"sticky-view-scroll","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},"expected":{"name":"sticky-view-scroll","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":43,"col":0},"anchor":{"row":43,"col":0}}} +{"name":"sticky-view-then-move","reason":"Terminal scroll limits change where scrolloff places the cursor.","reference":{"name":"sticky-view-then-move","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":6,"col":0},"anchor":{"row":6,"col":0}},"expected":{"name":"sticky-view-then-move","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":44,"col":0},"anchor":{"row":44,"col":0}}} +{"name":"sticky-view-ignores","reason":"Terminal scroll limits change where scrolloff places the cursor.","reference":{"name":"sticky-view-ignores","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},"expected":{"name":"sticky-view-ignores","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":43,"col":0},"anchor":{"row":43,"col":0}}} +{"name":"view-z-not-sticky","reason":"Terminal scroll limits change where scrolloff places the cursor.","reference":{"name":"view-z-not-sticky","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":5,"col":0},"anchor":{"row":5,"col":0}},"expected":{"name":"view-z-not-sticky","text":"line 0\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\nline 32\nline 33\nline 34\nline 35\nline 36\nline 37\nline 38\nline 39\nline 40\nline 41\nline 42\nline 43\nline 44\nline 45\nline 46\nline 47\nline 48\nline 49\nline 50\nline 51\nline 52\nline 53\nline 54\nline 55\nline 56\nline 57\nline 58\nline 59\n","mode":"normal","cursor":{"row":44,"col":0},"anchor":{"row":44,"col":0}}} diff --git a/test/hxcases/waivers.jsonl b/test/hxcases/waivers.jsonl index 82da2a08..6abc7de2 100644 --- a/test/hxcases/waivers.jsonl +++ b/test/hxcases/waivers.jsonl @@ -1,6 +1,2 @@ -{"name":"wiX-edit-drops-sel","reason":"Insert edits clear the selection instead of tracking its anchor.","reference":{"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":6}},"expected":{"name":"wiX-edit-drops-sel","text":"Xalpha beta\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}} {"name":"alt-c-window-op","reason":"Alt-c moves a pane to a new column instead of Helix's change-noyank.","reference":{"name":"alt-c-window-op","text":"bc def\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},"expected":{"name":"alt-c-window-op","text":"abc def\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}} -{"name":"msel-append","reason":"Append-mode selection origins are tracked only for the primary selection.","reference":{"name":"msel-append","text":"aza\nbzb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1},"expected":{"name":"msel-append","text":"aza\nbzb\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":0}}],"primary":1}} -{"name":"msel-yank-paste","reason":"Pardes has one combined yank register, not one value per selection.","reference":{"name":"msel-yank-paste","text":"aab\nccd\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1},"expected":{"name":"msel-yank-paste","text":"aa\ncb\nca\ncd\n","mode":"normal","cursor":{"row":3,"col":0},"anchor":{"row":2,"col":1},"sels":[{"cursor":{"row":1,"col":0},"anchor":{"row":0,"col":1}},{"cursor":{"row":3,"col":0},"anchor":{"row":2,"col":1}}],"primary":1}} -{"name":"sel-regex-dot-newline","reason":"The regex engine includes newlines in dot matches.","reference":{"name":"sel-regex-dot-newline","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}],"primary":0},"expected":{"name":"sel-regex-dot-newline","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}}],"primary":0}} -{"name":"sel-regex-caret","reason":"The regex engine lacks Helix's multiline anchors.","reference":{"name":"sel-regex-caret","text":"ab\nab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0},"sels":[{"cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}},{"cursor":{"row":1,"col":0},"anchor":{"row":1,"col":0}}],"primary":0},"expected":{"name":"sel-regex-caret","text":"ab\nab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}} +{"name":"msel-ins-checkpoint","reason":"Undo restores the primary range alone: a history entry keeps one range, not every one, so the others stay where the edit left them.","reference":{"name":"msel-ins-checkpoint","text":"Xab\nXab\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1},"expected":{"name":"msel-ins-checkpoint","text":"Xab\nXab\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1}} diff --git a/test/hxdiff.zig b/test/hxdiff.zig index 627bbac3..48c7ab93 100644 --- a/test/hxdiff.zig +++ b/test/hxdiff.zig @@ -419,6 +419,9 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result } else if (std.mem.startsWith(u8, name, "A-")) { key.alt = true; name = name[2..]; + } else if (std.mem.startsWith(u8, name, "S-")) { + key.shift = true; + name = name[2..]; } else break; } if (name.len == 1 or (name.len > 1 and name.len <= 4 and name[0] >= 0x80)) { diff --git a/test/mode.zig b/test/mode.zig index c7d7d548..cba93c30 100644 --- a/test/mode.zig +++ b/test/mode.zig @@ -79,14 +79,14 @@ test "Mode from actual tag Exec preserves parked file and output insert transact p.update(.{ .key = .{ .cp = pardes.Key.tab } }); try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); try std.testing.expect(pane.focus == .body); - try std.testing.expect(pane.body.append_at == null); + try std.testing.expect(!pane.body.restore_cursor); p.update(.{ .key = .{ .cp = 'u' } }); try std.testing.expectEqualStrings("ab\n", pane.file.?.content); p.update(.{ .key = .{ .cp = 'a' } }); p.update(.{ .key = .{ .cp = 'z', .text = "z" } }); - try std.testing.expect(pane.body.append_at != null); + try std.testing.expect(pane.body.restore_cursor); try mode(p, 0, .normal); - try std.testing.expect(pane.body.append_at == null); + try std.testing.expect(!pane.body.restore_cursor); p.update(.{ .key = .{ .cp = 'u' } }); try std.testing.expectEqualStrings("ab\n", pane.file.?.content); } diff --git a/test/panes.zig b/test/panes.zig index 5e26cdd0..40b4b6ba 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -308,7 +308,7 @@ const TagNavigationTests = struct { try std.testing.expectEqual(@as(usize, 2), got.n); for (ranges[0..got.n]) |range| try std.testing.expectEqualStrings("S", text[@min(range.anchor, range.head)..@max(range.anchor, range.head)]); // the body's selection is its own - try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); // `/` from the tag is the body's search, as acme's Look from a tag is key(p, ','); key(p, '/'); @@ -317,6 +317,19 @@ const TagNavigationTests = struct { try std.testing.expect(pane.prompt == .none and pane.focus == .body); } + test "ms and md edit a tag's own text, and the text the edit made is read back whole" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + p.update(.tick); + key(p, ':'); // the tag, on `Save` the first time + for ("miwms(") |cp| key(p, cp); + try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), "(Save)") != null); + for ("md(") |cp| key(p, cp); + try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), " Save ") != null); + try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), "(") == null); + } + test "the keyboard reaches a tag's path, which it can select and yank but not edit" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); defer p.deinit(); @@ -328,7 +341,7 @@ const TagNavigationTests = struct { try std.testing.expectEqual(@as(i32, 0), pane.tag.cur_col); key(p, 'E'); key(p, 'y'); - try std.testing.expectEqualStrings("/test.txt", p.yank.?); + try std.testing.expectEqualStrings("/test.txt", p.registers.text(p.gpa, '"').?); // an edit that reaches into it is refused, the cursor with it const before = try std.testing.allocator.dupe(u8, try pardes.edit.flatSurface(p, &pane.tag)); defer std.testing.allocator.free(before); @@ -851,12 +864,12 @@ const TtySelectionTests = struct { const x = rect.x + config.GUTTER; const y = p.bodyTop(pane, rect); JumpSelectionTests.drag(p, x + 1, y, x + 3, y); - try std.testing.expectEqualStrings("lph", p.yank orelse return error.MissingAutomaticTtyYank); + try std.testing.expectEqualStrings("lph", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); p.update(.{ .key = .{ .cp = 'l' } }); try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); p.update(.{ .key = .{ .cp = 'y' } }); - try std.testing.expectEqualStrings("a", p.yank.?); + try std.testing.expectEqualStrings("a", p.registers.text(p.gpa, '"').?); try noChildInput(p); } @@ -884,7 +897,7 @@ const TtySelectionTests = struct { const expected = if (prompt) "$ ab" else "lph\nrav"; try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); - try std.testing.expectEqualStrings(expected, p.yank orelse return error.MissingAutomaticTtyYank); + try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); try noChildInput(p); try std.testing.expect(p.executeBuiltinLine(0, "ClipYank")); @@ -900,7 +913,7 @@ const TtySelectionTests = struct { } try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); p.update(.{ .key = .{ .cp = 'y' } }); - try std.testing.expectEqualStrings(expected, p.yank.?); + try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"').?); try noChildInput(p); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); @@ -911,7 +924,7 @@ const TtySelectionTests = struct { _ = try p.render(frame.allocator()); try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); - try std.testing.expectEqualStrings(expected, p.yank.?); + try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"').?); try noChildInput(p); } } @@ -923,7 +936,7 @@ const TtySelectionTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 24 }); defer p.deinit(); p.presentation.enabled = false; - pardes.exec.spawnTty(p, 0); + _ = pardes.exec.spawnTty(p, 0); for (p.panes[0..2]) |slot| { slot.?.body.mode = .tty; slot.?.tty_filter = false; @@ -943,7 +956,7 @@ const TtySelectionTests = struct { const sx = source.x + config.GUTTER; const sy = if (p.settings.tag_bottom) source.y else source.y + pardes.BOX_H; JumpSelectionTests.drag(p, sx + 1, sy, sx + 3, sy + 1); - try std.testing.expectEqualStrings("lph\nrav", p.yank orelse return error.MissingAutomaticTtyYank); + try std.testing.expectEqualStrings("lph\nrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); try noChildInput(p); const target = p.rects[1]; @@ -1004,7 +1017,7 @@ const TtySelectionTests = struct { try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); if (bracketed) p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } }); try std.testing.expectEqual(bracketed, panes.Terminal.bracketedPaste(pane)); - p.yank = try p.gpa.dupe(u8, "one\ntwo"); // what a `y` anywhere left behind + p.registers.put(p.gpa, '"', "one\ntwo", 0, 1, true); // what a `y` anywhere left behind var buf: [256]u8 = undefined; _ = childInput(p, &buf); @@ -1036,7 +1049,7 @@ const TtySelectionTests = struct { ); } // ...and neither chord put the clipboard in the register on its way. - try std.testing.expectEqualStrings("one\ntwo", p.yank orelse ""); + try std.testing.expectEqualStrings("one\ntwo", p.registers.text(p.gpa, '"') orelse ""); } } }; @@ -1944,7 +1957,11 @@ test "a prompt owns the pipe behavior through cancellation and submission" { p.update(.{ .key = .{ .cp = Key.enter } }); const pending = p.pipe.wait orelse return error.MissingPipe; try std.testing.expectEqual(how, pending.how); - try std.testing.expectEqualStrings("cat", pending.command); + // `$` runs the command in a subshell that reports its status + if (how == .keep) + try std.testing.expect(std.mem.indexOf(u8, pending.command, "\ncat\n") != null) + else + try std.testing.expectEqualStrings("cat", pending.command); try std.testing.expectEqual(@as(usize, 1), pending.inputs.len); try std.testing.expectEqualStrings(if (how.pipes()) "one" else "", pending.inputs[0].bytes); try std.testing.expect(pane.prompt == .none); @@ -1970,8 +1987,8 @@ test "prompt cancellation restores the selection before regex previews" { p.update(.{ .key = .{ .cp = case.pattern[0], .text = case.pattern } }); try std.testing.expectEqual(case.secondary, pane.body.nsel); p.update(.{ .key = .{ .cp = Key.escape } }); - try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); - try std.testing.expectEqual(@as(u8, 0), pane.nsel_snap); + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 0), pane.nsel_snap); try std.testing.expectEqual(@as(i32, 10), pane.body.cur_col); try std.testing.expectEqual(@as(i32, 0), pane.body.vsel.col); try std.testing.expect(pane.body.vsel.active and pane.body.vsel.explicit); @@ -2747,7 +2764,7 @@ const OutputTests = struct { try fillResults(p, 0, "/", .{ .query = .hover }, "", try p.gpa.dupe(u8, "short\n"), null); try std.testing.expectEqualStrings("short\n", result.file.?.content); try std.testing.expect(!result.body.vsel.active and !result.body.msel.active and !result.body.select); - try std.testing.expectEqual(@as(u8, 0), result.body.nsel); + try std.testing.expectEqual(@as(u16, 0), result.body.nsel); try std.testing.expectEqual(@as(i32, 0), result.body.cur_row); try std.testing.expectEqual(@as(i32, 0), result.body.cur_col); try std.testing.expectEqual(@as(usize, 0), result.file.?.scroll); @@ -4245,7 +4262,7 @@ test "an empty counted paste leaves no count for the next motion" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); defer p.deinit(); const pane = try p.setTestFile("abcdefgh\n"); - if (empty_register) p.yank = try p.gpa.dupe(u8, ""); + if (empty_register) p.registers.put(p.gpa, '"', "", 0, 1, true); for ("3p") |cp| p.update(.{ .key = .{ .cp = cp } }); try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); p.update(.{ .key = .{ .cp = 'l' } }); @@ -4546,11 +4563,12 @@ const LookResetTests = struct { pane.body.cur_col = 3; pane.body.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 0 }; pane.body.msel = .{ .active = true, .r0 = 0, .r1 = 1 }; + _ = pane.body.reserveSels(); pane.body.sels[0] = .{ .row = 1, .col = 2, .arow = 0, .acol = 1 }; pane.body.nsel = 1; pane.body.select = true; pane.body.normal.count = 5; - pane.body.append_at = .{ .row = 0, .col = 3 }; + pane.body.restore_cursor = true; pane.body.sticky_col = 7; for (0..pane.sel.len) |slot| { pane.clearPointerSelection(slot); @@ -4567,10 +4585,10 @@ const LookResetTests = struct { fn cleared(pane: *const Pane) !void { try std.testing.expect(!pane.body.msel.active); - try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); + try std.testing.expectEqual(@as(u16, 0), pane.body.nsel); try std.testing.expect(!pane.body.select); try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); - try std.testing.expect(pane.body.append_at == null); + try std.testing.expect(!pane.body.restore_cursor); try std.testing.expectEqual(@as(i32, -1), pane.body.sticky_col); for (pane.sel) |selection| try std.testing.expectEqual(.none, selection.state); for (pane.pointer_selections) |selection| try std.testing.expect(selection == null); diff --git a/test/pdf.zig b/test/pdf.zig index 93418727..47f8a9e8 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -860,7 +860,8 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { p.update(.{ .key = .{ .cp = '2' } }); p.update(.{ .key = .{ .cp = 'l' } }); try std.testing.expect(pane.pdf.?.pan_x > 0); - p.update(.{ .key = .{ .cp = '$' } }); + p.update(.{ .key = .{ .cp = 'g' } }); + p.update(.{ .key = .{ .cp = 'l' } }); try std.testing.expectEqual(std.math.maxInt(u16), pane.pdf.?.pan_x); p.update(.{ .key = .{ .cp = '2' } }); p.update(.{ .key = .{ .cp = 'h' } }); diff --git a/test/snapshots/argv.golden b/test/snapshots/argv.golden index e9caf508..ab143a88 100644 --- a/test/snapshots/argv.golden +++ b/test/snapshots/argv.golden @@ -1,7 +1,7 @@ == snap noargs grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/argv/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/argv/cwd Tty+bash Save Mode Filter Collapse Del | $ echo ARGV''TTY | ARGVTTY | $ @@ -49,6 +49,6 @@ == snap dirarg grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/argv/cwd/sub Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/argv/cwd/sub Tty+bash Save Mode Filter Collapse Del | ls | inside.txt diff --git a/test/snapshots/badutf.snap b/test/snapshots/badutf.snap index ddd1588b..7171df00 100644 --- a/test/snapshots/badutf.snap +++ b/test/snapshots/badutf.snap @@ -19,6 +19,6 @@ stable 700 20000 # first visible CJK glyph intact instead of starting inside its UTF-8 bytes. key space t w stable 400 5000 -key $ +key g l stable 700 10000 snap hscroll-midglyph diff --git a/test/snapshots/boot.golden b/test/snapshots/boot.golden index f5fa64a8..7172049a 100644 --- a/test/snapshots/boot.golden +++ b/test/snapshots/boot.golden @@ -1,13 +1,13 @@ == snap boot grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/boot/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/boot/cwd Tty+bash Save Mode Filter Collapse Del | ls | hello.txt == style boot grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-31 #dfbce5,#26362d, 32-61 #becfc4,#26362d, 62-99 d,#26362d, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-66 #becfc4,#26362d, 67-99 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-99 d,#0d1410, diff --git a/test/snapshots/builtins.snap b/test/snapshots/builtins.snap index 9a58502f..84de680f 100644 --- a/test/snapshots/builtins.snap +++ b/test/snapshots/builtins.snap @@ -26,6 +26,7 @@ wheel up 110 50 wheel up 110 50 wheel up 110 50 wheel up 110 50 +wheel up 110 50 wait 10000 Look enter, right-click wait 10000 Exec tab, middle-click wait 10000 Filter diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden index b814d888..d5266717 100644 --- a/test/snapshots/chordcut.golden +++ b/test/snapshots/chordcut.golden @@ -1,7 +1,7 @@ == snap opened grid=120x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/chordcut/cwd Tty Save Mode Filter Collap /tmp/pardes-snap/chordcut/cwd Tty Save Mode Filter Collap +| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C | ls ls | f.txt g.txt f.txt g.txt | @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/chordcut/cwd Tty Save Mode Filter Collap +| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C | ls | f.txt g.txt == snap cut grid=120x31 cursor=7,8 @@ -30,11 +30,12 @@ == snap cancelexec grid=120x31 cursor=25,8 |8: 2 2echo B''OO g.txt == snap execafter grid=120x31 cursor=25,8 -|3: f.txt ls -|4: BOO f.txt g.txt -|7: 1 line 1 Exec +|6: /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa +|7: 1 line 1 Exec % echo B''OO +|8: 2 2echo B''OO g.txt BOO +|9: 3 line 3 exit 0 == snap cancellook grid=120x31 cursor=25,8 -|7: 1 line 1 +|7: 1 line 1 % echo B''OO == snap lookafter grid=120x31 cursor=7,13 |12: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse Del |13: 1 line 1 @@ -43,11 +44,11 @@ == style chordsel grid=120x31 |0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-35 #d3b0d9,#1d2824, 36-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-95 #d3b0d9,#1d2824, 96-119 #b2c3b8,#1d2824, -|3: 0-0 d,#152019, 1-1 d,#0d1410, 2-8 #e5eee5,#0d1410, 9-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-100 #d3b0d9,#1d2824, 101-119 #b2c3b8,#1d2824, +|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|5: 0-0 d,#152019, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, -|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, +|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-119 #b2c3b8,#1d2824, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, @@ -58,7 +59,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-35 #d3b0d9,#1d2824, 36-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, @@ -77,7 +78,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, == snap ttyprobe grid=120x31 cursor=42,22 |12: /tmp/pardes-snap/chordcut/cwd/g.txt * Save Tty Collapse D -|17: $ /tmp/pardes-snap/chordcut/cwd Tty Save Mode Filter Collap +|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C |18: $ ls |20: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[? |21: 1006h'; cat -v diff --git a/test/snapshots/cmdexit.golden b/test/snapshots/cmdexit.golden new file mode 100644 index 00000000..de95c383 --- /dev/null +++ b/test/snapshots/cmdexit.golden @@ -0,0 +1,15 @@ +== snap opened grid=120x31 cursor=7,3 +|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit +| New Tty Find Grep Joincol Delcol +| /tmp/pardes-snap/cmdexit/cwd/cmds.txt Save Tty Collapse Del +| 1 sleep 30 & echo BG''STARTED +| 2 exec </dev/null >/dev/null 2>&1; sleep 1; false +== snap background grid=120x31 cursor=7,3 +|17: /tmp/pardes-snap/cmdexit/cwd (sleep 30 & echo BG''STARTED) exit 0 Kill Save Collapse Del +|18: % sleep 30 & echo BG''STARTED +|19: BGSTARTED +|20: exit 0 +== snap letgo grid=120x31 cursor=7,3 +|17: /tmp/pardes-snap/cmdexit/cwd (exec </dev/null >/dev/null 2>&1; sleep 1; false) exit 1 Kill Save Collapse Del +|21: % exec </dev/null >/dev/null 2>&1; sleep 1; false +|22: exit 1 diff --git a/test/snapshots/cmdexit.snap b/test/snapshots/cmdexit.snap new file mode 100644 index 00000000..ff60a881 --- /dev/null +++ b/test/snapshots/cmdexit.snap @@ -0,0 +1,26 @@ +# A command pane's command is over when its process exits, not when its pty +# closes. A job it leaves in the background holds the pty open, and the pane +# still says `exit 0` as soon as the command itself is done; a command that +# lets go of its terminal and runs on is not hung up at its pty's end, and +# says how it ended when it does. +file cmds.txt sleep 30 & echo BG''STARTED\nexec </dev/null >/dev/null 2>&1; sleep 1; false +config Verbose off +start 31 120 cmds.txt +wait 8000 cmds.txt +stable 700 20000 +snap opened +# line 1: `sleep 30` keeps the pty, and the pane says exit 0 regardless +press middle 7 4 +drag middle 34 4 +release middle 34 4 +wait 10000 exit 0 +stable 700 15000 +snap background +# line 2, in the same pane now its command is done: its end of file comes at +# once, and its exit a second later, from `false` +press middle 7 5 +drag middle 54 5 +release middle 54 5 +wait 10000 exit 1 +stable 700 15000 +snap letgo diff --git a/test/snapshots/cmdword.golden b/test/snapshots/cmdword.golden index 99ad446b..eec32bcb 100644 --- a/test/snapshots/cmdword.golden +++ b/test/snapshots/cmdword.golden @@ -4,21 +4,25 @@ | /tmp/pardes-snap/cmdword/cwd/cmds.txt Save Tty Collapse Del | 1 @`echo ZZ''E` @`echo ZZ''L` Look p.txt @`Look .` == snap atexec grid=120x31 cursor=7,3 -|17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del -|18: echo ZZ''E +|17: /tmp/pardes-snap/cmdword/cwd (echo ZZ''E) exit 0 Kill Save Collapse Del +|18: % echo ZZ''E |19: ZZE -== snap atlook grid=120x31 cursor=4,22 -|20: echo ZZ''L -|21: ZZL +|20: exit 0 +== snap atlook grid=120x31 cursor=2,24 +|17: /tmp/pardes-snap/cmdword/cwd (echo ZZ''L) exit 0 Kill Save Collapse Del +|21: % echo ZZ''L +|22: ZZL +|23: exit 0 == snap lookword grid=120x31 cursor=7,11 |10: /tmp/pardes-snap/cmdword/cwd/p.txt Save Tty Collapse Del |11: 1 just text -== snap nested grid=120x31 cursor=4,24 -|22: ls -|23: cmds.txt p.txt zzmark.txt -== snap execword grid=120x31 cursor=4,24 -|17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec -== snap tagcmd grid=120x31 cursor=4,24 -|17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec @`echo ZZ''T` -|24: echo ZZ''T -|25: ZZT +== snap nested grid=120x31 cursor=4,9 +|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del +|7: ls +|8: cmds.txt p.txt zzmark.txt +== snap execword grid=120x31 cursor=4,9 +|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec +== snap tagcmd grid=120x31 cursor=4,7 +|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec @`echo ZZ''T` +|7: echo ZZ''T +|8: ZZT diff --git a/test/snapshots/cmdword.snap b/test/snapshots/cmdword.snap index 361e37ba..631fab8b 100644 --- a/test/snapshots/cmdword.snap +++ b/test/snapshots/cmdword.snap @@ -7,9 +7,10 @@ # whole run instead, which is what makes a single CLICK the way you invoke one # (acme does the same for its own `<`/`|`/`>` command words). # -# EXECUTE runs it and leaves you where you were; LOOK runs it and lands you in -# the shell that answered — looking at a thing means being shown it, and a -# directory's look has always been exactly that (a shell there, ls'd). +# EXECUTE runs it, as a command pane, and leaves you where you were; LOOK runs +# it and lands you in the pane that answered — looking at a thing means being +# shown it, and a directory's look has always been exactly that (a shell +# there, ls'd). # # And Look and Exec are ORDINARY BUILTINS now, so their names are executable # words like Save or Grep: `Look p.txt` run off the page is the same look a @@ -28,14 +29,15 @@ wait 8000 cmds.txt stable 700 20000 snap opened # a single middle CLICK inside the run (col 11 is inside `echo`) expands to the -# whole `@`echo ZZ''E`` and executes it in a shell; focus stays on the file +# whole `@`echo ZZ''E`` and runs it as a command pane; focus stays on the file press middle 11 4 release middle 11 4 wait 10000 ZZE stable 700 15000 snap atexec -# LOOK on the second one (col 24, inside its `echo`) runs it too AND focuses -# the shell that answered — that is the whole difference between the two verbs +# LOOK on the second one (col 24, inside its `echo`) runs it too, in the same +# command pane now its first is done, below it, AND focuses it — that is the +# whole difference between the two verbs press right 24 4 release right 24 4 wait 10000 ZZL @@ -49,16 +51,16 @@ wait 10000 just text stable 700 15000 snap lookword # nesting: `@`Look .`` — Exec unwraps it, `Look .` is parsed as the ordinary -# Look builtin with an argument, and Look opens the directory (an existing -# shell there, ls'd — zzmark.txt is only ever in that listing) +# Look builtin with an argument, and Look opens the directory (a shell there, +# ls'd — zzmark.txt is only ever in that listing; a command pane is no shell) press middle 49 4 release middle 49 4 wait 10000 zzmark.txt stable 700 15000 snap nested # the same syntax typed into a TAG — the other place the expansion has to know -# about backticks. The look above left the shell focused, so `:` opens ITS -# tag; `A` appends at the end of it, Esc and `h` put the normal-mode cursor on +# about backticks. The look above left that shell focused, so `:` opens ITS +# tag, and the word run from a shell's own tag is typed into it; `A` appends at the end of it, Esc and `h` put the normal-mode cursor on # the last character typed, and the execute key runs the word under it # exactly as the body's does. key : diff --git a/test/snapshots/colgrip.golden b/test/snapshots/colgrip.golden index 3ad6b2b6..dd16074d 100644 --- a/test/snapshots/colgrip.golden +++ b/test/snapshots/colgrip.golden @@ -1,7 +1,7 @@ == style rest grid=100x24 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #ffffff,#887455, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-84 #dfbce5,#26362d, 85-99 #becfc4,#26362d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -28,7 +28,7 @@ == snap resize-drag grid=100x24 cursor=54,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filt +| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode | 1 x ╎ ls | ╎ w.txt | ╎ @@ -52,7 +52,7 @@ | ╎ == style resize-drag grid=100x24 |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-84 #dfbce5,#26362d, 85-99 #becfc4,#26362d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -76,7 +76,7 @@ |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, == snap resize-done grid=100x24 cursor=44,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filter Collaps +|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co |3: 1 x ls |4: w.txt |5: @@ -122,7 +122,7 @@ |23: ╎ == style reorder-drag grid=100x24 |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #000000,#f4c086, 42-42 d,#26362d, 43-74 #becfc4,#26362d, 75-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-74 #dfbce5,#26362d, 75-99 #becfc4,#26362d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-79 #dfbce5,#26362d, 80-99 #becfc4,#26362d, |3: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410, @@ -146,7 +146,7 @@ |23: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, == snap reorder-done grid=100x24 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filter Collaps /tmp/pardes-snap/colgrip/cwd/w.txt Sa +|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/colgrip/cwd/w.txt Sa |3: ls 1 x |4: w.txt |5: diff --git a/test/snapshots/cornerdrag-left.golden b/test/snapshots/cornerdrag-left.golden index ad7f2759..c9ccf859 100644 --- a/test/snapshots/cornerdrag-left.golden +++ b/test/snapshots/cornerdrag-left.golden @@ -1,11 +1,11 @@ == snap right-split grid=100x31 cursor=54,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S | ls ls | w.txt w.txt | -| /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S | ls | w.txt | @@ -16,16 +16,16 @@ | | | -| /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S | ls | w.txt == snap hover-corner grid=100x31 cursor=54,9 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty Save ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S |3: ls ╎ ls |4: w.txt ╎ w.txt |5: ╎ -|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S |7: ╎ ls |8: ╎ w.txt |9: ╎ @@ -36,7 +36,7 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdrag-left/cwd Tty Save ╎ +|17: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ |18: ls ╎ |19: w.txt ╎ |20: ╎ @@ -53,11 +53,11 @@ == snap hover-left-corner grid=100x31 cursor=54,9 == snap corner-drag grid=100x31 cursor=54,9 |1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty Save M /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S |3: ls ╎ ls |4: w.txt ╎ w.txt |5: ╎ -|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty Save M +|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S |7: ╎ ls |8: ╎ w.txt |9: ╎ @@ -68,7 +68,7 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty Save M +|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S |18: ls ╎ |19: w.txt ╎ |20: ╎ @@ -84,7 +84,7 @@ |30: ╎ == snap corner-done grid=100x31 cursor=34,16 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag-left/cwd Tty Save Mode Filter Collapse +|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll |3: ls ls |4: w.txt w.txt |5: @@ -95,7 +95,7 @@ |10: |11: |12: -|13: /tmp/pardes-snap/cornerdrag-left/cwd Tty Save Mode Filter Collapse +|13: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll |14: ls |15: w.txt |16: diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden index ae726cae..fc212219 100644 --- a/test/snapshots/cornerdrag.golden +++ b/test/snapshots/cornerdrag.golden @@ -1,7 +1,7 @@ == snap hover-corner grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode ╎ /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode F +| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M | ls ╎ ls | w.txt ╎ w.txt | ╎ @@ -16,7 +16,7 @@ | ╎ | ╎ | ╎ -| /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode ╎ +| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ | ls ╎ | w.txt ╎ | ╎ @@ -32,7 +32,7 @@ | ╎ == snap edge-drag grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdra╎/cwd Tty Save Mode F /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode F +|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M |3: ls ╎ ls |4: w.txt ╎ w.txt |5: ╎ @@ -47,7 +47,7 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdra╎/cwd Tty Save Mode F +|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M |18: ls ╎ |19: w.txt ╎ |20: ╎ @@ -63,7 +63,7 @@ |30: ╎ == snap edge-done grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse |3: ls ls |4: w.txt w.txt |5: @@ -94,7 +94,7 @@ |30: == snap corner-drag grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol ╎elcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdra╎/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse |3: ls ls ╎ |4: w.txt w.txt ╎ |5: ╎ @@ -125,14 +125,14 @@ |30: ╎ == snap corner-done grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll /tmp/pardes-snap/cornerdrag/cwd Tty S +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b |3: ls ls |4: w.txt w.txt |5: |6: |7: |8: -|9: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll +|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter |10: ls |11: w.txt |12: @@ -156,14 +156,14 @@ |30: == snap clamp-x grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol D╎lcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll /tmp/pardes-snap/cornerdrag╎cwd Tty S +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag╎cwd Tty+b |3: ls ls ╎ |4: w.txt w.txt ╎ |5: ╎ |6: ╎ |7: ╎ |8: ╎ -|9: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll ╎ +|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter ╎ |10: ls ╎ |11: w.txt ╎ |12: ╎ @@ -187,14 +187,14 @@ |30: ╎ == snap clamp-y grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Col╎ /tmp/pardes-snap/cornerdrag/cwd Tty S +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+b |3: ls ╎ ls |4: w.txt ╎ w.txt |5: ╎ |6: ╎ |7: ╎ |8: ╎ -|9: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Col╎ +|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ |10: ls ╎ |11: w.txt ╎ |12: ╎ @@ -218,7 +218,7 @@ |30: ╎ == snap clamp-done grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll /tmp/pardes-snap/cornerdrag/cwd Tty S +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b |3: ls ls |4: w.txt w.txt |5: @@ -246,4 +246,4 @@ |27: |28: |29: -|30: /tmp/pardes-snap/cornerdrag/cwd Tty Save Mode Filter Coll +|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter diff --git a/test/snapshots/cursorline.golden b/test/snapshots/cursorline.golden index e4383dca..e03eeef7 100644 --- a/test/snapshots/cursorline.golden +++ b/test/snapshots/cursorline.golden @@ -1,7 +1,7 @@ == snap top grid=150x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/cursorline/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/cursorline/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/cursorline/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/cursorline/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | cur.txt cur.txt | @@ -16,13 +16,13 @@ | 8 line 8 | 9 line 9 | 10 line 10 -| /tmp/pardes-snap/cursorline/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/cursorline/cwd Tty+bash Save Mode Filter Collapse Del | ls | cur.txt == style top grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-74 d,#26362d, 75-76 #ffffff,#887455, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-149 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-37 #d3b0d9,#1d2824, 38-67 #b2c3b8,#1d2824, 68-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-112 #d3b0d9,#1d2824, 113-142 #b2c3b8,#1d2824, 143-149 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-42 #d3b0d9,#1d2824, 43-72 #b2c3b8,#1d2824, 73-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-117 #d3b0d9,#1d2824, 118-147 #b2c3b8,#1d2824, 148-149 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-37 #d3b0d9,#1d2824, 38-67 #b2c3b8,#1d2824, 68-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-42 #d3b0d9,#1d2824, 43-72 #b2c3b8,#1d2824, 73-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/doccol.golden b/test/snapshots/doccol.golden index 76af647b..fe21d23a 100644 --- a/test/snapshots/doccol.golden +++ b/test/snapshots/doccol.golden @@ -1,7 +1,7 @@ == snap boot grid=100x31 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Coll /tmp/pardes-snap/doccol/cwd Tty Save Mode Filte +| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Coll /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode | 1 AAA ls | a.txt b.txt c.txt == snap two-docs grid=100x31 cursor=7,18 diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden index 41f29b0c..f061e7be 100644 --- a/test/snapshots/dump.golden +++ b/test/snapshots/dump.golden @@ -26,7 +26,7 @@ .panes = .{ .{ .kind = .terminal, - .tag = "/tmp/pardes-snap/dump/cwd Tty Save Mode Filter Collapse Del", + .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", .body = "ls\nd.txt\n\n\n\n typed-marker\n\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, @@ -46,7 +46,7 @@ }, .{ .kind = .terminal, - .tag = "/tmp/pardes-snap/dump/cwd Tty Save Mode Filter Collapse Del", + .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, @@ -66,7 +66,7 @@ }, .{ .kind = .terminal, - .tag = "/tmp/pardes-snap/dump/cwd Tty Save Mode Filter Collapse Del", + .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, diff --git a/test/snapshots/edit.golden b/test/snapshots/edit.golden index 12261959..63388124 100644 --- a/test/snapshots/edit.golden +++ b/test/snapshots/edit.golden @@ -1,7 +1,7 @@ == snap inserted grid=100x31 cursor=16,10 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/edit/cwd Tty Save Mode Filter /tmp/pardes-snap/edit/cwd Tty Save Mode Filter +| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi | ls ls | e.txt e.txt | @@ -16,13 +16,13 @@ | | | -| /tmp/pardes-snap/edit/cwd Tty Save Mode Filter +| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi | ls | e.txt == style inserted grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-31 #dfbce5,#26362d, 32-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-81 #d3b0d9,#1d2824, 82-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-31 #d3b0d9,#1d2824, 32-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/exec-bigdrag.golden b/test/snapshots/exec-bigdrag.golden index 375922d7..87d3b9bb 100644 --- a/test/snapshots/exec-bigdrag.golden +++ b/test/snapshots/exec-bigdrag.golden @@ -1,7 +1,7 @@ == snap alive grid=200x61 cursor=119,38 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/exec-bigdrag/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/exec-bigdrag/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del | ls bash: line: command not found | big.txt 4line 2705line 2706line 2707line 2708line 2709line 2710line 2711line 2712line 2713line 2714lin | bash: 4line: command not found @@ -31,7 +31,7 @@ | bash: 2835line: command not found | 2846line 2847line 2848line 2849line 2850line 2851line 2852line 2853line 2854line 2855line 285 | bash: 2846line: command not found -| /tmp/pardes-snap/exec-bigdrag/cwd Tty Save Mode Filter Collapse Del e 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28 +| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del e 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28 | ls bash: e: command not found | big.txt ne 2868line 2869line 2870line 2871line 2872line 2873line 2874line 2875line 2876line 2877line 2 | bash: ne: command not found diff --git a/test/snapshots/exec.golden b/test/snapshots/exec.golden index 68a9737f..216327b3 100644 --- a/test/snapshots/exec.golden +++ b/test/snapshots/exec.golden @@ -1,7 +1,7 @@ == snap exec grid=140x31 cursor=16,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/exec/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/exec/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/exec/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/exec/cwd Tty+bash Save Mode Filter Collapse Del | ls Exec ls | e.txt e.txt | @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/exec/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/exec/cwd Tty+bash Save Mode Filter Collapse Del | ls | e.txt == snap kept grid=140x31 cursor=6,3 diff --git a/test/snapshots/fileedit.golden b/test/snapshots/fileedit.golden index 42b50bbf..4d367d72 100644 --- a/test/snapshots/fileedit.golden +++ b/test/snapshots/fileedit.golden @@ -1,7 +1,7 @@ == snap edited grid=150x31 cursor=17,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/fileedit/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/fileedit/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/fileedit/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/fileedit/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | f.txt f.txt | @@ -16,7 +16,7 @@ | 8 line 8 | 9 line 9 | 10 line 10 -| /tmp/pardes-snap/fileedit/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/fileedit/cwd Tty+bash Save Mode Filter Collapse Del | ls | f.txt == snap deleted grid=150x31 cursor=7,8 diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden index d6d38274..de44f0ca 100644 --- a/test/snapshots/filewatch.golden +++ b/test/snapshots/filewatch.golden @@ -1,7 +1,7 @@ == snap edited grid=210x31 cursor=15,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del | 1 liMYEDITne 1 ls | 2 line 2 sub w.txt | 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt @@ -27,7 +27,7 @@ | 8 line 8 | 9 == snap reloaded grid=210x31 cursor=15,3 -|2: /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del |3: 1 EXTERNAL --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/w.txt ls |4: 2 sub w.txt |5: @@ -52,7 +52,7 @@ |25: |26: == snap undone grid=210x31 cursor=15,3 -|2: /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del |3: 1 liMYEDITne 1 ls |4: 2 line 2 sub w.txt |5: 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt diff --git a/test/snapshots/fsearch.golden b/test/snapshots/fsearch.golden index 01b4a54a..2d4fcb64 100644 --- a/test/snapshots/fsearch.golden +++ b/test/snapshots/fsearch.golden @@ -1,7 +1,7 @@ == snap opened grid=180x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/fsearch/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/fsearch/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/fsearch/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/fsearch/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | g.txt g.txt | @@ -16,7 +16,7 @@ | 8 l8 | 9 l9 | 10 MARK c -| /tmp/pardes-snap/fsearch/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/fsearch/cwd Tty+bash Save Mode Filter Collapse Del | ls | g.txt == snap typing grid=180x31 cursor=89,7 @@ -31,7 +31,7 @@ == style results grid=180x31 |0: 0-70 #b2c3b8,#1d2824, 71-179 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-89 d,#26362d, 90-91 #ffffff,#887455, 92-92 d,#1d2824, 93-124 #b2c3b8,#1d2824, 125-179 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-34 #d3b0d9,#1d2824, 35-64 #b2c3b8,#1d2824, 65-89 d,#1d2824, 90-91 #ffffff,#496656, 92-92 d,#1d2824, 93-121 #b2c3b8,#1d2824, 122-124 #d3b0d9,#1d2824, 125-154 #b2c3b8,#1d2824, 155-179 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-69 #b2c3b8,#1d2824, 70-89 d,#1d2824, 90-91 #ffffff,#496656, 92-92 d,#1d2824, 93-121 #b2c3b8,#1d2824, 122-129 #d3b0d9,#1d2824, 130-159 #b2c3b8,#1d2824, 160-179 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-89 d,#0d1410, 90-90 d,#608269, 91-179 d,#0d1410, @@ -46,7 +46,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-34 #d3b0d9,#1d2824, 35-64 #b2c3b8,#1d2824, 65-89 d,#1d2824, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-69 #b2c3b8,#1d2824, 70-89 d,#1d2824, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410, diff --git a/test/snapshots/ghostty-mid.golden b/test/snapshots/ghostty-mid.golden index 70a40fe7..965f6896 100644 --- a/test/snapshots/ghostty-mid.golden +++ b/test/snapshots/ghostty-mid.golden @@ -1,7 +1,7 @@ == snap ghostty-mid grid=100x31 cursor=4,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ghostty-mid/cwd Tty Save Mode /tmp/pardes-snap/ghostty-mid/cwd Tty Save Mode +| /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save | ZZPAD ls | ./g.sh g.txt | ./g.sh @@ -16,6 +16,6 @@ | | | -| /tmp/pardes-snap/ghostty-mid/cwd Tty Save Mode +| /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save | ls | g.txt diff --git a/test/snapshots/grep.golden b/test/snapshots/grep.golden index 7ecb24af..29b18db0 100644 --- a/test/snapshots/grep.golden +++ b/test/snapshots/grep.golden @@ -1,7 +1,7 @@ == snap opened grid=180x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/grep/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/grep/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/grep/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/grep/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | a.txt noise.txt sub a.txt noise.txt sub | @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/grep/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/grep/cwd Tty+bash Save Mode Filter Collapse Del | ls | a.txt noise.txt sub == snap typing grid=180x31 cursor=89,7 diff --git a/test/snapshots/hscroll.golden b/test/snapshots/hscroll.golden index bc1a7ff1..4e9fd9af 100644 --- a/test/snapshots/hscroll.golden +++ b/test/snapshots/hscroll.golden @@ -1,7 +1,7 @@ == snap opened grid=90x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/hscroll/cwd Tty Save Mode /tmp/pardes-snap/hscroll/cwd Tty Save Mode +| /tmp/pardes-snap/hscroll/cwd Tty+bash Save /tmp/pardes-snap/hscroll/cwd Tty+bash Save | ls ls | wide.txt wide.txt | @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/hscroll/cwd Tty Save Mode +| /tmp/pardes-snap/hscroll/cwd Tty+bash Save | ls | wide.txt == snap wheeled grid=90x31 cursor=7,7 diff --git a/test/snapshots/hscroll.snap b/test/snapshots/hscroll.snap index 6f73154f..05366323 100644 --- a/test/snapshots/hscroll.snap +++ b/test/snapshots/hscroll.snap @@ -36,10 +36,10 @@ send \x1b[<67;10;8M send \x1b[<67;10;8M stable 400 5000 snap clamped -# $ walks the cursor to the line end: the view follows with the margin +# gl walks the cursor to the line end: the view follows with the margin key g g stable 400 5000 -key $ +key g l stable 400 5000 snap dollar # 0 back to column zero: the view snaps home diff --git a/test/snapshots/images.golden b/test/snapshots/images.golden index 5b72d72b..11d4ae3b 100644 --- a/test/snapshots/images.golden +++ b/test/snapshots/images.golden @@ -1,7 +1,7 @@ == snap img-open grid=150x31 cursor=11,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | pic.ppm pic.ppm | @@ -16,13 +16,13 @@ | ▚i,V🬆🬹 | mu🬆🬻 | 🬆🬻 -| /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del | ls | pic.ppm == style img-open grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-74 d,#26362d, 75-76 #ffffff,#887455, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-149 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-105 #b2c3b8,#1d2824, 106-108 #d3b0d9,#1d2824, 109-138 #b2c3b8,#1d2824, 139-149 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-105 #b2c3b8,#1d2824, 106-113 #d3b0d9,#1d2824, 114-143 #b2c3b8,#1d2824, 144-149 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#152019, 1-27 d,#0d1410, 28-31 #959595,#6c6c6c, 32-32 #959595,#6c5eb5, 33-33 #352879,#6c5eb5, 34-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-30 #6c6c6c,#6c5eb5, 31-31 #352879,#6c6c6c, 32-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#6c5eb5, 29-29 #352879,#6c6c6c, 30-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, @@ -78,7 +78,7 @@ == snap tag-edit grid=400x31 cursor=87,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | pic.ppm pic.ppm | @@ -93,6 +93,6 @@ | 🬜🬬██🬸▄╲ | 🬺█🬝🬵█ | 🬝▄ -| /tmp/pardes-snap/images/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del | ls | pic.ppm diff --git a/test/snapshots/jumps.golden b/test/snapshots/jumps.golden index fa50f66c..75e39f9b 100644 --- a/test/snapshots/jumps.golden +++ b/test/snapshots/jumps.golden @@ -32,7 +32,7 @@ | 28 line 28 == snap to-shell grid=150x31 cursor=79,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/jumps/cwd/big.txt Save Tty Collapse Del /tmp/pardes-snap/jumps/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/jumps/cwd/big.txt Save Tty Collapse Del /tmp/pardes-snap/jumps/cwd Tty+bash Save Mode Filter Collapse Del |3: 1 line 1 ls |4: 2 line 2 big.txt == snap to-file grid=150x31 cursor=7,3 @@ -143,7 +143,7 @@ == style step1-shell grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-74 d,#1d2824, 75-76 #ffffff,#887455, 77-77 d,#26362d, 78-109 #becfc4,#26362d, 110-149 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-36 #d3b0d9,#1d2824, 37-58 #b2c3b8,#1d2824, 59-74 d,#1d2824, 75-76 #000000,#add38a, 77-77 d,#26362d, 78-104 #becfc4,#26362d, 105-107 #dfbce5,#26362d, 108-137 #becfc4,#26362d, 138-149 d,#26362d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-36 #d3b0d9,#1d2824, 37-58 #b2c3b8,#1d2824, 59-74 d,#1d2824, 75-76 #000000,#add38a, 77-77 d,#26362d, 78-104 #becfc4,#26362d, 105-112 #dfbce5,#26362d, 113-142 #becfc4,#26362d, 143-149 d,#26362d, |3: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, diff --git a/test/snapshots/layout-close.golden b/test/snapshots/layout-close.golden index 926ff1f7..a7d159b5 100644 --- a/test/snapshots/layout-close.golden +++ b/test/snapshots/layout-close.golden @@ -1,19 +1,19 @@ == snap built grid=140x41 cursor=4,17 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps | ls ls | marker.txt marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps | ls | marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps | ls | marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps | ls | marker.txt | @@ -21,12 +21,12 @@ | | | -| /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps | ls | marker.txt == snap altc grid=140x41 cursor=39,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps |3: ls ls ls |4: marker.txt marker.txt marker.txt |6: /tmp/pardes-snap/layout-close/cw @@ -39,7 +39,7 @@ |6: |7: |8: -== snap exit grid=140x41 cursor=39,8 +== snap exit grid=140x41 cursor=4,8 |10: |11: |12: diff --git a/test/snapshots/layout-close.snap b/test/snapshots/layout-close.snap index 50a11cc3..4ae3e31f 100644 --- a/test/snapshots/layout-close.snap +++ b/test/snapshots/layout-close.snap @@ -35,8 +35,8 @@ key k stable 700 10000 snap del # exit the (now) 2nd shell through its real tty: pty EOF, again only the pane -# above absorbs — and focus falls back to the -# pane that had it before this one (the moved-out column), not to slot 0 +# above absorbs — and takes the focus too, not the pane that had it before +# this one (the moved-out column) press left 40 9 release left 40 9 stable 400 5000 diff --git a/test/snapshots/layout-open.golden b/test/snapshots/layout-open.golden index f682924a..79492382 100644 --- a/test/snapshots/layout-open.golden +++ b/test/snapshots/layout-open.golden @@ -1,7 +1,7 @@ == snap before grid=100x31 cursor=2,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/layout-open/cwd Tty Save Mode /tmp/pardes-snap/layout-open/cwd Tty Save Mode +| /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save | ls ls | sub sub | @@ -16,21 +16,22 @@ | | | -| /tmp/pardes-snap/layout-open/cwd Tty Save Mode +| /tmp/pardes-snap/layout-open/cwd Tty+bash Save | ls | sub == snap altn-left grid=100x31 cursor=4,9 -|6: /tmp/pardes-snap/layout-open/cwd Tty Save Mode +|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save |7: ls |8: sub == snap altn-right grid=100x31 cursor=54,9 -|6: /tmp/pardes-snap/layout-open/cwd Tty Save Mode /tmp/pardes-snap/layout-open/cwd Tty Save Mode +|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save |7: ls ls |8: sub sub == snap file-open grid=100x31 cursor=7,22 |21: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save |22: 1 echo 'TTY'OK == snap ttydir grid=100x31 cursor=7,22 -|10: /tmp/pardes-snap/layout-open/cwd/sub Tty Save M -|11: echo 'TTY'OK +|10: /tmp/pardes-snap/layout-open/cwd/sub (echo 'TTY +|11: % echo 'TTY'OK |12: TTYOK +|13: exit 0 diff --git a/test/snapshots/layout-open.snap b/test/snapshots/layout-open.snap index d7d7e19e..04770f27 100644 --- a/test/snapshots/layout-open.snap +++ b/test/snapshots/layout-open.snap @@ -1,5 +1,5 @@ # every pane open splits ONE existing pane, the rest of the column keeps its -# sizes: Alt-n splits the active pane, an execute-spawned shell for a file's +# sizes: Alt-n splits the active pane, an execute-spawned command pane for a file's # dir splits only the bottom pane of the rightmost column. At this narrow # width the first looked-up file also splits below its source, leaving every # unrelated pane (including the ones above the source) at its exact rectangle. @@ -44,9 +44,9 @@ press right 7 28 release right 7 28 stable 700 15000 snap file-open -# execute the file's command (middle drag over line 1): no shell has cwd sub/, -# so one is forked at the bottom of the rightmost column, splitting only the -# pane above it +# execute the file's command (middle drag over line 1): it runs as a command +# pane for sub/, forked at the bottom of the rightmost column, splitting only +# the pane above it press middle 8 23 drag middle 19 23 release middle 19 23 diff --git a/test/snapshots/load.golden b/test/snapshots/load.golden index 4671787a..3a3c596a 100644 --- a/test/snapshots/load.golden +++ b/test/snapshots/load.golden @@ -1,7 +1,7 @@ == snap loaded grid=100x31 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/load/cwd Tty Save Mode Filter /tmp/pardes-snap/load/cwd Tty Save Mode Filter +| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi | $ echo LOAD''MARK; seq 1 40; echo E''ND $ ls | LOADMARK d.txt | 1 $ @@ -16,7 +16,7 @@ | 10 | 11 | 12 -| /tmp/pardes-snap/load/cwd Tty Save Mode Filter +| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi | $ ls | d.txt | $ @@ -24,7 +24,7 @@ == style loaded grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-31 #dfbce5,#26362d, 32-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-81 #d3b0d9,#1d2824, 82-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -39,7 +39,7 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-31 #d3b0d9,#1d2824, 32-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/logquiet.golden b/test/snapshots/logquiet.golden index e3fbe0f1..366fc0f7 100644 --- a/test/snapshots/logquiet.golden +++ b/test/snapshots/logquiet.golden @@ -1,7 +1,7 @@ == snap quiet grid=100x31 cursor=62,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/logquiet/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/logquiet/cwd Tty+bash Save Mode Filter Collapse Del | $ ls | $ printf '\033_Ga=q,i=1,s=1,v=1,f=24,t=d;AAAA\033\\\033[>0p' | $ '\033_Ga=q,i=1,s=1,v=1,f=24,t=d;AAAA\033\\\033[>0p'Gi=1;OK diff --git a/test/snapshots/look-center.golden b/test/snapshots/look-center.golden index bd97a25d..3eaa819b 100644 --- a/test/snapshots/look-center.golden +++ b/test/snapshots/look-center.golden @@ -1,7 +1,7 @@ == snap center-open grid=150x31 cursor=7,12 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/look-center/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/look-center/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | big.txt big.txt | @@ -16,7 +16,7 @@ | 102 line 102 | 103 line 103 | 104 line 104 -| /tmp/pardes-snap/look-center/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del | ls | big.txt == snap center-dedup grid=150x31 cursor=7,12 diff --git a/test/snapshots/look-dir.golden b/test/snapshots/look-dir.golden index 1d9f12e3..42627413 100644 --- a/test/snapshots/look-dir.golden +++ b/test/snapshots/look-dir.golden @@ -1,11 +1,11 @@ == snap dir-open grid=100x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/look-dir/cwd Tty Save Mode Fil /tmp/pardes-snap/look-dir/cwd Tty Save Mode Fil +| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod | ls ls | sub top.txt sub top.txt | -| /tmp/pardes-snap/look-dir/cwd/sub Tty Save Mode +| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Save | ls | inner.txt | @@ -16,6 +16,6 @@ | | | -| /tmp/pardes-snap/look-dir/cwd Tty Save Mode Fil +| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod | ls | sub top.txt diff --git a/test/snapshots/look-file-1col.golden b/test/snapshots/look-file-1col.golden index fb0bd6cd..f70f787b 100644 --- a/test/snapshots/look-file-1col.golden +++ b/test/snapshots/look-file-1col.golden @@ -1,7 +1,7 @@ == snap file-open-1col grid=100x31 cursor=7,19 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/look-file-1col/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/look-file-1col/cwd Tty+bash Save Mode Filter Collapse Del | ls | big.txt | diff --git a/test/snapshots/look-file.golden b/test/snapshots/look-file.golden index 0091c966..19b7899d 100644 --- a/test/snapshots/look-file.golden +++ b/test/snapshots/look-file.golden @@ -1,7 +1,7 @@ == snap file-open grid=150x31 cursor=7,12 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/look-file/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/look-file/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | big.txt big.txt | @@ -16,13 +16,13 @@ | 42 line 42 | 43 line 43 | 44 line 44 -| /tmp/pardes-snap/look-file/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del | ls | big.txt == style file-open grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-74 d,#26362d, 75-76 #ffffff,#887455, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-149 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-36 #d3b0d9,#1d2824, 37-66 #b2c3b8,#1d2824, 67-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-108 #b2c3b8,#1d2824, 109-111 #d3b0d9,#1d2824, 112-141 #b2c3b8,#1d2824, 142-149 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-41 #d3b0d9,#1d2824, 42-71 #b2c3b8,#1d2824, 72-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-108 #b2c3b8,#1d2824, 109-116 #d3b0d9,#1d2824, 117-146 #b2c3b8,#1d2824, 147-149 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-36 #d3b0d9,#1d2824, 37-66 #b2c3b8,#1d2824, 67-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-41 #d3b0d9,#1d2824, 42-71 #b2c3b8,#1d2824, 72-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/lookloaded.golden b/test/snapshots/lookloaded.golden index cc2f4313..8883609a 100644 --- a/test/snapshots/lookloaded.golden +++ b/test/snapshots/lookloaded.golden @@ -1,7 +1,7 @@ == snap file-open grid=100x31 cursor=7,12 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lookloaded/cwd Tty Save Mode F /tmp/pardes-snap/lookloaded/cwd Tty Save Mode F +| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M | ls ls | big.txt big.txt | @@ -16,7 +16,7 @@ | 42 line 42 | 43 line 43 | 44 line 44 -| /tmp/pardes-snap/lookloaded/cwd Tty Save Mode F +| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M | ls | big.txt == snap removed grid=100x31 cursor=54,7 diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden index ae63b50f..bfaf1d04 100644 --- a/test/snapshots/lookpanes.golden +++ b/test/snapshots/lookpanes.golden @@ -1,11 +1,11 @@ == snap shells grid=130x51 cursor=122,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd Tty Save Mode Filter Collapse D +| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Colla | 1 both.txt ls | 2 b-marker alpha beta zzboot.txt zqonly.txt pick.txt twin.txt | -| /tmp/pardes-snap/lookpanes/cwd/alpha Tty Save Mode Filter Coll +| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter | ls | amark.txt both.txt pick.txt twin.txt zqonly.txt == snap other-dir grid=130x51 cursor=7,28 @@ -23,11 +23,11 @@ == style mru-beta grid=130x51 |0: 0-70 #b2c3b8,#1d2824, 71-129 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-64 d,#26362d, 65-66 #ffffff,#887455, 67-67 d,#1d2824, 68-99 #b2c3b8,#1d2824, 100-129 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-64 #b2c3b8,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-101 #d3b0d9,#1d2824, 102-129 #b2c3b8,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-64 #b2c3b8,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-106 #d3b0d9,#1d2824, 107-129 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-121 #e5eee5,#0d1410, 122-129 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-107 #d3b0d9,#1d2824, 108-129 #b2c3b8,#1d2824, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, |9: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-65 d,#608269, 66-129 d,#0d1410, diff --git a/test/snapshots/modal.golden b/test/snapshots/modal.golden index 2f71290b..0147d5c8 100644 --- a/test/snapshots/modal.golden +++ b/test/snapshots/modal.golden @@ -1,7 +1,7 @@ == snap top grid=100x31 cursor=2,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/modal/cwd Tty Save Mode Filter /tmp/pardes-snap/modal/cwd Tty Save Mode Filter +| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F | ls ls | words.txt words.txt | printf 'one two three\nfour five six\nseven ei @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/modal/cwd Tty Save Mode Filter +| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F | ls | words.txt == snap moved grid=100x31 cursor=8,5 @@ -24,7 +24,7 @@ == style xsel grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-32 #dfbce5,#26362d, 33-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-82 #d3b0d9,#1d2824, 83-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -39,7 +39,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-32 #d3b0d9,#1d2824, 33-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/multicursor.golden b/test/snapshots/multicursor.golden index 8f4c4451..96762bd5 100644 --- a/test/snapshots/multicursor.golden +++ b/test/snapshots/multicursor.golden @@ -1,7 +1,7 @@ == snap three-cursors grid=150x31 cursor=7,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/multicursor/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/multicursor/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/multicursor/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/multicursor/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | f.txt f.txt | @@ -16,13 +16,13 @@ | 8 line 8 | 9 line 9 | 10 line 10 -| /tmp/pardes-snap/multicursor/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/multicursor/cwd Tty+bash Save Mode Filter Collapse Del | ls | f.txt == style three-cursors grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-74 d,#26362d, 75-76 #ffffff,#887455, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-149 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-35 #b2c3b8,#1d2824, 36-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-110 #b2c3b8,#1d2824, 111-113 #d3b0d9,#1d2824, 114-143 #b2c3b8,#1d2824, 144-149 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-35 #b2c3b8,#1d2824, 36-43 #d3b0d9,#1d2824, 44-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-110 #b2c3b8,#1d2824, 111-118 #d3b0d9,#1d2824, 119-148 #b2c3b8,#1d2824, 149-149 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-35 #b2c3b8,#1d2824, 36-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-35 #b2c3b8,#1d2824, 36-43 #d3b0d9,#1d2824, 44-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/nested.golden b/test/snapshots/nested.golden index 2b69b055..3485b4be 100644 --- a/test/snapshots/nested.golden +++ b/test/snapshots/nested.golden @@ -1,7 +1,7 @@ == snap file-arg grid=100x31 cursor=7,27 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/nested/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/nested/cwd Tty+bash Save Mode Filter Collapse Del | $ $(readlink /proc/$PPID/exe) nested.txt | $ | @@ -32,7 +32,7 @@ == snap no-arg grid=100x31 cursor=4,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/nested/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/nested/cwd Tty+bash Save Mode Filter Collapse Del | $ $(readlink /proc/$PPID/exe) | pardes: this shell is already inside pardes, and a pardes inside a pardes | is spicy. Name a file or a directory and the outer session opens it, or @@ -56,10 +56,10 @@ == snap dir-arg grid=100x31 cursor=4,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/nested/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/nested/cwd Tty+bash Save Mode Filter Collapse Del | $ $(readlink /proc/$PPID/exe) nestdir | $ -| /tmp/pardes-snap/nested/cwd/nestdir Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/nested/cwd/nestdir Tty+bash Save Mode Filter Collapse Del | ls | inside-nested.txt | diff --git a/test/snapshots/psearch.golden b/test/snapshots/psearch.golden index f21d8ae1..459b667b 100644 --- a/test/snapshots/psearch.golden +++ b/test/snapshots/psearch.golden @@ -1,7 +1,7 @@ == snap output grid=140x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/psearch/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/psearch/cwd Tty+bash Save Mode Filter Collapse Del | printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' | x MARK a | zz @@ -36,7 +36,7 @@ == style results grid=140x31 |0: 0-70 #b2c3b8,#1d2824, 71-139 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-139 d,#26362d, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-34 #dfbce5,#26362d, 35-64 #becfc4,#26362d, 65-139 d,#26362d, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-69 #becfc4,#26362d, 70-139 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-3 #e5eee5,#0d1410, 4-7 #f5f7ee,#3c614a, 8-139 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410, @@ -71,10 +71,10 @@ |12: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig |13: 1 == snap ttyinert grid=140x31 cursor=4,9 -|2: $ /tmp/pardes-snap/psearch/cwd Tty Save Mode Filter Collapse Del +|2: $ /tmp/pardes-snap/psearch/cwd Tty+bash Save Mode Filter Collapse Del |3: $ printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' |9: $ == snap rightsearch grid=140x31 cursor=7,6 -|2: /tmp/pardes-snap/psearch/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/psearch/cwd Tty+bash Save Mode Filter Collapse Del |3: printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' |9: diff --git a/test/snapshots/reflow.golden b/test/snapshots/reflow.golden index 958fe934..ab8bfe9b 100644 --- a/test/snapshots/reflow.golden +++ b/test/snapshots/reflow.golden @@ -1,7 +1,7 @@ == snap widened grid=200x25 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/reflow/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/reflow/cwd Tty+bash Save Mode Filter Collapse Del | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy | yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy diff --git a/test/snapshots/respawn.golden b/test/snapshots/respawn.golden index f6027895..d6d21a81 100644 --- a/test/snapshots/respawn.golden +++ b/test/snapshots/respawn.golden @@ -1,10 +1,10 @@ == snap respawn grid=140x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/respawn/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/respawn/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | -|$ /tmp/pardes-snap/respawn/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del | $ ls | $ echo RESP''AWNOK | RESPAWNOK diff --git a/test/snapshots/restore.golden b/test/snapshots/restore.golden index d6c81e75..771b2ac0 100644 --- a/test/snapshots/restore.golden +++ b/test/snapshots/restore.golden @@ -1,12 +1,12 @@ == snap dumped grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit Restore /tmp/pardes-snap/res | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/restore/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/restore/cwd Tty+bash Save Mode Filter Collapse Del | $ ls | $ echo KEEP''ME | KEEPME | $ == snap restored grid=100x31 cursor=4,8 |0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit -|2: /tmp/pardes-snap/restore/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/restore/cwd Tty+bash Save Mode Filter Collapse Del |7: ── restored history ── diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden index 2f8214e0..8312e1bc 100644 --- a/test/snapshots/rsearch.golden +++ b/test/snapshots/rsearch.golden @@ -1,7 +1,7 @@ == snap opened grid=210x31 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/rsearch/cwd/code.txt Save Tty Collapse Del /tmp/pardes-snap/rsearch/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/rsearch/cwd/code.txt Save Tty Collapse Del /tmp/pardes-snap/rsearch/cwd Tty+bash Save Mode Filter Collapse Del | 1 run count_x now ls | 2 open sub/dep.txt:3 here code.txt sub | 3 count_x again diff --git a/test/snapshots/savepane.golden b/test/snapshots/savepane.golden index 9a27342c..5fcdf656 100644 --- a/test/snapshots/savepane.golden +++ b/test/snapshots/savepane.golden @@ -1,7 +1,7 @@ == snap scratch grid=100x31 cursor=20,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/savepane/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/savepane/cwd Tty+bash Save Mode Filter Collapse Del | ls | | /tmp/pardes-snap/savepane/cwd/+New * Save Tty Collapse Del diff --git a/test/snapshots/scroll.golden b/test/snapshots/scroll.golden index 8a48dcca..5960bdc5 100644 --- a/test/snapshots/scroll.golden +++ b/test/snapshots/scroll.golden @@ -1,7 +1,7 @@ == snap bottom grid=100x31 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/scroll/cwd Tty Save Mode Filte /tmp/pardes-snap/scroll/cwd Tty Save Mode Filte +| /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode | 188 ls | 189 s.txt | 190 @@ -16,7 +16,7 @@ | 199 | 200 | -| /tmp/pardes-snap/scroll/cwd Tty Save Mode Filte +| /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode | ls | s.txt == snap wheel3 grid=100x31 cursor=4,16 @@ -82,7 +82,7 @@ == style wheel-down grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-33 #dfbce5,#26362d, 34-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-83 #d3b0d9,#1d2824, 84-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-38 #dfbce5,#26362d, 39-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-88 #d3b0d9,#1d2824, 89-99 #b2c3b8,#1d2824, |3: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -97,7 +97,7 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/scrolloff.golden b/test/snapshots/scrolloff.golden index acc15f71..3201ec6c 100644 --- a/test/snapshots/scrolloff.golden +++ b/test/snapshots/scrolloff.golden @@ -1,7 +1,7 @@ == snap top grid=210x12 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/scrolloff/cwd/g.txt Save Tty Collapse Del /tmp/pardes-snap/scrolloff/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/scrolloff/cwd/g.txt Save Tty Collapse Del /tmp/pardes-snap/scrolloff/cwd Tty+bash Save Mode Filter Collapse Del | 1 line 1 ls | 2 line 2 g.txt | 3 line 3 diff --git a/test/snapshots/selarg.golden b/test/snapshots/selarg.golden index 068a256d..cc4cdec1 100644 --- a/test/snapshots/selarg.golden +++ b/test/snapshots/selarg.golden @@ -1,7 +1,7 @@ == snap kept grid=100x31 cursor=6,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/selarg/cwd Tty Save Mode Filte /tmp/pardes-snap/selarg/cwd Tty Save Mode Filte +| /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode | ZZARG ZZVEE ./c.sh | | @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/selarg/cwd Tty Save Mode Filte +| /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode | ls | s.txt == snap crosspane grid=100x31 cursor=6,3 diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden index f68065b9..85fcb474 100644 --- a/test/snapshots/shellset.golden +++ b/test/snapshots/shellset.golden @@ -7,6 +7,6 @@ |3: 1 Shell zznosuchshell Shell == snap fallback-shell grid=90x21 cursor=49,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Save /tmp/pardes-snap/shellset/cwd Tty Save Mod +|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Save /tmp/pardes-snap/shellset/cwd Tty+bash Sav |3: 1 Shell zznosuchshell ls |4: cmd.txt zmarkfile.txt diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden index 1fb12c15..8ed65105 100644 --- a/test/snapshots/shrinktag.golden +++ b/test/snapshots/shrinktag.golden @@ -1,7 +1,7 @@ == snap before grid=100x31 cursor=2,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi +| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo | ls ls | w.txt w.txt | @@ -16,11 +16,11 @@ | | | -| /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi +| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo | ls | w.txt == snap shrunk grid=100x31 cursor=2,9 -|3: /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi ls +|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo ls |4: ls w.txt |5: w.txt |8: @@ -34,11 +34,11 @@ |4: w.txt w.txt |5: |8: echo one; echo two -|17: /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi +|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo |18: ls |19: w.txt == snap bottom-shrunk grid=100x31 cursor=2,9 |17: |18: |19: -|30: /tmp/pardes-snap/shrinktag/cwd Tty Save Mode Fi +|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden index 14b95f26..55488973 100644 --- a/test/snapshots/splitparent.golden +++ b/test/snapshots/splitparent.golden @@ -1,26 +1,26 @@ == snap shrunk grid=100x31 cursor=4,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode /tmp/pardes-snap/splitparent/cwd Tty Save Mode -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode ls +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls | ls == snap swap-parent grid=100x31 cursor=4,8 -|6: /tmp/pardes-snap/splitparent/cwd Tty Save Mode +|6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save |7: ls == snap tallest grid=100x6 cursor=54,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode /tmp/pardes-snap/splitparent/cwd Tty Save Mode -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode /tmp/pardes-snap/splitparent/cwd Tty Save Mode +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save == snap regrown grid=100x31 cursor=54,19 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode /tmp/pardes-snap/splitparent/cwd Tty Save Mode -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode ls +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls | ls | -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save | ls | | @@ -31,5 +31,5 @@ | | | -| /tmp/pardes-snap/splitparent/cwd Tty Save Mode +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save | ls diff --git a/test/snapshots/syntax.golden b/test/snapshots/syntax.golden index cf8c6d9a..3e3679c6 100644 --- a/test/snapshots/syntax.golden +++ b/test/snapshots/syntax.golden @@ -1,7 +1,7 @@ == snap zig-open grid=150x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/syntax/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/syntax/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/syntax/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/syntax/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | demo.zig demo.zig | @@ -16,13 +16,13 @@ | | | -| /tmp/pardes-snap/syntax/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/syntax/cwd Tty+bash Save Mode Filter Collapse Del | ls | demo.zig == style zig-open grid=150x31 |0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-74 d,#26362d, 75-76 #ffffff,#887455, 77-77 d,#1d2824, 78-109 #b2c3b8,#1d2824, 110-149 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-105 #b2c3b8,#1d2824, 106-108 #d3b0d9,#1d2824, 109-138 #b2c3b8,#1d2824, 139-149 d,#1d2824, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-76 #ffffff,#496656, 77-77 d,#1d2824, 78-105 #b2c3b8,#1d2824, 106-113 #d3b0d9,#1d2824, 114-143 #b2c3b8,#1d2824, 144-149 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, @@ -65,7 +65,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-75 d,#608269, 76-149 d,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-149 d,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-3 #e5eee5,#0d1410, 4-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-9 #e5eee5,#0d1410, 10-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, @@ -93,7 +93,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-33 #d3b0d9,#1d2824, 34-63 #b2c3b8,#1d2824, 64-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/tag.golden b/test/snapshots/tag.golden index b6403902..0f9af5dc 100644 --- a/test/snapshots/tag.golden +++ b/test/snapshots/tag.golden @@ -1,7 +1,7 @@ -== snap tag-focus grid=150x31 cursor=31,2 +== snap tag-focus grid=150x31 cursor=36,2 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | t.txt t.txt | @@ -16,15 +16,15 @@ | | | -| /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del | ls | t.txt -== snap tag-typed grid=150x31 cursor=37,2 -|2: /tmp/pardes-snap/tag/cwd Tty t.txt Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del -== snap tag-del grid=150x31 cursor=33,2 -|2: /tmp/pardes-snap/tag/cwd Tty t Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del +== snap tag-typed grid=150x31 cursor=42,2 +|2: /tmp/pardes-snap/tag/cwd Tty+bash t.txt Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del +== snap tag-del grid=150x31 cursor=38,2 +|2: /tmp/pardes-snap/tag/cwd Tty+bash t Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del == snap tag-exit grid=150x31 cursor=4,5 -|2: /tmp/pardes-snap/tag/cwd Tty t.txt Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/tag/cwd Tty+bash t.txt Save Mode Filter Collapse Del /tmp/pardes-snap/tag/cwd Tty+bash Save Mode Filter Collapse Del == snap tag-look grid=150x31 cursor=7,7 |6: /tmp/pardes-snap/tag/cwd/t.txt Save Tty Collapse Del |7: 1 ZZLOOKED diff --git a/test/snapshots/tagalign.golden b/test/snapshots/tagalign.golden index 72133440..6873c19a 100644 --- a/test/snapshots/tagalign.golden +++ b/test/snapshots/tagalign.golden @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep == snap fits grid=87x31 cursor=4,20 @@ -37,7 +37,7 @@ | | | -| /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep == snap overby1 grid=86x31 cursor=4,20 @@ -58,7 +58,7 @@ | | | -| /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep == snap clicked grid=100x31 cursor=11,2 @@ -79,7 +79,7 @@ | | | -| /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep == snap revealed grid=70x31 cursor=69,2 @@ -100,11 +100,11 @@ | | | -| /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De | ls | deep == snap alone grid=70x31 cursor=4,5 -|2: /tmp/pardes-snap/tagalign/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De |3: ls |4: deep |5: diff --git a/test/snapshots/tagalign2col.golden b/test/snapshots/tagalign2col.golden index 81329d0c..25d42e37 100644 --- a/test/snapshots/tagalign2col.golden +++ b/test/snapshots/tagalign2col.golden @@ -1,18 +1,18 @@ == snap onecol grid=160x31 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse De /tmp/pardes-snap/tagalign2col/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse De /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del | 1 alpha ls Left | 2 beta deep | 3 gamma == snap aligned grid=160x31 cursor=4,20 |3: 1 alpha ls -|17: /tmp/pardes-snap/tagalign2col/cwd Tty Save Mode Filter Collapse Del +|17: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del |18: ls |19: deep == snap widened grid=160x31 cursor=4,20 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty Save Mode Filter Co +|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filt |3: 1 alpha ╎ ls |4: 2 beta ╎ deep |5: 3 gamma ╎ @@ -27,7 +27,7 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/tagalign2col/cwd Tty Save Mode Filter Collapse Del ╎ +|17: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del ╎ |18: ls ╎ |19: deep ╎ |20: ╎ diff --git a/test/snapshots/tagbottomimage.golden b/test/snapshots/tagbottomimage.golden index 06d23077..421f90a0 100644 --- a/test/snapshots/tagbottomimage.golden +++ b/test/snapshots/tagbottomimage.golden @@ -1,7 +1,7 @@ == snap img-top grid=150x31 cursor=11,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagbottomimage/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tagbottomimage/cwd Tty+bash Save Mode Filter Collapse Del | ls | pic.ppm | @@ -34,7 +34,7 @@ |2: ls |3: pic.ppm |4: -|5: /tmp/pardes-snap/tagbottomimage/cwd Tty Save Mode Filter Collapse Del +|5: /tmp/pardes-snap/tagbottomimage/cwd Tty+bash Save Mode Filter Collapse Del |6: Tagbottom |7: {DbgY🬲s▜M^#▐FUJ[zNq=🬝g/y╱?V@🬱🬙▌UK╱🬍("🬕KJ8hSo🬝🬻 |8: tX🬆s>🬌`🬵<w_🬏##?h🬫(}╲J,╳`bWz🬴Ea{╳j🬑🬢j-^d%^P🬝🬻█ @@ -66,7 +66,7 @@ |2: 0-0 d,#608269, 1-1 d,#0d1410, 2-149 #e5eee5,#0d1410, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-149 d,#0d1410, -|5: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-41 #d3b0d9,#1d2824, 42-71 #b2c3b8,#1d2824, 72-149 d,#1d2824, +|5: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-76 #b2c3b8,#1d2824, 77-149 d,#1d2824, |6: 0-0 d,#152019, 1-138 d,#0d1410, 139-139 d,#1d2824, 140-148 #b2c3b8,#1d2824, 149-149 d,#1d2824, |7: 0-0 d,#152019, 1-52 d,#0d1410, 53-55 #959595,#6c6c6c, 56-56 #6c6c6c,#959595, 57-57 #959595,#6c6c6c, 58-58 #6c6c6c,#959595, 59-60 #959595,#6c6c6c, 61-63 #6c6c6c,#959595, 64-65 #959595,#6c6c6c, 66-66 #6c6c6c,#959595, 67-71 #959595,#6c6c6c, 72-72 #6c6c6c,#959595, 73-73 #959595,#6c6c6c, 74-76 #6c6c6c,#959595, 77-77 #959595,#6c6c6c, 78-81 #6c6c6c,#959595, 82-84 #959595,#6c6c6c, 85-85 #6c6c6c,#959595, 86-87 #959595,#6c6c6c, 88-88 #6c6c6c,#959595, 89-89 #959595,#6c6c6c, 90-90 #6c6c6c,#959595, 91-92 #959595,#6c6c6c, 93-96 #6c6c6c,#959595, 97-97 #6c6c6c,#352879, 98-98 #352879,#959595, 99-149 d,#0d1410, |8: 0-0 d,#152019, 1-52 d,#0d1410, 53-58 #959595,#6c6c6c, 59-59 #444444,#959595, 60-64 #959595,#6c6c6c, 65-66 #6c6c6c,#959595, 67-71 #959595,#6c6c6c, 72-72 #444444,#959595, 73-73 #959595,#6c6c6c, 74-74 #6c6c6c,#959595, 75-75 #959595,#6c6c6c, 76-76 #444444,#959595, 77-81 #6c6c6c,#959595, 82-82 #959595,#6c6c6c, 83-85 #6c6c6c,#959595, 86-87 #959595,#6c6c6c, 88-89 #6c6c6c,#959595, 90-90 #959595,#6c6c6c, 91-92 #6c6c6c,#959595, 93-93 #959595,#6c6c6c, 94-94 #6c6c6c,#959595, 95-95 #6c6c6c,#352879, 96-96 #352879,#959595, 97-97 #352879,#6c5eb5, 98-98 #352879,#352879, 99-149 d,#0d1410, @@ -120,4 +120,4 @@ |27: |28: |29: -|30: /tmp/pardes-snap/tagbottomimage/cwd Tty Save Mode Filter Collapse Del +|30: /tmp/pardes-snap/tagbottomimage/cwd Tty+bash Save Mode Filter Collapse Del diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden index 61a40af5..929ae828 100644 --- a/test/snapshots/tagnav.golden +++ b/test/snapshots/tagnav.golden @@ -1,7 +1,7 @@ == snap start grid=100x31 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode | 1 alpha ls | 2 beta w.txt | 3 gamma @@ -16,31 +16,31 @@ | | | -| /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +| /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode | ls Up | w.txt == snap colon grid=100x31 cursor=28,2 -|2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +|2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode |18: ls == snap word grid=100x31 cursor=32,2 == snap in-tag grid=100x31 cursor=34,2 == snap right grid=100x31 cursor=54,5 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode |3: 1 alpha Right ls == snap right-tag grid=100x31 cursor=70,2 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll p/tagnav/cwd Tty Save Mode Filter Collapse Del +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll nav/cwd Tty+bash Save Mode Filter Collapse Del |3: 1 alpha ls == snap left-tag grid=100x31 cursor=43,2 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode == snap back-to-body grid=100x31 cursor=7,3 == snap tty-on grid=100x31 cursor=54,5 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode |3: 1 alpha $ ls |5: 3 gamma $ -== snap tty-tag grid=100x31 cursor=90,2 +== snap tty-tag grid=100x31 cursor=95,2 == snap tty-restored grid=100x31 cursor=54,5 == snap column-tag grid=100x31 cursor=3,1 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode |3: 1 alpha Up ls |5: 3 gamma == snap column-word grid=100x31 cursor=6,1 diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap index 95c4b751..f145480e 100644 --- a/test/snapshots/tagnav.snap +++ b/test/snapshots/tagnav.snap @@ -98,7 +98,7 @@ snap workspace-tag # lives up here too, so a builtin that frees the session must never return # into a focused header. Dump grows a `Restore <path>` word, which is also the # case where the line gets longer than the column you were parked on. -key $ +key g l stable 400 5000 snap topbar-dollar key B B B B @@ -114,7 +114,7 @@ snap topbar-exec key c-w k key c-w k stable 400 5000 -key $ B B +key g l B B stable 400 5000 snap topbar-restore-word key tab diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden index 02750663..4986eaae 100644 --- a/test/snapshots/theme.golden +++ b/test/snapshots/theme.golden @@ -1,7 +1,7 @@ == snap orchard grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/theme/cwd Tty Save Mode Filter /tmp/pardes-snap/theme/cwd Tty Save Mode Filter +| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F | ls ls | t.txt t.txt | @@ -16,13 +16,13 @@ | | | -| /tmp/pardes-snap/theme/cwd Tty Save Mode Filter +| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F | ls | t.txt == style orchard grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-32 #dfbce5,#26362d, 33-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-82 #d3b0d9,#1d2824, 83-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-32 #d3b0d9,#1d2824, 33-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -55,7 +55,7 @@ == style dusk grid=100x31 |0: 0-50 #c6b6c5,#2c2630, 51-59 #fff0eb,#654053, 60-70 #c6b6c5,#2c2630, 71-99 d,#2c2630, |1: 0-1 #ffffff,#757a5f, 2-2 d,#3b2f3b, 3-34 #d2c2d1,#3b2f3b, 35-49 d,#3b2f3b, 50-51 #ffffff,#757a5f, 52-52 d,#2c2630, 53-84 #c6b6c5,#2c2630, 85-99 d,#2c2630, -|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-32 #a1d5c4,#3b2f3b, 33-49 #d2c2d1,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-82 #96c9b8,#2c2630, 83-99 #c6b6c5,#2c2630, +|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-37 #a1d5c4,#3b2f3b, 38-49 #d2c2d1,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-87 #96c9b8,#2c2630, 88-99 #c6b6c5,#2c2630, |3: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |4: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |5: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-99 d,#39323b, @@ -70,7 +70,7 @@ |14: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |15: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |16: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-32 #96c9b8,#2c2630, 33-49 #c6b6c5,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-37 #96c9b8,#2c2630, 38-49 #c6b6c5,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |18: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |19: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |20: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, @@ -88,7 +88,7 @@ == style ink grid=100x31 |0: 0-50 #d5e4f1,#15212c, 51-59 #ffffff,#1b547c, 60-70 #d5e4f1,#15212c, 71-99 d,#15212c, |1: 0-1 #ffffff,#8f7d5b, 2-2 d,#223342, 3-34 #e1eef9,#223342, 35-49 d,#223342, 50-51 #ffffff,#8f7d5b, 52-52 d,#15212c, 53-84 #d5e4f1,#15212c, 85-99 d,#15212c, -|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-32 #ffeab8,#223342, 33-49 #e1eef9,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-82 #f5e0ac,#15212c, 83-99 #d5e4f1,#15212c, +|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-37 #ffeab8,#223342, 38-49 #e1eef9,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-87 #f5e0ac,#15212c, 88-99 #d5e4f1,#15212c, |3: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |4: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |5: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-99 d,#000000, @@ -103,7 +103,7 @@ |14: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |15: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |16: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-32 #f5e0ac,#15212c, 33-49 #d5e4f1,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-37 #f5e0ac,#15212c, 38-49 #d5e4f1,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |18: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |19: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |20: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, @@ -121,7 +121,7 @@ == style paper grid=100x31 |0: 0-50 #52625a,#e8eae3, 51-59 #203e31,#c5e2d0, 60-70 #52625a,#e8eae3, 71-99 d,#e8eae3, |1: 0-1 #000000,#bda27e, 2-2 d,#dce5dc, 3-34 #4a5a52,#dce5dc, 35-49 d,#dce5dc, 50-51 #000000,#bda27e, 52-52 d,#e8eae3, 53-84 #52625a,#e8eae3, 85-99 d,#e8eae3, -|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-32 #69447a,#dce5dc, 33-49 #4a5a52,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-82 #724c83,#e8eae3, 83-99 #52625a,#e8eae3, +|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-37 #69447a,#dce5dc, 38-49 #4a5a52,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-87 #724c83,#e8eae3, 88-99 #52625a,#e8eae3, |3: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |4: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |5: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-99 d,#f5f3ed, @@ -136,7 +136,7 @@ |14: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |15: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |16: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-32 #724c83,#e8eae3, 33-49 #52625a,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-37 #724c83,#e8eae3, 38-49 #52625a,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |18: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |19: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |20: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, @@ -154,7 +154,7 @@ == style daybreak grid=100x31 |0: 0-50 #29465e,#edf2f7, 51-59 #10364f,#b9dffb, 60-70 #29465e,#edf2f7, 71-99 d,#edf2f7, |1: 0-1 #000000,#b79a7a, 2-2 d,#dfeaf3, 3-34 #213e56,#dfeaf3, 35-49 d,#dfeaf3, 50-51 #000000,#b79a7a, 52-52 d,#edf2f7, 53-84 #29465e,#edf2f7, 85-99 d,#edf2f7, -|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-32 #52255f,#dfeaf3, 33-49 #213e56,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-82 #5a2d67,#edf2f7, 83-99 #29465e,#edf2f7, +|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-37 #52255f,#dfeaf3, 38-49 #213e56,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-87 #5a2d67,#edf2f7, 88-99 #29465e,#edf2f7, |3: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |4: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |5: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-99 d,#ffffff, @@ -169,7 +169,7 @@ |14: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |15: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |16: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-32 #5a2d67,#edf2f7, 33-49 #29465e,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-37 #5a2d67,#edf2f7, 38-49 #29465e,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |18: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |19: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |20: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, @@ -187,7 +187,7 @@ == style atelier grid=100x31 |0: 0-50 #3d6055,#e4f0ed, 51-59 #35492d,#edf2a5, 60-70 #3d6055,#e4f0ed, 71-99 d,#e4f0ed, |1: 0-1 #000000,#bca481, 2-2 d,#d3e7df, 3-34 #35584d,#d3e7df, 35-49 d,#d3e7df, 50-51 #000000,#bca481, 52-52 d,#e4f0ed, 53-84 #3d6055,#e4f0ed, 85-99 d,#e4f0ed, -|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-32 #653f6b,#d3e7df, 33-49 #35584d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-82 #6e4774,#e4f0ed, 83-99 #3d6055,#e4f0ed, +|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-37 #653f6b,#d3e7df, 38-49 #35584d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-87 #6e4774,#e4f0ed, 88-99 #3d6055,#e4f0ed, |3: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |5: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, @@ -202,7 +202,7 @@ |14: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |15: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |16: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-32 #6e4774,#e4f0ed, 33-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, @@ -232,7 +232,7 @@ |14: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |15: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |16: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-32 #6e4774,#e4f0ed, 33-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb, |18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, @@ -251,7 +251,7 @@ == style forge grid=100x31 |0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b, |1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-32 #f8cba7,#2d3440, 33-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-82 #ecbf9b,#22252b, 83-99 #c5c9d2,#22252b, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b, |3: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |4: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |5: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-99 d,#0c0c0e, @@ -266,7 +266,7 @@ |14: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |15: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |16: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-32 #ecbf9b,#22252b, 33-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |18: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |19: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |20: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, @@ -284,7 +284,7 @@ == style lagoon grid=100x31 |0: 0-50 #bbd7cb,#29443f, 51-59 #e6f2f3,#325f6c, 60-70 #bbd7cb,#29443f, 71-99 d,#29443f, |1: 0-1 #ffffff,#9a7662, 2-2 d,#34574d, 3-34 #c7e3d7,#34574d, 35-49 d,#34574d, 50-51 #ffffff,#9a7662, 52-52 d,#29443f, 53-84 #bbd7cb,#29443f, 85-99 d,#29443f, -|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-32 #f0cef8,#34574d, 33-49 #c7e3d7,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-82 #e4c2ec,#29443f, 83-99 #bbd7cb,#29443f, +|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-37 #f0cef8,#34574d, 38-49 #c7e3d7,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-87 #e4c2ec,#29443f, 88-99 #bbd7cb,#29443f, |3: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |4: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |5: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-99 d,#34464c, @@ -299,7 +299,7 @@ |14: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |15: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |16: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-32 #e4c2ec,#29443f, 33-49 #bbd7cb,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-37 #e4c2ec,#29443f, 38-49 #bbd7cb,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |18: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |19: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |20: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, @@ -317,7 +317,7 @@ == style solarium grid=100x31 |0: 0-50 #c7d4bd,#123f43, 51-59 #eee8d5,#1f5c64, 60-70 #c7d4bd,#123f43, 71-99 d,#123f43, |1: 0-1 #ffffff,#347a9a, 2-2 d,#214e50, 3-34 #d3e0c9,#214e50, 35-49 d,#214e50, 50-51 #ffffff,#347a9a, 52-52 d,#123f43, 53-84 #c7d4bd,#123f43, 85-99 d,#123f43, -|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-32 #f5cdec,#214e50, 33-49 #d3e0c9,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-82 #e9c1e0,#123f43, 83-99 #c7d4bd,#123f43, +|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-37 #f5cdec,#214e50, 38-49 #d3e0c9,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-87 #e9c1e0,#123f43, 88-99 #c7d4bd,#123f43, |3: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |4: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |5: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-99 d,#173e45, @@ -332,7 +332,7 @@ |14: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |15: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |16: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-32 #e9c1e0,#123f43, 33-49 #c7d4bd,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-37 #e9c1e0,#123f43, 38-49 #c7d4bd,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |18: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |19: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |20: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, @@ -350,7 +350,7 @@ == style spectrum grid=100x31 |0: 0-50 #ddd8bb,#3b3830, 51-59 #fcfcfa,#513f60, 60-70 #ddd8bb,#3b3830, 71-99 d,#3b3830, |1: 0-1 #ffffff,#706596, 2-2 d,#4d4736, 3-34 #e9e4c7,#4d4736, 35-49 d,#4d4736, 50-51 #ffffff,#706596, 52-52 d,#3b3830, 53-84 #ddd8bb,#3b3830, 85-99 d,#3b3830, -|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-32 #d7e3ff,#4d4736, 33-49 #e9e4c7,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-82 #c5d7ff,#3b3830, 83-99 #ddd8bb,#3b3830, +|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-37 #d7e3ff,#4d4736, 38-49 #e9e4c7,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-87 #c5d7ff,#3b3830, 88-99 #ddd8bb,#3b3830, |3: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |4: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |5: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-99 d,#100e11, @@ -365,7 +365,7 @@ |14: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |15: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |16: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-32 #c5d7ff,#3b3830, 33-49 #ddd8bb,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-37 #c5d7ff,#3b3830, 38-49 #ddd8bb,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |18: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |19: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |20: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, @@ -383,7 +383,7 @@ == style harvest grid=100x31 |0: 0-50 #d5d8b8,#343b2e, 51-59 #f3f2cc,#4f5834, 60-70 #d5d8b8,#343b2e, 71-99 d,#343b2e, |1: 0-1 #ffffff,#9b583f, 2-2 d,#424d38, 3-34 #e1e4c4,#424d38, 35-49 d,#424d38, 50-51 #ffffff,#9b583f, 52-52 d,#343b2e, 53-84 #d5d8b8,#343b2e, 85-99 d,#343b2e, -|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-32 #c5e7f7,#424d38, 33-49 #e1e4c4,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-82 #badbeb,#343b2e, 83-99 #d5d8b8,#343b2e, +|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-37 #c5e7f7,#424d38, 38-49 #e1e4c4,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-87 #badbeb,#343b2e, 88-99 #d5d8b8,#343b2e, |3: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |4: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |5: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-99 d,#100e0b, @@ -398,7 +398,7 @@ |14: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |15: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |16: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-32 #badbeb,#343b2e, 33-49 #d5d8b8,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-37 #badbeb,#343b2e, 38-49 #d5d8b8,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |18: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |19: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |20: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, @@ -416,7 +416,7 @@ == style clay grid=100x31 |0: 0-50 #d7d6b6,#343f36, 51-59 #fbf1c7,#505c38, 60-70 #d7d6b6,#343f36, 71-99 d,#343f36, |1: 0-1 #ffffff,#875e65, 2-2 d,#424f40, 3-34 #e3e2c2,#424f40, 35-49 d,#424f40, 50-51 #ffffff,#875e65, 52-52 d,#343f36, 53-84 #d7d6b6,#343f36, 85-99 d,#343f36, -|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-32 #b5ecec,#424f40, 33-49 #e3e2c2,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-82 #aae0e0,#343f36, 83-99 #d7d6b6,#343f36, +|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-37 #b5ecec,#424f40, 38-49 #e3e2c2,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-87 #aae0e0,#343f36, 88-99 #d7d6b6,#343f36, |3: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |4: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |5: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-99 d,#10100e, @@ -431,7 +431,7 @@ |14: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |15: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |16: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-32 #aae0e0,#343f36, 33-49 #d7d6b6,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-37 #aae0e0,#343f36, 38-49 #d7d6b6,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |18: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |19: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |20: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, @@ -449,7 +449,7 @@ == style forge-black grid=100x31 |0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b, |1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-32 #f8cba7,#2d3440, 33-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-82 #ecbf9b,#22252b, 83-99 #c5c9d2,#22252b, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b, |3: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |4: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |5: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-99 d,#000000, @@ -464,7 +464,7 @@ |14: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |15: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |16: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-32 #ecbf9b,#22252b, 33-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |18: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |19: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |20: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, @@ -482,7 +482,7 @@ == style forge-soft grid=100x31 |0: 0-50 #c5c9d2,#2a2d34, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#2a2d34, 71-99 d,#2a2d34, |1: 0-1 #ffffff,#6d7f6c, 2-2 d,#373e49, 3-34 #d1d5de,#373e49, 35-49 d,#373e49, 50-51 #ffffff,#6d7f6c, 52-52 d,#2a2d34, 53-84 #c5c9d2,#2a2d34, 85-99 d,#2a2d34, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-32 #f8cba7,#373e49, 33-49 #d1d5de,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-82 #ecbf9b,#2a2d34, 83-99 #c5c9d2,#2a2d34, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-37 #f8cba7,#373e49, 38-49 #d1d5de,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-87 #ecbf9b,#2a2d34, 88-99 #c5c9d2,#2a2d34, |3: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |4: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |5: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-99 d,#35383e, @@ -497,7 +497,7 @@ |14: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |15: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |16: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-32 #ecbf9b,#2a2d34, 33-49 #c5c9d2,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-37 #ecbf9b,#2a2d34, 38-49 #c5c9d2,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |18: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |19: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |20: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, @@ -515,7 +515,7 @@ == style orchard-black grid=100x31 |0: 0-50 #b2c3b8,#1d2824, 51-59 #f5f7ee,#3c614a, 60-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-32 #dfbce5,#26362d, 33-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-82 #d3b0d9,#1d2824, 83-99 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |4: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |5: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-99 d,#000000, @@ -530,7 +530,7 @@ |14: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |15: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |16: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-32 #d3b0d9,#1d2824, 33-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |18: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |19: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |20: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden index 0be511c7..9b5d06d4 100644 --- a/test/snapshots/tinywin.golden +++ b/test/snapshots/tinywin.golden @@ -1,7 +1,7 @@ == snap stacked grid=100x31 cursor=14,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tinywin/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Collapse Del | $ :^[n^[n^[n | | @@ -25,12 +25,12 @@ == snap shrunk grid=60x4 cursor=59,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tinywin/cwd Tty Save Mode Filter Collaps +|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Co | /+New Save Tty Collapse Del == snap columns grid=100x31 cursor=23,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tinywin/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Collapse Del | $ :^[n^[n^[n cn cn cn | | diff --git a/test/snapshots/topbarexec.golden b/test/snapshots/topbarexec.golden index d83dff70..83a12f52 100644 --- a/test/snapshots/topbarexec.golden +++ b/test/snapshots/topbarexec.golden @@ -1,7 +1,7 @@ == snap selected grid=90x21 cursor=14,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/topbarexec/cwd Tty Save M /tmp/pardes-snap/topbarexec/cwd Tty Save M +| /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S | ls ls | zmarkfile.txt zmarkfile.txt | @@ -11,12 +11,12 @@ | | | -| /tmp/pardes-snap/topbarexec/cwd Tty Save M +| /tmp/pardes-snap/topbarexec/cwd Tty+bash S | ls | zmarkfile.txt == snap noarg-runs grid=90x21 cursor=14,4 |1: New Tty Find Grep Joincol Delcol New ╭────────────────────────────────────╮ -|2: /tmp/pardes-snap/topbarexec/cwd Tty Save M /tmp│pardes resizes=1 │ +|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp│pardes resizes=1 │ |3: ls ls │theme orchard │ |4: zmarkfile.txt zmark│font default │ |5: │terms 3 cols 2 │ @@ -27,7 +27,7 @@ |10: ╰────────────────────────────────────╯ == snap arg-still-spliced grid=90x21 cursor=14,4 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/topbarexec/cwd Tty Save M /tmp/pardes-snap/topbarexec/cwd Tty Save M +|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S |3: ls ls |4: zmarkfile.txt zmarkfile.txt |5: diff --git a/test/snapshots/tty.golden b/test/snapshots/tty.golden index 3bb73d72..6c17c625 100644 --- a/test/snapshots/tty.golden +++ b/test/snapshots/tty.golden @@ -1,7 +1,7 @@ == snap tty-on grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C +|$ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil | $ ls ls | m.txt m.txt | $ @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C +| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil | ls | m.txt == snap tty-echo grid=100x31 cursor=4,7 @@ -24,12 +24,12 @@ |6: SNAPOK |7: $ == snap tty-off grid=100x31 cursor=4,7 -|2: /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C +|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil |3: ls ls |5: echo SNAP''OK |7: == snap shift-esc-on grid=100x31 cursor=4,7 -|2: $ /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C +|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil |3: $ ls ls |5: $ echo SNAP''OK |7: $ @@ -37,6 +37,6 @@ |3: $ ls Last ls == snap ctrl-b-off grid=100x31 cursor=4,20 |3: $ ls ls -|17: $ /tmp/pardes-snap/tty/cwd Tty Save Mode Filter C +|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil |18: $ ls |20: $ diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden index 5eb8bc25..5233b8a9 100644 --- a/test/snapshots/ttyclick.golden +++ b/test/snapshots/ttyclick.golden @@ -1,7 +1,7 @@ == snap tty-bodyclick grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil +|$ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod | $ ls ls | probe.txt probe.txt | $ @@ -16,12 +16,12 @@ | | | -| /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil +| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod | ls | probe.txt == snap tty-tagedit grid=100x31 cursor=33,2 == snap tty-tagexit grid=100x31 cursor=4,5 == snap ins-bodyclick grid=100x31 cursor=11,4 -|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil +|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod |3: ls ls |5: diff --git a/test/snapshots/ttyedit.golden b/test/snapshots/ttyedit.golden index 092cb481..144c97a0 100644 --- a/test/snapshots/ttyedit.golden +++ b/test/snapshots/ttyedit.golden @@ -1,7 +1,7 @@ == snap before grid=100x31 cursor=7,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt +| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode | ls ls | words.txt words.txt | printf 'one two three\nfour five six\nseven ei @@ -16,17 +16,17 @@ | | | -| /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt +| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode | ls | words.txt == snap split grid=100x31 cursor=2,8 -|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt +|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode |7: one t |8: wo three |9: four five six |10: seven eight nine == snap opened grid=100x31 cursor=6,9 -|2: /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt /tmp/pardes-snap/ttyedit/cwd Tty Save Mode Filt +|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode |9: OPEN |10: four five six |11: seven eight nine diff --git a/test/snapshots/ttyfollow.golden b/test/snapshots/ttyfollow.golden index d0b554bd..692ecf8d 100644 --- a/test/snapshots/ttyfollow.golden +++ b/test/snapshots/ttyfollow.golden @@ -1,7 +1,7 @@ == snap held-pre grid=100x31 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi +| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo | 185 ls | 186 s.txt | 187 @@ -16,18 +16,18 @@ | 196 | 197 | 198 -| /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi +| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo | ls | s.txt == snap held-post grid=100x31 cursor=54,8 -|2: /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi$ /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi +|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo |3: 185 $ ls |5: 187 $ touch go; until test -f done1; do sleep 0.05; |6: 188 done; echo SYN''CED |7: 189 SYNCED |8: 190 $ == snap tty-follows grid=100x31 cursor=4,16 -|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi$ /tmp/pardes-snap/ttyfollow/cwd Tty Save Mode Fi +|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo |3: 292 $ ls |4: 293 s.txt |5: 294 $ touch go; until test -f done1; do sleep 0.05; diff --git a/test/snapshots/ttyhelp.golden b/test/snapshots/ttyhelp.golden index be95be2b..828da2fb 100644 --- a/test/snapshots/ttyhelp.golden +++ b/test/snapshots/ttyhelp.golden @@ -1,7 +1,7 @@ == snap boot grid=100x31 cursor=4,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttyhelp/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/ttyhelp/cwd Tty+bash Save Mode Filter Collapse Del | $ | | diff --git a/test/snapshots/ttylook.golden b/test/snapshots/ttylook.golden index 108a8f86..6bfe5470 100644 --- a/test/snapshots/ttylook.golden +++ b/test/snapshots/ttylook.golden @@ -1,7 +1,7 @@ == snap tok1 grid=120x31 cursor=2,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttylook/cwd Tty Save Mode Filter Collaps /tmp/pardes-snap/ttylook/cwd Tty Save Mode Filter Collaps +| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co | ls ls | big.txt plain.txt sub big.txt plain.txt sub | @@ -16,13 +16,13 @@ | | | -| /tmp/pardes-snap/ttylook/cwd Tty Save Mode Filter Collaps +| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co | ls | big.txt plain.txt sub == style tok1 grid=120x31 |0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-34 #dfbce5,#26362d, 35-59 #becfc4,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-94 #d3b0d9,#1d2824, 95-119 #b2c3b8,#1d2824, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-59 #becfc4,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-99 #d3b0d9,#1d2824, 100-119 #b2c3b8,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-8 #f5f7ee,#3c614a, 9-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, @@ -37,7 +37,7 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-34 #d3b0d9,#1d2824, 35-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, @@ -58,7 +58,7 @@ == snap tok3 grid=120x31 cursor=22,4 |4: big.txt plain.txt sub big.txt plain.txt sub == snap diropen grid=120x31 cursor=4,8 -|6: /tmp/pardes-snap/ttylook/cwd/sub Tty Save Mode Filter Col +|6: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filte |7: ls == snap noop grid=120x31 cursor=2,4 == snap fileopen grid=120x31 cursor=7,10 diff --git a/test/snapshots/ttyonly.golden b/test/snapshots/ttyonly.golden index e4376308..8c201176 100644 --- a/test/snapshots/ttyonly.golden +++ b/test/snapshots/ttyonly.golden @@ -1,7 +1,7 @@ == snap ttyonly grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttyonly/cwd Tty Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/ttyonly/cwd Tty+bash Save Mode Filter Collapse Del | $ echo TTY''ONLY | TTYONLY | $ @@ -25,7 +25,7 @@ == style ttyonly grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-34 #dfbce5,#26362d, 35-64 #becfc4,#26362d, 65-99 d,#26362d, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-69 #becfc4,#26362d, 70-99 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410, diff --git a/test/snapshots/ttypaste.golden b/test/snapshots/ttypaste.golden index 6222bf03..448cbce3 100644 --- a/test/snapshots/ttypaste.golden +++ b/test/snapshots/ttypaste.golden @@ -1,7 +1,7 @@ == snap bracketed-paste-reached-the-shell grid=100x31 cursor=18,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttypaste/cwd Tty Save Mode Fil /tmp/pardes-snap/ttypaste/cwd Tty Save Mode Fil +|$ /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod | $ ls ls | src.txt src.txt | $ echo BRACKETED @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/ttypaste/cwd Tty Save Mode Fil +| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod | ls | src.txt == snap ctrl-v-with-empty-register-is-inert grid=100x31 cursor=18,5 diff --git a/test/snapshots/ttyprompt.golden b/test/snapshots/ttyprompt.golden index 28cdbbee..bfdfcd4b 100644 --- a/test/snapshots/ttyprompt.golden +++ b/test/snapshots/ttyprompt.golden @@ -1,7 +1,7 @@ == snap greeted grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi +| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo | ls ls | probe.txt probe.txt | @@ -16,14 +16,14 @@ | | | -| /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi +| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo | ls | probe.txt == snap in-tty grid=100x31 cursor=17,5 -|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi +|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo |3: $ ls ls |5: $ echo ABCDEFGH == snap out-of-tty grid=100x31 cursor=17,5 -|2: /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi /tmp/pardes-snap/ttyprompt/cwd Tty Save Mode Fi +|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo |3: ls ls |5: echo ABCDEFGH diff --git a/test/snapshots/ttytaken.golden b/test/snapshots/ttytaken.golden index 72708e64..d3aba990 100644 --- a/test/snapshots/ttytaken.golden +++ b/test/snapshots/ttytaken.golden @@ -1,47 +1,48 @@ -== snap opened grid=210x31 cursor=7,3 +== snap opened grid=210x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit -| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttytaken/cwd/cmd.txt Save Tty Collapse Del /tmp/pardes-snap/ttytaken/cwd Tty Save Mode Filter Collapse Del -| 1 echo TAKEN''ONE ls -| 2 echo TAKEN''TWO big.txt cmd.txt -== snap at-prompt grid=210x31 cursor=7,3 -|3: 1 echo TAKEN''ONE Exec ls -|5: echo TAKEN''ONE -|6: TAKENONE -== snap paging grid=210x31 cursor=114,30 -|2: /tmp/pardes-snap/ttytaken/cwd/cmd.txt Save Tty Collapse Del $ /tmp/pardes-snap/ttytaken/cwd Tty Save Mode Filter Collapse Del -|3: 1 echo TAKEN''ONE line 1 -|4: 2 echo TAKEN''TWO line 2 -|5: line 3 -|6: line 4 -|7: line 5 -|8: line 6 -|9: line 7 -|10: line 8 -|11: line 9 -|12: line 10 -|13: line 11 -|14: line 12 -|15: line 13 -|16: line 14 -|17: line 15 -|18: line 16 -|19: line 17 -|20: line 18 -|21: line 19 -|22: line 20 -|23: line 21 -|24: line 22 -|25: line 23 -|26: line 24 -|27: line 25 -|28: line 26 -|29: line 27 -|30: big.txt -== snap spawned grid=210x31 cursor=108,26 -|3: 1 echo TAKEN''ONE Exec line 1 -|26: : -|27: /tmp/pardes-snap/ttytaken/cwd Tty Save Mode Filter Collapse Del -|28: echo TAKEN''TWO -|29: TAKENTWO +| New Tty Find Grep Joincol Delcol +| /tmp/pardes-snap/ttytaken/cwd Tty+bash Save Mode Filter Collapse Del +| ls +| big.txt +== snap at-prompt grid=210x31 cursor=4,5 +|2: /tmp/pardes-snap/ttytaken/cwd Tty+bash Save Mode Filter Collapse Del @`echo TAKEN''ONE` +|3: ls Exec +|5: echo TAKEN''ONE +|6: TAKENONE +== snap paging grid=210x31 cursor=9,30 +|3: line 1 +|4: line 2 +|5: line 3 +|6: line 4 +|7: line 5 +|8: line 6 +|9: line 7 +|10: line 8 +|11: line 9 +|12: line 10 +|13: line 11 +|14: line 12 +|15: line 13 +|16: line 14 +|17: line 15 +|18: line 16 +|19: line 17 +|20: line 18 +|21: line 19 +|22: line 20 +|23: line 21 +|24: line 22 +|25: line 23 +|26: line 24 +|27: line 25 +|28: line 26 +|29: line 27 +|30: big.txt +== snap spawned grid=210x31 cursor=109,2 +|2: /tmp/pardes-snap/ttytaken/cwd Tty+bash Save Mode Filter Collapse Del @`echo TAKEN''ONE` @`echo TAKEN''TWO` +|3: line 1 Exec +|26: : +|27: /tmp/pardes-snap/ttytaken/cwd (echo TAKEN''TWO) exit 0 Kill Save Collapse Del +|28: TAKENTWO +|29: exit 0 |30: diff --git a/test/snapshots/ttytaken.snap b/test/snapshots/ttytaken.snap index 47554876..a6ccb632 100644 --- a/test/snapshots/ttytaken.snap +++ b/test/snapshots/ttytaken.snap @@ -1,43 +1,50 @@ -# Process monitoring, end to end. A pane whose tty is held by a full-screen -# program is not somewhere a command line may be typed: the SAME middle-drag -# Exec goes into the shell while it sits at its prompt, and into a NEW shell -# beside it once `less` owns the terminal — where it runs, instead of becoming -# keystrokes the pager would eat. Nothing about the pager's screen changes. -file cmd.txt echo TAKEN''ONE\necho TAKEN''TWO +# Process monitoring, end to end. A terminal whose tty is held by a +# full-screen program is not somewhere a command line may be typed: the SAME +# Exec from that terminal goes into its shell while it sits at its prompt, +# and into a command pane of its own once `less` owns the terminal -- where +# it runs, instead of becoming keystrokes the pager would eat. Nothing about +# the pager's screen changes. (From any pane but a terminal, an Exec always +# runs as a command pane: exec.snap and cmdword.snap.) lines big.txt 40 start 31 210 -n 1 -wait 8000 cmd.txt -stable 700 20000 -# open the command file by looking its name in the greeting `ls` -press right 14 5 -release right 14 5 -wait 10000 echo TAKEN +wait 8000 big.txt stable 700 20000 snap opened -# at the prompt: the drag runs in the shell that is already on this directory -press middle 8 4 -drag middle 22 4 -release middle 22 4 +# at the prompt: a command word in the terminal's own tag, executed, is +# typed into its shell. `:` opens the tag, `A` appends, and the execute key +# runs the word under the cursor. +key : +key A +key space +text @`echo TAKEN''ONE` +key esc +settle 100 +key h +key tab wait 10000 TAKENONE stable 700 20000 snap at-prompt -# ...and now a pager owns that terminal. Ctrl-b is raw tty (config.tty_toggle), -# which is how a script types AT the shell instead of into the pane's own edit -# buffer — the same way exec.snap runs a command line. -press left 150 9 -release left 150 9 -stable 400 5000 +# ...and now a pager owns that terminal. Ctrl-b is raw tty +# (config.tty_toggle), which is how a script types AT the shell instead of +# into the pane's own edit buffer. key c-b stable 400 5000 text env LESS= less big.txt key enter wait 10000 line 1 stable 700 20000 +key c-b +stable 400 5000 snap paging -# the same gesture on the second line: a new shell, not `less` keystrokes -press middle 8 5 -drag middle 22 5 -release middle 22 5 +# the same gesture from that terminal: a command pane, not `less` keystrokes +key : +key A +key space +text @`echo TAKEN''TWO` +key esc +settle 100 +key h +key tab wait 10000 TAKENTWO stable 700 20000 snap spawned diff --git a/test/snapshots/tutor.golden b/test/snapshots/tutor.golden index d9a87df8..2c523ca4 100644 --- a/test/snapshots/tutor.golden +++ b/test/snapshots/tutor.golden @@ -1,7 +1,7 @@ == snap tutor grid=150x31 cursor=7,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tutor/cwd Tty Save Mode Filter Collapse Del /tmp/pardes-snap/tutor/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tutor/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/tutor/cwd Tty+bash Save Mode Filter Collapse Del | ls ls | | /Tutor Save Tty Collapse Del @@ -16,5 +16,5 @@ | 7 Press j until you reach the introduction. | 8 | 9 -| /tmp/pardes-snap/tutor/cwd Tty Save Mode Filter Collapse Del +| /tmp/pardes-snap/tutor/cwd Tty+bash Save Mode Filter Collapse Del | ls diff --git a/test/snapshots/winch.golden b/test/snapshots/winch.golden index b9732a32..2e960cc6 100644 --- a/test/snapshots/winch.golden +++ b/test/snapshots/winch.golden @@ -1,7 +1,7 @@ == snap storm grid=100x31 cursor=4,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/winch/cwd Tty Save Mode Filter /tmp/pardes-snap/winch/cwd Tty Save Mode Filter +|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F | $ ls ls | $ for i in $(seq 600); do kill -WINCH $PPID; don | e; echo SUR''VIVED @@ -16,12 +16,12 @@ | | | -| /tmp/pardes-snap/winch/cwd Tty Save Mode Filter +| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F | ls == snap resized grid=106x35 cursor=4,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/winch/cwd Tty Save Mode Filter Co /tmp/pardes-snap/winch/cwd Tty Save Mode Filter Co +|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt | $ ls ls | $ for i in $(seq 600); do kill -WINCH $PPID; done; | echo SUR''VIVED @@ -38,5 +38,5 @@ | | | -| /tmp/pardes-snap/winch/cwd Tty Save Mode Filter Co +| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt | ls diff --git a/test/snapshots/windownav.golden b/test/snapshots/windownav.golden index 5bdfb99f..f89dbe80 100644 --- a/test/snapshots/windownav.golden +++ b/test/snapshots/windownav.golden @@ -1,7 +1,7 @@ == snap start grid=100x31 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty C /tmp/pardes-snap/windownav/cwd Tty Save Mode Fi +| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty C /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo | 1 alpha ls Left | 2 beta w.txt | 3 gamma @@ -15,7 +15,7 @@ |3: 1 alpha ls Last == snap altn grid=100x31 cursor=4,20 |3: 1 alpha ls -|17: /tmp/pardes-snap/windownav/cwd Tty Save Mode Fi +|17: /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo |18: ls |19: w.txt == snap up grid=100x31 cursor=7,3 diff --git a/test/snapshots/windowops.golden b/test/snapshots/windowops.golden index 9ee02837..63a7574a 100644 --- a/test/snapshots/windowops.golden +++ b/test/snapshots/windowops.golden @@ -1,7 +1,7 @@ == snap hover-v grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/windowops/cwd Tty Save Mode F╎ /tmp/pardes-snap/windowops/cwd Tty Save Mode Fi +| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo | ls ╎ ls | w.txt ╎ w.txt | ╎ @@ -16,7 +16,7 @@ | ╎ | ╎ | ╎ -| /tmp/pardes-snap/windowops/cwd Tty Save Mode F╎ +| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ | ls ╎ | w.txt ╎ | ╎ @@ -32,7 +32,7 @@ | ╎ == snap hover-h grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty Save Mode Fi /tmp/pardes-snap/windowops/cwd Tty Save Mode Fi +|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo |3: ls ls |4: w.txt w.txt |5: @@ -47,7 +47,7 @@ |14: |15: |16: -|17: /tmp/pardes-snap/windowops/cwd Tty Save Mode Fi +|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo |18: ls |19: w.txt |20: @@ -63,7 +63,7 @@ |30: == snap border-drag grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty S╎ve Mode Fi /tmp/pardes-snap/windowops/cwd Tty Save Mode Fi +|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo |3: ls ╎ ls |4: w.txt ╎ w.txt |5: ╎ @@ -78,7 +78,7 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/windowops/cwd Tty S╎ve Mode Fi +|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo |18: ls ╎ |19: w.txt ╎ |20: ╎ @@ -94,13 +94,13 @@ |30: ╎ == snap border-done grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty S╎ /tmp/pardes-snap/windowops/cwd Tty Save Mode Filter Colla +|2: /tmp/pardes-snap/windowops/cwd Tty+b╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter |3: ls ╎ ls |4: w.txt ╎ w.txt -|17: /tmp/pardes-snap/windowops/cwd Tty S╎ +|17: /tmp/pardes-snap/windowops/cwd Tty+b╎ == snap hborder-done grid=100x31 cursor=4,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty Sa /tmp/pardes-snap/windowops/cwd Tty Save Mode Filter Colla +|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter |3: ls ls |4: w.txt w.txt |5: @@ -111,7 +111,7 @@ |10: |11: |12: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ -|13: /tmp/pardes-snap/windowops/cwd Tty Sa +|13: /tmp/pardes-snap/windowops/cwd Tty+ba |14: ls |15: w.txt |16: @@ -133,14 +133,14 @@ |12: == snap moved grid=100x31 cursor=4,16 == snap altn grid=100x31 cursor=4,20 -|17: /tmp/pardes-snap/windowops/cwd Tty Sa +|17: /tmp/pardes-snap/windowops/cwd Tty+ba |18: ls |19: w.txt == snap focus-right grid=100x31 cursor=44,5 |18: ls Right == snap debug-on grid=100x31 cursor=44,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep J╭────────────────────────────────────╮ -|2: /tmp/pardes-snap/windowops/cwd Tty Sa /tmp/pardes-snap/wi│pardes resizes=1 │ +|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/wi│pardes resizes=1 │ |3: ls ls │theme orchard │ |4: w.txt w.txt │font default │ |5: │terms 4 cols 2 │ @@ -153,7 +153,7 @@ == style debug-on grid=100x31 |0: 0-60 #b2c3b8,#1d2824, 61-65 #f5f7ee,#3c614a, 66-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #ffffff,#887455, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-99 d,d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d, |5: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-61 d,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d, @@ -164,11 +164,11 @@ |10: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-99 d,d, |11: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |12: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, @@ -184,7 +184,7 @@ |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, == snap debug-off grid=100x31 cursor=44,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty Sa /tmp/pardes-snap/windowops/cwd Tty Save Mode Filter Colla +|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter |3: ls ls |4: w.txt w.txt |5: diff --git a/transactions.txt b/transactions.txt index 6c2e7646..8d2fda3f 100644 --- a/transactions.txt +++ b/transactions.txt @@ -1,13 +1,8 @@ -Selection anchors through insert-mode edits, and the transaction model -======================================================================= - -STATUS: still open. `wiX-edit-drops-sel` is line 1 of test/hxcases/waivers.jsonl, -so the gap below describes the code as it stands rather than something already -fixed. One correction to the sentence that follows: it is not quite "the one -place". Line 3 of the same file, `msel-append`, says in its own words "Same -family as wiX-edit-drops-sel: pardes does not map selections through insert-mode -edits" — so the gap has two waivers, and what is singular is the CAUSE below, -not the number of divergences it produces. +STATUS: closed for the waivers. Insert mode now carries every range through +each edit (edit.insertKey maps the range with helix's Range::map rules after +the edit is made), so `wiX-edit-drops-sel` and `msel-append` are gone. What +follows is the original analysis; the delta layer it proposes was not needed +for that and stays unbuilt. The one place the helix differential suite shows a real structural gap (waiver "wiX-edit-drops-sel" in test/hxcases/waivers.jsonl): helix keeps a diff --git a/vendor/queries/README.md b/vendor/queries/README.md new file mode 100644 index 00000000..082f4e6b --- /dev/null +++ b/vendor/queries/README.md @@ -0,0 +1,18 @@ +# Textobject queries + +`<grammar>/textobjects.scm` is helix's own query for that grammar, vendored +from helix (https://github.com/helix-editor/helix) at 278b24389, the +reference build docs/helix-keys.md names. Each file says where it came from +and what it inherits, inlined. They are Mozilla Public License 2.0, whose text +is `helix.LICENSE` here. They give `]f`, `[f`, `mif` and the other +tree-sitter textobject keys what a function, class, argument, comment, test, +entry or element is. + +`zig/textobjects.scm` is helix's query MODIFIED by hand for the zig grammar +pardes builds, which names its nodes differently; its header says how. + +Not vendored: fortran, markdown and powershell (helix has no query for +them), and erlang, whose helix query does not compile against the grammar +version pardes builds. Those +grammars have no textobjects, and the keys do nothing there, as in helix +without a query. diff --git a/vendor/queries/ada/textobjects.scm b/vendor/queries/ada/textobjects.scm new file mode 100644 index 00000000..a6f9c2ff --- /dev/null +++ b/vendor/queries/ada/textobjects.scm @@ -0,0 +1,26 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/ada/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +;; Support for high-level text objects selections. +;; For instance: +;; maf (v)isually select (a) (f)unction or subprogram +;; mif (v)isually select (i)nside a (f)unction or subprogram +;; mai (v)isually select (a) (i)f statement (or loop) +;; mii (v)isually select (i)nside an (i)f statement (or loop) +;; +;; For navigations using textobjects, check link below: +;; https://docs.helix-editor.com/master/usage.html#navigating-using-tree-sitter-textobjects +;; +;; For Textobject queries explanation, check out link below: +;; https://docs.helix-editor.com/master/guides/textobject.html + +(subprogram_body) @function.around +(subprogram_body (non_empty_declarative_part) @function.inside) +(subprogram_body (handled_sequence_of_statements) @function.inside) +(function_specification) @function.around +(procedure_specification) @function.around +(package_declaration) @function.around +(generic_package_declaration) @function.around +(package_body) @function.around diff --git a/vendor/queries/bash/textobjects.scm b/vendor/queries/bash/textobjects.scm new file mode 100644 index 00000000..8e7ad4f9 --- /dev/null +++ b/vendor/queries/bash/textobjects.scm @@ -0,0 +1,17 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/bash/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_definition + body: (_) @function.inside) @function.around + +(command + argument: (_) @parameter.inside) + +(comment) @comment.inside + +(comment)+ @comment.around + +(array + (_) @entry.around) diff --git a/vendor/queries/c/textobjects.scm b/vendor/queries/c/textobjects.scm new file mode 100644 index 00000000..34a5682f --- /dev/null +++ b/vendor/queries/c/textobjects.scm @@ -0,0 +1,32 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/c/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_definition + body: (_) @function.inside) @function.around + +(struct_specifier + body: (_) @class.inside) @class.around + +(enum_specifier + body: (_) @class.inside) @class.around + +(union_specifier + body: (_) @class.inside) @class.around + +(parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(comment) @comment.inside + +(comment)+ @comment.around + +(enumerator + (_) @entry.inside) @entry.around + +(initializer_list + (_) @entry.around) diff --git a/vendor/queries/c_sharp/textobjects.scm b/vendor/queries/c_sharp/textobjects.scm new file mode 100644 index 00000000..0b90f428 --- /dev/null +++ b/vendor/queries/c_sharp/textobjects.scm @@ -0,0 +1,25 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/c-sharp/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +[ + (class_declaration body: (_) @class.inside) + (struct_declaration body: (_) @class.inside) + (interface_declaration body: (_) @class.inside) + (enum_declaration body: (_) @class.inside) + (delegate_declaration) + (record_declaration body: (_) @class.inside) +] @class.around + +(constructor_declaration body: (_) @function.inside) @function.around + +(destructor_declaration body: (_) @function.inside) @function.around + +(method_declaration body: (_) @function.inside) @function.around + +(property_declaration (_) @function.inside) @function.around + +(parameter (_) @parameter.inside) @parameter.around + +(comment)+ @comment.around diff --git a/vendor/queries/clojure/textobjects.scm b/vendor/queries/clojure/textobjects.scm new file mode 100644 index 00000000..5121ff8c --- /dev/null +++ b/vendor/queries/clojure/textobjects.scm @@ -0,0 +1,76 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/clojure/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Function definitions (defn, defn-, defmacro, defmethod, etc.) +(list_lit + . + (sym_lit) @_keyword + . + (sym_lit) + (_)* @function.inside + (#match? @_keyword "^(defn|defn-|defmacro|defmethod|defmulti|definline)$")) @function.around + +; Anonymous functions (fn) +(list_lit + . + (sym_lit) @_fn + (_)* @function.inside + (#match? @_fn "^fn$")) @function.around + +; Anonymous function shorthand #() +(anon_fn_lit + (_)* @function.inside) @function.around + +; deftype, defrecord, defprotocol +(list_lit + . + (sym_lit) @_keyword + . + (sym_lit) + (_)* @class.inside + (#match? @_keyword "^(deftype|defrecord|defprotocol|definterface|defstruct)$")) @class.around + +; Test definitions (deftest) +(list_lit + . + (sym_lit) @_keyword + . + (sym_lit) + (_)* @test.inside + (#match? @_keyword "^deftest$")) @test.around + +; Function parameters in vectors +(vec_lit + (_)* @parameter.inside) @parameter.around + +; List entries +(list_lit + (_) @entry.inside @entry.around) + +; Vector entries +(vec_lit + (_) @entry.inside @entry.around) + +; Map entries +(map_lit + (_) @entry.inside @entry.around) + +; Set entries +(set_lit + (_) @entry.inside @entry.around) + +; Comments +(comment) @comment.inside +(comment)+ @comment.around + +; Discard expressions (also treated as comments) +(dis_expr) @comment.inside + +; Comment special form (comment ...) +(list_lit + . + (sym_lit) @_comment + (_)* @comment.inside + (#match? @_comment "^comment$")) @comment.around diff --git a/vendor/queries/cpp/textobjects.scm b/vendor/queries/cpp/textobjects.scm new file mode 100644 index 00000000..dba417a9 --- /dev/null +++ b/vendor/queries/cpp/textobjects.scm @@ -0,0 +1,39 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/cpp/textobjects.scm, with what it inherits from c inlined. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_definition + body: (_) @function.inside) @function.around + +(struct_specifier + body: (_) @class.inside) @class.around + +(enum_specifier + body: (_) @class.inside) @class.around + +(union_specifier + body: (_) @class.inside) @class.around + +(parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(comment) @comment.inside + +(comment)+ @comment.around + +(enumerator + (_) @entry.inside) @entry.around + +(initializer_list + (_) @entry.around) + + +(lambda_expression + body: (_) @function.inside) @function.around + +(class_specifier + body: (_) @class.inside) @class.around diff --git a/vendor/queries/css/textobjects.scm b/vendor/queries/css/textobjects.scm new file mode 100644 index 00000000..c6713c53 --- /dev/null +++ b/vendor/queries/css/textobjects.scm @@ -0,0 +1,27 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/css/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Rule sets and their declaration blocks +(rule_set) @function.around +(rule_set + (block) @function.inside) + +(keyframe_block_list) @function.around +(keyframe_block + (block) @function.inside) + +; At-rules with a body (e.g. @media, @supports) +(media_statement) @class.around +(media_statement + (block) @class.inside) + +; Declarations as key/value entries +(declaration) @entry.around +(declaration + ":" + (_) @entry.inside) + +(comment) @comment.inside +(comment)+ @comment.around diff --git a/vendor/queries/elixir/textobjects.scm b/vendor/queries/elixir/textobjects.scm new file mode 100644 index 00000000..ebe18d08 --- /dev/null +++ b/vendor/queries/elixir/textobjects.scm @@ -0,0 +1,40 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/elixir/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Function heads and guards have no body at all, so `keywords` and `do_block` nodes are both optional +((call + target: (identifier) @_keyword + (arguments + [ + (call + (arguments (_)? @parameter.inside)) + ; function has a guard + (binary_operator + left: + (call + (arguments (_)? @parameter.inside))) + ] + ; body is "do: body" instead of a do-block + (keywords + (pair + value: (_) @function.inside))?)? + (do_block (_)* @function.inside)?) + (#match? @_keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) @function.around + +(anonymous_function + (stab_clause right: (body) @function.inside)) @function.around + +((call + target: (identifier) @_keyword + (do_block (_)* @class.inside)) + (#match? @_keyword "^(defmodule|defprotocol|defimpl)$")) @class.around + +((call + target: (identifier) @_keyword + (arguments ((string) . (_)?)) + (do_block (_)* @test.inside)?) + (#match? @_keyword "^(test|describe)$")) @test.around + +(comment)+ @comment.around @comment.inside diff --git a/vendor/queries/go/textobjects.scm b/vendor/queries/go/textobjects.scm new file mode 100644 index 00000000..48529015 --- /dev/null +++ b/vendor/queries/go/textobjects.scm @@ -0,0 +1,39 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/go/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_declaration + body: (block)? @function.inside) @function.around + +(func_literal + body: (block)? @function.inside) @function.around + +(method_declaration + body: (block)? @function.inside) @function.around + +;; struct and interface declaration as class textobject? +(type_declaration + (type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around + +(type_declaration + (type_spec (type_identifier) (interface_type (method_elem)+ @class.inside))) @class.around + +(type_parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(parameter_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(comment) @comment.inside + +(comment)+ @comment.around + +; Go test/benchmark/example/fuzz functions (all live in _test.go). +((function_declaration + name: (identifier) @_name + body: (block)? @test.inside) @test.around + (#match? @_name "^(Test|Benchmark|Example|Fuzz)")) diff --git a/vendor/queries/haskell/textobjects.scm b/vendor/queries/haskell/textobjects.scm new file mode 100644 index 00000000..2a085248 --- /dev/null +++ b/vendor/queries/haskell/textobjects.scm @@ -0,0 +1,29 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/haskell/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(comment) @comment.inside +(comment)+ @comment.around + +(newtype + (newtype_constructor + (_) @class.inside)) @class.around +(data_type + constructors: (_) @class.inside) @class.around +(decl/function + (match expression:(_) @function.inside)) @function.around +(lambda + expression:(_) @function.inside) @function.around + +(decl/function + patterns: (patterns + (_) @parameter.inside)) + +(expression/lambda + patterns: (patterns + (_) @parameter.inside)) + +(decl/function + (infix + (pattern) @parameter.inside)) diff --git a/vendor/queries/helix.LICENSE b/vendor/queries/helix.LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/vendor/queries/helix.LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/vendor/queries/html/textobjects.scm b/vendor/queries/html/textobjects.scm new file mode 100644 index 00000000..b7c4b83e --- /dev/null +++ b/vendor/queries/html/textobjects.scm @@ -0,0 +1,14 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/html/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(script_element (start_tag) (_) @xml-element.inside (end_tag)) @xml-element.around + +(style_element (start_tag) (_) @xml-element.inside (end_tag)) @xml-element.around + +(element (start_tag) (_)* @xml-element.inside (end_tag)) + +(element) @xml-element.around + +(comment) @comment.around diff --git a/vendor/queries/java/textobjects.scm b/vendor/queries/java/textobjects.scm new file mode 100644 index 00000000..306efb56 --- /dev/null +++ b/vendor/queries/java/textobjects.scm @@ -0,0 +1,49 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/java/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(method_declaration + body: (_)? @function.inside) @function.around + +(constructor_declaration + body: (_) @function.inside) @function.around + +(interface_declaration + body: (_) @class.inside) @class.around + +(class_declaration + body: (_) @class.inside) @class.around + +(record_declaration + body: (_) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(formal_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[ + (line_comment) + (block_comment) +] @comment.inside + +(line_comment)+ @comment.around + +(block_comment) @comment.around + +(array_initializer + (_) @entry.around) + +(enum_body + (enum_constant) @entry.around) diff --git a/vendor/queries/javascript/textobjects.scm b/vendor/queries/javascript/textobjects.scm new file mode 100644 index 00000000..50fe599c --- /dev/null +++ b/vendor/queries/javascript/textobjects.scm @@ -0,0 +1,8 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/javascript/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; See runtime/queries/ecma/README.md for more info. + +; inherits: _javascript,ecma diff --git a/vendor/queries/json/textobjects.scm b/vendor/queries/json/textobjects.scm new file mode 100644 index 00000000..e0df6ecd --- /dev/null +++ b/vendor/queries/json/textobjects.scm @@ -0,0 +1,13 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/json/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) + +(comment) @comment.inside +(comment)+ @comment.around diff --git a/vendor/queries/kotlin/textobjects.scm b/vendor/queries/kotlin/textobjects.scm new file mode 100644 index 00000000..b02a8f7e --- /dev/null +++ b/vendor/queries/kotlin/textobjects.scm @@ -0,0 +1,50 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/kotlin/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_declaration + (function_body)? @function.inside) @function.around + +; Unlike function_body above, the constructor body is does not have its own +; symbol in the current grammar. +(secondary_constructor) @function.around + +(class_declaration + (class_body)? @class.inside) @class.around + +(class_declaration + (enum_class_body) @class.inside) @class.around + +[ + (line_comment) + (multiline_comment) +] @comment.inside + +(line_comment)+ @comment.around + +(multiline_comment) @comment.around + +(enum_entry) @entry.around +(lambda_literal) @entry.around +(property_declaration) @entry.around +(object_declaration) @entry.around +(assignment) @entry.around + +; TODO: This doesn't work with annotations yet, but fixing it without breaking +; the case of multiple parameters is non-trivial. +(function_value_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +; secondary constructor uses function_value_parameters above +(primary_constructor + ((_)@parameter.inside . ","? @parameter.around) @parameter.around) + +(function_type_parameters + ((_)@parameter.inside . ","? @parameter.around) @parameter.around) + +(value_arguments + ((_)@parameter.inside . ","? @parameter.around) @parameter.around) + +(lambda_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) diff --git a/vendor/queries/ocaml/textobjects.scm b/vendor/queries/ocaml/textobjects.scm new file mode 100644 index 00000000..9e6bb2cd --- /dev/null +++ b/vendor/queries/ocaml/textobjects.scm @@ -0,0 +1,37 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/ocaml/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Value/function definitions +(value_definition) @function.around +(value_definition + (let_binding + body: (_) @function.inside)) + +(fun_expression + body: (_) @function.inside) @function.around + +(function_expression) @function.around + +(method_definition + body: (_) @function.inside) @function.around + +; Types and modules +(type_definition) @class.around +(type_definition + (type_binding + body: (_) @class.inside)) + +(module_definition) @class.around +(module_definition + (module_binding + body: (_) @class.inside)) + +; Parameters +(parameter) @parameter.around +(parameter + pattern: (_) @parameter.inside) + +(comment) @comment.inside +(comment)+ @comment.around diff --git a/vendor/queries/pascal/textobjects.scm b/vendor/queries/pascal/textobjects.scm new file mode 100644 index 00000000..9abb7871 --- /dev/null +++ b/vendor/queries/pascal/textobjects.scm @@ -0,0 +1,14 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/pascal/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(declType (declClass (declSection) @class.inside)) @class.around + +(defProc body: (_) @function.inside) @function.around + +(declArgs (_) @parameter.inside) @parameter.around +(exprArgs (_) @parameter.inside) @parameter.around + +(comment) @comment.inside +(comment)+ @comment.around diff --git a/vendor/queries/php/textobjects.scm b/vendor/queries/php/textobjects.scm new file mode 100644 index 00000000..8bf7fab9 --- /dev/null +++ b/vendor/queries/php/textobjects.scm @@ -0,0 +1,63 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/php/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(class_declaration + body: (_) @class.inside) @class.around + +(interface_declaration + body: (_) @class.inside) @class.around + +(trait_declaration + body: (_) @class.inside) @class.around + +(enum_declaration + body: (_) @class.inside) @class.around + +(function_definition + body: (_) @function.inside) @function.around + +(method_declaration + body: (_) @function.inside) @function.around + +(arrow_function + body: (_) @function.inside) @function.around + +(anonymous_function + body: (_) @function.inside) @function.around + +(anonymous_class + body: (_) @class.inside) @class.around + +(property_hook + body: (_) @function.inside) @function.around + +(anonymous_function_use_clause + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(formal_parameters + ([ + (simple_parameter) + (variadic_parameter) + (property_promotion_parameter) + ] @parameter.inside . ","? @parameter.around) @parameter.around) + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(comment) @comment.inside + +(comment)+ @comment.around + +(array_creation_expression + (array_element_initializer + (_) @entry.inside + ) @entry.around @entry.movement) + +(list_literal + (_) @entry.inside @entry.around @entry.movement) + +[ + (enum_case) +] @entry.around @entry.movement diff --git a/vendor/queries/python/textobjects.scm b/vendor/queries/python/textobjects.scm new file mode 100644 index 00000000..c4dbcd0e --- /dev/null +++ b/vendor/queries/python/textobjects.scm @@ -0,0 +1,40 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/python/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_definition + body: (block)? @function.inside) @function.around + +(class_definition + body: (block)? @class.inside) @class.around + +(parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(lambda_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(argument_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(comment) @comment.inside + +(comment)+ @comment.around + +((function_definition + name: (identifier) @_name + body: (block)? @test.inside) @test.around + (#match? @_name "^test_")) + +(list + (_) @entry.around) + +(tuple + (_) @entry.around) + +(set + (_) @entry.around) + +(pair + (_) @entry.inside) @entry.around diff --git a/vendor/queries/ruby/textobjects.scm b/vendor/queries/ruby/textobjects.scm new file mode 100644 index 00000000..368d80b9 --- /dev/null +++ b/vendor/queries/ruby/textobjects.scm @@ -0,0 +1,61 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/ruby/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Class and Modules +(class + body: (_)? @class.inside) @class.around + +(singleton_class + value: (_) + (_)+ @class.inside) @class.around + +(call + receiver: (constant) @class_const + method: (identifier) @class_method + (#match? @class_const "Class") + (#match? @class_method "new") + (do_block (_)+ @class.inside)) @class.around + +(module + body: (_)? @class.inside) @class.around + +; Functions and Blocks +(singleton_method + body: (_)? @function.inside) @function.around + +(method + body: (_)? @function.inside) @function.around + +(do_block + body: (_)? @function.inside) @function.around + +(block + body: (_)? @function.inside) @function.around + +; Parameters +(method_parameters + (_) @parameter.inside) @parameter.around + +(block_parameters + (_) @parameter.inside) @parameter.around + +(lambda_parameters + (_) @parameter.inside) @parameter.around + +; Comments +(comment) @comment.inside +(comment)+ @comment.around + +(pair + (_) @entry.inside) @entry.around + +(array + (_) @entry.around) + +(string_array + (_) @entry.around) + +(symbol_array + (_) @entry.around) diff --git a/vendor/queries/rust/textobjects.scm b/vendor/queries/rust/textobjects.scm new file mode 100644 index 00000000..d888c762 --- /dev/null +++ b/vendor/queries/rust/textobjects.scm @@ -0,0 +1,90 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/rust/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(function_item + body: (_) @function.inside) @function.around + +(closure_expression + body: (_) @function.inside) @function.around + +(struct_item + body: (_) @class.inside) @class.around + +(enum_item + body: (_) @class.inside) @class.around + +(union_item + body: (_) @class.inside) @class.around + +(trait_item + body: (_) @class.inside) @class.around + +(impl_item + body: (_) @class.inside) @class.around + +(parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(closure_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(field_initializer_list + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[ + (line_comment) + (block_comment) +] @comment.inside + +(line_comment)+ @comment.around + +(block_comment) @comment.around + +(; #[test] + (attribute_item + (attribute + (identifier) @_test_attribute)) + ; allow other attributes like #[should_panic] and comments + [ + (attribute_item) + (line_comment) + ]* + ; the test function + (function_item + body: (_) @test.inside) @test.around + (#eq? @_test_attribute "test")) + +(array_expression + (_) @entry.around) + +(tuple_expression + (_) @entry.around) + +(tuple_pattern + (_) @entry.around) + +; Commonly used vec macro initializer is special cased +(macro_invocation + (identifier) @_id (token_tree (_) @entry.around) + (#eq? @_id "vec")) + +(enum_variant) @entry.around + +(field_declaration + (_) @entry.inside) @entry.around + +(field_initializer + (_) @entry.inside) @entry.around + +(shorthand_field_initializer) @entry.around diff --git a/vendor/queries/scala/textobjects.scm b/vendor/queries/scala/textobjects.scm new file mode 100644 index 00000000..7ff60759 --- /dev/null +++ b/vendor/queries/scala/textobjects.scm @@ -0,0 +1,70 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/scala/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +; Function queries + +(function_definition + body: (_) @function.inside) @function.around ; Does not include end marker + +(lambda_expression + (_) @function.inside) @function.around + +; Scala 3 braceless lambda +(colon_argument + (_) @function.inside) @function.around + + +; Class queries + +(object_definition + body: (_)? @class.inside) @class.around + +(class_definition + body: (_)? @class.inside) @class.around + +(trait_definition + body: (_)? @class.inside) @class.around + +(type_definition) @class.around + +(enum_case_definitions) @class.around + +(enum_definition + body: (_)? @class.inside) @class.around + + +; Parameter queries + +(parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(class_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(parameter_types + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(bindings + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +; Does not match context bounds or higher-kinded types +(type_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + + +; Comment queries + +[(comment) (block_comment)] @comment.inside +[(comment) (block_comment)] @comment.around ; Does not match consecutive block comments + + +; Test queries +; Not supported diff --git a/vendor/queries/typst/textobjects.scm b/vendor/queries/typst/textobjects.scm new file mode 100644 index 00000000..64c99275 --- /dev/null +++ b/vendor/queries/typst/textobjects.scm @@ -0,0 +1,28 @@ +; Vendored from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/typst/textobjects.scm. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(let + pattern: (call) + value: (_) @function.inside) @function.around + +(call + (group + ((_) @parameter.inside . ","? @parameter.around) @parameter.around)) + +(lambda + pattern: + (group + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + value: (_) @function.inside) @function.around + +(group + [ + (tagged (_) @entry.inside) + (_) + ] @entry.around) + +(comment) @comment.inside + +(comment)+ @comment.around diff --git a/vendor/queries/zig/textobjects.scm b/vendor/queries/zig/textobjects.scm new file mode 100644 index 00000000..1c40b137 --- /dev/null +++ b/vendor/queries/zig/textobjects.scm @@ -0,0 +1,40 @@ +; Adapted from helix (https://github.com/helix-editor/helix) at 278b24389: +; runtime/queries/zig/textobjects.scm, MODIFIED for the zig grammar pardes +; builds (maxxnino/tree-sitter-zig), whose node names differ from the +; tree-sitter-grammars one helix uses: function_declaration is a Decl holding +; an FnProto and a Block, with `pub` a sibling before it; variable_declaration +; is VarDecl; struct/enum/union are one ContainerDecl; parameters and +; arguments are ParamDeclList and FnCallArguments; comments come in three +; kinds. +; Licensed under the Mozilla Public License 2.0 (vendor/queries/helix.LICENSE, +; https://mozilla.org/MPL/2.0/). + +(_ + "pub"? @function.around + . + (Decl + (FnProto) + (Block) @function.inside) @function.around) + +(TestDecl (Block) @test.inside) @test.around + +; matches all of: struct, enum, union (one ContainerDecl in this grammar), +; `pub` with them where there is one +(_ + "pub"? @class.around + . + (Decl + (VarDecl + (ErrorUnionExpr + (SuffixExpr + (ContainerDecl + [(ContainerField) (Decl) (TestDecl) (ComptimeDecl)] @class.inside))))) @class.around) + +(ParamDeclList + ((ParamDecl) @parameter.inside . ","? @parameter.around) @parameter.around) + +(FnCallArguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[(line_comment) (doc_comment) (container_doc_comment)] @comment.inside +[(line_comment) (doc_comment) (container_doc_comment)]+ @comment.around |
