diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/bench_util.zig | 43 | ||||
| -rw-r--r-- | test/column_grip.py | 132 | ||||
| -rw-r--r-- | test/fs.py | 2 | ||||
| -rw-r--r-- | test/fs_soak.py | 284 | ||||
| -rw-r--r-- | test/look_placement.py | 143 | ||||
| -rw-r--r-- | test/lspbench.zig | 170 | ||||
| -rw-r--r-- | test/output.zig | 5 | ||||
| -rw-r--r-- | test/panes.zig | 22 | ||||
| -rw-r--r-- | test/pdf.zig | 1 | ||||
| -rw-r--r-- | test/pdf_bench.zig | 23 | ||||
| -rw-r--r-- | test/pdf_pointer_bench.zig | 14 | ||||
| -rw-r--r-- | test/pdf_scroll_bench.zig | 43 | ||||
| -rw-r--r-- | test/pdf_sections_bench.zig | 65 | ||||
| -rw-r--r-- | test/snapshot.zig | 2 | ||||
| -rw-r--r-- | test/snapshots/lsp.golden | 5 | ||||
| -rw-r--r-- | test/snapshots/lsp.snap | 10 | ||||
| -rw-r--r-- | test/snapshots/lspcomplete.golden | 20 | ||||
| -rw-r--r-- | test/snapshots/lspcomplete.snap | 21 | ||||
| -rw-r--r-- | test/snapshots/lspcompletemoved.golden | 2 | ||||
| -rw-r--r-- | test/snapshots/lspdebug.golden | 2 | ||||
| -rw-r--r-- | test/snapshots/lspdebug.snap | 23 | ||||
| -rw-r--r-- | test/snapshots/lsprelpath.golden | 120 | ||||
| -rw-r--r-- | test/snapshots/lsprelpath.snap | 34 |
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() @@ -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(©)); -} - -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 |
