summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/lspbench.zig264
-rw-r--r--test/snapshots/chordcut.golden56
-rw-r--r--test/snapshots/chordcut.snap20
-rw-r--r--test/snapshots/leader.golden116
-rw-r--r--test/snapshots/leader.snap38
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 ?