summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 09:43:51 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit218d8577cbffb45a6ee80cea52418864a044e452 (patch)
treeb0b31c1e8091d6dea6fb767883e8c78e48cc385e
parent599dd82f96b9d091aae78300aa6c3fbc81f9eb69 (diff)
downloadpardes-218d8577cbffb45a6ee80cea52418864a044e452.tar.gz
pardes-218d8577cbffb45a6ee80cea52418864a044e452.zip
lsp rows: paths relative to the asking file, and the completion text
-rw-r--r--docs/lsp.md56
-rw-r--r--src/lsp/lsp.zig29
-rw-r--r--src/lsp/lsp_zls.zig99
-rw-r--r--test/lspbench.zig6
-rw-r--r--test/snapshots/lsp.golden18
-rw-r--r--test/snapshots/lspcomplete.golden20
-rw-r--r--test/snapshots/lsprelpath.golden245
-rw-r--r--test/snapshots/lsprelpath.snap87
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