diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-09 09:43:51 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 218d8577cbffb45a6ee80cea52418864a044e452 (patch) | |
| tree | b0b31c1e8091d6dea6fb767883e8c78e48cc385e /src/lsp | |
| parent | 599dd82f96b9d091aae78300aa6c3fbc81f9eb69 (diff) | |
| download | pardes-218d8577cbffb45a6ee80cea52418864a044e452.tar.gz pardes-218d8577cbffb45a6ee80cea52418864a044e452.zip | |
lsp rows: paths relative to the asking file, and the completion text
Diffstat (limited to 'src/lsp')
| -rw-r--r-- | src/lsp/lsp.zig | 29 | ||||
| -rw-r--r-- | src/lsp/lsp_zls.zig | 99 |
2 files changed, 93 insertions, 35 deletions
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"), })); } |
