diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/lspfixture/client/Cargo.toml | 11 | ||||
| -rw-r--r-- | test/lspfixture/client/lib.rs | 4 | ||||
| -rw-r--r-- | test/lspfixture/client/main.rs | 7 | ||||
| -rw-r--r-- | test/lspmock.zig | 271 | ||||
| -rw-r--r-- | test/snapshot.zig | 17 | ||||
| -rw-r--r-- | test/snapshots/builtins.golden | 66 | ||||
| -rw-r--r-- | test/snapshots/builtins.snap | 11 | ||||
| -rw-r--r-- | test/snapshots/leader.golden | 30 | ||||
| -rw-r--r-- | test/snapshots/lsp-client-edit.golden | 44 | ||||
| -rw-r--r-- | test/snapshots/lsp-client-edit.snap | 35 | ||||
| -rw-r--r-- | test/snapshots/lsp-client.golden | 27 | ||||
| -rw-r--r-- | test/snapshots/lsp-client.snap | 34 | ||||
| -rw-r--r-- | test/snapshots/lsp-rename.golden | 8 | ||||
| -rw-r--r-- | test/snapshots/lspdebug.golden | 2 |
14 files changed, 518 insertions, 49 deletions
diff --git a/test/lspfixture/client/Cargo.toml b/test/lspfixture/client/Cargo.toml new file mode 100644 index 00000000..01a28899 --- /dev/null +++ b/test/lspfixture/client/Cargo.toml @@ -0,0 +1,11 @@ +# Root marker for the client fixture: rootOf stops here, and a real +# rust-analyzer pointed at this directory (instead of the mock) finds a +# well-formed, dependency-free crate. +[package] +name = "lspfixture-client" +version = "0.0.0" +edition = "2021" + +[[bin]] +name = "lspfixture-client" +path = "main.rs" diff --git a/test/lspfixture/client/lib.rs b/test/lspfixture/client/lib.rs new file mode 100644 index 00000000..2887517e --- /dev/null +++ b/test/lspfixture/client/lib.rs @@ -0,0 +1,4 @@ +// mock fixture +pub fn add(a: i32, b: i32) -> i32 { + a + b +} diff --git a/test/lspfixture/client/main.rs b/test/lspfixture/client/main.rs new file mode 100644 index 00000000..ab29f8bc --- /dev/null +++ b/test/lspfixture/client/main.rs @@ -0,0 +1,7 @@ +mod lib; + +fn main() { + let x = add(1, 2); + let y = add(3, 4); + let _ = x + y; +} diff --git a/test/lspmock.zig b/test/lspmock.zig new file mode 100644 index 00000000..7b379b49 --- /dev/null +++ b/test/lspmock.zig @@ -0,0 +1,271 @@ +//! A deterministic language server for the protocol client's tests. +//! +//! Speaks just enough LSP over stdin/stdout to pin the client end to end +//! WITHOUT a real analyser in the loop: every answer is a fixed shape over +//! the request's own uri, so a golden can quote it byte for byte. The +//! fixture it narrates is test/lspfixture/client/ — the ranges below are +//! POSITIONS IN THOSE FILES, and moving either without the other breaks the +//! snapshot, which is the point. +//! +//! The snapshot harness points the client at this binary with +//! `PARDES_LSP_RS=<abs path>`; nothing outside a test should ever run it. +//! +//! `LSPMOCK_MODE` selects a failure personality for the degradation tests: +//! (unset) answer everything +//! mute handshake, then never answer another request (timeout path) +//! die exit right after the handshake (dead-server path) +//! utf16 refuse the utf-8 negotiation (column-conversion path) +const std = @import("std"); +const libc = std.c; + +extern "c" fn getenv(name: [*:0]const u8) ?[*:0]const u8; + +var mode: enum { normal, mute, die, utf16 } = .normal; + +/// stdout, written with plain libc write(2): the transport is a socketpair +/// and the frames are small, so partial writes are retried and nothing else +/// is needed. +fn send(body: []const u8) void { + var hdr_buf: [64]u8 = undefined; + const hdr = std.fmt.bufPrint(&hdr_buf, "Content-Length: {d}\r\n\r\n", .{body.len}) catch return; + writeAll(hdr); + writeAll(body); +} + +fn writeAll(bytes: []const u8) void { + var off: usize = 0; + while (off < bytes.len) { + const n = libc.write(1, bytes.ptr + off, bytes.len - off); + if (n < 0) { + if (libc.errno(n) == .INTR) continue; + std.process.exit(0); // client hung up; nothing left to say + } + off += @intCast(n); + } +} + +fn reply(a: std.mem.Allocator, id: i64, comptime result_fmt: []const u8, args: anytype) void { + const body = std.fmt.allocPrint(a, "{{\"jsonrpc\":\"2.0\",\"id\":{d},\"result\":" ++ result_fmt ++ "}}", .{id} ++ args) catch return; + send(body); +} + +fn notify(a: std.mem.Allocator, comptime method: []const u8, comptime params_fmt: []const u8, args: anytype) void { + const body = std.fmt.allocPrint(a, "{{\"jsonrpc\":\"2.0\",\"method\":\"" ++ method ++ "\",\"params\":" ++ params_fmt ++ "}}", args) catch return; + send(body); +} + +// std.json.Value navigation, the client's own idiom. +fn get(v: ?std.json.Value, key: []const u8) ?std.json.Value { + const o = v orelse return null; + if (o != .object) return null; + return o.object.get(key); +} +fn str(v: ?std.json.Value) ?[]const u8 { + const o = v orelse return null; + return if (o == .string) o.string else null; +} +fn num(v: ?std.json.Value) ?i64 { + const o = v orelse return null; + return if (o == .integer) o.integer else null; +} + +/// The request's document uri — under `textDocument` for most methods, under +/// `item` for the hierarchy follow-ups. Every cross-file answer derives from +/// it, so the mock stays inside the caller's own fixture tree. +fn reqUri(params: ?std.json.Value) []const u8 { + return str(get(get(params, "textDocument"), "uri")) orelse + str(get(get(params, "item"), "uri")) orelse "file:///dev/null"; +} + +/// The same uri with its basename replaced. +fn sibling(a: std.mem.Allocator, uri: []const u8, name: []const u8) []const u8 { + const cut = std.mem.lastIndexOfScalar(u8, uri, '/') orelse return uri; + return std.fmt.allocPrint(a, "{s}/{s}", .{ uri[0..cut], name }) catch uri; +} + +/// `{"start":{"line":L,"character":A},"end":{"line":L,"character":B}}` +fn range(a: std.mem.Allocator, line: u32, from: u32, to: u32) []const u8 { + return std.fmt.allocPrint( + a, + "{{\"start\":{{\"line\":{d},\"character\":{d}}},\"end\":{{\"line\":{d},\"character\":{d}}}}}", + .{ line, from, line, to }, + ) catch "{}"; +} + +/// rootUri captured at initialize — workspace/symbol has no textDocument, so +/// its answers derive from here instead of the /dev/null fallback. +var root_uri: [512]u8 = undefined; +var root_uri_len: usize = 0; + +pub fn main(init: std.process.Init) !void { + _ = init; + if (getenv("LSPMOCK_MODE")) |m| { + const s = std.mem.span(m); + if (std.mem.eql(u8, s, "mute")) mode = .mute; + if (std.mem.eql(u8, s, "die")) mode = .die; + if (std.mem.eql(u8, s, "utf16")) mode = .utf16; + } + + const gpa = std.heap.page_allocator; + var buf = try gpa.alloc(u8, 1 << 20); + var len: usize = 0; + + while (true) { + // one Content-Length frame at the head of buf + const msg: ?[]const u8 = blk: { + const sep = std.mem.indexOf(u8, buf[0..len], "\r\n\r\n") orelse break :blk null; + var clen: ?usize = null; + var it = std.mem.splitSequence(u8, buf[0..sep], "\r\n"); + while (it.next()) |ln| { + const colon = std.mem.indexOfScalar(u8, ln, ':') orelse continue; + if (std.ascii.eqlIgnoreCase(std.mem.trim(u8, ln[0..colon], " "), "content-length")) + clen = std.fmt.parseInt(usize, std.mem.trim(u8, ln[colon + 1 ..], " "), 10) catch null; + } + const n = clen orelse std.process.exit(1); + if (len < sep + 4 + n) break :blk null; + break :blk buf[sep + 4 .. sep + 4 + n]; + }; + const raw = msg orelse { + if (len == buf.len) buf = try gpa.realloc(buf, buf.len * 2); + const got = libc.read(0, buf.ptr + len, buf.len - len); + if (got < 0) { + if (libc.errno(got) == .INTR) continue; + return; + } + if (got == 0) return; // editor hung up + len += @intCast(got); + continue; + }; + + var arena_state: std.heap.ArenaAllocator = .init(gpa); + defer arena_state.deinit(); + const a = arena_state.allocator(); + + handle(a, raw); + + // compact the consumed frame away + const consumed = (@intFromPtr(raw.ptr) - @intFromPtr(buf.ptr)) + raw.len; + std.mem.copyForwards(u8, buf[0 .. len - consumed], buf[consumed..len]); + len -= consumed; + } +} + +fn handle(a: std.mem.Allocator, raw: []const u8) void { + const v = std.json.parseFromSliceLeaky(std.json.Value, a, raw, .{}) catch return; + const method = str(get(v, "method")) orelse ""; + const id = num(get(v, "id")); + const params = get(v, "params"); + + if (std.mem.eql(u8, method, "initialize")) { + if (str(get(params, "rootUri"))) |r| { + root_uri_len = @min(r.len, root_uri.len); + @memcpy(root_uri[0..root_uri_len], r[0..root_uri_len]); + } + const enc: []const u8 = if (mode == .utf16) "utf-16" else "utf-8"; + reply(a, id orelse 0, + \\{{"capabilities":{{"positionEncoding":"{s}", + \\"textDocumentSync":1, + \\"definitionProvider":true,"declarationProvider":true, + \\"typeDefinitionProvider":true,"implementationProvider":true, + \\"referencesProvider":true,"documentHighlightProvider":true, + \\"hoverProvider":true,"documentSymbolProvider":true, + \\"workspaceSymbolProvider":true,"renameProvider":true, + \\"documentFormattingProvider":true,"codeActionProvider":true, + \\"completionProvider":{{}}, + \\"callHierarchyProvider":true,"typeHierarchyProvider":true, + \\"diagnosticProvider":{{"interFileDependencies":false,"workspaceDiagnostics":false}}, + \\"workspace":{{"workspaceFolders":{{"supported":true}}}}}}, + \\"serverInfo":{{"name":"lspmock"}}}} + , .{enc}); + return; + } + if (std.mem.eql(u8, method, "initialized")) { + if (mode == .die) std.process.exit(0); + // the progress trio, so the status pipeline has something to narrate + notify(a, "$/progress", "{{\"token\":\"t1\",\"value\":{{\"kind\":\"begin\",\"title\":\"mocking\"}}}}", .{}); + notify(a, "$/progress", "{{\"token\":\"t1\",\"value\":{{\"kind\":\"report\",\"percentage\":50,\"message\":\"halfway\"}}}}", .{}); + notify(a, "$/progress", "{{\"token\":\"t1\",\"value\":{{\"kind\":\"end\"}}}}", .{}); + return; + } + if (id == null) return; // didOpen/didChange/$/cancelRequest: nothing to say + + if (mode == .mute) return; // swallow every request after the handshake + + const rid = id.?; + const uri = reqUri(params); + + // ---- fixture geometry (0-based lines/chars, over lspfixture/client/) -- + // main.rs L3/L4: ` let x = add(1, 2);` — "add" at 12..15 + // lib.rs L1: `pub fn add(a: i32, b: i32) -> i32 {` — "add" at 7..10 + // main.rs L2: `fn main() {` — "main" at 3..7 + const lib = sibling(a, uri, "lib.rs"); + const mainrs = sibling(a, uri, "main.rs"); + + if (std.mem.eql(u8, method, "textDocument/definition") or + std.mem.eql(u8, method, "textDocument/declaration") or + std.mem.eql(u8, method, "textDocument/typeDefinition") or + std.mem.eql(u8, method, "textDocument/implementation")) + { + reply(a, rid, "[{{\"uri\":\"{s}\",\"range\":{s}}}]", .{ lib, range(a, 1, 7, 10) }); + } else if (std.mem.eql(u8, method, "textDocument/references")) { + reply(a, rid, "[{{\"uri\":\"{s}\",\"range\":{s}}},{{\"uri\":\"{s}\",\"range\":{s}}},{{\"uri\":\"{s}\",\"range\":{s}}}]", .{ + mainrs, range(a, 3, 12, 15), + mainrs, range(a, 4, 12, 15), + lib, range(a, 1, 7, 10), + }); + } else if (std.mem.eql(u8, method, "textDocument/documentHighlight")) { + reply(a, rid, "[{{\"range\":{s}}},{{\"range\":{s}}}]", .{ range(a, 3, 12, 15), range(a, 4, 12, 15) }); + } else if (std.mem.eql(u8, method, "textDocument/hover")) { + reply(a, rid, "{{\"contents\":{{\"kind\":\"markdown\",\"value\":\"mock hover: pub fn add\"}}}}", .{}); + } else if (std.mem.eql(u8, method, "textDocument/documentSymbol")) { + reply(a, rid, "[{{\"name\":\"main\",\"kind\":12,\"range\":{s},\"selectionRange\":{s}}}]", .{ + range(a, 2, 0, 11), range(a, 2, 3, 7), + }); + } else if (std.mem.eql(u8, method, "workspace/symbol")) { + const q = str(get(params, "query")) orelse ""; + reply(a, rid, "[{{\"name\":\"{s}_sym\",\"kind\":12,\"location\":{{\"uri\":\"{s}/lib.rs\",\"range\":{s}}}}}]", .{ + q, root_uri[0..root_uri_len], range(a, 1, 7, 10), + }); + } else if (std.mem.eql(u8, method, "textDocument/rename")) { + const nn = str(get(params, "newName")) orelse "renamed"; + reply(a, rid, "{{\"changes\":{{\"{s}\":[{{\"range\":{s},\"newText\":\"{s}\"}},{{\"range\":{s},\"newText\":\"{s}\"}}]}}}}", .{ + uri, range(a, 3, 12, 15), nn, range(a, 4, 12, 15), nn, + }); + } else if (std.mem.eql(u8, method, "textDocument/formatting")) { + reply(a, rid, "[{{\"range\":{s},\"newText\":\"// formatted by mock\\n\"}}]", .{range(a, 0, 0, 0)}); + } else if (std.mem.eql(u8, method, "textDocument/codeAction")) { + reply(a, rid, "[{{\"title\":\"mock action: extract\"}}]", .{}); + } else if (std.mem.eql(u8, method, "textDocument/completion")) { + reply(a, rid, "{{\"items\":[{{\"label\":\"alpha\",\"detail\":\"fn alpha()\"}},{{\"label\":\"beta\",\"detail\":\"fn beta()\"}}]}}", .{}); + } else if (std.mem.eql(u8, method, "textDocument/diagnostic")) { + reply(a, rid, "{{\"kind\":\"full\",\"items\":[{{\"range\":{s},\"severity\":2,\"message\":\"mock diagnostic\"}}]}}", .{range(a, 0, 0, 7)}); + } else if (std.mem.eql(u8, method, "textDocument/prepareCallHierarchy")) { + reply(a, rid, "[{{\"name\":\"add\",\"kind\":12,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}}]", .{ + lib, range(a, 1, 0, 35), range(a, 1, 7, 10), + }); + } else if (std.mem.eql(u8, method, "callHierarchy/incomingCalls")) { + reply(a, rid, "[{{\"from\":{{\"name\":\"main\",\"kind\":12,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}},\"fromRanges\":[{s},{s}]}}]", .{ + mainrs, range(a, 2, 0, 11), range(a, 2, 3, 7), range(a, 3, 12, 15), range(a, 4, 12, 15), + }); + } else if (std.mem.eql(u8, method, "callHierarchy/outgoingCalls")) { + reply(a, rid, "[{{\"to\":{{\"name\":\"callee\",\"kind\":12,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}},\"fromRanges\":[]}}]", .{ + lib, range(a, 1, 0, 35), range(a, 1, 7, 10), + }); + } else if (std.mem.eql(u8, method, "textDocument/prepareTypeHierarchy")) { + reply(a, rid, "[{{\"name\":\"MockType\",\"kind\":5,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}}]", .{ + uri, range(a, 2, 3, 7), range(a, 2, 3, 7), + }); + } else if (std.mem.eql(u8, method, "typeHierarchy/supertypes")) { + reply(a, rid, "[{{\"name\":\"SuperMock\",\"kind\":5,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}}]", .{ + lib, range(a, 1, 7, 10), range(a, 1, 7, 10), + }); + } else if (std.mem.eql(u8, method, "typeHierarchy/subtypes")) { + reply(a, rid, "[{{\"name\":\"SubMock\",\"kind\":5,\"uri\":\"{s}\",\"range\":{s},\"selectionRange\":{s}}}]", .{ + mainrs, range(a, 2, 3, 7), range(a, 2, 3, 7), + }); + } else if (std.mem.eql(u8, method, "shutdown")) { + reply(a, rid, "null", .{}); + } else { + reply(a, rid, "null", .{}); + } +} diff --git a/test/snapshot.zig b/test/snapshot.zig index 7f8c4f21..5dc2e6e9 100644 --- a/test/snapshot.zig +++ b/test/snapshot.zig @@ -79,6 +79,9 @@ const SNAP_BASE = "/tmp/pardes-snap"; var trace_stable = false; /// set for the serial re-run pass; children widen their probes (see beCareful) var careful = false; +/// abs path of the deterministic mock language server the build handed over +/// (--lspmock=); null leaves the protocol client fully disabled for the run +var lspmock_bin: ?[:0]const u8 = null; const gpa = std.heap.page_allocator; pub fn main(init: std.process.Init) !void { @@ -102,11 +105,14 @@ pub fn main(init: std.process.Init) !void { const cpus = std.Thread.getCpuCount() catch 4; var jobs: usize = cpus + cpus / 4; var script_count: usize = 0; + var lspmock: ?[]const u8 = null; for (args[1..]) |a| { if (std.mem.eql(u8, a, "--update")) { update = true; } else if (std.mem.eql(u8, a, "--trace-stable")) { trace_stable = true; + } else if (std.mem.startsWith(u8, a, "--lspmock=")) { + lspmock = try absPath(arena, orig_cwd, a["--lspmock=".len..]); } else if (std.mem.startsWith(u8, a, "--jobs=")) { jobs = @max(1, try std.fmt.parseInt(usize, a["--jobs=".len..], 10)); } else if (exe == null) { @@ -129,6 +135,7 @@ pub fn main(init: std.process.Init) !void { for (args[1..]) |a| { if (std.mem.eql(u8, a, "--update") or std.mem.eql(u8, a, "--trace-stable") or + std.mem.startsWith(u8, a, "--lspmock=") or std.mem.startsWith(u8, a, "--jobs=")) continue; if (!saw_exe) { saw_exe = true; @@ -139,6 +146,7 @@ pub fn main(init: std.process.Init) !void { } if (exe == null) fatal("usage: pardes-snap <exe> [scripts...] [--update] [--jobs=N]", .{}); const exe_z = try arena.dupeZ(u8, exe.?); + lspmock_bin = if (lspmock) |p| try arena.dupeZ(u8, p) else null; if (scripts.len == 0) { scripts = try listSnaps(arena, try absPath(arena, orig_cwd, "test/snapshots")); @@ -389,6 +397,15 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const // views' durations (lsp_zls.zig hideTime) and the transient message row's // time of day (message.zig stamp) _ = setenv("PARDES_NOTIME", "1", 1); + // The protocol client must be HERMETIC under the harness: `.rs` files + // speak to the deterministic mock when the build handed one over, and + // every other spec is disabled outright — a script that opened a stray + // `.c` or `.ts` file must never spawn whatever clangd this machine has. + _ = setenv("PARDES_LSP_RS", lspmock_bin orelse "", 1); + _ = setenv("PARDES_LSP_C", "", 1); + _ = setenv("PARDES_LSP_GO", "", 1); + _ = setenv("PARDES_LSP_TS", "", 1); + _ = setenv("PARDES_LSP_PY", "", 1); // Pin the dump path: live runs use a timestamped file under the user data // dir (dump.outPath), which would make the dump/load/restore goldens // nondeterministic. Per-SCRIPT, not one shared /tmp/pardes-dump.zon: four diff --git a/test/snapshots/builtins.golden b/test/snapshots/builtins.golden index 5019c855..555a6fc4 100644 --- a/test/snapshots/builtins.golden +++ b/test/snapshots/builtins.golden @@ -42,39 +42,39 @@ |64: 30 SPC h t Tutor topbar |65: 31 SPC j i Forward C-i == snap index-tail grid=120x66 cursor=7,64 -|35: 49 SPC s a Attach -|36: 50 SPC s d Dump topbar -|37: 51 SPC s r Restore -|38: 52 SPC t a Ascii -|39: 53 SPC t b Tagbottom -|40: 54 SPC t c Colors -|41: 55 SPC t d Debug topbar -|42: 56 SPC t i PdfTint -|43: 57 SPC t l Palette -|44: 58 SPC t n NextColor topbar -|45: 59 SPC t p Petscii -|46: 60 SPC t s PdfSections -|47: 61 SPC t t ThemeSel -|48: 62 SPC t w Wrap -|49: 63 SPC t z PdfFit -|50: 64 SPC w h Left C-w h, C-w left -|51: 65 SPC w j Down C-w j, C-w down -|52: 66 SPC w k Up C-w k, C-w up -|53: 67 SPC w l Right C-w l, C-w right -|54: 68 SPC y ClipYank -|55: 69 Look enter, right-click -|56: 70 Exec tab, middle-click -|57: 71 Msg -|58: 72 ThemeFile -|59: 73 DumpThemes -|60: 74 Filter -|61: 75 EffectCode -|62: 76 9p -|63: 77 Theme -|64: 78 Shell -|65: 79 -== snap not-a-picker grid=120x66 cursor=7,64 -== snap window-group grid=120x66 cursor=7,35 +|35: 66 SPC t w Wrap +|36: 67 SPC t z PdfFit +|37: 68 SPC w h Left C-w h, C-w left +|38: 69 SPC w j Down C-w j, C-w down +|39: 70 SPC w k Up C-w k, C-w up +|40: 71 SPC w l Right C-w l, C-w right +|41: 72 SPC y ClipYank +|42: 73 Look enter, right-click +|43: 74 Exec tab, middle-click +|44: 75 Msg +|45: 76 ThemeFile +|46: 77 DumpThemes +|47: 78 Filter +|48: 79 EffectCode +|49: 80 9p +|50: 81 Theme +|51: 82 Shell +|52: 83 +|53: 84 language keys (motions, not words): +|54: 85 gd gD gy gi gr goto: definition, +|55: 86 declaration, type-def, +|56: 87 implementation, refs +|57: 88 ]d [d ]D [D diagnostics: next, +|58: 89 prev, last, first +|59: 90 = format (applies, one +|60: 91 undo step) +|61: 92 Tab after a . completion, in insert +|62: 93 C-left-click definition, by mouse +|63: 94 SPC l ... hover, rename, symbols, +|64: 95 calls, types: above +|65: 96 +== snap not-a-picker grid=120x66 cursor=19,61 +== snap window-group grid=120x66 cursor=19,35 |35: 1 pardes builtins under SPC w |36: 2 |37: 3 SPC w h Left C-w h, C-w left diff --git a/test/snapshots/builtins.snap b/test/snapshots/builtins.snap index ad8e27f3..4eaa3543 100644 --- a/test/snapshots/builtins.snap +++ b/test/snapshots/builtins.snap @@ -55,12 +55,15 @@ snap window-group # which is the whole reason n/N were never allowed to be the picker. # # The one place in this suite a builtin cannot be added for free: the click is -# a SCREEN coordinate, so every row inserted above `SPC h t` in the listing -# moves Tutor down one and this number with it. The five clipboard builtins -# put three there — SPC P, SPC R and SPC Y, uppercase sorting ahead of the -# lowercase `SPC h`; the eleven panel effects now follow them as well. +# a SCREEN COORDINATE. The rows above `SPC h t` in the listing move Tutor and +# this number with them — the five clipboard builtins put three there. And +# since the language-keys footer made the page longer than the pane, the +# re-opened view RETAINS the scroll the `ge` above left behind, which is why +# `gg` now pins the top before the click: an exec aimed one row into the +# footer would run "declaration," in a shell instead of opening the tutor. key space ? wait 10000 SPC c n +key g g stable 700 15000 press middle 20 65 release middle 20 65 diff --git a/test/snapshots/leader.golden b/test/snapshots/leader.golden index 38b4f9dc..5d3adba2 100644 --- a/test/snapshots/leader.golden +++ b/test/snapshots/leader.golden @@ -160,17 +160,21 @@ |36: == snap help-language grid=100x40 cursor=7,22 |22: 1 pardes builtins under SPC l -|24: 3 SPC l D WsDiagnostics -|25: 4 SPC l S WsSymbols -|26: 5 SPC l a CodeAction -|27: 6 SPC l d Diagnostics -|28: 7 SPC l h SelectRefs -|29: 8 SPC l i Lspinfo -|30: 9 SPC l k Hover -|31: 10 SPC l r Rename -|32: 11 SPC l s Symbols -|33: 12 SPC l w Lspwhy -|34: 13 +|24: 3 SPC l C Callees +|25: 4 SPC l D WsDiagnostics +|26: 5 SPC l S WsSymbols +|27: 6 SPC l T Subtypes +|28: 7 SPC l a CodeAction +|29: 8 SPC l c Callers +|30: 9 SPC l d Diagnostics +|31: 10 SPC l h SelectRefs +|32: 11 SPC l i Lspinfo +|33: 12 SPC l k Hover +|34: 13 SPC l r Rename +|35: 14 SPC l s Symbols +|36: 15 SPC l t Supertypes +|37: 16 SPC l w Lspwhy +|38: 17 == snap del-output grid=100x40 cursor=7,2 |21: |22: @@ -186,3 +190,7 @@ |32: |33: |34: +|35: +|36: +|37: +|38: diff --git a/test/snapshots/lsp-client-edit.golden b/test/snapshots/lsp-client-edit.golden new file mode 100644 index 00000000..15a477b2 --- /dev/null +++ b/test/snapshots/lsp-client-edit.golden @@ -0,0 +1,44 @@ +== snap formatted grid=100x24 cursor=7,3 +|New Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save New Newtty Del +| 1 // formatted by mock +| 2 mod lib; +| 3 +| 4 fn main() { +| 5 let x = add(1, 2); +| 6 let y = add(3, 4); +| 7 let _ = x + y; +| 8 } +| 9 +| +| +| +| +| +| +| +| +| +| +| +| +| formatted 1 range(s) +== snap unformatted grid=100x24 cursor=7,2 +|2: 1 mod lib; +|3: 2 +|4: 3 fn main() { +|5: 4 let x = add(1, 2); +|6: 5 let y = add(3, 4); +|7: 6 let _ = x + y; +|8: 7 } +|9: 8 +|10: +|23: +== snap renamed grid=100x24 cursor=12,4 +|5: 4 let x = sum(1, 2); +|6: 5 let y = sum(3, 4); +|23: renamed 2 range(s) +== snap unrenamed grid=100x24 cursor=12,4 +|5: 4 let x = add(1, 2); +|6: 5 let y = add(3, 4); +|23: diff --git a/test/snapshots/lsp-client-edit.snap b/test/snapshots/lsp-client-edit.snap new file mode 100644 index 00000000..a9e66e51 --- /dev/null +++ b/test/snapshots/lsp-client-edit.snap @@ -0,0 +1,35 @@ +# The protocol client's MUTATING answers, through the @put record path: a +# server TextEdit response becomes one atomic buffer edit with one undo step. +# = applies the mock's formatting edit (a header line) and narrates +# "formatted 1 range(s)" on the message row +# SPC l r renames both `add` calls via the server's WorkspaceEdit +# u reverses each as a single transaction +file main.rs mod lib;\n\nfn main() {\n let x = add(1, 2);\n let y = add(3, 4);\n let _ = x + y;\n}\n +file lib.rs // mock fixture\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n +start 24 100 main.rs +wait 8000 add(1, 2) +stable 700 20000 +key = +wait 10000 formatted by mock +stable 700 15000 +snap formatted +key u +wait 8000 mod lib; +stable 700 15000 +snap unformatted +# rename through the leader prompt: both call sites change in one step +press left 13 5 +release left 13 5 +key space +key l +key r +stable 400 5000 +text sum +key enter +wait 10000 sum(3, 4) +stable 700 15000 +snap renamed +key u +wait 8000 add(3, 4) +stable 700 15000 +snap unrenamed diff --git a/test/snapshots/lsp-client.golden b/test/snapshots/lsp-client.golden new file mode 100644 index 00000000..a203cc7c --- /dev/null +++ b/test/snapshots/lsp-client.golden @@ -0,0 +1,27 @@ +== snap gd-jumped grid=100x24 cursor=16,15 +|New Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/lsp-client/cwd/main.rs Save New Newtty Del +| 1 mod lib; +| 2 +| 3 fn main() { +| 4 let x = add(1, 2); +| 5 let y = add(3, 4); +| 6 let _ = x + y; +| 7 } +| 8 +| +| +| --:--:-- lsp rust-analyzer: ready +| /tmp/pardes-snap/lsp-client/cwd/lib.rs Save New Newtty Del +| 1 // mock fixture +| 2 pub fn add(a: i32, b: i32) -> i32 { +| 3 a + b +| 4 } +| 5 +== snap gr-list grid=100x24 cursor=16,15 +|12: +|20: /tmp/pardes-snap/lsp-client/cwd/+Search Save New Newtty Del +|21: 1 main.rs:4:13-15 let x = add(1, 2); +|22: 2 main.rs:5:13-15 let y = add(3, 4); +|23: 3 lib.rs:2:8-10 pub fn add(a: i32, b: i32) -> i32 { +== snap gr-opened grid=100x24 cursor=21,6 diff --git a/test/snapshots/lsp-client.snap b/test/snapshots/lsp-client.snap new file mode 100644 index 00000000..bc5267cc --- /dev/null +++ b/test/snapshots/lsp-client.snap @@ -0,0 +1,34 @@ +# The protocol client end to end, against the deterministic mock server the +# harness pins via PARDES_LSP_RS (test/lspmock.zig). What this exercises is +# everything BETWEEN the keymap and the analyser: spawn, handshake, the reader +# thread, $/progress narration onto the transient message row, and the +# location shapes coming back as +Search rows. +# gd on a cross-file Location -> a look: lib.rs opens with `add` selected, +# and the message row still carries the server's settled state +# ("lsp lspmock: ready") because nothing has cleared it yet. +# gr -> three rows with column SPANS, n arms the walk, Enter opens one. +file main.rs mod lib;\n\nfn main() {\n let x = add(1, 2);\n let y = add(3, 4);\n let _ = x + y;\n}\n +file lib.rs // mock fixture\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n +start 24 100 main.rs +wait 8000 add(1, 2) +stable 700 20000 +# cursor into the first `add` call, then helix's goto-definition +press left 13 5 +release left 13 5 +key g +key d +wait 10000 pub fn add +stable 700 15000 +snap gd-jumped +# references from the definition the look landed on (cursor sits on `add`) +key g +key r +wait 10000 add(3, 4) +stable 700 15000 +snap gr-list +# step the list and open the second hit: main.rs with the span selected +key n +key n +key enter +stable 700 15000 +snap gr-opened diff --git a/test/snapshots/lsp-rename.golden b/test/snapshots/lsp-rename.golden index e21228e4..a241a93c 100644 --- a/test/snapshots/lsp-rename.golden +++ b/test/snapshots/lsp-rename.golden @@ -16,9 +16,17 @@ | 13 _ = helper; | 14 } | 15 +| +| +| +| +| +| +| renamed 2 range(s) == snap undone grid=110x24 cursor=17,9 |4: 3 fn helper(x: u32) u32 { |9: 8 _ = helper(41); +|23: == snap redone grid=110x24 cursor=17,9 |4: 3 fn renamed_helper(x: u32) u32 { |9: 8 _ = renamed_helper(41); diff --git a/test/snapshots/lspdebug.golden b/test/snapshots/lspdebug.golden index 91ab400a..cd9ac0c7 100644 --- a/test/snapshots/lspdebug.golden +++ b/test/snapshots/lspdebug.golden @@ -27,7 +27,7 @@ |43: 9 0 row(s) == snap info grid=120x44 cursor=7,4 |18: /tmp/pardes-snap/lspdebug/cwd/+Lsp Save New Newtty Del -|19: 1 backend: zls-inproc +|19: 1 backend: zls-inproc+lsp-client |20: 2 zls: 0.16.1-dev+3e0d0820 (compiled in — no server process, no JSON-RPC) |21: 3 zig lib dir: /usr/lib/zig [OK] |22: 4 offsets: utf-8 walk caps: 512 files, 2000 rows |
