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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
|
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 ");
}
|