summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/bench_util.zig43
-rw-r--r--test/column_grip.py132
-rw-r--r--test/fs.py2
-rw-r--r--test/fs_soak.py284
-rw-r--r--test/look_placement.py143
-rw-r--r--test/lspbench.zig170
-rw-r--r--test/output.zig5
-rw-r--r--test/panes.zig22
-rw-r--r--test/pdf.zig1
-rw-r--r--test/pdf_bench.zig23
-rw-r--r--test/pdf_pointer_bench.zig14
-rw-r--r--test/pdf_scroll_bench.zig43
-rw-r--r--test/pdf_sections_bench.zig65
-rw-r--r--test/snapshot.zig2
-rw-r--r--test/snapshots/lsp.golden5
-rw-r--r--test/snapshots/lsp.snap10
-rw-r--r--test/snapshots/lspcomplete.golden20
-rw-r--r--test/snapshots/lspcomplete.snap21
-rw-r--r--test/snapshots/lspcompletemoved.golden2
-rw-r--r--test/snapshots/lspdebug.golden2
-rw-r--r--test/snapshots/lspdebug.snap23
-rw-r--r--test/snapshots/lsprelpath.golden120
-rw-r--r--test/snapshots/lsprelpath.snap34
23 files changed, 368 insertions, 818 deletions
diff --git a/test/bench_util.zig b/test/bench_util.zig
new file mode 100644
index 00000000..9b91e621
--- /dev/null
+++ b/test/bench_util.zig
@@ -0,0 +1,43 @@
+//! What the PDF benches share: a clock, a fatal exit named for the bench, a
+//! seeded hash, a count parser and the core's effect drain.
+const std = @import("std");
+
+pub 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));
+}
+
+/// `fatal` for the bench called `who`: says so, prefixed, and exits 1.
+pub fn Fatal(comptime who: []const u8) type {
+ return struct {
+ pub fn f(comptime format: []const u8, args: anytype) noreturn {
+ std.debug.print(who ++ ": " ++ format ++ "\n", args);
+ std.process.exit(1);
+ }
+ };
+}
+
+/// A seeded hash of `value`'s bytes; a comptime integer hashes as a u64.
+pub fn mix(seed: u64, value: anytype) u64 {
+ const Value = @TypeOf(value);
+ const Stable = switch (@typeInfo(Value)) {
+ .comptime_int => u64,
+ else => Value,
+ };
+ var stable: Stable = value;
+ return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
+}
+
+/// A flag's value as a count in 1..`maximum`, or `fatal` saying why not.
+pub fn parsePositive(comptime fatal: anytype, name: []const u8, text: []const u8, maximum: usize) usize {
+ const value = std.fmt.parseUnsigned(usize, text, 10) catch
+ fatal("{s} expects a positive integer", .{name});
+ if (value == 0 or value > maximum)
+ fatal("{s} must be in 1..{d}", .{ name, maximum });
+ return value;
+}
+
+pub fn drainEffects(core: anytype) void {
+ while (core.nextEffect()) |_| {}
+}
diff --git a/test/column_grip.py b/test/column_grip.py
deleted file mode 100644
index ac0341c3..00000000
--- a/test/column_grip.py
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env python3
-"""Drag a column grip in the real SDL window with pixel mouse events.
-
-WAYLAND_DISPLAY=/run/user/1000/wayland-1 \\
- python3 -B test/column_grip.py zig-out/bin/pardes-gui /tmp/pardes-column-grip [--pad 7]
-
-The window is hidden (PARDES_TEST). Mouse events enter as ESC]777;mouse
-sequences on stdin, which the GUI builds into SDL events and dispatches like
-its own, so pixel-to-cell and tag hit testing run as they do under a hand.
-9P observes the grid; each step leaves a PNG of the GPU frame in the output
-directory: grip-rest, grip-resize-drag, grip-reorder-drag, grip-done.
-"""
-import argparse
-import os
-from pathlib import Path
-import shutil
-import subprocess
-import time
-
-from fs import session
-from ui_review import require, wait_for
-
-
-def rows(client):
- frame = client.screen()
- cols = frame['cols']
- return [''.join(cell[0] for cell in frame['cells'][at:at + cols]) for at in range(0, len(frame['cells']), cols)]
-
-
-def rail(client):
- """The one column the drag preview's rail runs down, if any."""
- bottom = rows(client)[-1]
- found = [x for x, ch in enumerate(bottom) if ch == '╎']
- require(len(found) <= 1, f'more than one rail: {found}')
- return found[0] if found else None
-
-
-def grab(root, label):
- time.sleep(.5)
- def convert():
- shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm'))
- return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))],
- stderr=subprocess.DEVNULL).returncode == 0
- require(wait_for(convert, 8), 'GPU capture did not contain a complete image')
- return root / (label + '.png')
-
-
-def pixel(path, x, y):
- out = subprocess.run(['magick', str(path), '-format', f'%[pixel:p{{{x},{y}}}]', 'info:'],
- capture_output=True, text=True, check=True)
- return out.stdout.strip()
-
-
-def run(args):
- root = args.output.resolve()
- shutil.rmtree(root, ignore_errors=True)
- root.mkdir(parents=True, mode=0o700)
- (root / 'config/pardes').mkdir(parents=True)
- (root / 'config/pardes/init').write_text('Shell /bin/sh\n')
- (root / 'w.txt').write_text('alpha\nbeta\n')
- name = 'column-grip-' + str(os.getpid())
- launch = ['-c', 'stty cols 100 rows 30; exec "$@"', 'column-grip',
- str(args.binary.resolve()), '--9p=' + name, str(root / 'w.txt')]
- env = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), 'PARDES_TEST_COLS': '100',
- 'PARDES_TEST_ROWS': '30', 'PARDES_TEST_PAD': str(args.pad)}
- handles = {}
- with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch,
- inherited=env, terminal=handles) as (client, _):
- def send(data):
- os.write(handles['input_fd'], data.encode())
-
- def mouse(kind, button, col, row):
- # the middle of a grid cell, in window pixels
- send(f'\x1b]777;mouse;{kind};{button};{(col + .5) * cw};{(row + .5) * ch}\x07')
- time.sleep(.15)
-
- require(wait_for(lambda: 'Newcol' in rows(client)[0]), 'workspace tag never drew')
- send('\x1bn')
- time.sleep(.3)
- send('\x1bc')
- require(wait_for(lambda: rows(client)[1].count('New Tty') == 2), 'no second column')
- require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing')
- time.sleep(.5)
- with open(root / 'latest.ppm', 'rb') as ppm:
- ppm.readline()
- width, height = map(int, ppm.readline().split())
- frame = client.screen()
- cw = (width - args.pad) // frame['cols']
- ch = (height - args.pad) // frame['rows']
- edge = rows(client)[1].index('New Tty', 10) - 3
-
- rest = grab(root, 'grip-rest')
- mouse('down', 1, edge, 1)
- held = grab(root, 'grip-held')
- require(pixel(held, int((edge + 1) * cw), int(1.5 * ch)) != pixel(rest, int((edge + 1) * cw), int(1.5 * ch)),
- 'a grabbed grip looks the same as a resting one')
- require(rail(client) is None, 'a grip held in place previews a move')
-
- # Short of the left column's middle: the left edge follows, and the
- # rail is the seam cell left of where the rule will be.
- for col in range(edge, edge - 11, -1):
- mouse('motion', 1, col, 1)
- require(rail(client) == edge - 11, f'resize rail at {rail(client)}, wanted {edge - 11}')
- grab(root, 'grip-resize-drag')
- mouse('up', 1, edge - 10, 1)
- require(wait_for(lambda: rows(client)[1].index('New Tty', 10) - 3 == edge - 10), 'left edge did not follow the grip')
- edge -= 10
-
- # Over the left column's left half: it goes first, and the rail runs
- # just past the left window edge's scroll rail.
- mouse('down', 1, edge, 1)
- for col in range(edge, 4, -5):
- mouse('motion', 1, col, 10)
- require(rail(client) == 1, f'reorder rail at {rail(client)}, wanted 1')
- grab(root, 'grip-reorder-drag')
- mouse('up', 1, 5, 10)
- require(wait_for(lambda: 'alpha' in rows(client)[3][50:]), 'the column did not move first')
- require(rail(client) is None, 'the rail outlived the drag')
- grab(root, 'grip-done')
- print('ok', root)
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument('binary', type=Path)
- parser.add_argument('output', type=Path)
- parser.add_argument('--pad', type=int, default=0, help='leftover pixels past the grid (PARDES_TEST_PAD)')
- run(parser.parse_args())
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/fs.py b/test/fs.py
index 06643385..6f1165e5 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1327,7 +1327,7 @@ def test(binary, quic=False):
time.sleep(.005)
assert len(matches) == 1, matches
result = client.read(f'/pane/{matches[0]}/body')
- assert result.startswith(b'backend: zls-inproc+lsp-client\n'), result
+ assert result.startswith(b'backend: lsp-client\n'), result
assert b'protocol servers (lsp-client):' in result, result
assert client.read('/pane/1/body') == b'initial\n'
assert '+Lsp' in ''.join(cell[0] for cell in client.screen()['cells'])
diff --git a/test/fs_soak.py b/test/fs_soak.py
deleted file mode 100644
index a62f03e6..00000000
--- a/test/fs_soak.py
+++ /dev/null
@@ -1,284 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import json
-import os
-from pathlib import Path
-import random
-import socket
-import struct
-import tempfile
-import time
-
-from fs import new_pane, session
-from ninep import Client
-
-
-BODY_LIMIT = 4096
-SCREEN_LIMIT = 4 * 1024 * 1024
-FRAGMENTS = [
- b'const value = 42;\n', b'fn check() void { if (true) return; }\n',
- b'\tspaces and tabs\n', 'λ 界 e\u0301 👩\u200d🚀\n'.encode(), b'x',
-]
-OPERATIONS = ['append', 'range', 'replace', 'read', 'save', 'reload',
- 'cycle', 'reconnect', 'screen']
-
-
-def emit(**record):
- print(json.dumps(record, separators=(',', ':')), flush=True)
-
-
-def payload(rng, limit=256):
- result = bytearray()
- for _ in range(rng.randint(1, 12)):
- fragment = rng.choice(FRAGMENTS)
- if len(result) + len(fragment) <= limit:
- result.extend(fragment)
- return bytes(result) or b'x'[:limit]
-
-
-def check_bytes(actual, expected, label):
- if actual == expected:
- return
- at = next((i for i, pair in enumerate(zip(actual, expected))
- if pair[0] != pair[1]), min(len(actual), len(expected)))
- raise AssertionError(f'{label}: mismatch at byte {at}; '
- f'length {len(actual)} != {len(expected)}; '
- f'{actual[at:at + 32]!r} != {expected[at:at + 32]!r}')
-
-
-def read_fid(client, fid, limit, count):
- result = bytearray()
- while chunk := client.read_fid(fid, len(result), count):
- result.extend(chunk)
- if len(result) > limit:
- raise AssertionError(f'read exceeded fixture limit {limit}')
- return bytes(result)
-
-
-def check_body(client, pane, count=4096):
- fid = client.open(f'/pane/{pane["serial"]}/body')
- try:
- actual = read_fid(client, fid, BODY_LIMIT, count)
- finally:
- client.close(fid)
- check_bytes(actual, pane['body'], f'pane {pane["serial"]}')
-
-
-def check_screen(data):
- screen = json.loads(data)
- cols, rows = screen['cols'], screen['rows']
- assert cols > 0 and rows > 0 and cols * rows <= 65536, (cols, rows)
- assert len(screen['cells']) == cols * rows
- styles = screen['styles']
- assert styles
- for cell in screen['cells']:
- assert len(cell) == 2 and isinstance(cell[0], str), cell
- assert isinstance(cell[1], int) and 0 <= cell[1] < len(styles), cell
-
-
-def session_pid(client):
- if not hasattr(socket, 'SO_PEERCRED'):
- return None
- pid, uid, _ = struct.unpack('3i', client.socket.getsockopt(
- socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i')))
- assert pid > 0 and uid == os.getuid(), (pid, uid)
- return pid
-
-
-def check_session(client, control, pid):
- assert session_pid(control) == pid
- if pid is not None:
- actual = (session_pid(client) if client.socket.family == socket.AF_UNIX
- else int(client.read('/os/proc/self/stat').split()[0]))
- assert actual == pid, (actual, pid)
-
-
-def memory(pid):
- result = {'rss_bytes': None, 'cumulative_peak_rss_bytes': None}
- if pid is None:
- return result
- try:
- for line in Path(f'/proc/{pid}/status').read_text().splitlines():
- fields = line.split()
- if fields[0] == 'VmRSS:':
- result['rss_bytes'] = int(fields[1]) * 1024
- elif fields[0] == 'VmHWM:':
- result['cumulative_peak_rss_bytes'] = int(fields[1]) * 1024
- except OSError:
- pass
- return result
-
-
-def run(args):
- rng = random.Random(args.seed)
- started = time.monotonic()
- deadline = None
- batch = 0
- operation = 'start'
- operation_count = 0
- pid = None
- try:
- with tempfile.TemporaryDirectory(prefix='pardes-soak-') as directory:
- root = Path(directory)
- options = ['--9p-tcp=tcp!127.0.0.1!0'] if args.transport == 'tcp' else []
- with session(str(args.binary.resolve()), root, 'soak', *options,
- tty=args.tty) as (control, unix_address):
- pid = session_pid(control)
- address = unix_address
- if args.transport == 'tcp':
- listeners = [line.split('!') for line in
- control.read('/listeners').decode().splitlines()
- if line.startswith('tcp!')]
- assert len(listeners) == 1, listeners
- transport, host, port = listeners[0]
- assert transport == 'tcp' and host == '127.0.0.1' and 0 < int(port) < 65536, listeners
- address = (host, int(port))
- client = Client(address)
- panes = []
- try:
- check_session(client, control, pid)
- for slot in range(4):
- body = payload(rng)
- serial = 1 if slot == 0 else new_pane(client, body)
- base = f'/pane/{serial}'
- if slot == 0:
- client.write(base + '/body', body, truncate=True)
- path = root / f'pane-{slot}-0.zig'
- client.write(base + '/name', f'{path}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- panes.append({'serial': serial, 'body': body, 'path': path, 'variant': 0})
- emit(kind='start', seed=args.seed, session_pid=pid,
- transport=args.transport, host='tty' if args.tty else 'detached',
- batches=args.batches, duration_seconds=args.duration,
- interval_seconds=args.interval, body_limit_bytes=BODY_LIMIT,
- live_panes=len(panes), **memory(pid))
- if args.duration is not None:
- deadline = time.monotonic() + args.duration
- while (args.batches is None or batch < args.batches) and (
- deadline is None or time.monotonic() < deadline):
- batch_started = time.monotonic()
- operations = OPERATIONS * 2
- rng.shuffle(operations)
- for operation in operations:
- slot = rng.randrange(len(panes))
- pane = panes[slot]
- base = f'/pane/{pane["serial"]}'
- if operation == 'append':
- addition = payload(rng, min(256, BODY_LIMIT - len(pane['body'])))
- client.write(base + '/body', addition)
- pane['body'] += addition
- elif operation == 'range':
- body = pane['body']
- # Keep range endpoints outside combining and ZWJ clusters.
- boundaries = [0] + [i for i in range(1, len(body))
- if body[i - 1] < 128 and body[i] < 128] + [len(body)]
- lo, hi = sorted((rng.choice(boundaries), rng.choice(boundaries)))
- replacement = payload(rng, min(256, BODY_LIMIT - len(body) + hi - lo))
- if replacement:
- client.write(base + '/addr', f'#{lo},#{hi}'.encode())
- client.write(base + '/data', replacement)
- pane['body'] = body[:lo] + replacement + body[hi:]
- elif operation == 'replace':
- pane['body'] = payload(rng, BODY_LIMIT)
- client.write(base + '/body', pane['body'], truncate=True)
- elif operation == 'read':
- check_body(client, pane, rng.choice([97, 251, 1024, 4096]))
- elif operation == 'save':
- pane['variant'] ^= 1
- pane['path'] = root / f'pane-{slot}-{pane["variant"]}.zig'
- client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- check_bytes(pane['path'].read_bytes(), pane['body'], 'saved file')
- check_bytes(client.read('/os' + str(pane['path'])), pane['body'], 'OS mount')
- elif operation == 'reload':
- pane['body'] = payload(rng)
- pane['path'].write_bytes(pane['body'])
- client.write(base + '/ctl', b'get\n')
- elif operation == 'cycle':
- old_serial = pane['serial']
- client.remove(base)
- pane['body'] = payload(rng)
- pane['serial'] = new_pane(client, pane['body'])
- assert pane['serial'] != old_serial
- base = f'/pane/{pane["serial"]}'
- client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- elif operation == 'reconnect':
- client.socket.close()
- client = Client(address)
- check_session(client, control, pid)
- check_body(control, pane)
- elif operation == 'screen':
- frozen = client.open('/screen')
- try:
- before = read_fid(client, frozen, SCREEN_LIMIT, 4096)
- check_screen(before)
- addition = payload(rng, min(64, BODY_LIMIT - len(pane['body'])))
- client.write(base + '/body', addition)
- pane['body'] += addition
- after = read_fid(client, frozen, SCREEN_LIMIT, rng.choice([251, 1024]))
- check_bytes(after, before, 'frozen screen')
- finally:
- client.close(frozen)
- fresh = client.open('/screen')
- try:
- check_screen(read_fid(client, fresh, SCREEN_LIMIT, 4096))
- finally:
- client.close(fresh)
- operation_count += 1
- check_body(client, pane)
- expected = {str(pane['serial']) for pane in panes}
- assert set(client.list('/pane')) == expected
- assert set(control.list('/pane')) == expected
- for pane in panes:
- check_body(client, pane)
- check_body(control, pane)
- batch += 1
- emit(kind='batch', seed=args.seed, batch=batch,
- duration_seconds=time.monotonic() - batch_started,
- elapsed_seconds=time.monotonic() - started,
- operations=len(operations), total_operations=operation_count,
- model_bytes=sum(len(pane['body']) for pane in panes), **memory(pid))
- delay = args.interval - (time.monotonic() - batch_started)
- if deadline is not None:
- delay = min(delay, deadline - time.monotonic())
- if delay > 0:
- time.sleep(delay)
- finally:
- client.socket.close()
- emit(kind='summary', seed=args.seed, batches=batch,
- transport=args.transport, host='tty' if args.tty else 'detached',
- operations=operation_count, duration_seconds=time.monotonic() - started,
- status='passed')
- except BaseException as error:
- emit(kind='failure', seed=args.seed, batch=batch + 1, operation=operation,
- transport=args.transport, host='tty' if args.tty else 'detached',
- operations=operation_count, session_pid=pid,
- elapsed_seconds=time.monotonic() - started, error=str(error))
- raise
-
-
-def main():
- parser = argparse.ArgumentParser(description='Deterministic valid-operation editor/9P soak; writes JSONL.')
- parser.add_argument('binary', type=Path)
- parser.add_argument('--seed', type=int, default=4200)
- parser.add_argument('--transport', choices=['unix', 'tcp'], default='unix')
- parser.add_argument('--tty', action='store_true', help='run an owned TTY host instead of a detached host')
- limit = parser.add_mutually_exclusive_group()
- limit.add_argument('--batches', type=int)
- limit.add_argument('--duration', type=float, help='seconds, stopping after the current batch')
- parser.add_argument('--interval', type=float, default=0, help='minimum seconds between batch starts')
- args = parser.parse_args()
- if args.batches is None and args.duration is None:
- args.batches = 100
- if args.batches is not None and args.batches < 1:
- parser.error('--batches must be positive')
- if args.duration is not None and not 0 < args.duration < float('inf'):
- parser.error('--duration must be finite and positive')
- if not 0 <= args.interval < float('inf'):
- parser.error('--interval must be finite and nonnegative')
- run(args)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/look_placement.py b/test/look_placement.py
deleted file mode 100644
index 8a31157f..00000000
--- a/test/look_placement.py
+++ /dev/null
@@ -1,143 +0,0 @@
-#!/usr/bin/env python3
-"""Check narrow-window first-document Look placement in owned SDL/TTY sessions.
-
-python3 -B test/look_placement.py zig-out/column-review/bin/pardes-gui
-python3 -B test/look_placement.py zig-out/column-review/bin/pardes --tty
-
-Starts the classic three-terminal layout, then opens the first document from
-the lower-left or right terminal. Screen-grid geometry must split only that
-terminal, immediately below it, and a repeated Look must reuse the file pane.
-Use --output DIR to retain grid observations and SDL GPU screenshots.
-"""
-import argparse
-from contextlib import nullcontext
-from pathlib import Path
-import tempfile
-
-from fs import session
-from ui_review import capture, panes, require, wait_for
-
-
-def geometry(client, markers):
- frame = client.screen()
- width = frame['cols']
- lines = [''.join(cell[0] for cell in frame['cells'][row * width:(row + 1) * width])
- for row in range(frame['rows'])]
- positions = {}
- for serial, marker in markers.items():
- hits = [(row, line.index(marker)) for row, line in enumerate(lines) if marker in line]
- if len(hits) != 1:
- return None
- row, offset = hits[0]
- column_x = 0 if offset < width // 2 else width // 2
- positions[serial] = (column_x, row)
- rectangles = {}
- for serial, (column_x, row) in positions.items():
- following = [other_y for other_x, other_y in positions.values()
- if other_x == column_x and other_y > row]
- end = min(following, default=frame['rows'])
- rectangles[serial] = (column_x, row, width // 2, end - row)
- return frame, rectangles
-
-
-def rectangle_text(frame, rect):
- x, y, width, height = rect
- return tuple(tuple(cell[0] for cell in frame['cells'][row * frame['cols'] + x:
- row * frame['cols'] + x + width])
- for row in range(y, y + height))
-
-
-def run_case(args, root, source_index):
- root.mkdir(parents=True, exist_ok=True, mode=0o700)
- root.chmod(0o700)
- config = root / 'config/pardes'
- config.mkdir(parents=True)
- (config / 'init').write_text('Shell /bin/sh\nTheme forge\n')
- target = root / 'opened.txt'
- contents = b'First document opened from this terminal.\n' * 40
- target.write_bytes(contents)
- name = 'look-placement'
- inherited = {} if args.tty else {
- 'PARDES_TEST': '1', 'PARDES_TEST_COLS': '150', 'PARDES_TEST_ROWS': '48',
- 'PARDES_TEST_CAPTURE_DIR': str(root), 'SDL_VIDEODRIVER': 'x11',
- }
- # No document argument: the regression concerns the very first document.
- launch = ['-c', 'stty cols 150 rows 48; exec "$@"', name,
- str(args.binary.resolve()), '--9p=' + name, '-n', '3']
- with session('/bin/sh', root, name, socket_name=name, tty=True,
- launch=launch, inherited=inherited) as (client, _):
- serials = list(panes(client))
- require(len(serials) == 3, 'classic fixture did not boot three terminals')
- markers = {}
- for index, serial in enumerate(serials):
- pane = f'/pane/{serial}'
- require('pty' in client.list(pane), 'fixture contains an existing document')
- marker = f'LP_{index}'
- markers[serial] = marker
- client.write(pane + '/tag', b'', truncate=True)
- client.write(pane + '/tag', f' {marker} Tty Del Collapse'.encode())
- # Establish non-pristine shells and a stable prompt before measuring.
- client.write(pane + '/pty/data', f"printf 'ready-{index}\\n'\r".encode())
- require(wait_for(lambda: f'\nready-{index}\n'.encode() in
- client.read(pane + '/body').replace(b'\r', b'')),
- 'terminal did not execute readiness command')
- before_frame, before = require(wait_for(lambda: geometry(client, markers)),
- 'terminal tags not visible in fixture')
- source = serials[source_index]
- source_rect = before[source]
- require(source_rect[3] >= 8, 'source terminal too short for a local split')
- require(before[serials[0]][:2] == (0, 2) and before[serials[1]][0] == 0 and
- before[serials[1]][1] > 2 and before[serials[2]][:2] == (75, 2),
- f'unexpected initial classic layout: {before}')
- client.write(f'/pane/{source}/look', f'{target}\n'.encode())
- opened = require(wait_for(lambda: next((serial for serial in panes(client)
- if serial not in serials), None)),
- 'Look did not open the first document')
- markers[opened] = 'opened.txt'
- after_frame, after = require(wait_for(lambda: geometry(client, markers)),
- 'Look pane tags not visible')
- require(len(panes(client)) == 4, 'Look removed a terminal or added extra panes')
- x, y, width, height = source_rect
- require(after[source][:3] == (x, y, width),
- f'Look moved its originating terminal: {source_rect} -> {after[source]}')
- require(after[opened][:3] == (x, y + after[source][3], width) and
- after[source][3] + after[opened][3] == height,
- f'Look did not split directly below its caller: {after}')
- for serial in serials:
- if serial == source:
- continue
- require(after[serial] == before[serial],
- f'Look resized or moved unrelated terminal {serial}: {before} -> {after}')
- require(rectangle_text(before_frame, before[serial]) ==
- rectangle_text(after_frame, after[serial]),
- f'Look changed unrelated terminal {serial} rendered text')
- require(client.read(f'/pane/{opened}/body') == contents,
- 'opened document contents differ')
- client.write(f'/pane/{source}/look', f'{target}:2\n'.encode())
- require(list(panes(client)) == serials + [opened], 'repeated Look created a duplicate pane')
- require(wait_for(lambda: geometry(client, markers)[1] == after),
- 'repeated Look changed layout')
- require(target.read_bytes() == contents, 'Look modified the fixture on disk')
- if args.output:
- capture(client, root, 'look-below-caller', not args.tty)
- print(f'{"TTY" if args.tty else "SDL"} source {"lower-left" if source_index == 1 else "right"}: '
- 'local split, unchanged other terminals, first-file contents and re-Look reuse passed', flush=True)
-
-
-def run(args):
- # Cache-rooted fixtures avoid relying on /tmp quota in developer sessions.
- cache = Path.home() / '.cache'
- cache.mkdir(exist_ok=True)
- context = nullcontext(str(args.output.resolve())) if args.output else tempfile.TemporaryDirectory(
- prefix='lp-', dir=cache)
- with context as directory:
- for source_index, label in [(1, 'left'), (2, 'right')]:
- run_case(args, Path(directory) / label, source_index)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('binary', type=Path)
- parser.add_argument('--tty', action='store_true')
- parser.add_argument('--output', type=Path)
- run(parser.parse_args())
diff --git a/test/lspbench.zig b/test/lspbench.zig
index 36969680..02138d24 100644
--- a/test/lspbench.zig
+++ b/test/lspbench.zig
@@ -40,7 +40,6 @@ const anchors = [_]Anchor{
.{ .file = "src/lsp/lsp.zig", .needle = "pub const Kind", .at = 11, .kind = .workspace_symbols, .expect = "" },
.{ .file = "test/lspfixture/broken.zig", .needle = "unused_local", .at = 0, .kind = .workspace_diagnostics, .expect = "broken.zig" },
.{ .file = "src/lsp/lsp.zig", .needle = "std.mem.count(u8", .at = 4, .kind = .completion, .expect = "std.zig" },
- .{ .file = "src/lsp/lsp_zls.zig", .needle = ".declaration => try rowForToken", .at = 1, .kind = .completion, .expect = "type_definition" },
.{ .file = "src/normal.zig", .needle = ".definition => .definition,", .at = 1, .kind = .completion, .expect = "" },
// The repeated name proves the result includes the declaration, not just the cursor line.
.{ .file = "test/lspfixture/dotcomplete.zig", .needle = "return switch (s) {\n .", .at = 29, .kind = .completion, .expect = "verdigris verdigris," },
@@ -115,6 +114,7 @@ pub fn main(init: std.process.Init) !void {
const gpa = std.heap.page_allocator;
const args = try init.minimal.args.toSlice(init.arena.allocator());
+ if (args.len > 1 and std.mem.eql(u8, args[1], "probe")) return probeCommand(init);
const options = try Options.parse(args[1..]);
const rss0 = rssKib();
@@ -296,3 +296,171 @@ fn rssKib() u64 {
}
return 0;
}
+
+// ---- probe: one query from the command line ----
+
+fn nowUs() u64 {
+ var ts: std.c.timespec = undefined;
+ _ = std.c.clock_gettime(.MONOTONIC, &ts);
+ return @as(u64, @intCast(ts.sec)) *| 1_000_000 +| @as(u64, @intCast(ts.nsec)) / 1000;
+}
+
+const aliases = .{
+ .{ "gd", lsp.Kind.definition },
+ .{ "gD", lsp.Kind.declaration },
+ .{ "gy", lsp.Kind.type_definition },
+ .{ "gi", lsp.Kind.implementation },
+ .{ "gr", lsp.Kind.references },
+};
+
+fn probeUsage() noreturn {
+ std.debug.print(
+ \\usage: pardes-lspbench probe <kind> <file> <line>:<col> [arg] [--reps N] [--warm S]
+ \\ kind: gd gD gy gi gr, or any lsp.Kind tag (hover, references,
+ \\ document_symbols, workspace_symbols, incoming_calls, ...)
+ \\ arg: the new name for rename, the query for workspace_symbols
+ \\ --warm sleep S seconds after the first (cold) query — long enough
+ \\ for an indexing server to reach ready, narrated on stderr
+ \\
+ , .{});
+ std.process.exit(2);
+}
+
+/// The editor's message row, here: the client's reader threads narrate server
+/// state through the same sink the shells register, printed to stderr so the
+/// rows on stdout stay clean.
+fn statusSink(ctx: ?*anyopaque, text: []const u8) void {
+ _ = ctx;
+ std.debug.print("# status: {s}\n", .{text});
+}
+
+extern "c" fn usleep(usec: c_uint) c_int;
+
+/// `lspbench probe`: one language query against the real seam, from the
+/// command line (the old lspprobe): `probe <kind> <file> <line>:<col> [arg]
+/// [--reps N] [--warm S]`, rows on stdout, the timing on stderr.
+fn probeCommand(init: std.process.Init) !void {
+ const gpa = std.heap.page_allocator;
+ const arena = init.arena.allocator();
+ const io = std.Io.Threaded.global_single_threaded.io();
+
+ // args[1] is `probe`: the rest are where lspprobe's were.
+ const all = try init.minimal.args.toSlice(arena);
+ const args = all[1..];
+ if (args.len < 4) probeUsage();
+
+ // comma-separated kinds share ONE server instance and its index — the
+ // whole battery against a big workspace pays the cold cost once
+ var kinds: [16]lsp.Kind = undefined;
+ var nkinds: usize = 0;
+ var kit = std.mem.tokenizeScalar(u8, args[1], ',');
+ while (kit.next()) |word| {
+ if (nkinds == kinds.len) probeUsage();
+ kinds[nkinds] = blk: {
+ inline for (aliases) |a| if (std.mem.eql(u8, word, a[0])) break :blk a[1];
+ break :blk std.meta.stringToEnum(lsp.Kind, word) orelse probeUsage();
+ };
+ nkinds += 1;
+ }
+ if (nkinds == 0) probeUsage();
+
+ var pz: [4096:0]u8 = undefined;
+ const pz_s = std.fmt.bufPrintSentinel(&pz, "{s}", .{args[2]}, 0) catch probeUsage();
+ var realbuf: [4096]u8 = undefined;
+ const path = std.mem.span(realpath(pz_s.ptr, &realbuf) orelse {
+ std.debug.print("cannot resolve {s}\n", .{args[2]});
+ std.process.exit(1);
+ });
+ const source = std.Io.Dir.cwd().readFileAllocOptions(
+ io,
+ path,
+ arena,
+ .limited(64 << 20),
+ .of(u8),
+ 0,
+ ) catch {
+ std.debug.print("cannot read {s}\n", .{path});
+ std.process.exit(1);
+ };
+
+ const colon = std.mem.indexOfScalar(u8, args[3], ':') orelse probeUsage();
+ const line = std.fmt.parseInt(u32, args[3][0..colon], 10) catch probeUsage();
+ const col = std.fmt.parseInt(u32, args[3][colon + 1 ..], 10) catch probeUsage();
+ if (line == 0 or col == 0) probeUsage();
+
+ var arg: []const u8 = "";
+ var reps: u32 = 1;
+ var warm_s: u32 = 0;
+ var i: usize = 4;
+ while (i < args.len) : (i += 1) {
+ if (std.mem.eql(u8, args[i], "--reps")) {
+ i += 1;
+ if (i >= args.len) probeUsage();
+ reps = @max(1, std.fmt.parseInt(u32, args[i], 10) catch probeUsage());
+ } else if (std.mem.eql(u8, args[i], "--warm")) {
+ i += 1;
+ if (i >= args.len) probeUsage();
+ warm_s = std.fmt.parseInt(u32, args[i], 10) catch probeUsage();
+ } else arg = args[i];
+ }
+
+ lsp.setStatusSink(null, statusSink);
+
+ // 1-based line:col -> byte offset
+ var off: usize = 0;
+ var ln: u32 = 1;
+ while (ln < line and off < source.len) : (off += 1) {
+ if (source[off] == '\n') ln += 1;
+ }
+ off = @min(off + col - 1, source.len);
+
+ const root = std.fs.path.dirname(path) orelse "/";
+ const timings = try arena.alloc(u64, reps);
+ var stdout_buf: [64 * 1024]u8 = undefined;
+ var stdout = std.Io.File.stdout().writer(io, &stdout_buf);
+ var first_query = true;
+
+ for (kinds[0..nkinds]) |kind| {
+ var rows: []const u8 = "";
+ for (0..reps) |r| {
+ // one warm pause for the whole battery, right after the very
+ // first (cold, server-spawning) query — the status lines on
+ // stderr narrate what the server does with the time
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ var scratch: std.heap.ArenaAllocator = .init(gpa);
+ defer scratch.deinit();
+ const t0 = nowUs();
+ try lsp.query(gpa, scratch.allocator(), .{
+ .kind = kind,
+ .path = path,
+ .source = source,
+ .offset = @intCast(off),
+ .arg = arg,
+ .root = root,
+ }, &out.writer);
+ timings[r] = nowUs() -| t0;
+ rows = try arena.dupe(u8, out.written());
+ if (first_query) {
+ first_query = false;
+ if (warm_s > 0) for (0..warm_s * 10) |_| {
+ _ = usleep(100_000);
+ };
+ }
+ }
+
+ try stdout.interface.print("## {t}\n", .{kind});
+ try stdout.interface.writeAll(rows);
+ try stdout.interface.flush();
+
+ const n = std.mem.count(u8, rows, "\n");
+ const cold = timings[0];
+ std.mem.sort(u64, timings[0..reps], {}, std.sort.asc(u64));
+ if (reps == 1)
+ std.debug.print("# {t}: {d} row(s) in {d}us\n", .{ kind, n, cold })
+ else
+ std.debug.print("# {t}: {d} row(s); first {d}us, min {d}us, median {d}us ({d} reps)\n", .{
+ kind, n, cold, timings[0], timings[reps / 2], reps,
+ });
+ }
+}
diff --git a/test/output.zig b/test/output.zig
index da17eccc..c67c021e 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -669,10 +669,7 @@ test "EffectCode lists readable virtual sources used by this backend" {
try std.testing.expect(std.mem.indexOf(u8, out.content, "/virtual/src/layout.zig\n") != null);
const backend_source = switch (platform) {
.tty => "/virtual/src/tty/panel_compositor.zig",
- .gui => if (@import("pardes_config").gui_shader_sources_prebuilt)
- "/virtual/shaders/prebuilt/ui.vert.glsl"
- else
- "/virtual/shaders/ui.vert.glsl",
+ .gui => "/virtual/shaders/ui.vert.glsl",
.macos => "/virtual/src/macos/Sources/ScenePostprocessor.swift",
.web, .esp32p4 => unreachable,
};
diff --git a/test/panes.zig b/test/panes.zig
index f7ca9fc5..d7fa3514 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -3134,7 +3134,6 @@ const ImageTests = struct {
const togglePalette = panes.Image.togglePalette;
const toggleAscii = panes.Image.toggleAscii;
const tagPrefix = panes.Image.tagPrefix;
- const legacySavedPrefix = panes.Image.legacySavedPrefix;
test "pane-local renderer choices are visible in the image tag" {
var state: State = .{ .path = @constCast("/tmp/picture.ppm") };
@@ -3147,10 +3146,6 @@ const ImageTests = struct {
config.tag_image ++ " petscii:on palette:terminal ascii:off /tmp/picture.ppm",
tag,
);
- try std.testing.expectEqualStrings(
- "img /tmp/picture.ppm",
- legacySavedPrefix(&state, "img /tmp/picture.ppm Keep Del").?,
- );
}
test "dump restore propagates malformed embedded image bytes" {
@@ -3173,7 +3168,6 @@ const PdfTests = struct {
const Point = panes.Pdf.Point;
const Document = panes.Pdf.Document;
const isPath = panes.Pdf.isPath;
- const legacySavedPrefix = panes.Pdf.legacySavedPrefix;
const State = panes.Pdf.State;
const probeWord = panes.Pdf.probeWord;
const pointAtGeometry = panes.Pdf.pointAtGeometry;
@@ -3186,26 +3180,10 @@ const PdfTests = struct {
try std.testing.expect(!isPath("pdf"));
}
- test "legacy PDF prefix is delimited by its stable marker and exact path" {
- const path = "/tmp/a document.pdf";
- const tag = "pdf 2/9 height PdfFit full PdfTint PdfSections " ++ path ++ " Keep Del";
- try std.testing.expectEqualStrings(
- "pdf 2/9 height PdfFit full PdfTint PdfSections " ++ path,
- legacySavedPrefix(path, tag).?,
- );
- }
-
test "feature-off PDF state is zero-sized" {
if (!enabled) try std.testing.expectEqual(@as(usize, 0), @sizeOf(State));
}
- test "compact PDF saved prefix separates page status from editable commands" {
- const path = "/tmp/a document.pdf";
- try std.testing.expectEqualStrings(path ++ " [2/9]", legacySavedPrefix(path, path ++ " [2/9] Keep Del").?);
- try std.testing.expect(legacySavedPrefix(path, path ++ " [oops] Keep Del") == null);
- try std.testing.expect(legacySavedPrefix(path, path ++ " [2/9/4] Keep Del") == null);
- }
-
test "PDF section rows preserve DFS ordinals and sanitise hierarchy" {
if (!enabled) return;
const entries = [_]pdf.OutlineEntry{
diff --git a/test/pdf.zig b/test/pdf.zig
index 4cb8f3dd..48869de1 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -287,6 +287,7 @@ test "PDF dump fallback remains a byte-preserving file" {
const saved_panes = [_]dump.Pane{.{
.kind = .image,
.tag = "pdf 3/9 height PdfFit full PdfTint PdfSections " ++ path ++ " Keep Del",
+ .tag_tail = " Keep Del",
.body = "",
.scroll = 2,
.cols = 80,
diff --git a/test/pdf_bench.zig b/test/pdf_bench.zig
index 0f09f72a..23565106 100644
--- a/test/pdf_bench.zig
+++ b/test/pdf_bench.zig
@@ -13,6 +13,9 @@
//! filesystem cache is deliberately uncontrolled), while warm scenarios
//! retain one Document.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-bench").f;
const pdf = @import("mupdf");
pub const std_options: std.Options = .{ .log_level = .err };
@@ -205,14 +208,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parsePositive(name: []const u8, text: []const u8, maximum: usize) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{name});
- if (value == 0 or value > maximum)
- fatal("{s} must be in 1..{d}", .{ name, maximum });
- return value;
-}
-
fn usage() noreturn {
printUsage();
std.process.exit(2);
@@ -226,13 +221,6 @@ fn printUsage() void {
);
}
-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));
-}
-
fn measure(
config: Config,
profile_index: usize,
@@ -617,7 +605,6 @@ fn reportJson(
std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
+fn parsePositive(name: []const u8, text: []const u8, maximum: usize) usize {
+ return bench_util.parsePositive(fatal, name, text, maximum);
}
diff --git a/test/pdf_pointer_bench.zig b/test/pdf_pointer_bench.zig
index f1d61229..ae2de2f5 100644
--- a/test/pdf_pointer_bench.zig
+++ b/test/pdf_pointer_bench.zig
@@ -15,6 +15,9 @@
//! frame each step settles on. It is the no-regression proof: a change to how highlights
//! are painted may change what it costs, never one presented pixel.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const mix = bench_util.mix;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -208,17 +211,6 @@ fn print(comptime format: []const u8, args: anytype) void {
_ = std.c.write(1, line.ptr, line.len);
}
-fn mix(seed: u64, value: anytype) u64 {
- var copy = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&copy));
-}
-
-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));
-}
-
comptime {
_ = pdf.Document;
}
diff --git a/test/pdf_scroll_bench.zig b/test/pdf_scroll_bench.zig
index 33dfe70a..a4511dbe 100644
--- a/test/pdf_scroll_bench.zig
+++ b/test/pdf_scroll_bench.zig
@@ -42,6 +42,11 @@
//!
//! Setup, teardown, checksumming, and reporting are outside every timer.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-scroll-bench").f;
+const mix = bench_util.mix;
+const drainEffects = bench_util.drainEffects;
const libc = std.c;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -211,14 +216,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parseCount(flag: []const u8, text: []const u8) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{flag});
- if (value == 0 or value > 10_000)
- fatal("{s} must be in 1..10000", .{flag});
- return value;
-}
-
const Travel = struct {
/// Scrollable display pixels: the whole strip minus one screenful.
max_scroll: f64,
@@ -520,20 +517,6 @@ fn stridedHash(seed: u64, rgba: []const u8) u64 {
return hash;
}
-fn mix(seed: u64, value: anytype) u64 {
- const Value = @TypeOf(value);
- const Stable = switch (@typeInfo(Value)) {
- .comptime_int => u64,
- else => Value,
- };
- var stable: Stable = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
-}
-
-fn drainEffects(core: *pardes.Pardes) void {
- while (core.nextEffect()) |_| {}
-}
-
fn verifyIdentity(
name: []const u8,
expected: *?Identity,
@@ -557,13 +540,6 @@ fn verifyIdentity(
} else expected.* = got;
}
-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));
-}
-
fn reportText(config: Config, travel: Travel, results: []const Result) void {
std.debug.print("pardes PDF fast-scroll benchmark ({s})\n", .{bench_config.core_optimize});
std.debug.print(
@@ -649,13 +625,12 @@ fn reportJson(io: std.Io, config: Config, travel: Travel, results: []const Resul
std.Io.File.stdout().writeStreamingAll(io, out.buffered()) catch {};
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-scroll-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
-}
-
comptime {
// The bench links the real MuPDF wrapper the core uses; keep the import
// load-bearing so a build that silently dropped it fails here.
_ = pdf.Document;
}
+
+fn parseCount(flag: []const u8, text: []const u8) usize {
+ return bench_util.parsePositive(fatal, flag, text, 10_000);
+}
diff --git a/test/pdf_sections_bench.zig b/test/pdf_sections_bench.zig
index b08af552..92c9ef7c 100644
--- a/test/pdf_sections_bench.zig
+++ b/test/pdf_sections_bench.zig
@@ -9,6 +9,11 @@
//! report nanoseconds per operation; `operations_per_sample` makes that
//! normalization explicit in both output formats.
const std = @import("std");
+const bench_util = @import("bench_util.zig");
+const nowNs = bench_util.nowNs;
+const fatal = bench_util.Fatal("pdf-sections-bench").f;
+const mix = bench_util.mix;
+const drainEffects = bench_util.drainEffects;
const libc = std.c;
const pardes = @import("pardes");
const pdf = @import("mupdf");
@@ -107,14 +112,6 @@ fn parseArgs(args: []const []const u8) Config {
return config;
}
-fn parseCount(flag: []const u8, text: []const u8) usize {
- const value = std.fmt.parseUnsigned(usize, text, 10) catch
- fatal("{s} expects a positive integer", .{flag});
- if (value == 0 or value > 10_000)
- fatal("{s} must be in 1..10000", .{flag});
- return value;
-}
-
fn measureOutline(path: []const u8, config: Config) !Result {
const samples = try gpa.alloc(u64, config.reps);
defer gpa.free(samples);
@@ -145,7 +142,7 @@ fn measureMaterialize(
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
- const content = try pardes.PdfSectionRows.render(gpa, path, entries);
+ const content = try pardes.panes.Pdf.SectionRows.render(gpa, path, entries);
const elapsed = nowNs() -| started;
const identity = contentIdentity(content);
gpa.free(content);
@@ -255,7 +252,7 @@ fn measureWorstShape(entries: []const pdf.OutlineEntry, config: Config) !Result
var expected: ?Identity = null;
for (0..config.warmup + config.reps) |round| {
const started = nowNs();
- const content = try pardes.PdfSectionRows.render(gpa, "/tmp/near-limit.pdf", entries);
+ const content = try pardes.panes.Pdf.SectionRows.render(gpa, "/tmp/near-limit.pdf", entries);
const elapsed = nowNs() -| started;
const identity = contentIdentity(content);
gpa.free(content);
@@ -300,11 +297,11 @@ fn perturbReopen(prepared: PreparedReopen, operation: usize) void {
const marker: i32 = @intCast(operation % 31 + 1);
const output_file = &prepared.output.file.?;
output_file.scroll = @intCast(operation % 17 + 1);
- prepared.output.cur_row = marker;
- prepared.output.cur_col = marker + 1;
- prepared.output.msel.active = true;
- prepared.output.vsel.active = true;
- prepared.output.nsel = 1;
+ prepared.output.body.cur_row = marker;
+ prepared.output.body.cur_col = marker + 1;
+ prepared.output.body.msel.active = true;
+ prepared.output.body.vsel.active = true;
+ prepared.output.body.nsel = 1;
prepared.owner.search_pane = null;
prepared.owner.search_row = operation % 23;
prepared.core.active = prepared.output_id;
@@ -313,21 +310,17 @@ fn perturbReopen(prepared: PreparedReopen, operation: usize) void {
fn observeReopen(prepared: PreparedReopen) u64 {
const file = prepared.output.file.?;
var observed: u64 = file.scroll;
- observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.cur_row)));
- observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.cur_col)));
- observed = observed *% 131 +% @intFromBool(prepared.output.msel.active);
- observed = observed *% 131 +% @intFromBool(prepared.output.vsel.active);
- observed = observed *% 131 +% prepared.output.nsel;
+ observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.body.cur_row)));
+ observed = observed *% 131 +% @as(u64, @intCast(@max(0, prepared.output.body.cur_col)));
+ observed = observed *% 131 +% @intFromBool(prepared.output.body.msel.active);
+ observed = observed *% 131 +% @intFromBool(prepared.output.body.vsel.active);
+ observed = observed *% 131 +% prepared.output.body.nsel;
observed = observed *% 131 +% (prepared.owner.search_pane orelse std.math.maxInt(usize));
observed = observed *% 131 +% (prepared.owner.search_row orelse std.math.maxInt(usize));
observed = observed *% 131 +% prepared.core.active;
return observed;
}
-fn drainEffects(core: *pardes.Pardes) void {
- while (core.nextEffect()) |_| {}
-}
-
fn sectionsOutputIdentity(core: *pardes.Pardes) Identity {
const owner = core.panes[0] orelse fatal("PDF pane disappeared", .{});
const output_id = owner.search_pane orelse fatal("+PdfSections was not armed", .{});
@@ -347,7 +340,7 @@ fn stepIdentity(core: *pardes.Pardes) Identity {
identity.checksum = mix(identity.checksum, view.page);
identity.checksum = mix(identity.checksum, @as(u64, @bitCast(view.document_scroll_y)));
identity.checksum = mix(identity.checksum, owner.search_row orelse std.math.maxInt(usize));
- identity.checksum = mix(identity.checksum, view.render_revision);
+ identity.checksum = mix(identity.checksum, view.next_raster_revision);
return identity;
}
@@ -389,16 +382,6 @@ fn outlineIdentity(entries: []const pdf.OutlineEntry) Identity {
return .{ .items = entries.len, .bytes = bytes, .checksum = checksum };
}
-fn mix(seed: u64, value: anytype) u64 {
- const Value = @TypeOf(value);
- const Stable = switch (@typeInfo(Value)) {
- .comptime_int => u64,
- else => Value,
- };
- var stable: Stable = value;
- return std.hash.Wyhash.hash(seed, std.mem.asBytes(&stable));
-}
-
fn makeWorstShape() ![]pdf.OutlineEntry {
const entries = try gpa.alloc(pdf.OutlineEntry, near_limit_items);
for (entries, 0..) |*entry, ordinal| {
@@ -457,13 +440,6 @@ fn summarize(
};
}
-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));
-}
-
/// A host benchmark compiler barrier. MuPDF benchmarks are build-and-run
/// steps, so they never target the freestanding web backend; an empty volatile
/// asm with only the abstract memory clobber is valid on every native backend
@@ -545,7 +521,6 @@ fn unlink(path: []const u8) void {
_ = libc.unlink(path_z);
}
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
- std.debug.print("pdf-sections-bench: " ++ format ++ "\n", args);
- std.process.exit(1);
+fn parseCount(flag: []const u8, text: []const u8) usize {
+ return bench_util.parsePositive(fatal, flag, text, 10_000);
}
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 0514d8d8..a176da7a 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -580,7 +580,7 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
_ = setenv("LC_ALL", "C", 1);
// a wall clock cannot live in a golden; everything that would print one
// prints fixed characters instead when this is set — the lsp introspection
- // views' durations (lsp_zls.zig hideTime) and the transient message row's
+ // views' durations and the transient message row's
// time of day (message.zig stamp)
_ = setenv("PARDES_NOTIME", "1", 1);
// A virtual core clock: animation moves a frame per timed-out wait,
diff --git a/test/snapshots/lsp.golden b/test/snapshots/lsp.golden
index 153ae5cc..048d1ed3 100644
--- a/test/snapshots/lsp.golden
+++ b/test/snapshots/lsp.golden
@@ -14,6 +14,7 @@
| 10
== snap cursor grid=120x41 cursor=17,10
== snap definition grid=120x41 cursor=15,5
+|3: 1 const std = @import("std"); --:--:-- lsp zls ready — /tmp/pardes-snap/lsp/cwd
== snap hover grid=120x41 cursor=15,5
|3: 1 const std = @import("std"); Hover
|12: /tmp/pardes-snap/lsp/cwd/+Hover Save Tty Collapse Del
@@ -26,9 +27,9 @@
|16: 1 a.zig:1:7-9
|17: 2 std
|18: 3 a.zig:3:4-9
-|19: 4 helper fn helper(x: u32) u32
+|19: 4 helper
|20: 5 a.zig:7:8-11
-|21: 6 main fn main() void
+|21: 6 main
|22: 7
== snap step grid=120x41 cursor=15,3
|3: 1 const std = @import("std");
diff --git a/test/snapshots/lsp.snap b/test/snapshots/lsp.snap
index b40484ee..97fcf7ed 100644
--- a/test/snapshots/lsp.snap
+++ b/test/snapshots/lsp.snap
@@ -1,11 +1,11 @@
# Language intelligence end to end: the helix keymap, the async round trip and
# the two dispositions of an answer.
#
-# The backend behind the seam here is ZLS linked in as a LIBRARY
-# (src/lsp_zls.zig) — no server process, no JSON-RPC — so `gd` is a function
-# call on a worker thread whose result comes back as an ordinary event, the
-# same shape a pty read already had. Nothing in this script knows that: it
-# presses keys and reads the grid, which is the point of the seam.
+# The backend behind the seam here is zls, run as a child process by the
+# protocol client (it must be on PATH): `gd` is a request on a worker thread
+# whose result comes back as an ordinary event, the same shape a pty read
+# already had. Nothing in this script knows that: it presses keys and reads
+# the grid, which is the point of the seam.
#
# a.zig is deliberately self-contained — a definition, a call site, and not one
# symbol from std — so these goldens do not encode this machine's stdlib
diff --git a/test/snapshots/lspcomplete.golden b/test/snapshots/lspcomplete.golden
index 29a81f93..a56101e8 100644
--- a/test/snapshots/lspcomplete.golden
+++ b/test/snapshots/lspcomplete.golden
@@ -19,16 +19,18 @@
|2: ^* /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext
|12: 10 }
== snap commentindent grid=120x41 cursor=19,14
+|3: 1 const Shade = enum { --:--:-- lsp zls ready — /tmp/pardes-snap/lspcomplete/cwd
|14: 12 // note. see below
== snap athedot grid=120x41 cursor=16,10
+|3: 1 const Shade = enum {
|10: 8 .
== snap completion grid=120x41 cursor=16,10
|15: /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
-|16: 1 a.zig:2:5-13 verdigris verdigris,
-|17: 2 a.zig:3:5-12 cinnabar cinnabar,
+|16: 1 a.zig:8:10 verdigris
+|17: 2 a.zig:8:10 cinnabar
|18: 3
== snap refill grid=120x41 cursor=16,10
-== snap step grid=120x41 cursor=19,4
+== snap step grid=120x41 cursor=16,10
|2: * /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext
== snap txtindent grid=120x41 cursor=15,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
@@ -40,7 +42,7 @@
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
| New Tty Find Grep Joincol Delcol
|^* /tmp/pardes-snap/lspcomplete/cwd/c.zig Save Tty Collapse Del TreeContext
-| 1 const Shade = enum {
+| 1 const Shade = enum { --:--:-- lsp zls ready — /tmp/pardes-snap/lspcomplete/cwd
| 2 verdigris,
| 3 cinnabar,
| 4 };
@@ -49,14 +51,14 @@
| 7 const z: Shade = .
| 8 }
| /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
-| 1 c.zig:2:5-13 verdigris verdigris,
-| 2 c.zig:3:5-12 cinnabar cinnabar,
+| 1 c.zig:7:23 verdigris Shade
+| 2 c.zig:7:23 cinnabar Shade
| 3
== snap nobrace grid=100x19 cursor=16,10
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
| New Tty Find Grep Joincol Delcol
|^* /tmp/pardes-snap/lspcomplete/cwd/d.zig Save Tty Collapse Del TreeContext
-| 1 const Shade = enum {
+| 1 const Shade = enum { --:--:-- lsp zls ready — /tmp/pardes-snap/lspcomplete/cwd
| 2 verdigris,
| 3 cinnabar,
| 4 };
@@ -64,6 +66,6 @@
| 6 pub fn nobrace(s: Shade) void {
| 7 switch (s) {
| /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
-| 1 d.zig:2:5-13 verdigris verdigris,
-| 2 d.zig:3:5-12 cinnabar cinnabar,
+| 1 d.zig:8:10 verdigris
+| 2 d.zig:8:10 cinnabar
| 3
diff --git a/test/snapshots/lspcomplete.snap b/test/snapshots/lspcomplete.snap
index 53ce7ef9..d9e165a1 100644
--- a/test/snapshots/lspcomplete.snap
+++ b/test/snapshots/lspcomplete.snap
@@ -1,6 +1,7 @@
-# Tab after a dot in insert mode: "what could go here, and where is each of
-# those defined". The answer is a `+Search` buffer of DEFINITION rows, the same
-# rows `gr` fills it with — nothing is inserted and nothing pops up.
+# Tab after a dot in insert mode: "what could go here". The answer is a
+# `+Search` buffer of zls's candidates, one row each at the cursor (a
+# completion item names no declaration) — nothing is inserted and nothing pops
+# up.
#
# a.zig is std-free for the reason lsp.snap's is: these goldens must not encode
# this machine's stdlib version or its install path. Its enum members are
@@ -58,12 +59,10 @@ text .
stable 400 5000
snap athedot
key tab
-# The row that proves it: `a.zig:2:` is `verdigris`'s own declaration line.
-# The cursor is on line 8; nothing about line 2 is reachable from the text
-# under it, so this row can only have come from resolving the switch's
-# condition to `Shade` and asking `Shade` what it declares. Line 8 still reads
-# `.` and nothing else — a completion INSERTS NOTHING.
-wait 15000 a.zig:2:
+# The rows that prove it: `verdigris` and `cinnabar` can only have come from
+# resolving the switch's condition to `Shade` and asking what it declares.
+# Line 8 still reads `.` and nothing else — a completion INSERTS NOTHING.
+wait 15000 +Search
stable 700 15000
snap completion
# Two more Tabs. Every one of them answers, and every answer REFILLS the one
@@ -128,7 +127,7 @@ key end
text .
stable 400 5000
key tab
-wait 15000 c.zig:2:
+wait 15000 +Search
stable 700 15000
snap nosemi
# ...and a construct still hanging OPEN. The switch has no closing brace at
@@ -149,6 +148,6 @@ key end
text .
stable 400 5000
key tab
-wait 15000 d.zig:2:
+wait 15000 +Search
stable 700 15000
snap nobrace
diff --git a/test/snapshots/lspcompletemoved.golden b/test/snapshots/lspcompletemoved.golden
index e759cbd8..c83e3038 100644
--- a/test/snapshots/lspcompletemoved.golden
+++ b/test/snapshots/lspcompletemoved.golden
@@ -2,7 +2,7 @@
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
| New Tty Find Grep Joincol Delcol
|^* /tmp/pardes-snap/lspcompletemoved/cwd/a.zig Save Tty Collapse Del TreeContext
-| 1 const Shade = enum {
+| 1 const Shade = enum { --:--:-- lsp zls ready — /tmp/pardes-snap/lspcompletemoved/cwd
| 2 verdigris,
| 3 cinnabar,
| 4 };
diff --git a/test/snapshots/lspdebug.golden b/test/snapshots/lspdebug.golden
index 650b41b2..75fba98f 100644
--- a/test/snapshots/lspdebug.golden
+++ b/test/snapshots/lspdebug.golden
@@ -14,6 +14,8 @@
| 10 }
| 11
== snap ctrlclick grid=120x45 cursor=15,5
+|3: 1 const std = @import("std"); --:--:-- lsp zls ready — /tmp/pardes-snap/lspdebug/cwd
== snap plainclick grid=120x45 cursor=21,10
+|3: 1 const std = @import("std");
== snap why-closed grid=120x45 cursor=7,5
== snap report-closed grid=120x45 cursor=7,5
diff --git a/test/snapshots/lspdebug.snap b/test/snapshots/lspdebug.snap
index ab330a76..3aca07cc 100644
--- a/test/snapshots/lspdebug.snap
+++ b/test/snapshots/lspdebug.snap
@@ -21,10 +21,8 @@ snap plainclick
text 3gg1g|
stable 400 10000
key space l w
-wait 10000 parsed 122 bytes, 0 syntax error(s)
-wait 10000 position context: .empty
-wait 10000 STOP: nothing to resolve
-wait 10000 0 row(s)
+wait 10000 lsp explain — the definition query
+wait 10000 row(s)
key space w j
key space d
stable 700 15000
@@ -32,20 +30,15 @@ snap why-closed
# Check the report's answers, not installed compiler paths or dependency counts.
key space l i
-wait 10000 backend: zls-inproc+lsp-client
-wait 10000 compiled in
-wait 10000 [OK]
-wait 10000 offsets: utf-8
+wait 10000 backend: lsp-client
+wait 10000 protocol servers (lsp-client):
+wait 10000 zls
key space w j
key /
-text last queries
+text client queries
key enter
-wait 10000 last queries
-wait 10000 definition z.zig
-# The results pane the search opened takes the bottom half, which leaves the
-# report one row short of its second query: step the cursor past it.
-key j j j
-wait 10000 explain z.zig
+wait 10000 client queries
+wait 10000 definition
# the report sits between the source and the results, so Del asks where its
# rows go: k, back to the source above
key space d
diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden
index 904e076a..b4022b1d 100644
--- a/test/snapshots/lsprelpath.golden
+++ b/test/snapshots/lsprelpath.golden
@@ -1,48 +1,49 @@
-== snap absrow grid=100x35 cursor=16,7
+== snap absrow grid=100x35 cursor=21,16
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext
-| 1 const shade = @import("../shade.zig");
+| /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext
+| 1 const shade = @import("../shad --:--:-- lsp zls ready — /tmp/pardes-snap/lsprelpath/cwd/sub
| 2
| 3 pub fn pick(s: shade.Shade) u8 {
| 4 return switch (s) {
-| 5 .
+| 5
| 6 };
| 7 }
-| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del
-| 1 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:2:5-13 verdigris verdigris,
-| 2 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:3:5-12 cinnabar cinnabar,
-| 3
-== snap abslook grid=100x35 cursor=19,15
-|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext
-|13: /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext
-|14: 1 pub const Shade = enum {
-|15: 2 verdigris,
-|16: 3 cinnabar,
-|17: 4 };
-|18: 5
+| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del LocationsConfig
+| 1 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:1:11-15
+| 2 pub const Shade = enum {
+| 3 parent.zig:3:22-26
+| 4 pub fn pick(s: shade.Shade) u8 {
+| /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext
+| 1 pub const Shade = enum {
+| 2 verdigris,
+| 3 cinnabar,
+| 4 };
+| 5
+== snap abslook grid=100x35 cursor=32,5
+|3: 1 const shade = @import("../shade.zig");
== style abslook grid=100x35
|0: 0-99 #b2c3b8,#1d2824,
|1: 0-1 d,#887455, 2-2 d,#314738, 3-99 #becfc4,#314738,
-|2: 0-0 d,#496656, 1-1 #000000,#f9cb70,b 2-2 #000000,#1d2824, 3-38 #b2c3b8,#1d2824, 39-49 #d3b0d9,#1d2824, 50-99 #b2c3b8,#1d2824,
+|2: 0-1 d,#add38a, 2-2 d,#314738, 3-38 #becfc4,#314738, 39-49 #dfbce5,#314738, 50-99 #becfc4,#314738,
|3: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-27 #c9a7ed,#0d1410, 28-28 #e5eee5,#0d1410, 29-42 #b8d88a,#0d1410, 43-99 #e5eee5,#0d1410,
|4: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
-|5: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-27 #e5eee5,#0d1410, 28-32 #f5f7ee,#3c614a, 33-99 #e5eee5,#0d1410,
|6: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-24 #c9a7ed,#0d1410, 25-99 #e5eee5,#0d1410,
-|7: 0-0 d,#608269, 1-4 d,#0d1410, 5-14 #59675c,#0d1410, 15-99 #e5eee5,#0d1410,
+|7: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
|8: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
|9: 0-0 d,#152019, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
|10: 0-1 d,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-99 #b2c3b8,#1d2824,
-|11: 0-0 d,#608269, 1-4 d,#0d1410, 5-55 #59675c,#0d1410, 56-99 #e5eee5,#0d1410,
-|12: 0-0 d,#152019, 1-4 d,#0d1410, 5-55 #59675c,#0d1410, 56-99 #e5eee5,#0d1410,
-|13: 0-1 d,#add38a, 2-2 d,#314738, 3-34 #becfc4,#314738, 35-44 #dfbce5,#314738, 45-99 #becfc4,#314738,
-|14: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-29 #c9a7ed,#0d1410, 30-99 #e5eee5,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-10 #616f64,#0d1410, 11-19 #f5f7ee,#3c614a, 20-99 #e5eee5,#0d1410,
-|16: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
-|17: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
-|19: 0-0 d,#608269, 1-99 d,#0d1410,
-|20: 0-0 d,#608269, 1-99 d,#0d1410,
+|11: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|12: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-21 #f7e4a5,#5e4923, 22-22 #f7e4a5,#0d1410, 23-24 #e5eee5,#0d1410, 25-29 #c9a7ed,#0d1410, 30-99 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|14: 0-0 d,#152019, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-27 #e5eee5,#0d1410, 28-32 #f7e4a5,#5e4923, 33-99 #e5eee5,#0d1410,
+|15: 0-1 d,#496656, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-44 #d3b0d9,#1d2824, 45-99 #b2c3b8,#1d2824,
+|16: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-21 #f5f7ee,#3c614a, 22-22 #f5f7ee,#0d1410, 23-24 #e5eee5,#0d1410, 25-29 #c9a7ed,#0d1410, 30-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
|21: 0-0 d,#608269, 1-99 d,#0d1410,
|22: 0-0 d,#608269, 1-99 d,#0d1410,
|23: 0-0 d,#608269, 1-99 d,#0d1410,
@@ -57,51 +58,52 @@
|32: 0-0 d,#608269, 1-99 d,#0d1410,
|33: 0-0 d,#608269, 1-99 d,#0d1410,
|34: 0-0 d,#608269, 1-99 d,#0d1410,
-== snap subrow grid=100x35 cursor=16,7
+== snap subrow grid=100x35 cursor=20,16
|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext
-| 1 const tint = @import("inner/tint.zig");
+| /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext
+| 1 const tint = @import("inner/ti --:--:-- lsp zls ready — /tmp/pardes-snap/lsprelpath/cwd/sub
| 2
| 3 pub fn hue(t: tint.Tint) u8 {
| 4 return switch (t) {
-| 5 .
+| 5
| 6 };
| 7 }
-| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del
-| 1 inner/tint.zig:2:5-9 ochre ochre,
-| 2 inner/tint.zig:3:5-10 indigo indigo,
-| 3
-== snap sublook grid=100x35 cursor=15,15
-|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext
-|13: /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext
-|14: 1 pub const Tint = enum {
-|15: 2 ochre,
-|16: 3 indigo,
-|17: 4 };
-|18: 5
+| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del LocationsConfig
+| 1 inner/tint.zig:1:11-14
+| 2 pub const Tint = enum {
+| 3 subdir.zig:3:20-23
+| 4 pub fn hue(t: tint.Tint) u8 {
+| /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext
+| 1 pub const Tint = enum {
+| 2 ochre,
+| 3 indigo,
+| 4 };
+| 5
+== snap sublook grid=100x35 cursor=29,5
+|3: 1 const tint = @import("inner/tint.zig");
== style sublook grid=100x35
|0: 0-99 #b2c3b8,#1d2824,
|1: 0-1 d,#887455, 2-2 d,#314738, 3-99 #becfc4,#314738,
-|2: 0-0 d,#496656, 1-1 #000000,#f9cb70,b 2-2 #000000,#1d2824, 3-38 #b2c3b8,#1d2824, 39-49 #d3b0d9,#1d2824, 50-99 #b2c3b8,#1d2824,
+|2: 0-1 d,#add38a, 2-2 d,#314738, 3-38 #becfc4,#314738, 39-49 #dfbce5,#314738, 50-99 #becfc4,#314738,
|3: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-26 #c9a7ed,#0d1410, 27-27 #e5eee5,#0d1410, 28-43 #b8d88a,#0d1410, 44-99 #e5eee5,#0d1410,
|4: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
-|5: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-25 #e5eee5,#0d1410, 26-29 #f5f7ee,#3c614a, 30-99 #e5eee5,#0d1410,
|6: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-24 #c9a7ed,#0d1410, 25-99 #e5eee5,#0d1410,
-|7: 0-0 d,#608269, 1-4 d,#0d1410, 5-14 #59675c,#0d1410, 15-99 #e5eee5,#0d1410,
+|7: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
|8: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
|9: 0-0 d,#152019, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
|10: 0-1 d,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-99 #b2c3b8,#1d2824,
-|11: 0-0 d,#608269, 1-4 d,#0d1410, 5-27 #59675c,#0d1410, 28-99 #e5eee5,#0d1410,
-|12: 0-0 d,#152019, 1-4 d,#0d1410, 5-28 #59675c,#0d1410, 29-99 #e5eee5,#0d1410,
-|13: 0-1 d,#add38a, 2-2 d,#314738, 3-44 #becfc4,#314738, 45-53 #dfbce5,#314738, 54-99 #becfc4,#314738,
-|14: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-23 #e5eee5,#0d1410, 24-28 #c9a7ed,#0d1410, 29-99 #e5eee5,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-4 d,#0d1410,b 5-6 #616f64,#0d1410,b 7-10 #616f64,#0d1410, 11-15 #f5f7ee,#3c614a, 16-99 #e5eee5,#0d1410,
-|16: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
-|17: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
-|19: 0-0 d,#608269, 1-99 d,#0d1410,
-|20: 0-0 d,#608269, 1-99 d,#0d1410,
+|11: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|12: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-20 #f7e4a5,#5e4923, 21-21 #f7e4a5,#0d1410, 22-23 #e5eee5,#0d1410, 24-28 #c9a7ed,#0d1410, 29-99 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
+|14: 0-0 d,#152019, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-13 #c9a7ed,#0d1410, 14-25 #e5eee5,#0d1410, 26-29 #f7e4a5,#5e4923, 30-99 #e5eee5,#0d1410,
+|15: 0-1 d,#496656, 2-2 d,#1d2824, 3-44 #b2c3b8,#1d2824, 45-53 #d3b0d9,#1d2824, 54-99 #b2c3b8,#1d2824,
+|16: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-20 #f5f7ee,#3c614a, 21-21 #f5f7ee,#0d1410, 22-23 #e5eee5,#0d1410, 24-28 #c9a7ed,#0d1410, 29-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-4 d,#0d1410, 5-10 #59675c,#0d1410, 11-99 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-4 d,#0d1410, 5-6 #59675c,#0d1410, 7-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-4 d,#0d1410, 5-99 #59675c,#0d1410,
|21: 0-0 d,#608269, 1-99 d,#0d1410,
|22: 0-0 d,#608269, 1-99 d,#0d1410,
|23: 0-0 d,#608269, 1-99 d,#0d1410,
@@ -116,4 +118,4 @@
|32: 0-0 d,#608269, 1-99 d,#0d1410,
|33: 0-0 d,#608269, 1-99 d,#0d1410,
|34: 0-0 d,#608269, 1-99 d,#0d1410,
-== snap clicked grid=100x35 cursor=16,16
+== snap clicked grid=100x35 cursor=15,12
diff --git a/test/snapshots/lsprelpath.snap b/test/snapshots/lsprelpath.snap
index 1d662f8a..667c2fc8 100644
--- a/test/snapshots/lsprelpath.snap
+++ b/test/snapshots/lsprelpath.snap
@@ -7,9 +7,8 @@
# to read pushed off the right edge, and a path outside the tree has nothing
# short to be called.
#
-# lspcomplete.snap already covers the easy middle case, a candidate in the SAME
-# directory, which shortens to a bare basename. The two that a prefix strip can
-# actually get wrong are here, one app each:
+# A row in the SAME directory shortens to a bare basename; the two that a
+# prefix strip can actually get wrong are here, one app each, as `gr` rows:
#
# PARENT the enum is one level UP from the asking file, so nothing is
# stripped and the row carries `/tmp/.../cwd/shade.zig`. This is
@@ -33,16 +32,14 @@ file sub/subdir.zig const tint = @import("inner/tint.zig");\n\npub fn hue(t: tin
start 35 100 sub/parent.zig
wait 8000 fn pick
stable 700 20000
-# file line 5 (grid row 6, 1-based click row 7) is the switch's empty body
-press left 8 8
-release left 8 8
+# `gr` on `Shade` in the signature (file line 3, click row 6): its
+# declaration's row is in the parent directory.
+press left 31 6
+release left 31 6
stable 400 5000
-key i
-key end
-text .
-stable 400 5000
-key tab
-wait 15000 cwd/shade.zig:2:
+key g
+key r
+wait 15000 cwd/shade.zig:1:
stable 700 15000
snap absrow
key esc
@@ -58,15 +55,12 @@ snapstyle abslook
start 35 100 sub/subdir.zig
wait 8000 fn hue
stable 700 20000
-press left 8 8
-release left 8 8
-stable 400 5000
-key i
-key end
-text .
+press left 29 6
+release left 29 6
stable 400 5000
-key tab
-wait 15000 inner/tint.zig:2:
+key g
+key r
+wait 15000 inner/tint.zig:1:
stable 700 15000
snap subrow
key esc