diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /src/lsp/lsp_zls.zig | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-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.zig | 138 |
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); + } + } +} |
