diff options
| -rw-r--r-- | docs/lsp.md | 56 | ||||
| -rw-r--r-- | src/lsp/lsp.zig | 29 | ||||
| -rw-r--r-- | src/lsp/lsp_zls.zig | 99 | ||||
| -rw-r--r-- | test/lspbench.zig | 6 | ||||
| -rw-r--r-- | test/snapshots/lsp.golden | 18 | ||||
| -rw-r--r-- | test/snapshots/lspcomplete.golden | 20 | ||||
| -rw-r--r-- | test/snapshots/lsprelpath.golden | 245 | ||||
| -rw-r--r-- | test/snapshots/lsprelpath.snap | 87 |
8 files changed, 493 insertions, 67 deletions
diff --git a/docs/lsp.md b/docs/lsp.md index 0c6a2f97..258e409e 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -51,11 +51,12 @@ Three rules make it safe: Every backend renders into one format: ``` -/abs/path/to/file.zig:LINE:COL text -/abs/path/to/file.zig:LINE:COL-ENDCOL text +sub/file.zig:LINE:COL text under the asking window's dir +sub/file.zig:LINE:COL-ENDCOL text +/abs/path/elsewhere.zig:LINE:COL text anywhere else ``` -1-based line and column, absolute path. The second form carries the answer's +1-based line and column. The second form carries the answer's RANGE where the protocol gave one on a single line (a token, a symbol's name), and a look on it SELECTS that span rather than parking at its first cell — so `gd` lands on the whole name and `gr` steps references with each one @@ -71,14 +72,46 @@ locations (`hover`, `code_action`, `format`, `rename`) open `+Hover`/`+Lsp` instead and do not arm the stepper — `n` over a documentation blurb would step to nowhere. +**A path UNDER `Req.root` is written relative to it; everything else keeps its +full absolute path** (`lsp.rel`). `Req.root` is the directory of the file the +query was asked about — the window that generated the buffer — and a `gr` over +one file was otherwise the same forty-character prefix repeated down the whole +pane, with the part you came to read pushed off the right edge. The short form +resolves because `Req.root` is also the directory the results buffer is *named* +in (`output_pane.open`), and a look resolves a relative word against the +directory of the pane it was clicked in — which is that buffer. + +Under, never "shorter": a hit outside the tree is **not** walked up to with +`../`. An absolute path resolves from anywhere and says where it is; a `../..` +chain says neither, and stops being true the moment the row is read anywhere but +beside its own buffer. `test/snapshots/lsprelpath.snap` pins both directions end +to end — the enum one level up (absolute row) and one level down (`inner/tint.zig`, +a stripped path that still has a separator in it), each with the `n` step that +opens it, plus a right click. + +This is the rule `look.grep` already follows for its own rows (`look.zig`, the +`shown` computation), written a second time; the two are now the same function +and want to become one. + `completion` is the kind this shape changes the most. Every other editor answers a dot with a popup of NAMES to insert; a seam that returns locations cannot insert anything, so this one answers with the candidates' **declarations** — -one `path:LINE:COL-ENDCOL` row each, in the same `+Search` buffer, steppable -with `n`. That is a different and arguably better answer to "what goes here": -you read the definitions rather than a list of words. It is the one location -kind that does NOT jump on a single row, because with one candidate you still -want to see the list rather than be teleported into it. +one row each, in the same `+Search` buffer, steppable with `n`: + +``` +path:LINE:COL-ENDCOL name the candidate's declaration line +a.zig:2:5-13 verdigris verdigris, +``` + +The name comes first because that is the thing you would type — the row answers +"what goes here" before "where does it come from", which is the order the +question was asked in; every other kind here answers a WHERE, and for this one +the location is the evidence rather than the answer. It is not padded into a +column: the location in front of it is already ragged, so there is nothing to +align to. That is a different and arguably better answer to "what goes here": +you get the word AND you can read the definition rather than a list of words. It +is the one location kind that does NOT jump on a single row, because with one +candidate you still want to see the list rather than be teleported into it. A results buffer is REFILLED rather than reopened when the same kind is asked again — the rule `runSearch` always had, and which the language path was @@ -232,9 +265,10 @@ read is in `req` (`path`, `source` (NUL-terminated), `offset`, `arg`, `root`). `out` is a plain `std.Io.Writer`: the shell owns the buffer behind it (an `Io.Writer.Allocating`), so a backend never allocates the result, never frees it, and cannot get the allocator wrong. `arena` is freed wholesale on return; -`gpa` is for a backend's own scratch. Use `lsp.row()` to emit a location and -`lsp.lineCol()` to convert an offset, so every backend's rows are -byte-identical in shape. +`gpa` is for a backend's own scratch. Use `lsp.row()` to emit a location, +`lsp.rel()` to spell its path against `req.root` and `lsp.lineCol()` to convert +an offset, so every backend's rows are byte-identical in shape. `rel` allocates +nothing — it returns a slice of what you hand it. ## How the implementations are judged diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig index a4489552..90d4fc6b 100644 --- a/src/lsp/lsp.zig +++ b/src/lsp/lsp.zig @@ -12,7 +12,8 @@ //! look.zig already resolves and what n/N already steps, so a multi-result //! answer IS helix's picker and a single result IS a jump, with no picker UI //! written for it. Free text (hover, a rename's diff) rides the same buffer as -//! plain lines. +//! plain lines. A path under `Req.root` is written relative to it and any +//! other keeps its full absolute self — see `rel`. //! //! `query` is the ONLY thing an implementation supplies. Swapping backends is //! swapping this one function, which is also how the three competing @@ -95,8 +96,34 @@ pub const Req = struct { root: []const u8 = "", }; +/// How a row SPELLS a path: relative to `base` if it lives UNDER it, its full +/// absolute self otherwise. +/// +/// `base` is `Req.root` — the directory of the file the query was asked about +/// — which is also the directory the results buffer is opened in, so a row +/// shortened here reads as the name that window would have typed and still +/// resolves when looked. `gr` over one file was otherwise the same +/// forty-character absolute prefix repeated down the whole pane, with the part +/// you came to read pushed off the right edge. +/// +/// UNDER, not "shorter": a path outside that tree is left absolute rather than +/// walked up to with `../`. An absolute path resolves from anywhere and says +/// where it is; `../../..` says neither, and the moment the row is read +/// somewhere other than beside its own buffer it is wrong. +/// +/// This is `look.grep`'s `shown` rule, spelled a second time — see the note +/// there; the two want to become one function. +pub fn rel(base: []const u8, path: []const u8) []const u8 { + if (base.len == 0) return path; + const home = std.mem.trimEnd(u8, base, "/"); + if (path.len > home.len and std.mem.startsWith(u8, path, home) and path[home.len] == '/') + return path[home.len + 1 ..]; + return path; +} + /// Emit one `path:LINE:COL text` row. Line and column are 1-based, the way /// every other row in a `+Search` buffer is (and the way look.zig parses one). +/// `path` has already been through `rel`: the caller holds the base. pub fn row( out: *std.Io.Writer, path: []const u8, diff --git a/src/lsp/lsp_zls.zig b/src/lsp/lsp_zls.zig index dfc2db90..e1624df0 100644 --- a/src/lsp/lsp_zls.zig +++ b/src/lsp/lsp_zls.zig @@ -310,17 +310,20 @@ fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std const off: usize = @min(req.offset, req.source.len); + // the rows of the kinds that only ever name THIS file spell it once + const here = lsp.rel(req.root, req.path); + switch (kind) { - .definition, .implementation => try goto(arena, &analyser, handle, off, .definition, out, tr), - .declaration => try goto(arena, &analyser, handle, off, .declaration, out, tr), - .type_definition => try goto(arena, &analyser, handle, off, .type_definition, out, tr), + .definition, .implementation => try goto(arena, &analyser, handle, off, .definition, req.root, out, tr), + .declaration => try goto(arena, &analyser, handle, off, .declaration, req.root, out, tr), + .type_definition => try goto(arena, &analyser, handle, off, .type_definition, req.root, out, tr), .hover => try hover(arena, &analyser, handle, off, out), - .document_symbols => try documentSymbols(gpa, arena, req.path, &handle.tree, out), - .references, .select_refs => try references(arena, &analyser, handle, off, null, out), - .rename => try references(arena, &analyser, handle, off, req.arg, out), - .diagnostics => try diagnostics(gpa, arena, req.path, &handle.tree, out), + .document_symbols => try documentSymbols(gpa, arena, here, &handle.tree, out), + .references, .select_refs => try references(arena, &analyser, handle, off, null, req.root, out), + .rename => try references(arena, &analyser, handle, off, req.arg, req.root, out), + .diagnostics => try diagnostics(gpa, arena, here, &handle.tree, out), .code_action => try codeActions(gpa, arena, &analyser, handle, off, out), - .completion => try completion(arena, &analyser, handle, off, req.source, out, tr), + .completion => try completion(arena, &analyser, handle, off, req.root, req.source, out, tr), else => {}, } } @@ -492,12 +495,13 @@ fn lineAt(src: []const u8, want: usize) []const u8 { /// Emit one row for a token in whatever file it lives in — which for a goto /// into `std` is not the file the cursor was in, so the path comes off the -/// handle's uri rather than off `req`. -fn rowForToken(arena: std.mem.Allocator, th: Analyser.TokenWithHandle, out: *std.Io.Writer) !void { +/// handle's uri rather than off `req`. `base` is `req.root`, the directory the +/// row is written relative to (lsp.rel). +fn rowForToken(arena: std.mem.Allocator, base: []const u8, th: Analyser.TokenWithHandle, out: *std.Io.Writer) !void { const tree = &th.handle.tree; if (th.token >= tree.tokens.len) return; const r = offsets.tokenToRange(tree, th.token, enc); - const path = th.handle.uri.toFsPath(arena) catch return; + const path = lsp.rel(base, th.handle.uri.toFsPath(arena) catch return); lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lineAt(tree.source, r.start.line)); } @@ -516,6 +520,8 @@ fn goto( handle: *DocumentStore.Handle, off: usize, kind: GotoKind, + /// req.root: what the rows' paths are written relative to (lsp.rel) + base: []const u8, out: *std.Io.Writer, tr: *Trace, ) !void { @@ -600,7 +606,7 @@ fn goto( // knows the graph and baked it in; consult that. if (moduleRoot(str)) |path| { tr.note("`{s}` is a build.zig dependency; resolved from the compiled-in module map", .{str}); - lsp.row(out, path, 0, 0, str); + lsp.row(out, lsp.rel(base, path), 0, 0, str); return; } tr.note("STOP: `{s}` does not resolve to a file. Relative paths, `std` and this", .{str}); @@ -608,8 +614,8 @@ fn goto( tr.note(" OWN internal module names need the build graph we do not run.", .{}); return; }, - .one => |u| lsp.row(out, u.toFsPath(arena) catch return, 0, 0, str), - .many => |us| for (us) |u| lsp.row(out, u.toFsPath(arena) catch continue, 0, 0, str), + .one => |u| lsp.row(out, lsp.rel(base, u.toFsPath(arena) catch return), 0, 0, str), + .many => |us| for (us) |u| lsp.row(out, lsp.rel(base, u.toFsPath(arena) catch continue), 0, 0, str), } return; }, @@ -625,8 +631,8 @@ fn goto( // access can name several and a partial answer beats none. for (decls.items) |d| { switch (kind) { - .declaration => try rowForToken(arena, d.definitionToken(analyser, false) catch continue, out), - .definition => try rowForToken(arena, d.definitionToken(analyser, true) catch continue, out), + .declaration => try rowForToken(arena, base, d.definitionToken(analyser, false) catch continue, out), + .definition => try rowForToken(arena, base, d.definitionToken(analyser, true) catch continue, out), .type_definition => { // peel error unions / pointers / optionals the way ZLS does, // then take the container's own name token @@ -638,14 +644,14 @@ fn goto( try analyser.resolveOptionalUnwrap(r) orelse break; } if (r.typeDefinitionToken()) |th| { - try rowForToken(arena, th, out); + try rowForToken(arena, base, th, out); continue; } } const nd = try d.typeDeclarationNode() orelse continue; const t2 = &nd.handle.tree; const rr = offsets.nodeToRange(t2, nd.node, enc); - const path = nd.handle.uri.toFsPath(arena) catch continue; + const path = lsp.rel(base, nd.handle.uri.toFsPath(arena) catch continue); lsp.spanRow(out, path, rr.start.line, rr.start.character, rr.end.line, rr.end.character, lineAt(t2.source, rr.start.line)); }, } @@ -741,6 +747,8 @@ fn completion( analyser: *Analyser, handle: *DocumentStore.Handle, off: usize, + /// req.root: what the rows' paths are written relative to (lsp.rel) + base: []const u8, /// the buffer as the USER has it, before completionSource repaired it. The /// rows are read out of THIS one; the handle's tree is the repaired copy /// and its text is not what is on screen. @@ -833,20 +841,37 @@ fn completion( lit = lit.resolveDeclLiteralResultType(); if (!(try lit.typeOf(analyser)).eql(try ty.typeOf(analyser))) continue; } + // The WORD, off the decl's own name token. Not the definition token + // below it: `definitionToken` resolves an ALIAS through to what it + // names, so for `pub const base64 = @import("base64.zig")` it lands on + // base64.zig's first token and the "name" would come out as that file's + // doc comment. What goes after the dot is what the container calls it. + const name_tok = d.nameToken(); + if (name_tok >= d.handle.tree.tokens.len) continue; + const name = offsets.tokenToSlice(&d.handle.tree, name_tok); + const th = d.definitionToken(analyser, true) catch continue; const dtree = &th.handle.tree; if (th.token >= dtree.tokens.len) continue; var r = offsets.tokenToRange(dtree, th.token, enc); - const path = th.handle.uri.toFsPath(arena) catch continue; - if (th.handle != handle) { - lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lineAt(dtree.source, r.start.line)); - } else { - if (pad != 0 and r.start.line == dot.line and @as(usize, r.start.character) >= dot.col + pad) { - r.start.character -= @intCast(pad); - if (r.end.line == r.start.line) r.end.character -= @intCast(pad); - } - lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lineAt(orig, r.start.line)); + const path = lsp.rel(base, th.handle.uri.toFsPath(arena) catch continue); + // The name, then its declaration line: "what goes here" before "where + // does it come from", which is the order the question was asked in — + // every other kind here answers a WHERE, this one answers a WHAT and + // the location is the evidence. Two spaces rather than a padded column, + // because the `path:LINE:COL-END` in front of it is already ragged and + // there is nothing to align to. + const text = try std.fmt.allocPrint(arena, "{s} {s}", .{ + name, + std.mem.trim(u8, lineAt(if (th.handle == handle) orig else dtree.source, r.start.line), " \t\r\n"), + }); + if (th.handle == handle and pad != 0 and r.start.line == dot.line and + @as(usize, r.start.character) >= dot.col + pad) + { + r.start.character -= @intCast(pad); + if (r.end.line == r.start.line) r.end.character -= @intCast(pad); } + lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, text); n += 1; } } @@ -943,7 +968,7 @@ fn workspaceSymbols( var tree: Ast = Ast.parse(arena, src, .zig) catch continue; defer tree.deinit(arena); const syms = zls.document_symbol.getDocumentSymbols(arena, &tree, enc) catch continue; - try filterSymbols(gpa, arena, path, syms, "", req.arg, &n, out); + try filterSymbols(gpa, arena, lsp.rel(req.root, path), syms, "", req.arg, &n, out); } } @@ -1006,6 +1031,8 @@ fn references( handle: *DocumentStore.Handle, off: usize, new_name: ?[]const u8, + /// req.root: what the rows' paths are written relative to (lsp.rel) + base: []const u8, out: *std.Io.Writer, ) !void { const tree = &handle.tree; @@ -1022,6 +1049,9 @@ fn references( if (want.len == 0) return; const lines: Lines = try .build(arena, tree.source); + // every row names THIS file (the walk is this file's tokens), so the path + // is spelled once rather than per hit + const path = lsp.rel(base, handle.uri.toFsPath(arena) catch return); var n: usize = 0; for (0..tree.tokens.len) |i| { if (n >= max_rows) return; @@ -1037,7 +1067,7 @@ fn references( try std.fmt.allocPrint(arena, "{s} -> {s} {s}", .{ want, nn, std.mem.trim(u8, lines.line(r.start.line), " \t") }) else lines.line(r.start.line); - lsp.spanRow(out, handle.uri.toFsPath(arena) catch return, r.start.line, r.start.character, r.end.line, r.end.character, text); + lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, text); } } @@ -1164,10 +1194,10 @@ fn workspaceDiagnostics( const src = readFileZ(arena, io, path) catch continue; var tree: Ast = Ast.parse(arena, src, .zig) catch continue; defer tree.deinit(arena); - total += treeDiagnostics(gpa, arena, path, &tree, out) catch continue; + total += treeDiagnostics(gpa, arena, lsp.rel(req.root, path), &tree, out) catch continue; if (total >= max_rows) return; } - if (total == 0) lsp.row(out, req.path, 0, 0, try std.fmt.allocPrint(arena, "no diagnostics in {d} file(s)", .{files.items.len})); + if (total == 0) lsp.row(out, lsp.rel(req.root, req.path), 0, 0, try std.fmt.allocPrint(arena, "no diagnostics in {d} file(s)", .{files.items.len})); } // ----------------------------------------------------------- code actions @@ -1220,10 +1250,11 @@ fn codeActions( /// honest shape of "format" behind a read-only seam, and it is still the /// answer to the question the keypress asks. fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !void { + const path = lsp.rel(req.root, req.path); var tree: Ast = try .parse(arena, req.source, .zig); defer tree.deinit(arena); if (tree.errors.len != 0) { - lsp.row(out, req.path, 0, 0, "cannot format: file does not parse"); + lsp.row(out, path, 0, 0, "cannot format: file does not parse"); return; } var buf: std.ArrayList(u8) = .empty; @@ -1231,7 +1262,7 @@ fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !voi tree.render(arena, &w.writer, .{}) catch return; const formatted = w.written(); if (std.mem.eql(u8, formatted, req.source)) { - lsp.row(out, req.path, 0, 0, "already formatted"); + lsp.row(out, path, 0, 0, "already formatted"); return; } var a = std.mem.splitScalar(u8, req.source, '\n'); @@ -1246,7 +1277,7 @@ fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !voi const ys = y orelse ""; if (std.mem.eql(u8, xs, ys)) continue; n += 1; - lsp.row(out, req.path, ln, 0, try std.fmt.allocPrint(arena, "- {s} + {s}", .{ + lsp.row(out, path, ln, 0, try std.fmt.allocPrint(arena, "- {s} + {s}", .{ std.mem.trim(u8, xs, " \t"), std.mem.trim(u8, ys, " \t"), })); } diff --git a/test/lspbench.zig b/test/lspbench.zig index b294e60c..329e5743 100644 --- a/test/lspbench.zig +++ b/test/lspbench.zig @@ -108,8 +108,10 @@ const anchors = [_]Anchor{ // keypress looks like: the file does not parse and the switch is not in // the tree at all. `verdigris` appears only on the enum member's own // line, so a row carrying it came from the DEFINITION and not from the - // line the cursor sits on. - .{ .file = "test/lspfixture/dotcomplete.zig", .needle = "return switch (s) {\n .", .at = 29, .kind = .completion, .expect = "verdigris" }, + // line the cursor sits on — and it has to appear TWICE, because a + // completion row carries the candidate's own name (the word that would go + // after the dot) before the declaration line it was read off. + .{ .file = "test/lspfixture/dotcomplete.zig", .needle = "return switch (s) {\n .", .at = 29, .kind = .completion, .expect = "verdigris verdigris," }, // ...and the same dot on a line that is ALSO missing its terminator, // which is one parse error repaired or not. This is the probe that // catches a repair chosen by error count rather than by whether the dot diff --git a/test/snapshots/lsp.golden b/test/snapshots/lsp.golden index 48577a7f..759e6cad 100644 --- a/test/snapshots/lsp.golden +++ b/test/snapshots/lsp.golden @@ -197,9 +197,9 @@ | | | /tmp/pardes-snap/lsp/cwd/+Search New Del -| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7-9 std -| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4-9 helper fn helper(x: u32) u32 -| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8-11 main fn main() void +| 1 a.zig:1:7-9 std +| 2 a.zig:3:4-9 helper fn helper(x: u32) u32 +| 3 a.zig:7:8-11 main fn main() void | /tmp/pardes-snap/lsp/cwd/+Hover New Del | 1 fn helper(x: u32) u32 | 2 (fn (u32) u32) @@ -238,9 +238,9 @@ | | | /tmp/pardes-snap/lsp/cwd/+Search New Del -| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7-9 std -| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4-9 helper fn helper(x: u32) u32 -| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8-11 main fn main() void +| 1 a.zig:1:7-9 std +| 2 a.zig:3:4-9 helper fn helper(x: u32) u32 +| 3 a.zig:7:8-11 main fn main() void | /tmp/pardes-snap/lsp/cwd/+Hover New Del | 1 fn helper(x: u32) u32 | 2 (fn (u32) u32) @@ -279,9 +279,9 @@ | | | /tmp/pardes-snap/lsp/cwd/+Search New Del -| 1 /tmp/pardes-snap/lsp/cwd/a.zig:1:7-9 std -| 2 /tmp/pardes-snap/lsp/cwd/a.zig:3:4-9 helper fn helper(x: u32) u32 -| 3 /tmp/pardes-snap/lsp/cwd/a.zig:7:8-11 main fn main() void +| 1 a.zig:1:7-9 std +| 2 a.zig:3:4-9 helper fn helper(x: u32) u32 +| 3 a.zig:7:8-11 main fn main() void | /tmp/pardes-snap/lsp/cwd/+Hover New Del | 1 fn helper(x: u32) u32 | 2 (fn (u32) u32) diff --git a/test/snapshots/lspcomplete.golden b/test/snapshots/lspcomplete.golden index 51c11c66..8a76f824 100644 --- a/test/snapshots/lspcomplete.golden +++ b/test/snapshots/lspcomplete.golden @@ -201,8 +201,8 @@ | | | /tmp/pardes-snap/lspcomplete/cwd/+Search New Del -| 1 /tmp/pardes-snap/lspcomplete/cwd/a.zig:2:5-13 verdigris, -| 2 /tmp/pardes-snap/lspcomplete/cwd/a.zig:3:5-12 cinnabar, +| 1 a.zig:2:5-13 verdigris verdigris, +| 2 a.zig:3:5-12 cinnabar cinnabar, == snap refill grid=120x40 cursor=16,9 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill |^ /tmp/pardes-snap/lspcomplete/cwd/a.zig Save New Del @@ -242,8 +242,8 @@ | | | /tmp/pardes-snap/lspcomplete/cwd/+Search New Del -| 1 /tmp/pardes-snap/lspcomplete/cwd/a.zig:2:5-13 verdigris, -| 2 /tmp/pardes-snap/lspcomplete/cwd/a.zig:3:5-12 cinnabar, +| 1 a.zig:2:5-13 verdigris verdigris, +| 2 a.zig:3:5-12 cinnabar cinnabar, == snap step grid=120x40 cursor=19,3 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/lspcomplete/cwd/a.zig Save New Del @@ -283,8 +283,8 @@ | | | /tmp/pardes-snap/lspcomplete/cwd/+Search New Del -| 1 /tmp/pardes-snap/lspcomplete/cwd/a.zig:2:5-13 verdigris, -| 2 /tmp/pardes-snap/lspcomplete/cwd/a.zig:3:5-12 cinnabar, +| 1 a.zig:2:5-13 verdigris verdigris, +| 2 a.zig:3:5-12 cinnabar cinnabar, == snap txtindent grid=120x40 cursor=15,2 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill |^ /tmp/pardes-snap/lspcomplete/cwd/notes.txt Save New Del @@ -343,8 +343,8 @@ | | | /tmp/pardes-snap/lspcomplete/cwd/+Search New Del -| 1 /tmp/pardes-snap/lspcomplete/cwd/c.zig:2:5-13 verdigris, -| 2 /tmp/pardes-snap/lspcomplete/cwd/c.zig:3:5-12 cinnabar, +| 1 c.zig:2:5-13 verdigris verdigris, +| 2 c.zig:3:5-12 cinnabar cinnabar, == snap nobrace grid=100x18 cursor=16,9 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill |^ /tmp/pardes-snap/lspcomplete/cwd/d.zig Save New Del @@ -362,5 +362,5 @@ | | | /tmp/pardes-snap/lspcomplete/cwd/+Search New Del -| 1 /tmp/pardes-snap/lspcomplete/cwd/d.zig:2:5-13 verdigris, -| 2 /tmp/pardes-snap/lspcomplete/cwd/d.zig:3:5-12 cinnabar, +| 1 d.zig:2:5-13 verdigris verdigris, +| 2 d.zig:3:5-12 cinnabar cinnabar, diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden new file mode 100644 index 00000000..885860b3 --- /dev/null +++ b/test/snapshots/lsprelpath.golden @@ -0,0 +1,245 @@ +== snap absrow grid=100x34 cursor=16,6 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +|^ /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save New Del +| 1 const shade = @import("../shade.zig"); +| 2 +| 3 pub fn pick(s: shade.Shade) u8 { +| 4 return switch (s) { +| 5 . +| 6 }; +| 7 } +| 8 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search New 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, +== snap abslook grid=100x34 cursor=16,6 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save New Del +| 1 const shade = @import("../shade.zig"); +| 2 +| 3 pub fn pick(s: shade.Shade) u8 { +| 4 return switch (s) { +| 5 . +| 6 }; +| 7 } +| 8 +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save New Del +| 1 pub const Shade = enum { +| 2 verdigris, +| 3 cinnabar, +| 4 }; +| 5 +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search New 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, +== style abslook +|0: 0-56 #949494,#121212, 57-99 d,#121212, +|1: 0-1 #ffffff,#626262, 2-79 #949494,#121212, 80-99 d,#121212, +|2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-20 #bdbdbd,#080808, 21-27 #cf87e8,#080808,b 28-28 #bdbdbd,#080808, 29-42 #e3c78a,#080808, 43-44 #bdbdbd,#080808, 45-99 d,#080808, +|3: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|4: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-12 #cf87e8,#080808,b 13-38 #bdbdbd,#080808, 39-99 d,#080808, +|5: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-16 #cf87e8,#080808,b 17-17 #bdbdbd,#080808, 18-23 #cf87e8,#080808,b 24-29 #bdbdbd,#080808, 30-99 d,#080808, +|6: 0-0 d,#444444, 1-1 d,#080808, 2-6 #949494,#121212, 7-15 #bdbdbd,#080808, 16-99 d,#080808, +|7: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-99 d,#080808, +|8: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #bdbdbd,#080808, 8-99 d,#080808, +|9: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|10: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|11: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|12: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|13: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|14: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|15: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|16: 0-1 #ffffff,#262626, 2-79 #949494,#121212, 80-99 d,#121212, +|17: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-24 #bdbdbd,#080808, 25-28 #cf87e8,#080808,b 29-30 #bdbdbd,#080808, 31-99 d,#080808, +|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-19 #080808,#80a0ff, 20-20 #bdbdbd,#080808, 21-99 d,#080808, +|19: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-19 #bdbdbd,#080808, 20-99 d,#080808, +|20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-8 #bdbdbd,#080808, 9-99 d,#080808, +|21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|24: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|25: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|26: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|27: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|28: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|29: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|30: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|31: 0-1 #ffffff,#262626, 2-79 #949494,#121212, 80-99 d,#121212, +|32: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 #080808,#80a0ff, +|33: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-74 #bdbdbd,#080808, 75-99 d,#080808, +== snap subrow grid=100x34 cursor=16,6 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +|^ /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save New Del +| 1 const tint = @import("inner/tint.zig"); +| 2 +| 3 pub fn hue(t: tint.Tint) u8 { +| 4 return switch (t) { +| 5 . +| 6 }; +| 7 } +| 8 +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search New Del +| 1 inner/tint.zig:2:5-9 ochre ochre, +| 2 inner/tint.zig:3:5-10 indigo indigo, +== snap sublook grid=100x34 cursor=16,6 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save New Del +| 1 const tint = @import("inner/tint.zig"); +| 2 +| 3 pub fn hue(t: tint.Tint) u8 { +| 4 return switch (t) { +| 5 . +| 6 }; +| 7 } +| 8 +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save New Del +| 1 pub const Tint = enum { +| 2 ochre, +| 3 indigo, +| 4 }; +| 5 +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search New Del +| 1 inner/tint.zig:2:5-9 ochre ochre, +| 2 inner/tint.zig:3:5-10 indigo indigo, +== style sublook +|0: 0-56 #949494,#121212, 57-99 d,#121212, +|1: 0-1 #ffffff,#626262, 2-79 #949494,#121212, 80-99 d,#121212, +|2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-19 #bdbdbd,#080808, 20-26 #cf87e8,#080808,b 27-27 #bdbdbd,#080808, 28-43 #e3c78a,#080808, 44-45 #bdbdbd,#080808, 46-99 d,#080808, +|3: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|4: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-12 #cf87e8,#080808,b 13-35 #bdbdbd,#080808, 36-99 d,#080808, +|5: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-16 #cf87e8,#080808,b 17-17 #bdbdbd,#080808, 18-23 #cf87e8,#080808,b 24-29 #bdbdbd,#080808, 30-99 d,#080808, +|6: 0-0 d,#444444, 1-1 d,#080808, 2-6 #949494,#121212, 7-15 #bdbdbd,#080808, 16-99 d,#080808, +|7: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-99 d,#080808, +|8: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #bdbdbd,#080808, 8-99 d,#080808, +|9: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|10: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|11: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|12: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|13: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|14: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|15: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|16: 0-1 #ffffff,#262626, 2-79 #949494,#121212, 80-99 d,#121212, +|17: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-23 #bdbdbd,#080808, 24-27 #cf87e8,#080808,b 28-29 #bdbdbd,#080808, 30-99 d,#080808, +|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-15 #080808,#80a0ff, 16-16 #bdbdbd,#080808, 17-99 d,#080808, +|19: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-17 #bdbdbd,#080808, 18-99 d,#080808, +|20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-8 #bdbdbd,#080808, 9-99 d,#080808, +|21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|24: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|25: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|26: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|27: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|28: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|29: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|30: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|31: 0-1 #ffffff,#262626, 2-79 #949494,#121212, 80-99 d,#121212, +|32: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 #080808,#80a0ff, +|33: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-43 #bdbdbd,#080808, 44-99 d,#080808, +== snap clicked grid=100x34 cursor=16,19 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save New Del +| 1 const tint = @import("inner/tint.zig"); +| 2 +| 3 pub fn hue(t: tint.Tint) u8 { +| 4 return switch (t) { +| 5 . +| 6 }; +| 7 } +| 8 +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save New Del +| 1 pub const Tint = enum { +| 2 ochre, +| 3 indigo, +| 4 }; +| 5 +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search New Del +| 1 inner/tint.zig:2:5-9 ochre ochre, +| 2 inner/tint.zig:3:5-10 indigo indigo, diff --git a/test/snapshots/lsprelpath.snap b/test/snapshots/lsprelpath.snap new file mode 100644 index 00000000..dce26d1e --- /dev/null +++ b/test/snapshots/lsprelpath.snap @@ -0,0 +1,87 @@ +# What a language row SPELLS its path as, and that both spellings still LOOK. +# +# A row is written relative to the directory of the file the query was asked +# about (lsp.rel, fed from Req.root) when the answer lives UNDER that directory, +# and keeps its full absolute path when it does not — a `gr` list was otherwise +# the same forty-character prefix repeated down the pane with the part you came +# 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: +# +# 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 +# the case that must NOT become `../shade.zig`. +# SUBDIRECTORY the enum is one level DOWN, so the strip leaves a path with a +# separator still in it — `inner/tint.zig`, not `tint.zig`. A +# strip that took the basename instead would produce a row that +# resolves to nothing. +# +# `n` is the assertion in both, not the row text: it looks row 1 and the right +# file opens with the member SELECTED on its own line. The style capture is +# where that selection shows, because focus goes back to the pane that asked, +# so the opened pane's cursor is not in the snap header. +dirmk sub +dirmk sub/inner +file shade.zig pub const Shade = enum {\n verdigris,\n cinnabar,\n};\n +file sub/inner/tint.zig pub const Tint = enum {\n ochre,\n indigo,\n};\n +file sub/parent.zig const shade = @import("../shade.zig");\n\npub fn pick(s: shade.Shade) u8 {\n return switch (s) {\n \n };\n}\n +file sub/subdir.zig const tint = @import("inner/tint.zig");\n\npub fn hue(t: tint.Tint) u8 {\n return switch (t) {\n \n };\n}\n +# ---- PARENT: outside the asking file's tree, so the row stays absolute ---- +start 34 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 7 +release left 8 7 +stable 400 5000 +key i +key end +text . +stable 400 5000 +key tab +wait 15000 cwd/shade.zig:2: +stable 700 15000 +snap absrow +key esc +settle 100 +# An absolute row has to keep resolving — it is the spelling every row had +# before this, and the one every hit outside the tree still has. +key n +stable 700 15000 +snap abslook +snapstyle abslook +# ---- SUBDIRECTORY: under it, so the strip leaves `inner/tint.zig` ---- +start 34 100 sub/subdir.zig +wait 8000 fn hue +stable 700 20000 +press left 8 7 +release left 8 7 +stable 400 5000 +key i +key end +text . +stable 400 5000 +key tab +wait 15000 inner/tint.zig:2: +stable 700 15000 +snap subrow +key esc +settle 100 +key n +stable 700 15000 +snap sublook +snapstyle sublook +# The mouse spelling, on the OTHER row (grid row 33, 1-based click row 34). A +# right click expands to the file-ish word under the cursor +# (config.wordBounds), which has to take `inner/tint.zig:3:5-10` WHOLE — the +# separator inside a stripped path is a file char, so the scan runs back over +# `inner/` and stops on the space of the line-number prefix. A look focuses +# what it opens, so the header's cursor is the assertion: the end of `indigo` +# on line 3 of tint.zig. +press right 12 34 +release right 12 34 +stable 700 15000 +snap clicked |
