summaryrefslogtreecommitdiff
path: root/src/lsp/lsp_zls.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/lsp/lsp_zls.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/lsp/lsp_zls.zig')
-rw-r--r--src/lsp/lsp_zls.zig138
1 files changed, 93 insertions, 45 deletions
diff --git a/src/lsp/lsp_zls.zig b/src/lsp/lsp_zls.zig
index fb9804e6..47e6a165 100644
--- a/src/lsp/lsp_zls.zig
+++ b/src/lsp/lsp_zls.zig
@@ -189,47 +189,41 @@ const Trace = struct {
}
};
-/// 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.Io.Writer) void {
+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 {};
+ if (req.kind == .status) return status(arena, req, out);
- // Rows land in bounded scratch storage 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. A query
- // that exceeds the row budget's byte allowance keeps its ordered prefix.
- const scratch_buf = gpa.alloc(u8, max_output_bytes) catch return;
+ const scratch_buf = try gpa.alloc(u8, max_output_bytes);
defer gpa.free(scratch_buf);
var scratch: std.Io.Writer = .fixed(scratch_buf);
var tr: Trace = .{ .on = req.kind == .explain };
const t0 = nowUs();
var err_name: []const u8 = "";
+ var failure: ?anyerror = null;
run(gpa, arena, req, &scratch, &tr) catch |e| {
+ failure = e;
err_name = @errorName(e);
- tr.note("ERROR: {s} — the query threw; the editor shows this as 'no result'", .{err_name});
+ tr.note("ERROR: {s}", .{err_name});
};
const us = nowUs() -| t0;
const rows = std.mem.count(u8, scratch.buffered(), "\n");
record(req, us, rows, err_name);
if (req.kind != .explain) {
- out.writeAll(scratch.buffered()) catch {};
+ if (failure) |err| return err;
+ try out.writeAll(scratch.buffered());
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", .{
+ try 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.written()) catch {};
+ });
+ try out.writeAll(tr.written());
if (hideTime())
- out.print("\n{d} row(s)\n", .{rows}) catch {}
+ try out.print("\n{d} row(s)\n", .{rows})
else
- out.print("\n{d} row(s) in {d}us\n", .{ rows, us }) catch {};
+ try out.print("\n{d} row(s) in {d}us\n", .{ rows, us });
}
fn run(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer, tr: *Trace) !void {
@@ -518,7 +512,7 @@ fn rowForToken(arena: std.mem.Allocator, base: []const u8, th: Analyser.TokenWit
if (th.token >= tree.tokens.len) return;
const r = offsets.tokenToRange(tree, th.token, enc);
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));
+ try lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lineAt(tree.source, r.start.line));
}
// ---------------------------------------------------------------- goto
@@ -627,7 +621,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, lsp.rel(base, path), 0, 0, str);
+ try 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});
@@ -635,8 +629,8 @@ fn goto(
tr.note(" OWN internal module names need the build graph we do not run.", .{});
return;
},
- .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),
+ .one => |u| try lsp.row(out, lsp.rel(base, u.toFsPath(arena) catch return), 0, 0, str),
+ .many => |us| for (us) |u| try lsp.row(out, lsp.rel(base, u.toFsPath(arena) catch continue), 0, 0, str),
}
return;
},
@@ -673,7 +667,7 @@ fn goto(
const t2 = &nd.handle.tree;
const rr = offsets.nodeToRange(t2, nd.node, enc);
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));
+ try lsp.spanRow(out, path, rr.start.line, rr.start.character, rr.end.line, rr.end.character, lineAt(t2.source, rr.start.line));
},
}
}
@@ -973,7 +967,7 @@ fn completion(
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);
+ try lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, text);
n += 1;
}
}
@@ -999,7 +993,7 @@ fn hover(
var it = std.mem.splitScalar(u8, text, '\n');
while (it.next()) |ln| {
if (std.mem.startsWith(u8, ln, "```")) continue;
- out.print("{s}\n", .{std.mem.trimEnd(u8, ln, " \t\r")}) catch return;
+ try out.print("{s}\n", .{std.mem.trimEnd(u8, ln, " \t\r")});
}
}
@@ -1043,7 +1037,7 @@ fn emitSymbols(
try std.fmt.allocPrint(arena, "{s} {s}", .{ name, d })
else
name;
- lsp.spanRow(out, path, s.selectionRange.start.line, s.selectionRange.start.character, s.selectionRange.end.line, s.selectionRange.end.character, text);
+ try lsp.spanRow(out, path, s.selectionRange.start.line, s.selectionRange.start.character, s.selectionRange.end.line, s.selectionRange.end.character, text);
if (s.children) |kids| try emitSymbols(gpa, arena, path, kids, name, n, out);
}
}
@@ -1091,7 +1085,7 @@ fn filterSymbols(
try std.fmt.allocPrint(arena, "{s}.{s}", .{ prefix, s.name });
if (containsIgnoreCase(s.name, needle)) {
n.* += 1;
- lsp.spanRow(out, path, s.selectionRange.start.line, s.selectionRange.start.character, s.selectionRange.end.line, s.selectionRange.end.character, name);
+ try lsp.spanRow(out, path, s.selectionRange.start.line, s.selectionRange.start.character, s.selectionRange.end.line, s.selectionRange.end.character, name);
}
if (s.children) |kids| try filterSymbols(gpa, arena, path, kids, name, needle, n, out);
}
@@ -1155,19 +1149,23 @@ fn references(
const path = if (new_name == null) lsp.rel(base, handle.uri.toFsPath(arena) catch return) else "";
var n: usize = 0;
for (0..tree.tokens.len) |i| {
- if (n >= max_rows) return;
+ if (new_name == null and n >= max_rows) return;
const tok: Ast.TokenIndex = @intCast(i);
if (tree.tokenTag(tok) != .identifier) continue;
if (!std.mem.eql(u8, offsets.identifierTokenToNameSlice(tree, tok), want)) continue;
const at = tree.tokenStart(tok);
- const d = (declAt(arena, analyser, handle, at) catch continue) orelse continue;
+ const d = (declAt(arena, analyser, handle, at) catch |err| {
+ if (new_name != null) return err;
+ continue;
+ }) orelse continue;
if (!d.eql(target)) continue;
+ if (n >= max_rows) return error.TooManyEdits;
n += 1;
if (new_name != null) {
- lsp.edit(out, at, at + want.len);
+ try lsp.edit(out, at, at + want.len);
} else {
const r = offsets.tokenToRange(tree, tok, enc);
- lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lines.?.line(r.start.line));
+ try lsp.spanRow(out, path, r.start.line, r.start.character, r.end.line, r.end.character, lines.?.line(r.start.line));
}
}
}
@@ -1216,7 +1214,7 @@ fn diagnostics(
out: *std.Io.Writer,
) !void {
const n = try treeDiagnostics(gpa, arena, path, tree, out);
- if (n == 0) lsp.row(out, path, 0, 0, "no diagnostics");
+ if (n == 0) try lsp.row(out, path, 0, 0, "no diagnostics");
}
/// `zig ast-check`, in this process. ZLS spawns the compiler for this when it
@@ -1258,7 +1256,7 @@ fn treeDiagnostics(
const at = tree.tokenStart(e.token);
const lc = lsp.lineCol(tree.source, at);
n += 1;
- lsp.row(out, path, lc.line, lc.col, try std.fmt.allocPrint(arena, "error: {s} {s}", .{
+ try lsp.row(out, path, lc.line, lc.col, try std.fmt.allocPrint(arena, "error: {s} {s}", .{
w.buffered(), std.mem.trim(u8, lines.line(lc.line), " \t"),
}));
}
@@ -1274,7 +1272,7 @@ fn treeDiagnostics(
if (em.src_loc == .none) continue;
const sl = bundle.getSourceLocation(em.src_loc);
n += 1;
- lsp.row(out, path, sl.line, sl.column, try std.fmt.allocPrint(arena, "error: {s}", .{
+ try lsp.row(out, path, sl.line, sl.column, try std.fmt.allocPrint(arena, "error: {s}", .{
bundle.nullTerminatedString(em.msg),
}));
}
@@ -1298,10 +1296,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, lsp.rel(req.root, path), &tree, out) catch continue;
+ total += try treeDiagnostics(gpa, arena, lsp.rel(req.root, path), &tree, out);
if (total >= max_rows) return;
}
- 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.len}));
+ if (total == 0) try lsp.row(out, lsp.rel(req.root, req.path), 0, 0, try std.fmt.allocPrint(arena, "no diagnostics in {d} file(s)", .{files.len}));
}
// ----------------------------------------------------------- code actions
@@ -1323,7 +1321,7 @@ fn codeActions(
) !void {
const tree = &handle.tree;
if (tree.errors.len != 0) {
- out.print("no code actions: file does not parse\n", .{}) catch {};
+ try out.print("no code actions: file does not parse\n", .{});
return;
}
var bundle = try astCheck(gpa, "", tree);
@@ -1345,9 +1343,9 @@ fn codeActions(
builder.generateCodeActionsInRange(at) catch {};
for (builder.actions.items[0..@min(builder.actions.items.len, max_rows)]) |a| {
- out.print("{s}\n", .{a.title}) catch return;
+ try out.print("{s}\n", .{a.title});
}
- if (builder.actions.items.len == 0) out.print("no code actions\n", .{}) catch {};
+ if (builder.actions.items.len == 0) try out.print("no code actions\n", .{});
}
// ---------------------------------------------------------------- format
@@ -1362,19 +1360,19 @@ fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !voi
var tree: Ast = try .parse(arena, req.source, .zig);
defer tree.deinit(arena);
if (tree.errors.len != 0) {
- lsp.row(out, path, 0, 0, "cannot format: file does not parse");
+ try lsp.row(out, path, 0, 0, "cannot format: file does not parse");
return;
}
var count_buf: [4096]u8 = undefined;
var counting: std.Io.Writer.Discarding = .init(&count_buf);
- tree.render(arena, &counting.writer, .{}) catch return;
+ try tree.render(arena, &counting.writer, .{});
const size = std.math.cast(usize, counting.fullCount()) orelse return;
const render_buf = try arena.alloc(u8, size);
var w: std.Io.Writer = .fixed(render_buf);
- tree.render(arena, &w, .{}) catch return;
+ try tree.render(arena, &w, .{});
const formatted = w.buffered();
if (std.mem.eql(u8, formatted, req.source)) {
- lsp.row(out, path, 0, 0, "already formatted");
+ try lsp.row(out, path, 0, 0, "already formatted");
return;
}
// one record, spanning only what changed: the common prefix and suffix
@@ -1388,7 +1386,7 @@ fn formatQuery(arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !voi
src_hi -= 1;
fmt_hi -= 1;
}
- lsp.put(out, lo, src_hi, formatted[lo..fmt_hi]);
+ try lsp.put(out, lo, src_hi, formatted[lo..fmt_hi]);
}
// ------------------------------------------------------------------ files
@@ -1426,3 +1424,53 @@ fn collectZigFiles(arena: std.mem.Allocator, io: std.Io, root: []const u8) ![]co
fn readFileZ(arena: std.mem.Allocator, io: std.Io, path: []const u8) ![:0]u8 {
return std.Io.Dir.cwd().readFileAllocOptions(io, path, arena, .limited(4 * 1024 * 1024), .of(u8), 0);
}
+
+test "LSP ZLS format propagates output failure and preserves successful encoding" {
+ const gpa = std.testing.allocator;
+ const req: lsp.Req = .{ .kind = .format, .path = "/file.zig", .source = "const value=1;\n", .offset = 6 };
+ const expected = "@put 11 12 %20=%20\n";
+ var buffer: [128]u8 = undefined;
+ for ([_]usize{ 0, expected.len - 1, expected.len }) |capacity| {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var out: std.Io.Writer = .fixed(buffer[0..capacity]);
+ const result = query(gpa, arena.allocator(), req, &out);
+ if (capacity < expected.len) {
+ try std.testing.expectError(error.WriteFailed, result);
+ } else {
+ try result;
+ try std.testing.expectEqualStrings(expected, out.buffered());
+ }
+ }
+}
+
+test "LSP ZLS rename refuses a partial edit set beyond its row budget" {
+ const gpa = std.testing.allocator;
+ for ([_]usize{ max_rows - 1, max_rows }) |references_count| {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var source: std.Io.Writer.Allocating = .init(gpa);
+ defer source.deinit();
+ try source.writer.writeAll("const value: u32 = 1;\nfn use() void {\n");
+ for (0..references_count) |_| try source.writer.writeAll(" _ = value;\n");
+ try source.writer.writeAll("}\n");
+ const text = try gpa.dupeZ(u8, source.written());
+ defer gpa.free(text);
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ const result = query(gpa, arena.allocator(), .{
+ .kind = .rename,
+ .path = "/rename.zig",
+ .source = text,
+ .offset = 6,
+ .arg = "renamed",
+ }, &out.writer);
+ if (references_count < max_rows) {
+ try result;
+ try std.testing.expectEqual(max_rows, std.mem.count(u8, out.written(), "@edit "));
+ } else {
+ try std.testing.expectError(error.TooManyEdits, result);
+ try std.testing.expectEqual(@as(usize, 0), out.written().len);
+ }
+ }
+}