diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/lspbench.zig | 264 | ||||
| -rw-r--r-- | test/snapshots/chordcut.golden | 56 | ||||
| -rw-r--r-- | test/snapshots/chordcut.snap | 20 | ||||
| -rw-r--r-- | test/snapshots/leader.golden | 116 | ||||
| -rw-r--r-- | test/snapshots/leader.snap | 38 |
5 files changed, 382 insertions, 112 deletions
diff --git a/test/lspbench.zig b/test/lspbench.zig new file mode 100644 index 00000000..73b82e2b --- /dev/null +++ b/test/lspbench.zig @@ -0,0 +1,264 @@ +//! The language-backend scoreboard: one binary, three numbers. +//! +//! zig build lspbench -- latency + feature matrix over pardes's own src/ +//! zig build lspbench -- --json -- the same, machine-readable +//! +//! Every competing backend links the same harness against the same corpus and +//! the same query list, so the columns mean the same thing across all of them. +//! What it measures, in the order the evaluation weighs it: +//! +//! FEATURES which lsp.Kind values actually return rows. Claiming support in +//! `lsp.supports` and returning nothing is a FALSE claim and shows +//! up as `claimed-empty` — the harness trusts results, not flags. +//! LATENCY cold (first query, index build included) and warm (median of N) +//! per kind. Cold is what a keypress costs the first time; warm is +//! what it costs forever after. Both matter and they differ by +//! orders of magnitude for an indexing backend. +//! MEMORY peak RSS delta across the run, read from /proc. +//! +//! Lines of code is not measured here — it is `jj diff --stat` against the +//! base commit, which is the honest number (a backend that vendors a library +//! pays for what it vendors only in build time, not in code we maintain). +const std = @import("std"); +const libc = std.c; +const lsp = @import("pardes").lsp; + +// 0.16 slimmed std.fs (no cwd(), no realpathAlloc) and this repo goes through +// libc everywhere for exactly that reason — see look.zig. +extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8; + +/// std.time.Timer is gone in 0.16; clock_gettime is what dump.zig already uses. +fn nowNs() u64 { + var ts: std.c.timespec = undefined; + _ = std.c.clock_gettime(.MONOTONIC, &ts); + return @as(u64, @intCast(ts.sec)) *| 1_000_000_000 +| @as(u64, @intCast(ts.nsec)); +} + +pub const std_options: std.Options = .{ .log_level = .err }; + +/// One probe. The corpus is pardes's own source: real Zig, in this repo, that +/// every implementation can reach without a fixture tree to keep in sync. +/// +/// The cursor is pinned by SYMBOL, not by line — the needle is searched for at +/// startup — so editing pardes.zig cannot silently rot the bench into probing +/// a blank line and calling the result "no support". +/// +/// `expect` is a substring the rows must contain for the probe to count as +/// CORRECT rather than merely non-empty. A backend that returns a plausible +/// wrong location scores worse than one that returns nothing, and only this +/// field can tell those two apart. +const Anchor = struct { + file: []const u8, + /// the exact source text to put the cursor on; the FIRST occurrence wins + needle: []const u8, + /// byte offset into `needle` where the cursor sits + at: u32 = 0, + kind: lsp.Kind, + expect: []const u8 = "", +}; + +const anchors = [_]Anchor{ + // a call to a function defined in the same file + .{ .file = "src/pardes.zig", .needle = "modal.hxLineCount(text)", .at = 6, .kind = .definition, .expect = "modal.zig" }, + // a plain local/decl reference within one file + .{ .file = "src/lsp.zig", .needle = "lineCol(source", .at = 0, .kind = .definition, .expect = "lsp.zig" }, + // a std reference: needs the zig lib dir, which is the first thing a + // single-file backend cannot do + .{ .file = "src/lsp.zig", .needle = "std.mem.count(u8", .at = 8, .kind = .definition, .expect = "mem.zig" }, + // hover over the same symbol + .{ .file = "src/lsp.zig", .needle = "lineCol(source", .at = 0, .kind = .hover, .expect = "" }, + // the file's own symbols + .{ .file = "src/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .document_symbols, .expect = "Kind" }, + .{ .file = "src/modal.zig", .needle = "pub fn ", .at = 7, .kind = .document_symbols, .expect = "" }, + // references to a symbol used in several places + .{ .file = "src/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .references, .expect = "" }, + // diagnostics on a file that should have none + .{ .file = "src/lsp.zig", .needle = "const std", .at = 6, .kind = .diagnostics, .expect = "" }, + // the remaining kinds, probed once each so the matrix is complete + .{ .file = "src/lsp.zig", .needle = "lineCol(source", .at = 0, .kind = .declaration, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "out: *std.ArrayList(u8)", .at = 10, .kind = .type_definition, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .implementation, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .select_refs, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub fn query", .at = 7, .kind = .format, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub fn query", .at = 7, .kind = .code_action, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub fn query", .at = 7, .kind = .rename, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .workspace_symbols, .expect = "" }, + .{ .file = "src/lsp.zig", .needle = "const std", .at = 6, .kind = .workspace_diagnostics, .expect = "" }, +}; + +const Result = struct { + kind: lsp.Kind, + /// no rows came back + empty: bool = true, + /// rows came back AND contained `expect` (or nothing was expected) + correct: bool = false, + rows: usize = 0, + cold_us: u64 = 0, + warm_us: u64 = 0, +}; + +const warm_iters = 20; + +pub fn main(init: std.process.Init) !void { + // page_allocator like the other harnesses: this measures the BACKEND, and + // a debug allocator's bookkeeping would land in every number. + const gpa = std.heap.page_allocator; + + const args = try init.minimal.args.toSlice(init.arena.allocator()); + var json = false; + var root: []const u8 = "."; + for (args[1..]) |a| { + if (std.mem.eql(u8, a, "--json")) json = true else root = a; + } + + const rss0 = rssKib(); + var results: std.ArrayList(Result) = .empty; + defer results.deinit(gpa); + + for (anchors) |an| { + const path = try std.fs.path.join(gpa, &.{ root, an.file }); + defer gpa.free(path); + const src = readZ(gpa, path) catch continue; + defer gpa.free(src); + const off = std.mem.indexOf(u8, src, an.needle) orelse { + std.debug.print("bench: anchor not found in {s}: `{s}`\n", .{ an.file, an.needle }); + continue; + }; + var realbuf: [4096]u8 = undefined; + var pz: [4096:0]u8 = undefined; + const pz_s = std.fmt.bufPrintSentinel(&pz, "{s}", .{path}, 0) catch continue; + const abs = std.mem.span(realpath(pz_s.ptr, &realbuf) orelse continue); + const dir = std.fs.path.dirname(abs) orelse "/"; + const req: lsp.Req = .{ + .kind = an.kind, + .path = abs, + .source = src, + .offset = @intCast(off + an.at), + .arg = if (an.kind == .rename) "renamed_by_bench" else if (an.kind == .workspace_symbols) "Kind" else "", + .root = dir, + }; + + var r: Result = .{ .kind = an.kind }; + // COLD: whatever the backend has to build the first time counts. + r.cold_us = runOnce(gpa, req, &r); + // WARM: median of warm_iters, so one scheduling hiccup cannot flatter + // or damn a backend. + var samples: [warm_iters]u64 = undefined; + for (&samples) |*s| { + var throwaway: Result = .{ .kind = an.kind }; + s.* = runOnce(gpa, req, &throwaway); + } + std.mem.sort(u64, &samples, {}, std.sort.asc(u64)); + r.warm_us = samples[warm_iters / 2]; + r.correct = !r.empty and (an.expect.len == 0 or blk: { + var out: std.ArrayList(u8) = .empty; + defer out.deinit(gpa); + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + lsp.query(gpa, arena.allocator(), req, &out); + break :blk std.mem.indexOf(u8, out.items, an.expect) != null; + }); + try results.append(gpa, r); + } + + const rss = rssKib() -| rss0; + if (json) reportJson(results.items, rss) else reportText(results.items, rss); +} + +fn runOnce(gpa: std.mem.Allocator, req: lsp.Req, r: *Result) u64 { + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + var out: std.ArrayList(u8) = .empty; + defer out.deinit(gpa); + const t0 = nowNs(); + lsp.query(gpa, arena.allocator(), req, &out); + const ns = nowNs() -| t0; + if (out.items.len > 0) { + r.empty = false; + r.rows = std.mem.count(u8, out.items, "\n"); + } + return ns / 1000; +} + +fn reportText(rs: []const Result, rss: u64) void { + const o = std.debug.print; + o("backend: {s}\n\n", .{lsp.backend_name}); + o("{s:<24} {s:>9} {s:>7} {s:>10} {s:>10} {s}\n", .{ "kind", "supports", "rows", "cold(us)", "warm(us)", "verdict" }); + o("{s}\n", .{"-" ** 78}); + var claimed: usize = 0; + var working: usize = 0; + var lies: usize = 0; + for (rs) |r| { + const sup = lsp.supports.contains(r.kind); + if (sup) claimed += 1; + const verdict: []const u8 = if (!sup and r.empty) + "-" + else if (sup and r.empty) + "CLAIMED-EMPTY" + else if (!sup and !r.empty) + "unclaimed-works" + else if (r.correct) + "ok" + else + "rows-but-wrong"; + if (sup and !r.empty and r.correct) working += 1; + if (sup and r.empty) lies += 1; + o("{s:<24} {s:>9} {d:>7} {d:>10} {d:>10} {s}\n", .{ + @tagName(r.kind), if (sup) "yes" else "no", r.rows, r.cold_us, r.warm_us, verdict, + }); + } + o("\nclaimed {d}, verified working {d}, false claims {d}\n", .{ claimed, working, lies }); + o("peak rss delta: {d} KiB\n", .{rss}); +} + +fn reportJson(rs: []const Result, rss: u64) void { + const o = std.debug.print; + o("{{\"backend\":\"{s}\",\"rss_kib\":{d},\"probes\":[", .{ lsp.backend_name, rss }); + for (rs, 0..) |r, i| { + if (i > 0) o(",", .{}); + o("{{\"kind\":\"{s}\",\"supports\":{},\"rows\":{d},\"correct\":{},\"cold_us\":{d},\"warm_us\":{d}}}", .{ + @tagName(r.kind), lsp.supports.contains(r.kind), r.rows, r.correct, r.cold_us, r.warm_us, + }); + } + o("]}}\n", .{}); +} + +fn readZ(gpa: std.mem.Allocator, path: []const u8) ![:0]u8 { + var pathbuf: [4096]u8 = undefined; + const path_z = try std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0); + const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY }); + if (fd < 0) return error.OpenFailed; + defer _ = libc.close(fd); + var buf: std.ArrayList(u8) = .empty; + errdefer buf.deinit(gpa); + var chunk: [16384]u8 = undefined; + while (true) { + const n = libc.read(fd, &chunk, chunk.len); + if (n < 0) { + if (libc.errno(n) == .INTR) continue; + return error.ReadFailed; + } + if (n == 0) break; + try buf.appendSlice(gpa, chunk[0..@intCast(n)]); + } + return buf.toOwnedSliceSentinel(gpa, 0); +} + +/// VmHWM from /proc/self/status — the peak, not the current, so a backend that +/// frees its index before returning still pays for having built it. +fn rssKib() u64 { + const fd = libc.open("/proc/self/status", .{ .ACCMODE = .RDONLY }); + if (fd < 0) return 0; + defer _ = libc.close(fd); + var buf: [4096]u8 = undefined; + const n = libc.read(fd, &buf, buf.len); + if (n <= 0) return 0; + var it = std.mem.tokenizeScalar(u8, buf[0..@intCast(n)], '\n'); + while (it.next()) |line| { + if (!std.mem.startsWith(u8, line, "VmHWM:")) continue; + var t = std.mem.tokenizeAny(u8, line["VmHWM:".len..], " \t"); + const v = t.next() orelse return 0; + return std.fmt.parseInt(u64, v, 10) catch 0; + } + return 0; +} diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden index be00dc57..6f2da745 100644 --- a/test/snapshots/chordcut.golden +++ b/test/snapshots/chordcut.golden @@ -339,21 +339,21 @@ |27: 0-1 d,#444444, 2-6 #626262,#080808, 7-29 d,#080808, 30-31 d,#444444, 32-59 #bdbdbd,#080808, 60-61 d,#444444, 62-89 #bdbdbd,#080808, |28: 0-1 d,#444444, 2-6 #626262,#080808, 7-29 d,#080808, 30-31 d,#444444, 32-59 #bdbdbd,#080808, 60-61 d,#444444, 62-89 #bdbdbd,#080808, |29: 0-1 d,#444444, 2-6 #626262,#080808, 7-29 d,#080808, 30-31 d,#444444, 32-59 #bdbdbd,#080808, 60-61 d,#444444, 62-89 #bdbdbd,#080808, -== snap ttyprobe grid=90x30 cursor=9,11 +== snap ttyprobe grid=90x30 cursor=44,13 |Kill Newcol Tutor Debug NextColor Dump Find Grep -| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu -| 1 line 1 +| NOR /tmp/pardes-snap/chordcu TTY /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 $ ls | 2 2echo B''OO g.txt f.txt g.txt f.txt g.txt -| 3 line 3 -| 4 line 4line -| 5 lline 5 -| 6 canon -echo; printf '\x f.txt -| 7 BOO -| -| -| -| -| +| 3 line 3 $ +| 4 line 4line $ +| 5 lline 5 $ +| 6 $ echo B''OO +| BOO +| $ stty -icanon -echo; printf +| '\x1b[?2004h\x1b[?1000h\x1b +| [?1006h'; cat -v +| ^[[<0;8;10M^[[<0;8;10m^[[200 +| ~line^[[201~ | | | NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu @@ -370,23 +370,23 @@ | | | -== snap ttyprompt grid=90x30 cursor=9,11 +== snap ttyprompt grid=90x30 cursor=38,15 |Kill Newcol Tutor Debug NextColor Dump Find Grep -| NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu -| 1 line 1 +| NOR /tmp/pardes-snap/chordcu TTY /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu +| 1 line 1 $ ls | 2 2echo B''OO g.txt f.txt g.txt f.txt g.txt -| 3 line 3 -| 4 line 4line -| 5 lline 5 -| 6 canon -echo; printf '\x f.txt -| 7 BOO -| -| -| -| -| -| -| +| 3 line 3 $ +| 4 line 4line $ +| 5 lline 5 $ +| 6 $ echo B''OO +| BOO +| $ stty -icanon -echo; printf +| '\x1b[?2004h\x1b[?1000h\x1b +| [?1006h'; cat -v +| ^[[<0;8;10M^[[<0;8;10m^[[200 +| ~line^[[201~ +| $ +| $ line | NOR /tmp/pardes-snap/chordcu NOR /tmp/pardes-snap/chordcu | 1 line 1 | 2 line 2 f.txt g.txt diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap index a412b57e..a1b65a4d 100644 --- a/test/snapshots/chordcut.snap +++ b/test/snapshots/chordcut.snap @@ -118,18 +118,18 @@ snapstyle clickdismiss # tty paste chord: a mouse-listening app gets the click forwarded (SGR, # body-relative) then the register between bracketed-paste markers — # cat -v prints the received bytes visibly -press left 5 11 -release left 5 11 +press left 35 11 +release left 35 11 stable 400 5000 key c-b stable 600 8000 text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v key enter stable 700 8000 -press left 10 12 -press right 10 12 -release right 10 12 -release left 10 12 +press left 40 12 +press right 40 12 +release right 40 12 +release left 40 12 stable 700 8000 snap ttyprobe # a plain prompt (mouse modes off) gets no click bytes — just the paste; @@ -139,9 +139,9 @@ stable 600 8000 text stty sane; printf '\x1b[?1000l\x1b[?1006l' key enter stable 600 8000 -press left 10 12 -press right 10 12 -release right 10 12 -release left 10 12 +press left 40 12 +press right 40 12 +release right 40 12 +release left 40 12 stable 700 8000 snap ttyprompt diff --git a/test/snapshots/leader.golden b/test/snapshots/leader.golden index 934573be..5215b6ae 100644 --- a/test/snapshots/leader.golden +++ b/test/snapshots/leader.golden @@ -39,9 +39,9 @@ | | | -== snap spc-h grid=100x40 cursor=7,2 +== snap spc-f grid=100x40 cursor=7,2 |Kill Newcol Tutor Debug NextColor Dump Find Grep -| NOR /tmp/pardes-snap/leader/cwd/cmds.txt S SPC h NOR /tmp/pardes-snap/leader/cwd Del +| NOR /tmp/pardes-snap/leader/cwd/cmds.txt S SPC f NOR /tmp/pardes-snap/leader/cwd Del | 1 Help | cmds.txt | @@ -189,20 +189,20 @@ | 2 or execute the name anywhere | 3 | 4 SPC ? Help -| 5 SPC c d Delcol -| 6 SPC c n Newcol -| 7 SPC d Del -| 8 SPC f f Find -| 9 SPC f g Grep -| 10 SPC f s Save -| 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC s d Dump -| 14 SPC s r Restore -| 15 SPC t a Ascii -| 16 SPC t c Colors -| 17 SPC t d Debug -| 18 SPC t l Palette +| 5 SPC D WsDiagnostics +| 6 SPC S WsSymbols +| 7 SPC T Tutor +| 8 SPC a CodeAction +| 9 SPC c d Delcol +| 10 SPC c n Newcol +| 11 SPC d Diagnostics +| 12 SPC f d Dump +| 13 SPC f f Find +| 14 SPC f g Grep +| 15 SPC f r Restore +| 16 SPC f s Save +| 17 SPC h SelectRefs +| 18 SPC k Hover == snap help-group grid=100x40 cursor=7,22 |Kill Newcol Tutor Debug NextColor Dump Find Grep | NOR /tmp/pardes-snap/leader/cwd/cmds.txt Save De NOR /tmp/pardes-snap/leader/cwd Del @@ -226,14 +226,14 @@ | | | NOR /tmp/pardes-snap/leader/cwd/+Help Del -| 1 pardes builtins under SPC h +| 1 pardes builtins under SPC f | 2 -| 3 SPC h t Tutor -| 4 -| -| -| -| +| 3 SPC f d Dump +| 4 SPC f f Find +| 5 SPC f g Grep +| 6 SPC f r Restore +| 7 SPC f s Save +| 8 | | | @@ -267,14 +267,14 @@ | 8 | 9 | NOR /tmp/pardes-snap/leader/cwd/+Help Del -| 1 pardes builtins under SPC h +| 1 pardes builtins under SPC f | 2 -| 3 SPC h t Tutor -| 4 -| -| -| -| +| 3 SPC f d Dump +| 4 SPC f f Find +| 5 SPC f g Grep +| 6 SPC f r Restore +| 7 SPC f s Save +| 8 | | | @@ -308,14 +308,14 @@ | | | NOR /tmp/pardes-snap/leader/cwd/+Help Del -| 1 pardes builtins under SPC h +| 1 pardes builtins under SPC f | 2 -| 3 SPC h t Tutor -| 4 -| -| -| -| +| 3 SPC f d Dump +| 4 SPC f f Find +| 5 SPC f g Grep +| 6 SPC f r Restore +| 7 SPC f s Save +| 8 | | | @@ -353,20 +353,20 @@ | 2 or execute the name anywhere | 3 | 4 SPC ? Help -| 5 SPC c d Delcol -| 6 SPC c n Newcol -| 7 SPC d Del -| 8 SPC f f Find -| 9 SPC f g Grep -| 10 SPC f s Save -| 11 SPC h t Tutor -| 12 SPC k Kill -| 13 SPC s d Dump -| 14 SPC s r Restore -| 15 SPC t a Ascii -| 16 SPC t c Colors -| 17 SPC t d Debug -| 18 SPC t l Palette +| 5 SPC D WsDiagnostics +| 6 SPC S WsSymbols +| 7 SPC T Tutor +| 8 SPC a CodeAction +| 9 SPC c d Delcol +| 10 SPC c n Newcol +| 11 SPC d Diagnostics +| 12 SPC f d Dump +| 13 SPC f f Find +| 14 SPC f g Grep +| 15 SPC f r Restore +| 16 SPC f s Save +| 17 SPC h SelectRefs +| 18 SPC k Hover == snap help-toggles grid=100x40 cursor=7,22 |Kill Newcol Tutor Debug NextColor Dump Find Grep | NOR /tmp/pardes-snap/leader/cwd/cmds.txt Save De NOR /tmp/pardes-snap/leader/cwd Del @@ -433,13 +433,13 @@ | NOR /tmp/pardes-snap/leader/cwd/+Help Del | 1 pardes builtins under SPC w | 2 -| 3 SPC w h Left -| 4 SPC w j Down -| 5 SPC w k Up -| 6 SPC w l Right -| 7 SPC w t Toggleterm -| 8 -| +| 3 SPC w c Del +| 4 SPC w h Left +| 5 SPC w j Down +| 6 SPC w k Up +| 7 SPC w l Right +| 8 SPC w t Toggleterm +| 9 | | | diff --git a/test/snapshots/leader.snap b/test/snapshots/leader.snap index c8e13b8e..e06d5a3c 100644 --- a/test/snapshots/leader.snap +++ b/test/snapshots/leader.snap @@ -6,6 +6,10 @@ # pane with no file behind it, like +Search). The paths, the Help lines and # the execute dispatch are all folded out of ONE enum at comptime — so # executing the WORD `Help` runs the same builtin the leader does. +# +# The single-letter slots k/r/a/h/s/S/d/D belong to helix's LSP commands, so +# pardes's own builtins live around them: Kill on `q`, Del in the window group +# as `w c`, Dump/Restore in the file group, Tutor on `T`. file cmds.txt Help start 40 100 cmds.txt wait 8000 Kill Newcol @@ -15,10 +19,10 @@ stable 700 20000 key space stable 400 5000 snap spc -# ... and grows as the path is typed (`h` = the docs group) -key h +# ... and grows as the path is typed (`f` = the file group) +key f stable 400 5000 -snap spc-h +snap spc-f # Esc abandons the sequence: indicator gone, nothing ran, cursor untouched key esc settle 100 @@ -27,41 +31,43 @@ snap abandoned # an UNMAPPED key abandons too, rather than waiting: a typo must not leave the # next keystroke armed at a builtin that closes panes key space -key q +key z stable 400 5000 snap unmapped # executing the WORD `Help` (middle-click) runs the builtin — the enum IS the # lookup, so a name works anywhere text lives. Every builtin, with its keys. press middle 9 3 release middle 9 3 -wait 10000 SPC h t +wait 10000 SPC T stable 700 15000 snap help-word -# `SPC h ?`: only the `h` group, rewritten into the SAME +Help buffer +# `SPC f ?`: only the `f` group, rewritten into the SAME +Help buffer key space -key h +key f key ? -wait 10000 under SPC h +wait 10000 under SPC f stable 700 15000 snap help-group -# `SPC h t`: the Tutor builtin, zero arguments. It runs from the +Help pane, +# `SPC T`: the Tutor builtin, zero arguments. It runs from the +Help pane, # but that is an output pane and not a document — so the tutor lands under # cmds.txt, the real doc, with the listing left at the foot of the column. key space -key h -key t +key T wait 10000 PARDES TUTOR stable 700 15000 snap tutor -# `SPC d`: Del is pane-scoped — it closes the tutor, focus falls back +# `SPC w c`: Del is pane-scoped — it closes the tutor, focus falls back. It +# sits in the window group because closing a pane IS a window operation, and +# because `d` now belongs to the diagnostics picker. key space -key d +key w +key c stable 700 15000 snap deleted # `SPC ?` at the root lists everything again key space key ? -wait 10000 SPC s r +wait 10000 SPC f r stable 700 15000 snap help-all # `SPC t ?`: the toggle group, seven of them now that the image renderer @@ -73,8 +79,8 @@ key ? wait 10000 Petscii stable 700 15000 snap help-toggles -# `SPC w ?`: the window group, whose five builtins are the tail of the full -# listing above and fall off the bottom of this pane there +# `SPC w ?`: the window group — the four focus moves, the terminal hop, and +# Del. They are the tail of the full listing above and fall off its bottom. key space key w key ? |
