feat: process textDocument/publishDiagnostics notifications from language server

This commit is contained in:
CJ van den Berg 2024-04-16 23:22:47 +02:00
parent ef61903334
commit cc607089df
4 changed files with 144 additions and 9 deletions

View file

@ -13,8 +13,8 @@ const sp_tag = "child";
const debug_lsp = true;
pub const Error = error{ OutOfMemory, Exit };
pub fn open(a: std.mem.Allocator, cmd: tp.message) Error!Self {
return .{ .a = a, .pid = try Process.create(a, cmd) };
pub fn open(a: std.mem.Allocator, project: []const u8, cmd: tp.message) Error!Self {
return .{ .a = a, .pid = try Process.create(a, project, cmd) };
}
pub fn deinit(self: *Self) void {
@ -59,6 +59,7 @@ const Process = struct {
recv_buf: std.ArrayList(u8),
parent: tp.pid,
tag: [:0]const u8,
project: [:0]const u8,
sp_tag: [:0]const u8,
log_file: ?std.fs.File = null,
next_id: i32 = 0,
@ -66,7 +67,7 @@ const Process = struct {
const Receiver = tp.Receiver(*Process);
pub fn create(a: std.mem.Allocator, cmd: tp.message) Error!tp.pid {
pub fn create(a: std.mem.Allocator, project: []const u8, cmd: tp.message) Error!tp.pid {
var tag: []const u8 = undefined;
if (try cmd.match(.{tp.extract(&tag)})) {
//
@ -87,6 +88,7 @@ const Process = struct {
.recv_buf = std.ArrayList(u8).init(a),
.parent = tp.self_pid().clone(),
.tag = try a.dupeZ(u8, tag),
.project = try a.dupeZ(u8, project),
.requests = std.AutoHashMap(i32, tp.pid).init(a),
.sp_tag = try sp_tag_.toOwnedSliceSentinel(0),
};
@ -323,13 +325,15 @@ const Process = struct {
var msg = std.ArrayList(u8).init(self.a);
defer msg.deinit();
const writer = msg.writer();
try cbor.writeArrayHeader(writer, 6);
try cbor.writeArrayHeader(writer, 7);
try cbor.writeValue(writer, sp_tag);
try cbor.writeValue(writer, self.project);
try cbor.writeValue(writer, self.tag);
try cbor.writeValue(writer, "request");
try cbor.writeValue(writer, method);
try cbor.writeValue(writer, id);
if (params) |p| _ = try writer.write(p) else try cbor.writeValue(writer, null);
try self.parent.send_raw(.{ .buf = msg.items });
}
fn receive_lsp_response(self: *Process, id: i32, result: ?[]const u8, err: ?[]const u8) !void {
@ -362,12 +366,14 @@ const Process = struct {
var msg = std.ArrayList(u8).init(self.a);
defer msg.deinit();
const writer = msg.writer();
try cbor.writeArrayHeader(writer, 5);
try cbor.writeArrayHeader(writer, 6);
try cbor.writeValue(writer, sp_tag);
try cbor.writeValue(writer, self.project);
try cbor.writeValue(writer, self.tag);
try cbor.writeValue(writer, "notify");
try cbor.writeValue(writer, method);
if (params) |p| _ = try writer.write(p) else try cbor.writeValue(writer, null);
try self.parent.send_raw(.{ .buf = msg.items });
}
fn write_log(self: *Process, comptime format: []const u8, args: anytype) void {

View file

@ -87,7 +87,7 @@ pub fn restore_state(self: *Self, data: []const u8) !void {
fn get_lsp(self: *Self, language_server: []const u8) !LSP {
if (self.language_servers.get(language_server)) |lsp| return lsp;
const lsp = try LSP.open(self.a, .{ .buf = language_server });
const lsp = try LSP.open(self.a, self.name, .{ .buf = language_server });
try self.language_servers.put(try self.a.dupe(u8, language_server), lsp);
const uri = try self.make_URI(null);
defer self.a.free(uri);
@ -410,7 +410,7 @@ pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row
}
}
fn navigate_to_location_link(self: *Self, from: tp.pid_ref, location_link: []const u8) !void {
fn navigate_to_location_link(_: *Self, from: tp.pid_ref, location_link: []const u8) !void {
var iter = location_link;
var targetUri: ?[]const u8 = null;
var targetRange: ?Range = null;
@ -437,8 +437,8 @@ fn navigate_to_location_link(self: *Self, from: tp.pid_ref, location_link: []con
}
if (targetUri == null or targetRange == null) return error.InvalidMessageField;
if (!std.mem.eql(u8, targetUri.?[0..7], "file://")) return error.InvalidTargetURI;
const file_path = try std.Uri.unescapeString(self.a, targetUri.?[7..]);
defer self.a.free(file_path);
var file_path_buf: [std.fs.max_path_bytes]u8 = undefined;
const file_path = std.Uri.percentDecodeBackwards(&file_path_buf, targetUri.?[7..]);
if (targetSelectionRange) |sel| {
try from.send(.{ "cmd", "navigate", .{
.file = file_path,
@ -462,6 +462,85 @@ fn navigate_to_location_link(self: *Self, from: tp.pid_ref, location_link: []con
}
}
pub fn publish_diagnostics(self: *Self, to: tp.pid_ref, params_cb: []const u8) !void {
var uri: ?[]const u8 = null;
var diagnostics: []const u8 = &.{};
var iter = params_cb;
var len = try cbor.decodeMapHeader(&iter);
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "uri")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&uri)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "diagnostics")) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&diagnostics)))) return error.InvalidMessageField;
} else {
try cbor.skipValue(&iter);
}
}
if (uri == null) return error.InvalidMessageField;
if (!std.mem.eql(u8, uri.?[0..7], "file://")) return error.InvalidURI;
var file_path_buf: [std.fs.max_path_bytes]u8 = undefined;
const file_path = std.Uri.percentDecodeBackwards(&file_path_buf, uri.?[7..]);
try self.send_clear_diagnostics(to, file_path);
iter = diagnostics;
len = try cbor.decodeArrayHeader(&iter);
while (len > 0) : (len -= 1) {
var diagnostic: []const u8 = undefined;
if (try cbor.matchValue(&iter, cbor.extract_cbor(&diagnostic))) {
try self.send_diagnostic(to, file_path, diagnostic);
} else return error.InvalidMessageField;
}
}
fn send_diagnostic(_: *Self, to: tp.pid_ref, file_path: []const u8, diagnostic: []const u8) !void {
var source: ?[]const u8 = null;
var code: ?[]const u8 = null;
var message: ?[]const u8 = null;
var severity: ?i64 = null;
var range: ?Range = null;
var iter = diagnostic;
var len = try cbor.decodeMapHeader(&iter);
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "source") or std.mem.eql(u8, field_name, "uri")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&source)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "code")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&code)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "message")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&message)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "severity")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&severity)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "range")) {
var range_: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return error.InvalidMessageField;
range = try read_range(range_);
} else {
try cbor.skipValue(&iter);
}
}
if (range == null) return error.InvalidMessageField;
try to.send(.{ "cmd", "add_diagnostic", .{
file_path,
source,
code,
message,
severity,
range.?.start.line,
range.?.start.character,
range.?.end.line,
range.?.end.character,
} });
}
fn send_clear_diagnostics(_: *Self, to: tp.pid_ref, file_path: []const u8) !void {
try to.send(.{ "cmd", "clear_diagnostics", file_path });
}
const Range = struct { start: Position, end: Position };
fn read_range(range: []const u8) !Range {
var iter = range;

View file

@ -158,6 +158,9 @@ const Process = struct {
var query: []const u8 = undefined;
var file_type: []const u8 = undefined;
var language_server: []const u8 = undefined;
var method: []const u8 = undefined;
var id: i32 = 0;
var params_cb: []const u8 = undefined;
var high: i64 = 0;
var low: i64 = 0;
var max: usize = 0;
@ -183,6 +186,10 @@ const Process = struct {
self.loaded(project_directory) catch |e| return from.forward_error(e);
} else if (try m.match(.{ "update_mru", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
self.update_mru(project_directory, path, row, col) catch |e| return from.forward_error(e);
} else if (try m.match(.{ "child", tp.extract(&project_directory), tp.extract(&language_server), "notify", tp.extract(&method), tp.extract_cbor(&params_cb) })) {
self.dispatch_notify(project_directory, language_server, method, params_cb) catch |e| return self.logger.err("notify", e);
} else if (try m.match(.{ "child", tp.extract(&project_directory), tp.extract(&language_server), "request", tp.extract(&method), tp.extract(&id), tp.extract_cbor(&params_cb) })) {
self.dispatch_request(project_directory, language_server, method, id, params_cb) catch |e| return self.logger.err("notify", e);
} else if (try m.match(.{ "open", tp.extract(&project_directory) })) {
self.open(project_directory) catch |e| return from.forward_error(e);
} else if (try m.match(.{ "request_recent_files", tp.extract(&project_directory), tp.extract(&max) })) {
@ -298,6 +305,24 @@ const Process = struct {
return project.update_mru(file_path, row, col) catch |e| tp.exit_error(e);
}
fn dispatch_notify(self: *Process, project_directory: []const u8, language_server: []const u8, method: []const u8, params_cb: []const u8) tp.result {
_ = language_server;
const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project");
return if (std.mem.eql(u8, method, "textDocument/publishDiagnostics"))
project.publish_diagnostics(self.parent.ref(), params_cb) catch |e| tp.exit_error(e)
else
tp.unexpected(.{ .buf = params_cb });
}
fn dispatch_request(self: *Process, project_directory: []const u8, language_server: []const u8, method: []const u8, id: i32, params_cb: []const u8) tp.result {
_ = self;
_ = project_directory;
_ = language_server;
_ = method;
_ = id;
return tp.unexpected(.{ .buf = params_cb });
}
fn persist_projects(self: *Process) void {
var i = self.projects.iterator();
while (i.next()) |p| self.persist_project(p.value_ptr.*) catch {};

View file

@ -3220,6 +3220,31 @@ pub const Editor = struct {
return project_manager.goto_definition(file_path, primary.cursor.row, primary.cursor.col);
}
pub fn clear_diagnostics(self: *Self, _: command.Context) tp.result {
self.logger.print("diag: clear", .{});
}
pub fn add_diagnostic(self: *Self, ctx: command.Context) tp.result {
var file_path: []const u8 = undefined;
var source: []const u8 = undefined;
var code: []const u8 = undefined;
var message: []const u8 = undefined;
var severity: i32 = 0;
var sel: Selection = .{};
if (!try ctx.args.match(.{
tp.extract(&file_path),
tp.extract(&source),
tp.extract(&code),
tp.extract(&message),
tp.extract(&severity),
tp.extract(&sel.begin.row),
tp.extract(&sel.begin.col),
tp.extract(&sel.end.row),
tp.extract(&sel.end.col),
})) return tp.exit_error(error.InvalidArgument);
self.logger.print("diag: {d} {s} {s} {s} {any}", .{ severity, source, code, message, sel });
}
pub fn select(self: *Self, ctx: command.Context) tp.result {
var sel: Selection = .{};
if (!try ctx.args.match(.{ tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) }))