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) try backends[0].query(gpa, arena, req, out); } // 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 { if (end_line != line or end_col <= col) return row(out, path, line, col, text); try out.print("{s}:{d}:{d}-{d} {s}\n", .{ path, line + 1, col + 1, end_col, std.mem.trim(u8, text, " \t\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, edit, put }, expected: []const u8 }{ .{ .kind = .row, .expected = "file:1:3 hi\n" }, .{ .kind = .span, .expected = "file:1:3-5 hi\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"), .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()); } } } }