From cc607089df1c87c8dc1a79c1ab8b75a6467af05d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 16 Apr 2024 23:22:47 +0200 Subject: [PATCH] feat: process textDocument/publishDiagnostics notifications from language server --- src/LSP.zig | 16 +++++--- src/Project.zig | 87 +++++++++++++++++++++++++++++++++++++++-- src/project_manager.zig | 25 ++++++++++++ src/tui/editor.zig | 25 ++++++++++++ 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/src/LSP.zig b/src/LSP.zig index 37f7357..bf294dd 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -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 { diff --git a/src/Project.zig b/src/Project.zig index 46bfe50..3527034 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -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; diff --git a/src/project_manager.zig b/src/project_manager.zig index 64599ae..492caa6 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -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(¶ms_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(¶ms_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 {}; diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 2e3e339..220a43a 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -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) }))