1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
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 {
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());
}
}
}
}
|