feat: make LSPs fully async

This commit is contained in:
CJ van den Berg 2025-03-24 22:07:38 +01:00
parent 38c170f876
commit 6d58ecb324
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
2 changed files with 293 additions and 130 deletions

View file

@ -15,7 +15,7 @@ const debug_lsp = true;
const OutOfMemoryError = error{OutOfMemory}; const OutOfMemoryError = error{OutOfMemory};
const SendError = error{SendFailed}; const SendError = error{SendFailed};
const CallError = tp.CallError; const SpawnError = error{ThespianSpawnFailed};
pub fn open(allocator: std.mem.Allocator, project: []const u8, cmd: tp.message) (error{ ThespianSpawnFailed, InvalidLspCommand } || cbor.Error)!Self { pub fn open(allocator: std.mem.Allocator, project: []const u8, cmd: tp.message) (error{ ThespianSpawnFailed, InvalidLspCommand } || cbor.Error)!Self {
return .{ .allocator = allocator, .pid = try Process.create(allocator, project, cmd) }; return .{ .allocator = allocator, .pid = try Process.create(allocator, project, cmd) };
@ -31,12 +31,18 @@ pub fn term(self: Self) void {
self.pid.deinit(); self.pid.deinit();
} }
pub fn send_request(self: Self, allocator: std.mem.Allocator, method: []const u8, m: anytype) CallError!tp.message { pub fn send_request(
self: Self,
allocator: std.mem.Allocator,
method: []const u8,
m: anytype,
ctx: anytype,
) (OutOfMemoryError || SpawnError)!void {
var cb = std.ArrayList(u8).init(self.allocator); var cb = std.ArrayList(u8).init(self.allocator);
defer cb.deinit(); defer cb.deinit();
try cbor.writeValue(cb.writer(), m); try cbor.writeValue(cb.writer(), m);
const request_timeout: u64 = @intCast(std.time.ns_per_s * tp.env.get().num("lsp-request-timeout")); const request_timeout: u64 = @intCast(std.time.ns_per_s * tp.env.get().num("lsp-request-timeout"));
return self.pid.call(allocator, request_timeout, .{ "REQ", method, cb.items }); return RequestContext(@TypeOf(ctx)).send(allocator, self.pid.ref(), ctx, request_timeout, tp.message.fmt(.{ "REQ", method, cb.items }));
} }
pub fn send_notification(self: Self, method: []const u8, m: anytype) (OutOfMemoryError || SendError)!void { pub fn send_notification(self: Self, method: []const u8, m: anytype) (OutOfMemoryError || SendError)!void {
@ -54,6 +60,58 @@ pub fn close(self: *Self) void {
self.deinit(); self.deinit();
} }
fn RequestContext(T: type) type {
return struct {
receiver: ReceiverT,
ctx: T,
to: tp.pid,
request: tp.message,
response: ?tp.message,
a: std.mem.Allocator,
const Self = @This();
const ReceiverT = tp.Receiver(*@This());
fn send(a: std.mem.Allocator, to: tp.pid_ref, ctx: T, timeout_ns: u64, request: tp.message) (OutOfMemoryError || SpawnError)!void {
_ = timeout_ns;
const self = try a.create(@This());
self.* = .{
.receiver = undefined,
.ctx = if (@hasDecl(T, "clone")) ctx.clone() else ctx,
.to = to.clone(),
.request = try request.clone(std.heap.c_allocator),
.response = null,
.a = a,
};
self.receiver = ReceiverT.init(receive_, self);
const proc = try tp.spawn_link(a, self, start, @typeName(@This()));
defer proc.deinit();
}
fn deinit(self: *@This()) void {
self.ctx.deinit();
std.heap.c_allocator.free(self.request.buf);
self.to.deinit();
self.a.destroy(self);
}
fn start(self: *@This()) tp.result {
_ = tp.set_trap(true);
if (@hasDecl(T, "link")) try self.ctx.link();
errdefer self.deinit();
try self.to.link();
try self.to.send_raw(self.request);
tp.receive(&self.receiver);
}
fn receive_(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result {
defer self.deinit();
self.ctx.receive(m) catch |e| return tp.exit_error(e, @errorReturnTrace());
return tp.exit_normal();
}
};
}
const Process = struct { const Process = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
cmd: tp.message, cmd: tp.message,

View file

@ -25,7 +25,6 @@ persistent: bool = false,
const Self = @This(); const Self = @This();
const OutOfMemoryError = error{OutOfMemory}; const OutOfMemoryError = error{OutOfMemory};
const CallError = tp.CallError;
const SpawnError = (OutOfMemoryError || error{ThespianSpawnFailed}); const SpawnError = (OutOfMemoryError || error{ThespianSpawnFailed});
pub const InvalidMessageError = error{ InvalidMessage, InvalidMessageField, InvalidTargetURI }; pub const InvalidMessageError = error{ InvalidMessage, InvalidMessageField, InvalidTargetURI };
pub const StartLspError = (error{ ThespianSpawnFailed, Timeout, InvalidLspCommand } || LspError || OutOfMemoryError || cbor.Error); pub const StartLspError = (error{ ThespianSpawnFailed, Timeout, InvalidLspCommand } || LspError || OutOfMemoryError || cbor.Error);
@ -234,11 +233,8 @@ fn get_language_server_instance(self: *Self, language_server: []const u8) StartL
defer self.allocator.free(uri); defer self.allocator.free(uri);
const basename_begin = std.mem.lastIndexOfScalar(u8, self.name, std.fs.path.sep); const basename_begin = std.mem.lastIndexOfScalar(u8, self.name, std.fs.path.sep);
const basename = if (basename_begin) |begin| self.name[begin + 1 ..] else self.name; const basename = if (basename_begin) |begin| self.name[begin + 1 ..] else self.name;
const response = try self.send_lsp_init_request(lsp, self.name, basename, uri);
defer self.allocator.free(response.buf); try self.send_lsp_init_request(lsp, self.name, basename, uri, language_server);
lsp.send_notification("initialized", .{}) catch return error.LspFailed;
if (lsp.pid.expired()) return error.LspFailed;
log.logger("lsp").print("initialized LSP: {s}", .{fmt_lsp_name_func(language_server)});
try self.language_servers.put(try self.allocator.dupe(u8, language_server), lsp); try self.language_servers.put(try self.allocator.dupe(u8, language_server), lsp);
return lsp; return lsp;
} }
@ -620,27 +616,43 @@ fn send_goto_request(self: *Self, from: tp.pid_ref, file_path: []const u8, row:
const lsp = try self.get_language_server(file_path); const lsp = try self.get_language_server(file_path);
const uri = try self.make_URI(file_path); const uri = try self.make_URI(file_path);
defer self.allocator.free(uri); defer self.allocator.free(uri);
const response = lsp.send_request(self.allocator, method, .{
.textDocument = .{ .uri = uri }, const handler: struct {
.position = .{ .line = row, .character = col }, from: tp.pid,
}) catch return error.LspFailed; name: []const u8,
defer self.allocator.free(response.buf);
pub fn deinit(self_: *@This()) void {
std.heap.c_allocator.free(self_.name);
self_.from.deinit();
}
pub fn receive(self_: @This(), response: tp.message) !void {
var link: []const u8 = undefined; var link: []const u8 = undefined;
var locations: []const u8 = undefined; var locations: []const u8 = undefined;
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.array })) { if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.array })) {
if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, .{tp.extract_cbor(&link)} })) { if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, .{tp.extract_cbor(&link)} })) {
try self.navigate_to_location_link(from, link); try navigate_to_location_link(self_.from.ref(), link);
} else if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&locations) })) { } else if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&locations) })) {
try self.send_reference_list(from, locations); try send_reference_list(self_.from.ref(), locations, self_.name);
} }
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) {
return; return;
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&link) })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&link) })) {
try self.navigate_to_location_link(from, link); try navigate_to_location_link(self_.from.ref(), link);
} }
}
} = .{
.from = from.clone(),
.name = try std.heap.c_allocator.dupe(u8, self.name),
};
lsp.send_request(self.allocator, method, .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
}, handler) catch return error.LspFailed;
} }
fn navigate_to_location_link(_: *Self, from: tp.pid_ref, location_link: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void { fn navigate_to_location_link(from: tp.pid_ref, location_link: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = location_link; var iter = location_link;
var targetUri: ?[]const u8 = null; var targetUri: ?[]const u8 = null;
var targetRange: ?Range = null; var targetRange: ?Range = null;
@ -704,21 +716,36 @@ pub fn references(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usi
defer self.allocator.free(uri); defer self.allocator.free(uri);
log.logger("lsp").print("finding references...", .{}); log.logger("lsp").print("finding references...", .{});
const response = lsp.send_request(self.allocator, "textDocument/references", .{ const handler: struct {
.textDocument = .{ .uri = uri }, from: tp.pid,
.position = .{ .line = row, .character = col }, name: []const u8,
.context = .{ .includeDeclaration = true },
}) catch return error.LspFailed; pub fn deinit(self_: *@This()) void {
defer self.allocator.free(response.buf); std.heap.c_allocator.free(self_.name);
self_.from.deinit();
}
pub fn receive(self_: @This(), response: tp.message) !void {
var locations: []const u8 = undefined; var locations: []const u8 = undefined;
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) { if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) {
return; return;
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&locations) })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&locations) })) {
try self.send_reference_list(from, locations); try send_reference_list(self_.from.ref(), locations, self_.name);
} }
}
} = .{
.from = from.clone(),
.name = try std.heap.c_allocator.dupe(u8, self.name),
};
lsp.send_request(self.allocator, "textDocument/references", .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
.context = .{ .includeDeclaration = true },
}, handler) catch return error.LspFailed;
} }
fn send_reference_list(self: *Self, to: tp.pid_ref, locations: []const u8) (ClientError || InvalidMessageError || GetLineOfFileError || cbor.Error)!void { fn send_reference_list(to: tp.pid_ref, locations: []const u8, name: []const u8) (ClientError || InvalidMessageError || GetLineOfFileError || cbor.Error)!void {
defer to.send(.{ "REF", "done" }) catch {}; defer to.send(.{ "REF", "done" }) catch {};
var iter = locations; var iter = locations;
var len = try cbor.decodeArrayHeader(&iter); var len = try cbor.decodeArrayHeader(&iter);
@ -726,13 +753,14 @@ fn send_reference_list(self: *Self, to: tp.pid_ref, locations: []const u8) (Clie
while (len > 0) : (len -= 1) { while (len > 0) : (len -= 1) {
var location: []const u8 = undefined; var location: []const u8 = undefined;
if (try cbor.matchValue(&iter, cbor.extract_cbor(&location))) { if (try cbor.matchValue(&iter, cbor.extract_cbor(&location))) {
try self.send_reference(to, location); try send_reference(to, location, name);
} else return error.InvalidMessageField; } else return error.InvalidMessageField;
} }
log.logger("lsp").print("found {d} references", .{count}); log.logger("lsp").print("found {d} references", .{count});
} }
fn send_reference(self: *Self, to: tp.pid_ref, location: []const u8) (ClientError || InvalidMessageError || GetLineOfFileError || cbor.Error)!void { fn send_reference(to: tp.pid_ref, location: []const u8, name: []const u8) (ClientError || InvalidMessageError || GetLineOfFileError || cbor.Error)!void {
const allocator = std.heap.c_allocator;
var iter = location; var iter = location;
var targetUri: ?[]const u8 = null; var targetUri: ?[]const u8 = null;
var targetRange: ?Range = null; var targetRange: ?Range = null;
@ -767,10 +795,10 @@ fn send_reference(self: *Self, to: tp.pid_ref, location: []const u8) (ClientErro
file_path[i] = '\\'; file_path[i] = '\\';
}; };
} }
const line = try self.get_line_of_file(self.allocator, file_path, targetRange.?.start.line); const line = try get_line_of_file(allocator, file_path, targetRange.?.start.line);
defer self.allocator.free(line); defer allocator.free(line);
const file_path_ = if (file_path.len > self.name.len and std.mem.eql(u8, self.name, file_path[0..self.name.len])) const file_path_ = if (file_path.len > name.len and std.mem.eql(u8, name, file_path[0..name.len]))
file_path[self.name.len + 1 ..] file_path[name.len + 1 ..]
else else
file_path; file_path;
to.send(.{ to.send(.{
@ -788,24 +816,44 @@ pub fn completion(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usi
const lsp = try self.get_language_server(file_path); const lsp = try self.get_language_server(file_path);
const uri = try self.make_URI(file_path); const uri = try self.make_URI(file_path);
defer self.allocator.free(uri); defer self.allocator.free(uri);
const response = lsp.send_request(self.allocator, "textDocument/completion", .{
.textDocument = .{ .uri = uri }, const handler: struct {
.position = .{ .line = row, .character = col }, from: tp.pid,
}) catch return error.LspFailed; file_path: []const u8,
defer self.allocator.free(response.buf); row: usize,
col: usize,
pub fn deinit(self_: *@This()) void {
std.heap.c_allocator.free(self_.file_path);
self_.from.deinit();
}
pub fn receive(self_: @This(), response: tp.message) !void {
var result: []const u8 = undefined; var result: []const u8 = undefined;
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) { if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) {
try send_content_msg_empty(from, "hover", file_path, row, col); try send_content_msg_empty(self_.from.ref(), "hover", self_.file_path, self_.row, self_.col);
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.array })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.array })) {
if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) }))
try self.send_completion_items(from, file_path, row, col, result, false); try send_completion_items(self_.from.ref(), self_.file_path, self_.row, self_.col, result, false);
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.map })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.map })) {
if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) }))
try self.send_completion_list(from, file_path, row, col, result); try send_completion_list(self_.from.ref(), self_.file_path, self_.row, self_.col, result);
} }
}
} = .{
.from = from.clone(),
.file_path = try std.heap.c_allocator.dupe(u8, file_path),
.row = row,
.col = col,
};
lsp.send_request(self.allocator, "textDocument/completion", .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
}, handler) catch return error.LspFailed;
} }
fn send_completion_list(self: *Self, to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, result: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void { fn send_completion_list(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, result: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = result; var iter = result;
var len = cbor.decodeMapHeader(&iter) catch return; var len = cbor.decodeMapHeader(&iter) catch return;
var items: []const u8 = ""; var items: []const u8 = "";
@ -821,20 +869,20 @@ fn send_completion_list(self: *Self, to: tp.pid_ref, file_path: []const u8, row:
try cbor.skipValue(&iter); try cbor.skipValue(&iter);
} }
} }
return self.send_completion_items(to, file_path, row, col, items, is_incomplete) catch error.ClientFailed; return send_completion_items(to, file_path, row, col, items, is_incomplete) catch error.ClientFailed;
} }
fn send_completion_items(self: *Self, to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, items: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void { fn send_completion_items(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, items: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = items; var iter = items;
var len = cbor.decodeArrayHeader(&iter) catch return; var len = cbor.decodeArrayHeader(&iter) catch return;
var item: []const u8 = ""; var item: []const u8 = "";
while (len > 0) : (len -= 1) { while (len > 0) : (len -= 1) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&item)))) return error.InvalidMessageField; if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&item)))) return error.InvalidMessageField;
self.send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete) catch return error.ClientFailed; send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete) catch return error.ClientFailed;
} }
} }
fn send_completion_item(_: *Self, to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, item: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void { fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, item: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void {
var label: []const u8 = ""; var label: []const u8 = "";
var label_detail: []const u8 = ""; var label_detail: []const u8 = "";
var label_description: []const u8 = ""; var label_description: []const u8 = "";
@ -948,22 +996,28 @@ pub fn rename_symbol(self: *Self, from: tp.pid_ref, file_path: []const u8, row:
const lsp = try self.get_language_server(file_path); const lsp = try self.get_language_server(file_path);
const uri = try self.make_URI(file_path); const uri = try self.make_URI(file_path);
defer self.allocator.free(uri); defer self.allocator.free(uri);
const response = lsp.send_request(self.allocator, "textDocument/rename", .{
.textDocument = .{ .uri = uri }, const handler: struct {
.position = .{ .line = row, .character = col }, from: tp.pid,
.newName = "PLACEHOLDER", file_path: []const u8,
}) catch return error.LspFailed;
defer self.allocator.free(response.buf); pub fn deinit(self_: *@This()) void {
std.heap.c_allocator.free(self_.file_path);
self_.from.deinit();
}
pub fn receive(self_: @This(), response: tp.message) !void {
const allocator = std.heap.c_allocator;
var result: []const u8 = undefined; var result: []const u8 = undefined;
// buffer the renames in order to send as a single, atomic message // buffer the renames in order to send as a single, atomic message
var renames = std.ArrayList(Rename).init(self.allocator); var renames = std.ArrayList(Rename).init(allocator);
defer renames.deinit(); defer renames.deinit();
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.map })) { if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.map })) {
if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) { if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) {
try self.decode_rename_symbol_map(result, &renames); try decode_rename_symbol_map(result, &renames);
// write the renames message manually since there doesn't appear to be an array helper // write the renames message manually since there doesn't appear to be an array helper
var msg_buf = std.ArrayList(u8).init(self.allocator); var msg_buf = std.ArrayList(u8).init(allocator);
defer msg_buf.deinit(); defer msg_buf.deinit();
const w = msg_buf.writer(); const w = msg_buf.writer();
try cbor.writeArrayHeader(w, 3); try cbor.writeArrayHeader(w, 3);
@ -980,7 +1034,7 @@ pub fn rename_symbol(self: *Self, from: tp.pid_ref, file_path: []const u8, row:
file_path_[i] = '\\'; file_path_[i] = '\\';
}; };
} }
const line = try self.get_line_of_file(self.allocator, file_path, rename.range.start.line); const line = try get_line_of_file(allocator, self_.file_path, rename.range.start.line);
try cbor.writeValue(w, .{ try cbor.writeValue(w, .{
file_path_, file_path_,
rename.range.start.line, rename.range.start.line,
@ -991,14 +1045,25 @@ pub fn rename_symbol(self: *Self, from: tp.pid_ref, file_path: []const u8, row:
line, line,
}); });
} }
from.send_raw(.{ .buf = msg_buf.items }) catch return error.ClientFailed; self_.from.send_raw(.{ .buf = msg_buf.items }) catch return error.ClientFailed;
} }
} }
}
} = .{
.from = from.clone(),
.file_path = try std.heap.c_allocator.dupe(u8, file_path),
};
lsp.send_request(self.allocator, "textDocument/rename", .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
.newName = "PLACEHOLDER",
}, handler) catch return error.LspFailed;
} }
// decode a WorkspaceEdit record which may have shape {"changes": {}} or {"documentChanges": []} // decode a WorkspaceEdit record which may have shape {"changes": {}} or {"documentChanges": []}
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEdit // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEdit
fn decode_rename_symbol_map(self: *Self, result: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void { fn decode_rename_symbol_map(result: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = result; var iter = result;
var len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage; var len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
var changes: []const u8 = ""; var changes: []const u8 = "";
@ -1007,11 +1072,11 @@ fn decode_rename_symbol_map(self: *Self, result: []const u8, renames: *std.Array
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "changes")) { if (std.mem.eql(u8, field_name, "changes")) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField; if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField;
try self.decode_rename_symbol_changes(changes, renames); try decode_rename_symbol_changes(changes, renames);
return; return;
} else if (std.mem.eql(u8, field_name, "documentChanges")) { } else if (std.mem.eql(u8, field_name, "documentChanges")) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField; if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField;
try self.decode_rename_symbol_doc_changes(changes, renames); try decode_rename_symbol_doc_changes(changes, renames);
return; return;
} else { } else {
try cbor.skipValue(&iter); try cbor.skipValue(&iter);
@ -1020,17 +1085,17 @@ fn decode_rename_symbol_map(self: *Self, result: []const u8, renames: *std.Array
return error.ClientFailed; return error.ClientFailed;
} }
fn decode_rename_symbol_changes(self: *Self, changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void { fn decode_rename_symbol_changes(changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = changes; var iter = changes;
var files_len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage; var files_len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
while (files_len > 0) : (files_len -= 1) { while (files_len > 0) : (files_len -= 1) {
var file_uri: []const u8 = undefined; var file_uri: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &file_uri))) return error.InvalidMessage; if (!(try cbor.matchString(&iter, &file_uri))) return error.InvalidMessage;
try decode_rename_symbol_item(self, file_uri, &iter, renames); try decode_rename_symbol_item(file_uri, &iter, renames);
} }
} }
fn decode_rename_symbol_doc_changes(self: *Self, changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void { fn decode_rename_symbol_doc_changes(changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = changes; var iter = changes;
var changes_len = cbor.decodeArrayHeader(&iter) catch return error.InvalidMessage; var changes_len = cbor.decodeArrayHeader(&iter) catch return error.InvalidMessage;
while (changes_len > 0) : (changes_len -= 1) { while (changes_len > 0) : (changes_len -= 1) {
@ -1050,14 +1115,14 @@ fn decode_rename_symbol_doc_changes(self: *Self, changes: []const u8, renames: *
} }
} else if (std.mem.eql(u8, field_name, "edits")) { } else if (std.mem.eql(u8, field_name, "edits")) {
if (file_uri.len == 0) return error.InvalidMessage; if (file_uri.len == 0) return error.InvalidMessage;
try decode_rename_symbol_item(self, file_uri, &iter, renames); try decode_rename_symbol_item(file_uri, &iter, renames);
} }
} }
} }
} }
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit
fn decode_rename_symbol_item(_: *Self, file_uri: []const u8, iter: *[]const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void { fn decode_rename_symbol_item(file_uri: []const u8, iter: *[]const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var text_edits_len = cbor.decodeArrayHeader(iter) catch return error.InvalidMessage; var text_edits_len = cbor.decodeArrayHeader(iter) catch return error.InvalidMessage;
while (text_edits_len > 0) : (text_edits_len -= 1) { while (text_edits_len > 0) : (text_edits_len -= 1) {
var m_range: ?Range = null; var m_range: ?Range = null;
@ -1088,20 +1153,39 @@ pub fn hover(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, c
defer self.allocator.free(uri); defer self.allocator.free(uri);
// log.logger("lsp").print("fetching hover information...", .{}); // log.logger("lsp").print("fetching hover information...", .{});
const response = lsp.send_request(self.allocator, "textDocument/hover", .{ const handler: struct {
.textDocument = .{ .uri = uri }, from: tp.pid,
.position = .{ .line = row, .character = col }, file_path: []const u8,
}) catch return error.LspFailed; row: usize,
defer self.allocator.free(response.buf); col: usize,
pub fn deinit(self_: *@This()) void {
self_.from.deinit();
std.heap.c_allocator.free(self_.file_path);
}
pub fn receive(self_: @This(), response: tp.message) !void {
var result: []const u8 = undefined; var result: []const u8 = undefined;
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) { if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.null_ })) {
try send_content_msg_empty(from, "hover", file_path, row, col); try send_content_msg_empty(self_.from.ref(), "hover", self_.file_path, self_.row, self_.col);
} else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&result) })) { } else if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.extract_cbor(&result) })) {
try self.send_hover(from, file_path, row, col, result); try send_hover(self_.from.ref(), self_.file_path, self_.row, self_.col, result);
} }
}
} = .{
.from = from.clone(),
.file_path = try std.heap.c_allocator.dupe(u8, file_path),
.row = row,
.col = col,
};
lsp.send_request(self.allocator, "textDocument/hover", .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
}, handler) catch return error.LspFailed;
} }
fn send_hover(self: *Self, to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, result: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void { fn send_hover(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, result: []const u8) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = result; var iter = result;
var len = cbor.decodeMapHeader(&iter) catch return; var len = cbor.decodeMapHeader(&iter) catch return;
var contents: []const u8 = ""; var contents: []const u8 = "";
@ -1120,11 +1204,10 @@ fn send_hover(self: *Self, to: tp.pid_ref, file_path: []const u8, row: usize, co
} }
} }
if (contents.len > 0) if (contents.len > 0)
return self.send_contents(to, "hover", file_path, row, col, contents, range); return send_contents(to, "hover", file_path, row, col, contents, range);
} }
fn send_contents( fn send_contents(
self: *Self,
to: tp.pid_ref, to: tp.pid_ref,
tag: []const u8, tag: []const u8,
file_path: []const u8, file_path: []const u8,
@ -1147,7 +1230,7 @@ fn send_contents(
}; };
if (is_list) { if (is_list) {
var content = std.ArrayList(u8).init(self.allocator); var content = std.ArrayList(u8).init(std.heap.c_allocator);
defer content.deinit(); defer content.deinit();
while (len > 0) : (len -= 1) { while (len > 0) : (len -= 1) {
if (try cbor.matchValue(&iter, cbor.extract(&value))) { if (try cbor.matchValue(&iter, cbor.extract(&value))) {
@ -1364,8 +1447,30 @@ pub fn send_lsp_response(self: *Self, from: tp.pid_ref, cbor_id: []const u8, res
from.send_raw(.{ .buf = cb.items }) catch return error.ClientFailed; from.send_raw(.{ .buf = cb.items }) catch return error.ClientFailed;
} }
fn send_lsp_init_request(self: *Self, lsp: LSP, project_path: []const u8, project_basename: []const u8, project_uri: []const u8) CallError!tp.message { fn send_lsp_init_request(self: *Self, lsp: LSP, project_path: []const u8, project_basename: []const u8, project_uri: []const u8, language_server: []const u8) !void {
return lsp.send_request(self.allocator, "initialize", .{ const handler: struct {
language_server: []const u8,
lsp: LSP,
pub fn deinit(self_: *@This()) void {
self_.lsp.pid.deinit();
std.heap.c_allocator.free(self_.language_server);
}
pub fn receive(self_: @This(), _: tp.message) !void {
self_.lsp.send_notification("initialized", .{}) catch return error.LspFailed;
if (self_.lsp.pid.expired()) return error.LspFailed;
log.logger("lsp").print("initialized LSP: {s}", .{fmt_lsp_name_func(self_.language_server)});
}
} = .{
.language_server = try std.heap.c_allocator.dupe(u8, language_server),
.lsp = .{
.allocator = lsp.allocator,
.pid = lsp.pid.clone(),
},
};
try lsp.send_request(self.allocator, "initialize", .{
.processId = if (builtin.os.tag == .linux) std.os.linux.getpid() else null, .processId = if (builtin.os.tag == .linux) std.os.linux.getpid() else null,
.rootPath = project_path, .rootPath = project_path,
.rootUri = project_uri, .rootUri = project_uri,
@ -1667,7 +1772,7 @@ fn send_lsp_init_request(self: *Self, lsp: LSP, project_path: []const u8, projec
}, },
}, },
}, },
}); }, handler);
} }
fn fmt_lsp_name_func(bytes: []const u8) std.fmt.Formatter(format_lsp_name_func) { fn fmt_lsp_name_func(bytes: []const u8) std.fmt.Formatter(format_lsp_name_func) {
@ -1698,7 +1803,7 @@ const eol = '\n';
pub const GetLineOfFileError = (OutOfMemoryError || std.fs.File.OpenError || std.fs.File.Reader.Error); pub const GetLineOfFileError = (OutOfMemoryError || std.fs.File.OpenError || std.fs.File.Reader.Error);
fn get_line_of_file(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, line_: usize) GetLineOfFileError![]const u8 { fn get_line_of_file(allocator: std.mem.Allocator, file_path: []const u8, line_: usize) GetLineOfFileError![]const u8 {
const line = line_ + 1; const line = line_ + 1;
const file = try std.fs.cwd().openFile(file_path, .{ .mode = .read_only }); const file = try std.fs.cwd().openFile(file_path, .{ .mode = .read_only });
defer file.close(); defer file.close();
@ -1712,13 +1817,13 @@ fn get_line_of_file(self: *Self, allocator: std.mem.Allocator, file_path: []cons
var line_count: usize = 1; var line_count: usize = 1;
for (0..buf.len) |i| { for (0..buf.len) |i| {
if (line_count == line) if (line_count == line)
return self.get_line(allocator, buf[i..]); return get_line(allocator, buf[i..]);
if (buf[i] == eol) line_count += 1; if (buf[i] == eol) line_count += 1;
} }
return allocator.dupe(u8, ""); return allocator.dupe(u8, "");
} }
pub fn get_line(_: *Self, allocator: std.mem.Allocator, buf: []const u8) ![]const u8 { pub fn get_line(allocator: std.mem.Allocator, buf: []const u8) ![]const u8 {
for (0..buf.len) |i| { for (0..buf.len) |i| {
if (buf[i] == eol) return allocator.dupe(u8, buf[0..i]); if (buf[i] == eol) return allocator.dupe(u8, buf[0..i]);
} }