const std = @import("std"); pub const Kind = enum { definition, declaration, type_definition, implementation, references, hover, document_symbols, workspace_symbols, diagnostics, workspace_diagnostics, rename, code_action, format, select_refs, completion, incoming_calls, outgoing_calls, supertypes, subtypes, status, explain, }; pub const Req = struct { kind: Kind, path: []const u8, source: [:0]const u8, offset: u32, arg: []const u8 = "", root: []const u8 = "", }; const backends = if (@import("pardes_config").zls_backend) .{ @import("lsp_zls.zig"), @import("lsp_client.zig") } else .{}; pub const backend_name = if (backends.len > 1) "zls-inproc+lsp-client" else "zls-inproc"; pub const supports: std.EnumSet(Kind) = blk: { var s: std.EnumSet(Kind) = .initEmpty(); for (0..backends.len) |i| s.setUnion(backends[i].supports); break :blk s; }; pub fn speaks(path: []const u8) bool { inline for (backends) |b| if (b.speaks(path)) return true; return false; } pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: Req, out: *std.Io.Writer) !void { if (req.kind == .status) { inline for (backends) |b| try b.query(gpa, arena, req, out); return; } inline for (backends) |b| { if (b.speaks(req.path) and b.supports.contains(req.kind)) return b.query(gpa, arena, req, out); } if (req.kind == .explain and backends.len > 0) return backends[0].query(gpa, arena, req, out); // Nothing asked: said why, where an empty answer would read as nothing // found. inline for (backends) |b| if (b.speaks(req.path)) return none(out, "the language server for this file does not answer it", .{}); try none(out, "no language server for this file", .{}); } /// The one line a backend answers with when it cannot answer at all, and /// why: the core says it rather than "nothing found". pub fn none(out: *std.Io.Writer, comptime fmt: []const u8, args: anytype) std.Io.Writer.Error!void { try out.print("@none " ++ fmt ++ "\n", args); } // Called on server reader threads; the sink must copy text before returning. pub fn setStatusSink(ctx: ?*anyopaque, cb: ?*const fn (ctx: ?*anyopaque, text: []const u8) void) void { if (@import("pardes_config").zls_backend) backends[1].setStatusSink(ctx, cb); } pub fn rel(base: []const u8, path: []const u8) []const u8 { if (base.len == 0) return path; const prefix = std.mem.trimEnd(u8, base, "/"); if (path.len > prefix.len and std.mem.startsWith(u8, path, prefix) and path[prefix.len] == '/') return path[prefix.len + 1 ..]; return path; } pub fn row(out: *std.Io.Writer, path: []const u8, line: usize, col: usize, text: []const u8) std.Io.Writer.Error!void { try out.print("{s}:{d}:{d} {s}\n", .{ path, line + 1, col + 1, std.mem.trim(u8, text, " \t\r\n"), }); } // Input positions are zero-based and end-exclusive; displayed spans are one-based and inclusive. pub fn spanRow(out: *std.Io.Writer, path: []const u8, line: usize, col: usize, end_line: usize, end_col: usize, text: []const u8) std.Io.Writer.Error!void { return sourceSpanRow(out, path, line, col, end_line, end_col, std.mem.trim(u8, text, " \t\r\n")); } /// Source bytes keep their indentation and trailing spaces so byte columns /// still identify the same token in the displayed snippet. pub fn sourceSpanRow(out: *std.Io.Writer, path: []const u8, line: usize, col: usize, end_line: usize, end_col: usize, text: []const u8) std.Io.Writer.Error!void { try out.print("{s}:{d}:{d}", .{ path, line + 1, col + 1 }); if (end_line == line and end_col > col) try out.print("-{d}", .{end_col}); try out.print(" {s}\n", .{std.mem.trimEnd(u8, text, "\r\n")}); } pub fn edit(out: *std.Io.Writer, start: usize, end: usize) std.Io.Writer.Error!void { try out.print("@edit {d} {d}\n", .{ start, end }); } pub fn put(out: *std.Io.Writer, start: usize, end: usize, text: []const u8) std.Io.Writer.Error!void { try out.print("@put {d} {d} ", .{ start, end }); for (text) |c| { if (c == '%' or c < 0x21) try out.print("%{X:0>2}", .{c}) else try out.writeByte(c); } try out.writeByte('\n'); } pub fn lineCol(source: []const u8, offset: usize) struct { line: usize, col: usize } { const upto = source[0..@min(offset, source.len)]; const line = std.mem.count(u8, upto, "\n"); const bol = if (std.mem.lastIndexOfScalar(u8, upto, '\n')) |i| i + 1 else 0; return .{ .line = line, .col = upto.len - bol }; } test "LSP encoders report every insufficient output capacity" { const cases = [_]struct { kind: enum { row, span, source_span, edit, put }, expected: []const u8 }{ .{ .kind = .row, .expected = "file:1:3 hi\n" }, .{ .kind = .span, .expected = "file:1:3-5 hi\n" }, .{ .kind = .source_span, .expected = "file:1:3-5 \t hi \t\n" }, .{ .kind = .edit, .expected = "@edit 1 3\n" }, .{ .kind = .put, .expected = "@put 1 3 hé%20%25%0A\n" }, }; for (cases) |case| { var buf: [128]u8 = undefined; for (0..case.expected.len + 1) |capacity| { var out: std.Io.Writer = .fixed(buf[0..capacity]); const result = switch (case.kind) { .row => row(&out, "file", 0, 2, " hi \n"), .span => spanRow(&out, "file", 0, 2, 0, 5, " hi \n"), .source_span => sourceSpanRow(&out, "file", 0, 2, 0, 5, "\t hi \t\r\n"), .edit => edit(&out, 1, 3), .put => put(&out, 1, 3, "hé %\n"), }; if (capacity < case.expected.len) { try std.testing.expectError(error.WriteFailed, result); } else { try result; try std.testing.expectEqualStrings(case.expected, out.buffered()); } } } } test "a question no backend answers for a file says why rather than find nothing" { if (backends.len == 0) return; var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); var buf: [256]u8 = undefined; var out: std.Io.Writer = .fixed(&buf); // The in-process ZLS answers no call hierarchy for a .zig file. try query(std.testing.allocator, arena.allocator(), .{ .kind = .incoming_calls, .path = "/x/main.zig", .source = "", .offset = 0 }, &out); try std.testing.expectStringStartsWith(out.buffered(), "@none "); }