summaryrefslogtreecommitdiff
path: root/src/lsp_zls.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/lsp_zls.zig')
-rw-r--r--src/lsp_zls.zig458
1 files changed, 393 insertions, 65 deletions
diff --git a/src/lsp_zls.zig b/src/lsp_zls.zig
index 99d20a10..6f5bbcdd 100644
--- a/src/lsp_zls.zig
+++ b/src/lsp_zls.zig
@@ -23,9 +23,21 @@
//! - `zig_exe_path` is null on purpose: shelling out to the compiler is the
//! thing this backend exists to avoid. `zig_lib_dir` is the build's own
//! (`b.graph.zig_lib_directory`), which is what makes `std` resolvable —
-//! `gd` on `std.mem.count` lands in the real `mem.zig`. The price is that
-//! `@import("builtin")`, `@import("<pkg>")` and `@cImport` resolve to
-//! nothing, silently, because those need a build graph we refuse to run.
+//! `gd` on `std.mem.count` lands in the real `mem.zig`.
+//!
+//! The price is DEPENDENCIES, and it is worth being exact about how far it
+//! goes. ZLS resolves a non-`std`, non-relative import only through a
+//! `BuildFile` whose config came from running `zig build --build-runner`, so
+//! with no compiler it resolves none of them. We paper over ONE case of
+//! that: `gd` on the import string itself (`moduleRoot`, fed by build.zig).
+//! That is a fallback in OUR handler and ZLS cannot see it, so the analyser
+//! still cannot type `vaxis`, and `gd` on `vaxis.init` still finds nothing.
+//! Making member access work means giving ZLS a real build config — either
+//! by letting it run the build runner (a subprocess, and with no cross-query
+//! cache that is once PER KEYPRESS) or by synthesizing a BuildConfig and
+//! reaching into `BuildFile.impl`. Both are real work; neither is smuggled
+//! in here. `@import("builtin")` and `@cImport` are unresolved for the same
+//! reason.
const std = @import("std");
const zls = @import("zls");
const lsp = @import("lsp.zig");
@@ -64,17 +76,142 @@ pub const supports: std.EnumSet(lsp.Kind) = .initMany(&.{
.rename,
.format,
.code_action,
+ .status,
+ .explain,
});
+// ------------------------------------------------------------ introspection
+//
+// Everything below exists because of the contract two lines down: this backend
+// never fails loudly. That is right for an editor — a thrown analyser must not
+// take the process with it — but it means a broken backend and a correct one
+// that found nothing look identical from the outside. So every query leaves a
+// record, and `SPC l i` reads them back.
+
+/// One finished query. Fixed-size and inline: this array is process-global and
+/// lives forever, so it must not own heap.
+const LogEntry = struct {
+ used: bool = false,
+ kind: lsp.Kind = .definition,
+ us: u64 = 0,
+ rows: usize = 0,
+ offset: u32 = 0,
+ /// the error `run` returned, or empty when it returned normally. THIS is
+ /// the thing you cannot see any other way.
+ err: [40]u8 = @splat(0),
+ err_len: u8 = 0,
+ /// basename only — a full path would need most of this struct
+ file: [64]u8 = @splat(0),
+ file_len: u8 = 0,
+
+ fn errName(e: *const LogEntry) []const u8 {
+ return e.err[0..e.err_len];
+ }
+ fn fileName(e: *const LogEntry) []const u8 {
+ return e.file[0..e.file_len];
+ }
+};
+
+const log_cap = 24;
+var log_buf: [log_cap]LogEntry = @splat(.{});
+var log_next: usize = 0;
+var log_total: u64 = 0;
+/// 0.16 has no std.Thread.Mutex; the critical sections here are a memcpy, so
+/// spinning on the lock-free one is enough — the same call gui.zig's pty queue
+/// makes. Only WORKER threads touch this; the core never does.
+var log_mu: std.atomic.Mutex = .unlocked;
+
+fn logLock() void {
+ while (!log_mu.tryLock()) std.atomic.spinLoopHint();
+}
+
+fn record(req: lsp.Req, us: u64, rows: usize, err: []const u8) void {
+ logLock();
+ defer log_mu.unlock();
+ const e = &log_buf[log_next];
+ e.* = .{ .used = true, .kind = req.kind, .us = us, .rows = rows, .offset = req.offset };
+ const base = std.fs.path.basename(req.path);
+ e.file_len = @intCast(@min(base.len, e.file.len));
+ @memcpy(e.file[0..e.file_len], base[0..e.file_len]);
+ e.err_len = @intCast(@min(err.len, e.err.len));
+ @memcpy(e.err[0..e.err_len], err[0..e.err_len]);
+ log_next = (log_next + 1) % log_cap;
+ log_total += 1;
+}
+
+/// A duration cannot go in a snapshot golden — it differs every run. The
+/// snapshot harness sets PARDES_LSP_NOTIME so the two introspection views
+/// print `-` instead, the same way it pins PARDES_DUMP for the dump tests.
+/// Nothing else reads it, and unset (i.e. every real run) shows real numbers.
+fn hideTime() bool {
+ const v = std.c.getenv("PARDES_LSP_NOTIME") orelse return false;
+ return std.mem.span(v).len != 0;
+}
+
+fn nowUs() u64 {
+ var ts: std.c.timespec = undefined;
+ _ = std.c.clock_gettime(.MONOTONIC, &ts);
+ return @as(u64, @intCast(ts.sec)) *| 1_000_000 +| @as(u64, @intCast(ts.nsec)) / 1000;
+}
+
+/// The narration `SPC l w` prints. It is threaded through the REAL resolution
+/// path rather than re-derived beside it: a debug view that reimplements the
+/// logic is a debug view that can disagree with it, and the whole point is to
+/// find out what actually happened.
+const Trace = struct {
+ on: bool = false,
+ arena: std.mem.Allocator,
+ buf: std.ArrayList(u8) = .empty,
+
+ fn note(t: *Trace, comptime fmt: []const u8, args: anytype) void {
+ if (!t.on) return;
+ t.buf.print(t.arena, fmt ++ "\n", args) catch {};
+ }
+};
+
/// The seam's contract: never fail, never panic, no rows is a legal answer.
/// Every error path in here — OOM, a cancelled io, a file that vanished —
/// collapses to "appended nothing", which the core already treats as "no
/// result". There is deliberately no error rendering.
-pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) void {
- run(gpa, arena, req, out) catch {};
+pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) void {
+ // status reads the log; recording it would push a real query out of a
+ // 24-entry ring every time you looked at it.
+ if (req.kind == .status) return status(arena, req, out) catch {};
+
+ // Rows land in a scratch buffer first, for two reasons a plain Writer
+ // cannot serve: the log wants an exact row count, and `explain` throws the
+ // rows away and prints the narration in their place — neither a count nor
+ // a rewind exists on a Writer you do not own.
+ var scratch: std.Io.Writer.Allocating = .init(gpa);
+ defer scratch.deinit();
+
+ var tr: Trace = .{ .arena = arena, .on = req.kind == .explain };
+ const t0 = nowUs();
+ var err_name: []const u8 = "";
+ run(gpa, arena, req, &scratch.writer, &tr) catch |e| {
+ err_name = @errorName(e);
+ tr.note("ERROR: {s} — the query threw; the editor shows this as 'no result'", .{err_name});
+ };
+ const us = nowUs() -| t0;
+ const rows = std.mem.count(u8, scratch.written(), "\n");
+ record(req, us, rows, err_name);
+
+ if (req.kind != .explain) {
+ out.writeAll(scratch.written()) catch {};
+ return;
+ }
+ // the question was never "where is it", it was "what did you do"
+ out.print("lsp explain — the definition query at byte {d} of {s}\n\n", .{
+ req.offset, if (req.path.len == 0) "(no file)" else std.fs.path.basename(req.path),
+ }) catch {};
+ out.writeAll(tr.buf.items) catch {};
+ if (hideTime())
+ out.print("\n{d} row(s)\n", .{rows}) catch {}
+ else
+ out.print("\n{d} row(s) in {d}us\n", .{ rows, us }) catch {};
}
-fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) !void {
+fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer, tr: *Trace) !void {
// The blocking, single-threaded std.Io — the same one look.zig walks
// directories with. ZLS wants an `Io` for every file read; a worker thread
// that is allowed to block wants the one that just does the syscall.
@@ -83,25 +220,35 @@ fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std
// The two workspace kinds are about the PROJECT, not about this buffer, so
// they answer from any pane and never need the analyser, the store or the
// intern pool — a walk and a parse each.
- switch (req.kind) {
+ // `explain` narrates the definition query; everywhere below dispatches on
+ // the effective kind, so the trace follows the code `gd` really runs.
+ const kind: lsp.Kind = if (req.kind == .explain) .definition else req.kind;
+
+ switch (kind) {
.workspace_symbols => return workspaceSymbols(gpa, arena, io, req, out),
.workspace_diagnostics => return workspaceDiagnostics(gpa, arena, io, req, out),
// Everything else is about THIS file, and this backend speaks Zig. The
// core does not gate the keymap by file type (a pane is a pane), so
// the gate is here: `gd` in a README must find nothing rather than
// parse prose as Zig and confidently resolve a word out of it.
- else => if (!std.mem.endsWith(u8, req.path, ".zig")) return,
+ else => if (!std.mem.endsWith(u8, req.path, ".zig")) {
+ tr.note("STOP: not a .zig file ({s}) — this backend only reads Zig", .{
+ if (req.path.len == 0) "no path: this pane has no file behind it" else req.path,
+ });
+ return;
+ },
}
// Pure tree, no analyser.
- if (req.kind == .format) return formatQuery(gpa, arena, req, out);
+ if (kind == .format) return formatQuery(arena, req, out);
var env: std.process.Environ.Map = .init(arena);
var lib_dir: ?std.Build.Cache.Directory = null;
if (zigLibPath()) |p| {
if (std.Io.Dir.cwd().openDir(io, p, .{})) |h| {
lib_dir = .{ .handle = h, .path = p };
- } else |_| {}
- }
+ tr.note("zig lib dir: {s} (open) — `std` imports resolve", .{p});
+ } else |e| tr.note("zig lib dir: {s} FAILED to open ({s}) — nothing in `std` will resolve", .{ p, @errorName(e) });
+ } else tr.note("zig lib dir: NOT SET — nothing in `std` will resolve", .{});
defer if (lib_dir) |d| d.handle.close(io);
var ip: InternPool = try .init(io, gpa);
@@ -131,28 +278,158 @@ fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std
// shell's snapshot, so unsaved edits are what gets analysed.
const uri: Uri = try .fromPath(arena, req.path);
try store.openLspSyncedDocument(uri, req.source);
- const handle = store.getHandle(uri) orelse return;
- if (handle.tree.mode == .zon) return;
+ const handle = store.getHandle(uri) orelse {
+ tr.note("STOP: the document store would not take this buffer", .{});
+ return;
+ };
+ if (handle.tree.mode == .zon) {
+ tr.note("STOP: parsed as .zon, not Zig code — there is nothing to resolve", .{});
+ return;
+ }
+ tr.note("parsed {d} bytes, {d} syntax error(s){s}", .{
+ req.source.len,
+ handle.tree.errors.len,
+ if (handle.tree.errors.len == 0) "" else " — a broken tree resolves badly or not at all",
+ });
var analyser: Analyser = .init(gpa, arena, &store, &ip, handle);
defer analyser.deinit();
const off: usize = @min(req.offset, req.source.len);
- switch (req.kind) {
- .definition, .implementation => try goto(gpa, arena, &analyser, handle, off, .definition, out),
- .declaration => try goto(gpa, arena, &analyser, handle, off, .declaration, out),
- .type_definition => try goto(gpa, arena, &analyser, handle, off, .type_definition, out),
- .hover => try hover(gpa, arena, &analyser, handle, off, out),
+ 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),
+ .hover => try hover(arena, &analyser, handle, off, out),
.document_symbols => try documentSymbols(gpa, arena, req.path, &handle.tree, out),
- .references, .select_refs => try references(gpa, arena, &analyser, handle, off, null, out),
- .rename => try references(gpa, arena, &analyser, handle, off, req.arg, 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),
.code_action => try codeActions(gpa, arena, &analyser, handle, off, out),
else => {},
}
}
+/// `SPC l i`. What the backend IS: which ZLS, which stdlib, what it will and
+/// will not answer, and the last two dozen queries with their timings and —
+/// the point of the whole exercise — the errors that `query` swallowed.
+fn status(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !void {
+ const io = std.Io.Threaded.global_single_threaded.io();
+ try out.print("backend: {s}\nzls: {s} (compiled in — no server process, no JSON-RPC)\n", .{
+ lsp.backend_name, cfg.zls_version,
+ });
+
+ // The single most common cause of "gd does nothing in std": say whether it
+ // is actually openable, not just what it is set to.
+ if (zigLibPath()) |p| {
+ if (std.Io.Dir.cwd().openDir(io, p, .{})) |h| {
+ var d = h;
+ d.close(io);
+ try out.print("zig lib dir: {s} [OK]\n", .{p});
+ } else |e| try out.print("zig lib dir: {s} [CANNOT OPEN: {s}] — nothing in `std` will resolve\n", .{ p, @errorName(e) });
+ } else try out.print("zig lib dir: NOT SET — nothing in `std` will resolve\n", .{});
+ try out.print("offsets: {s} walk caps: {d} files, {d} rows\n", .{ @tagName(enc), max_files, max_rows });
+ // The dependency module map, which is the answer to "why does gd work on
+ // std and not on @import(\"vaxis\")" — if a name is not on this list, the
+ // build did not import it and nothing can resolve it.
+ try out.print("\ndependency imports gd can follow ({d}):\n", .{cfg.module_names.len});
+ for (cfg.module_names, cfg.module_roots) |n, r| {
+ try out.print(" {s:<14} {s}\n", .{ n, r });
+ }
+ try out.print("asked from: {s}\n", .{if (req.path.len == 0) "a pane with no file" else req.path});
+
+ try out.print("\nanswers:", .{});
+ var it = supports.iterator();
+ var n: usize = 0;
+ while (it.next()) |k| : (n += 1) {
+ if (k == .status or k == .explain) continue; // these two are the debugger, not a feature
+ try out.print("{s}{s}", .{ if (n == 0) " " else ", ", @tagName(k) });
+ }
+ try out.print("\nrefuses: ", .{});
+ n = 0;
+ for (std.enums.values(lsp.Kind)) |k| {
+ if (supports.contains(k)) continue;
+ try out.print("{s}{s}", .{ if (n == 0) "" else ", ", @tagName(k) });
+ n += 1;
+ }
+ if (n == 0) try out.print("(nothing)", .{});
+ try out.print("\n", .{});
+
+ // The log. Oldest first, so it reads like a transcript.
+ logLock();
+ defer log_mu.unlock();
+ try out.print("\nlast queries ({d} total, keeping {d}):\n", .{ log_total, log_cap });
+ var shown: usize = 0;
+ for (0..log_cap) |i| {
+ const e = &log_buf[(log_next + i) % log_cap];
+ if (!e.used) continue;
+ shown += 1;
+ if (hideTime()) {
+ try out.print(" {s:<22} {s:<20} @{d:<7} {d:>4} row(s){s}{s}\n", .{
+ @tagName(e.kind), e.fileName(), e.offset, e.rows,
+ if (e.err_len == 0) "" else " ERROR: ", e.errName(),
+ });
+ } else {
+ try out.print(" {s:<22} {s:<20} @{d:<7} {d:>7}us {d:>4} row(s){s}{s}\n", .{
+ @tagName(e.kind), e.fileName(), e.offset, e.us, e.rows,
+ if (e.err_len == 0) "" else " ERROR: ", e.errName(),
+ });
+ }
+ }
+ if (shown == 0) try out.print(" (none yet — press gd somewhere, then ask again)\n", .{});
+ _ = arena;
+}
+
+/// The root source file of a module this build imports, or null.
+///
+/// ZLS answers `@import("std")` from `zig_lib_dir` and a relative `.zig` path
+/// from the filesystem, but ANY OTHER name — every dependency in
+/// build.zig.zon — it can only resolve by running `zig build --build-runner`
+/// to discover the module graph. This backend sets `zig_exe_path = null` on
+/// purpose, so that branch always returned nothing: `gd` worked perfectly into
+/// `std` and silently did nothing on `@import("vaxis")`.
+///
+/// We never needed the compiler for this. build.zig IS the module graph, so it
+/// folds its own `root_mod.import_table` into these two parallel arrays at
+/// configure time. Correct by construction: a dependency added or renamed in
+/// build.zig cannot forget to appear here.
+///
+/// LIMITS, in the order you will hit them:
+/// 1. This resolves the IMPORT STRING only. It is consulted from our own
+/// `goto`, not from inside ZLS, so the analyser still cannot type the
+/// `vaxis` const — `gd` on `vaxis.init` finds nothing. Fixing that means
+/// giving ZLS a BuildConfig; see the note at the top of this file.
+/// 2. These are THIS build's imports. A file inside a dependency importing
+/// that dependency's own internal module name is a miss — that would mean
+/// running ITS build.zig.
+/// 3. A module whose root source is a GENERATED file is absent from the
+/// table: it has no path until make() runs.
+fn moduleRoot(name: []const u8) ?[]const u8 {
+ comptime std.debug.assert(cfg.module_names.len == cfg.module_roots.len);
+ for (cfg.module_names, cfg.module_roots) |n, r| {
+ if (std.mem.eql(u8, n, name)) return r;
+ }
+ return null;
+}
+
+/// Why a field access came back empty. Worth its own function because ONE of
+/// the reasons is a known, explainable hole rather than "no idea": if the left
+/// side starts with a dependency module name, the analyser could not type it
+/// because ZLS has no build config, and no amount of staring at the code will
+/// reveal that.
+fn explainFieldMiss(tr: *Trace, lhs: []const u8) void {
+ var head: usize = 0;
+ while (head < lhs.len and (std.ascii.isAlphanumeric(lhs[head]) or lhs[head] == '_')) head += 1;
+ if (head != 0 and moduleRoot(lhs[0..head]) != null) {
+ tr.note(" `{s}` IS a build.zig dependency, and this is the known hole: the module", .{lhs[0..head]});
+ tr.note(" map is consulted by our @import handler, not by ZLS, so the analyser cannot", .{});
+ tr.note(" type it. `gd` on the import string works; `gd` on a member does not.", .{});
+ return;
+ }
+ tr.note(" (a value whose type needs comptime, an unresolved @import, or genuinely absent)", .{});
+}
+
/// The zig lib directory, which is the one thing this backend cannot work out
/// for itself: it is baked in at build time from `b.graph.zig_lib_directory`,
/// i.e. the exact stdlib pardes was compiled against, and ZIG_LIB_DIR wins if
@@ -202,12 +479,12 @@ 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(gpa: std.mem.Allocator, arena: std.mem.Allocator, th: Analyser.TokenWithHandle, out: *std.ArrayList(u8)) !void {
+fn rowForToken(arena: std.mem.Allocator, 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;
- lsp.row(gpa, out, path, r.start.line, r.start.character, lineAt(tree.source, r.start.line));
+ lsp.row(out, path, r.start.line, r.start.character, lineAt(tree.source, r.start.line));
}
// ---------------------------------------------------------------- goto
@@ -220,32 +497,65 @@ const GotoKind = enum { declaration, definition, type_definition };
/// collapse onto three ZLS kinds (`gi` behaves as `gd`, which is what ZLS's
/// own implementation handler does too).
fn goto(
- gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
kind: GotoKind,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
+ tr: *Trace,
) !void {
const tree = &handle.tree;
const ctx = try Analyser.getPositionContext(arena, tree, off, true);
+ tr.note("position context: .{s}", .{@tagName(ctx)});
var decls: std.ArrayList(Analyser.DeclWithHandle) = .empty;
switch (ctx) {
.var_access, .test_doctest_name => {
- const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
+ const name_loc = offsets.identifierLocFromIndex(tree, off) orelse {
+ tr.note("STOP: no identifier under the cursor", .{});
+ return;
+ };
const name = offsets.locToSlice(tree.source, name_loc);
- if (std.mem.eql(u8, name, "_")) return;
- if (std.zig.isPrimitive(name)) return;
- const d = try analyser.lookupSymbolGlobal(handle, name, off) orelse return;
+ tr.note("identifier: `{s}` — a plain name, so: innermost scope outward", .{name});
+ if (std.mem.eql(u8, name, "_")) {
+ tr.note("STOP: `_` is the discard, it declares nothing", .{});
+ return;
+ }
+ if (std.zig.isPrimitive(name)) {
+ tr.note("STOP: `{s}` is a builtin primitive — it has no declaration in any file", .{name});
+ return;
+ }
+ const d = try analyser.lookupSymbolGlobal(handle, name, off) orelse {
+ tr.note("STOP: lookupSymbolGlobal found no `{s}` in scope here", .{name});
+ return;
+ };
try decls.append(arena, d);
},
.field_access => |loc| {
- const name_loc = offsets.identifierLocFromIndex(tree, off) orelse return;
+ const name_loc = offsets.identifierLocFromIndex(tree, off) orelse {
+ tr.note("STOP: no identifier under the cursor", .{});
+ return;
+ };
const name = offsets.locToSlice(tree.source, name_loc);
const held = offsets.locMerge(loc, name_loc);
- const found = try analyser.getSymbolFieldAccesses(arena, handle, off, held, name) orelse return;
+ tr.note("field access: `{s}` on `{s}` — needs the TYPE of the left side", .{
+ name, offsets.locToSlice(tree.source, loc),
+ });
+ // NOTE both misses: a null result AND an empty one. ZLS returns an
+ // empty slice when it typed the left side but found no such member,
+ // and null when it could not type it at all — from the outside both
+ // are "gd did nothing", so both have to be explained.
+ const found = try analyser.getSymbolFieldAccesses(arena, handle, off, held, name) orelse {
+ tr.note("STOP: could not resolve the left side to a type at all", .{});
+ explainFieldMiss(tr, offsets.locToSlice(tree.source, loc));
+ return;
+ };
+ if (found.len == 0) {
+ tr.note("STOP: the left side resolved, but nothing named `{s}` was found on it", .{name});
+ explainFieldMiss(tr, offsets.locToSlice(tree.source, loc));
+ return;
+ }
try decls.appendSlice(arena, found);
},
.label_access, .label_decl => {
@@ -266,23 +576,43 @@ fn goto(
const loc = ctx.stringLiteralContentLoc(tree.source);
if (loc.start == loc.end) return;
const str = offsets.locToSlice(tree.source, loc);
+ tr.note("import string: \"{s}\"", .{str});
const res = try analyser.store.uriFromImportStr(arena, handle, str);
switch (res) {
- .none => return,
- .one => |u| lsp.row(gpa, out, u.toFsPath(arena) catch return, 0, 0, str),
- .many => |us| for (us) |u| lsp.row(gpa, out, u.toFsPath(arena) catch continue, 0, 0, str),
+ .none => {
+ // ZLS gave up, which for a DEPENDENCY name is not a real
+ // dead end — it just means it wanted the module graph and
+ // we would not start a compiler to get one. Our build.zig
+ // 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);
+ return;
+ }
+ tr.note("STOP: `{s}` does not resolve to a file. Relative paths, `std` and this", .{str});
+ tr.note(" build's own dependencies work; `builtin`, `root` and a dependency's", .{});
+ 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),
}
return;
},
- else => return,
+ else => {
+ tr.note("STOP: nothing to resolve in a .{s} context — `gd` only answers on", .{@tagName(ctx)});
+ tr.note(" an identifier, a field access, a label, an enum literal or an @import string.", .{});
+ return;
+ },
}
+ tr.note("resolved to {d} declaration(s); asking each for its {s} token", .{ decls.items.len, @tagName(kind) });
// One decl that fails to resolve must not swallow the others: a field
// access can name several and a partial answer beats none.
for (decls.items) |d| {
switch (kind) {
- .declaration => try rowForToken(gpa, arena, d.definitionToken(analyser, false) catch continue, out),
- .definition => try rowForToken(gpa, arena, d.definitionToken(analyser, true) catch continue, out),
+ .declaration => try rowForToken(arena, d.definitionToken(analyser, false) catch continue, out),
+ .definition => try rowForToken(arena, 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
@@ -294,7 +624,7 @@ fn goto(
try analyser.resolveOptionalUnwrap(r) orelse break;
}
if (r.typeDefinitionToken()) |th| {
- try rowForToken(gpa, arena, th, out);
+ try rowForToken(arena, th, out);
continue;
}
}
@@ -302,7 +632,7 @@ fn goto(
const t2 = &nd.handle.tree;
const rr = offsets.nodeToRange(t2, nd.node, enc);
const path = nd.handle.uri.toFsPath(arena) catch continue;
- lsp.row(gpa, out, path, rr.start.line, rr.start.character, lineAt(t2.source, rr.start.line));
+ lsp.row(out, path, rr.start.line, rr.start.character, lineAt(t2.source, rr.start.line));
},
}
}
@@ -315,12 +645,11 @@ fn goto(
/// as lines and nothing arms the n/N stepper. Fenced-code markers are dropped
/// because a plain-text pane has nothing to do with them.
fn hover(
- gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
const h = try zls.hover.hover(analyser, arena, handle, off, .markdown, enc) orelse return;
const text = switch (h.contents) {
@@ -330,7 +659,7 @@ fn hover(
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |ln| {
if (std.mem.startsWith(u8, ln, "```")) continue;
- out.print(gpa, "{s}\n", .{std.mem.trimEnd(u8, ln, " \t\r")}) catch return;
+ out.print("{s}\n", .{std.mem.trimEnd(u8, ln, " \t\r")}) catch return;
}
}
@@ -344,7 +673,7 @@ fn documentSymbols(
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
const syms = try zls.document_symbol.getDocumentSymbols(arena, tree, enc);
var n: usize = 0;
@@ -361,7 +690,7 @@ fn emitSymbols(
syms: []const types.DocumentSymbol,
prefix: []const u8,
n: *usize,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
for (syms) |s| {
if (n.* >= max_rows) return;
@@ -374,7 +703,7 @@ fn emitSymbols(
try std.fmt.allocPrint(arena, "{s} {s}", .{ name, d })
else
name;
- lsp.row(gpa, out, path, s.selectionRange.start.line, s.selectionRange.start.character, text);
+ lsp.row(out, path, s.selectionRange.start.line, s.selectionRange.start.character, text);
if (s.children) |kids| try emitSymbols(gpa, arena, path, kids, name, n, out);
}
}
@@ -389,7 +718,7 @@ fn workspaceSymbols(
arena: std.mem.Allocator,
io: std.Io,
req: lsp.Req,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
if (req.arg.len == 0) return;
var files: std.ArrayList([]const u8) = .empty;
@@ -413,7 +742,7 @@ fn filterSymbols(
prefix: []const u8,
needle: []const u8,
n: *usize,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
for (syms) |s| {
if (n.* >= max_rows) return;
@@ -423,7 +752,7 @@ fn filterSymbols(
try std.fmt.allocPrint(arena, "{s}.{s}", .{ prefix, s.name });
if (containsIgnoreCase(s.name, needle)) {
n.* += 1;
- lsp.row(gpa, out, path, s.selectionRange.start.line, s.selectionRange.start.character, name);
+ lsp.row(out, path, s.selectionRange.start.line, s.selectionRange.start.character, name);
}
if (s.children) |kids| try filterSymbols(gpa, arena, path, kids, name, needle, n, out);
}
@@ -459,13 +788,12 @@ fn containsIgnoreCase(hay: []const u8, needle: []const u8) bool {
/// with the replacement. The seam returns rows, not edits, so `SPC r` shows
/// what would change and changes nothing — an honest half of rename.
fn references(
- gpa: std.mem.Allocator,
arena: std.mem.Allocator,
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
new_name: ?[]const u8,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
const tree = &handle.tree;
const target = try declAt(arena, analyser, handle, off) orelse return;
@@ -496,7 +824,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.row(gpa, out, handle.uri.toFsPath(arena) catch return, r.start.line, r.start.character, text);
+ lsp.row(out, handle.uri.toFsPath(arena) catch return, r.start.line, r.start.character, text);
}
}
@@ -541,10 +869,10 @@ fn diagnostics(
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
const n = try treeDiagnostics(gpa, arena, path, tree, out);
- if (n == 0) lsp.row(gpa, out, path, 0, 0, "no diagnostics");
+ if (n == 0) lsp.row(out, path, 0, 0, "no diagnostics");
}
/// `zig ast-check`, in this process. ZLS spawns the compiler for this when it
@@ -569,7 +897,7 @@ fn treeDiagnostics(
arena: std.mem.Allocator,
path: []const u8,
tree: *const Ast,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !usize {
var n: usize = 0;
if (tree.errors.len != 0) {
@@ -582,7 +910,7 @@ fn treeDiagnostics(
const at = tree.tokenStart(e.token);
const lc = lsp.lineCol(tree.source, at);
n += 1;
- lsp.row(gpa, out, path, lc.line, lc.col, try std.fmt.allocPrint(arena, "error: {s} {s}", .{
+ lsp.row(out, path, lc.line, lc.col, try std.fmt.allocPrint(arena, "error: {s} {s}", .{
w.written(), std.mem.trim(u8, lines.line(lc.line), " \t"),
}));
}
@@ -598,7 +926,7 @@ fn treeDiagnostics(
if (em.src_loc == .none) continue;
const sl = bundle.getSourceLocation(em.src_loc);
n += 1;
- lsp.row(gpa, out, path, sl.line, sl.column, try std.fmt.allocPrint(arena, "error: {s}", .{
+ lsp.row(out, path, sl.line, sl.column, try std.fmt.allocPrint(arena, "error: {s}", .{
bundle.nullTerminatedString(em.msg),
}));
}
@@ -614,7 +942,7 @@ fn workspaceDiagnostics(
arena: std.mem.Allocator,
io: std.Io,
req: lsp.Req,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
var files: std.ArrayList([]const u8) = .empty;
try collectZigFiles(arena, io, req.root, &files);
@@ -626,7 +954,7 @@ fn workspaceDiagnostics(
total += treeDiagnostics(gpa, arena, path, &tree, out) catch continue;
if (total >= max_rows) return;
}
- if (total == 0) lsp.row(gpa, 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, req.path, 0, 0, try std.fmt.allocPrint(arena, "no diagnostics in {d} file(s)", .{files.items.len}));
}
// ----------------------------------------------------------- code actions
@@ -644,11 +972,11 @@ fn codeActions(
analyser: *Analyser,
handle: *DocumentStore.Handle,
off: usize,
- out: *std.ArrayList(u8),
+ out: *std.Io.Writer,
) !void {
const tree = &handle.tree;
if (tree.errors.len != 0) {
- out.print(gpa, "no code actions: file does not parse\n", .{}) catch {};
+ out.print("no code actions: file does not parse\n", .{}) catch {};
return;
}
var bundle = try astCheck(gpa, "", tree);
@@ -666,9 +994,9 @@ fn codeActions(
builder.generateCodeActionsInRange(at) catch {};
for (builder.actions.items) |a| {
- out.print(gpa, "{s}\n", .{a.title}) catch return;
+ out.print("{s}\n", .{a.title}) catch return;
}
- if (builder.actions.items.len == 0) out.print(gpa, "no code actions\n", .{}) catch {};
+ if (builder.actions.items.len == 0) out.print("no code actions\n", .{}) catch {};
}
// ---------------------------------------------------------------- format
@@ -678,11 +1006,11 @@ fn codeActions(
/// row per line `zig fmt` would change, in the `+Lsp` buffer. That is the
/// honest shape of "format" behind a read-only seam, and it is still the
/// answer to the question the keypress asks.
-fn formatQuery(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.ArrayList(u8)) !void {
+fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !void {
var tree: Ast = try .parse(arena, req.source, .zig);
defer tree.deinit(arena);
if (tree.errors.len != 0) {
- lsp.row(gpa, out, req.path, 0, 0, "cannot format: file does not parse");
+ lsp.row(out, req.path, 0, 0, "cannot format: file does not parse");
return;
}
var buf: std.ArrayList(u8) = .empty;
@@ -690,7 +1018,7 @@ fn formatQuery(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, o
tree.render(arena, &w.writer, .{}) catch return;
const formatted = w.written();
if (std.mem.eql(u8, formatted, req.source)) {
- lsp.row(gpa, out, req.path, 0, 0, "already formatted");
+ lsp.row(out, req.path, 0, 0, "already formatted");
return;
}
var a = std.mem.splitScalar(u8, req.source, '\n');
@@ -705,7 +1033,7 @@ fn formatQuery(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, o
const ys = y orelse "";
if (std.mem.eql(u8, xs, ys)) continue;
n += 1;
- lsp.row(gpa, out, req.path, ln, 0, try std.fmt.allocPrint(arena, "- {s} + {s}", .{
+ lsp.row(out, req.path, ln, 0, try std.fmt.allocPrint(arena, "- {s} + {s}", .{
std.mem.trim(u8, xs, " \t"), std.mem.trim(u8, ys, " \t"),
}));
}