├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── Header.zig ├── main.zig ├── types.zig └── zig_lsp.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zls-lsp contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-lsp 2 | 3 | Uses https://github.com/zigtools/zig-lsp-codegen to the fullest extent permissible by my smol brain. Make sure to periodically update the autogenerated bindings! 4 | 5 | ## License 6 | 7 | MIT 8 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | _ = b.addModule("zig-lsp", .{ 5 | .root_source_file = .{ .path = "src/zig_lsp.zig" }, 6 | }); 7 | 8 | // Standard target options allows the person running `zig build` to choose 9 | // what target to build for. Here we do not override the defaults, which 10 | // means any target is allowed, and the default is native. Other options 11 | // for restricting supported target set are available. 12 | const target = b.standardTargetOptions(.{}); 13 | 14 | // Standard optimization options allow the person running `zig build` to select 15 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 16 | // set a preferred release mode, allowing the user to decide how to optimize. 17 | const optimize = b.standardOptimizeOption(.{}); 18 | 19 | const exe = b.addExecutable(.{ 20 | .name = "zig-lsp", 21 | // In this case the main source file is merely a path, however, in more 22 | // complicated build scripts, this could be a generated file. 23 | .root_source_file = .{ .path = "src/main.zig" }, 24 | .target = target, 25 | .optimize = optimize, 26 | }); 27 | 28 | // This declares intent for the executable to be installed into the 29 | // standard location when the user invokes the "install" step (the default 30 | // step when running `zig build`). 31 | b.installArtifact(exe); 32 | 33 | // This *creates* a RunStep in the build graph, to be executed when another 34 | // step is evaluated that depends on it. The next line below will establish 35 | // such a dependency. 36 | const run_cmd = b.addRunArtifact(exe); 37 | 38 | // By making the run step depend on the install step, it will be run from the 39 | // installation directory rather than directly from within the cache directory. 40 | // This is not necessary, however, if the application depends on other installed 41 | // files, this ensures they will be present and in the expected location. 42 | run_cmd.step.dependOn(b.getInstallStep()); 43 | 44 | // This allows the user to pass arguments to the application in the build 45 | // command itself, like this: `zig build run -- arg1 arg2 etc` 46 | if (b.args) |args| { 47 | run_cmd.addArgs(args); 48 | } 49 | 50 | // This creates a build step. It will be visible in the `zig build --help` menu, 51 | // and can be selected like this: `zig build run` 52 | // This will evaluate the `run` step rather than the default, which is "install". 53 | const run_step = b.step("run", "Run the app"); 54 | run_step.dependOn(&run_cmd.step); 55 | 56 | // Creates a step for unit testing. 57 | const exe_tests = b.addTest(.{ 58 | .root_source_file = .{ .path = "src/main.zig" }, 59 | .target = target, 60 | .optimize = optimize, 61 | }); 62 | 63 | // Similar to creating the run step earlier, this exposes a `test` step to 64 | // the `zig build --help` menu, providing a way for the user to request 65 | // running the unit tests. 66 | const test_step = b.step("test", "Run unit tests"); 67 | test_step.dependOn(&exe_tests.step); 68 | } 69 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zig-lsp", 3 | .version = "0.1.0", 4 | .dependencies = .{}, 5 | .paths = .{ 6 | "build.zig", 7 | "build.zig.zon", 8 | "src", 9 | "LICENSE", 10 | "README.md", 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /src/Header.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const Header = @This(); 4 | 5 | content_length: usize, 6 | /// null implies "application/vscode-jsonrpc; charset=utf-8" 7 | content_type: ?[]const u8 = null, 8 | 9 | pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { 10 | if (self.content_type) |ct| allocator.free(ct); 11 | } 12 | 13 | pub fn decode(allocator: std.mem.Allocator, reader: anytype) !Header { 14 | var r = Header{ 15 | .content_length = undefined, 16 | .content_type = null, 17 | }; 18 | errdefer r.deinit(allocator); 19 | 20 | var has_content_length = false; 21 | while (true) { 22 | const header = try reader.readUntilDelimiterAlloc(allocator, '\n', 0x100); 23 | defer allocator.free(header); 24 | if (header.len == 0 or header[header.len - 1] != '\r') return error.MissingCarriageReturn; 25 | if (header.len == 1) break; 26 | 27 | const header_name = header[0 .. std.mem.indexOf(u8, header, ": ") orelse return error.MissingColon]; 28 | const header_value = header[header_name.len + 2 .. header.len - 1]; 29 | if (std.mem.eql(u8, header_name, "Content-Length")) { 30 | if (header_value.len == 0) return error.MissingHeaderValue; 31 | r.content_length = std.fmt.parseInt(usize, header_value, 10) catch return error.InvalidContentLength; 32 | has_content_length = true; 33 | } else if (std.mem.eql(u8, header_name, "Content-Type")) { 34 | r.content_type = try allocator.dupe(u8, header_value); 35 | } else { 36 | return error.UnknownHeader; 37 | } 38 | } 39 | if (!has_content_length) return error.MissingContentLength; 40 | 41 | return r; 42 | } 43 | 44 | pub fn encode(header: Header, writer: anytype) @TypeOf(writer).Error!void { 45 | try writer.print("Content-Length: {d}\r\n", .{header.content_length}); 46 | if (header.content_type) |ct| 47 | try writer.print("Content-Length: {s}\r\n", .{ct}); 48 | try writer.writeAll("\r\n"); 49 | } 50 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lsp = @import("zig_lsp.zig"); 3 | const types = lsp.types; 4 | 5 | const Connection = lsp.Connection( 6 | std.fs.File.Reader, 7 | std.fs.File.Writer, 8 | Context, 9 | ); 10 | 11 | pub const Context = struct { 12 | pub const Error = error{}; 13 | 14 | pub fn dataRecv( 15 | _: *Connection, 16 | data: []const u8, 17 | ) !void { 18 | std.log.info("RECV DATA {s}", .{data}); 19 | } 20 | 21 | pub fn dataSend( 22 | _: *Connection, 23 | data: []const u8, 24 | ) !void { 25 | std.log.info("SEND DATA {s}", .{data}); 26 | } 27 | 28 | pub fn lspRecvPre( 29 | _: *Connection, 30 | comptime method: []const u8, 31 | comptime kind: lsp.MessageKind, 32 | id: ?types.RequestId, 33 | payload: lsp.Payload(method, kind), 34 | ) !void { 35 | std.log.info("RECV LSPPRE id {any}: {any} {s} w/ payload type {s}", .{ id, kind, method, @typeName(@TypeOf(payload)) }); 36 | } 37 | 38 | pub fn lspRecvPost( 39 | _: *Connection, 40 | comptime method: []const u8, 41 | comptime kind: lsp.MessageKind, 42 | id: ?types.RequestId, 43 | payload: lsp.Payload(method, kind), 44 | ) !void { 45 | std.log.info("RECV LSPPOST id {any}: {any} {s} w/ payload type {s}", .{ id, kind, method, @typeName(@TypeOf(payload)) }); 46 | } 47 | 48 | pub fn lspSendPre( 49 | _: *Connection, 50 | comptime method: []const u8, 51 | comptime kind: lsp.MessageKind, 52 | id: ?types.RequestId, 53 | payload: lsp.Payload(method, kind), 54 | ) !void { 55 | std.log.info("SEND LSPPRE id {any}: {any} {s} w/ payload type {s}", .{ id, kind, method, @typeName(@TypeOf(payload)) }); 56 | } 57 | 58 | pub fn lspSendPost( 59 | _: *Connection, 60 | comptime method: []const u8, 61 | comptime kind: lsp.MessageKind, 62 | id: ?types.RequestId, 63 | payload: lsp.Payload(method, kind), 64 | ) !void { 65 | std.log.info("SEND LSPPOST id {any}: {any} {s} w/ payload type {s}", .{ id, kind, method, @typeName(@TypeOf(payload)) }); 66 | } 67 | 68 | pub fn @"window/logMessage"(_: *Connection, params: types.LogMessageParams) !void { 69 | const logMessage = std.log.scoped(.logMessage); 70 | switch (params.type) { 71 | .Error => logMessage.err("{s}", .{params.message}), 72 | .Warning => logMessage.warn("{s}", .{params.message}), 73 | .Info => logMessage.info("{s}", .{params.message}), 74 | .Log => logMessage.debug("{s}", .{params.message}), 75 | } 76 | } 77 | 78 | pub fn @"textDocument/publishDiagnostics"(_: *Connection, _: types.PublishDiagnosticsParams) !void {} 79 | pub fn @"client/registerCapability"(_: *Connection, _: types.RequestId, _: types.RegistrationParams) !void { 80 | return error.ServerNotInitialized; 81 | } 82 | }; 83 | 84 | pub fn main() !void { 85 | const allocator = std.heap.page_allocator; 86 | 87 | // var process = std.ChildProcess.init(&.{ "typescript-language-server", "--stdio" }, allocator); 88 | var process = std.ChildProcess.init(&.{ "C:\\Programming\\Zig\\zls\\zig-out\\bin\\zls.exe", "--enable-debug-log" }, allocator); 89 | 90 | process.stdin_behavior = .Pipe; 91 | process.stdout_behavior = .Pipe; 92 | 93 | try process.spawn(); 94 | defer _ = process.kill() catch {}; 95 | 96 | const reader = process.stdout.?.reader(); 97 | const writer = process.stdin.?.writer(); 98 | 99 | var context = Context{}; 100 | var conn = Connection.init( 101 | allocator, 102 | reader, 103 | writer, 104 | &context, 105 | ); 106 | 107 | // const cb = struct { 108 | // pub fn res(_: *Connection, result: lsp.InitializeResult) !void { 109 | // std.log.info("bruh {any}", .{result}); 110 | // } 111 | 112 | // pub fn err(_: *Connection) !void {} 113 | // }; 114 | // _ = .{cb}; 115 | 116 | // try conn.request("initialize", .{ 117 | // .capabilities = .{ 118 | // .textDocument = .{ 119 | // .documentSymbol = .{ 120 | // .hierarchicalDocumentSymbolSupport = true, 121 | // }, 122 | // }, 123 | // .workspace = .{ 124 | // .configuration = true, 125 | // .didChangeConfiguration = .{ 126 | // .dynamicRegistration = true, 127 | // }, 128 | // }, 129 | // }, 130 | // }, .{ .onResponse = cb.res, .onError = cb.err }); 131 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 132 | defer arena.deinit(); 133 | 134 | const arena_allocator = arena.allocator(); 135 | 136 | _ = try conn.requestSync(arena_allocator, "initialize", .{ 137 | .capabilities = .{ 138 | .textDocument = .{ 139 | .documentSymbol = .{ 140 | .hierarchicalDocumentSymbolSupport = true, 141 | }, 142 | }, 143 | .workspace = .{ 144 | .configuration = true, 145 | .didChangeConfiguration = .{ 146 | .dynamicRegistration = true, 147 | }, 148 | }, 149 | }, 150 | }); 151 | try conn.notify("initialized", .{}); 152 | 153 | _ = try conn.requestSync(arena_allocator, "workspace/willRenameFiles", .{ .files = &.{} }); 154 | 155 | // try conn.acceptUntilResponse(); 156 | 157 | // try conn.notify("textDocument/didOpen", .{ 158 | // .textDocument = .{ 159 | // .uri = "file:///file.js", 160 | // .languageId = "js", 161 | // .version = 123, 162 | // .text = 163 | // \\/** 164 | // \\ * @type {number} 165 | // \\ */ 166 | // \\var abc = 123; 167 | // , 168 | // }, 169 | // }); 170 | 171 | // const cb2 = struct { 172 | // pub fn res(_: *Connection, result: connection.RequestResult("textDocument/documentSymbol")) !void { 173 | // std.log.info("bruh {any}", .{result.?.array_of_DocumentSymbol}); 174 | // } 175 | 176 | // pub fn err(_: *Connection) !void {} 177 | // }; 178 | 179 | // try conn.request("textDocument/documentSymbol", .{ 180 | // .textDocument = .{ 181 | // .uri = "file:///file.js", 182 | // }, 183 | // }, .{ 184 | // .onResponse = cb2.res, 185 | // .onError = cb2.err, 186 | // }); 187 | 188 | // try conn.acceptUntilResponse(); 189 | 190 | // try conn.notify("workspace/didChangeConfiguration", .{ 191 | // .settings = .Null, 192 | // }); 193 | 194 | // while (true) { 195 | // try conn.accept(); 196 | // } 197 | 198 | // const cb = struct { 199 | // pub fn res(context: *Context, result: lsp.InitializeResult) !void { 200 | // std.log.info("bruh", .{}); 201 | // _ = context; 202 | // _ = result; 203 | // } 204 | 205 | // pub fn err(context: *Context) !void { 206 | // _ = context; 207 | // } 208 | // }; 209 | 210 | // try conn.request("initialize", .{ 211 | // .capabilities = .{}, 212 | // }, .{ .onResponse = cb.res, .onError = cb.err }); 213 | // try conn.callSuccessCallback(0); 214 | 215 | // while (true) { 216 | // try conn.accept(); 217 | // } 218 | } 219 | 220 | test "simple test" {} 221 | -------------------------------------------------------------------------------- /src/zig_lsp.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Header = @import("Header.zig"); 3 | pub const types = @import("types.zig"); 4 | 5 | const log = std.log.scoped(.zig_lsp); 6 | 7 | pub const MessageKind = enum { request, notification, response }; 8 | pub fn messageKind(method: []const u8) MessageKind { 9 | inline for (types.notification_metadata) |notif| { 10 | if (std.mem.eql(u8, method, notif.method)) return .notification; 11 | } 12 | 13 | inline for (types.request_metadata) |req| { 14 | if (std.mem.eql(u8, method, req.method)) return .request; 15 | } 16 | 17 | @panic("Couldn't find method"); 18 | } 19 | 20 | pub fn Params(comptime method: []const u8) type { 21 | for (types.notification_metadata) |notif| { 22 | if (std.mem.eql(u8, method, notif.method)) return notif.Params orelse void; 23 | } 24 | 25 | for (types.request_metadata) |req| { 26 | if (std.mem.eql(u8, method, req.method)) return req.Params orelse void; 27 | } 28 | 29 | @compileError("Couldn't find params for method named " ++ method); 30 | } 31 | 32 | pub fn Result(comptime method: []const u8) type { 33 | for (types.request_metadata) |req| { 34 | if (std.mem.eql(u8, method, req.method)) return req.Result; 35 | } 36 | 37 | @compileError("Couldn't find result for method named " ++ method); 38 | } 39 | 40 | pub fn Payload(comptime method: []const u8, comptime kind: MessageKind) type { 41 | return switch (kind) { 42 | .request, .notification => Params(method), 43 | .response => Result(method), 44 | }; 45 | } 46 | 47 | const StoredCallback = struct { 48 | method: []const u8, 49 | onResponse: *const fn () void, 50 | onError: *const fn () void, 51 | }; 52 | pub fn RequestCallback( 53 | comptime ConnectionType: type, 54 | comptime method: []const u8, 55 | ) type { 56 | return struct { 57 | const Self = @This(); 58 | 59 | const OnResponse = *const fn (conn: *ConnectionType, result: Result(method)) anyerror!void; 60 | const OnError = *const fn (conn: *ConnectionType, err: types.ResponseError) anyerror!void; 61 | 62 | onResponse: OnResponse, 63 | onError: OnError, 64 | 65 | pub fn store(self: Self) StoredCallback { 66 | return .{ 67 | .method = method, 68 | .onResponse = @ptrCast(self.onResponse), 69 | .onError = @ptrCast(self.onError), 70 | }; 71 | } 72 | 73 | pub fn unstore(stored: StoredCallback) Self { 74 | return .{ 75 | .onResponse = @ptrCast(stored.onResponse), 76 | .onError = @ptrCast(stored.onError), 77 | }; 78 | } 79 | }; 80 | } 81 | 82 | pub fn connection(allocator: std.mem.Allocator, reader: anytype, writer: anytype, context: anytype) Connection(@TypeOf(reader), @TypeOf(writer), @TypeOf(context)) { 83 | return Connection(@TypeOf(reader), @TypeOf(writer), @TypeOf(context)).init(allocator, reader, writer, context); 84 | } 85 | 86 | pub fn Connection( 87 | comptime ReaderType: type, 88 | comptime WriterType: type, 89 | comptime ContextType: type, 90 | ) type { 91 | return struct { 92 | const Self = @This(); 93 | 94 | allocator: std.mem.Allocator, 95 | reader: ReaderType, 96 | writer: WriterType, 97 | context: *ContextType, 98 | 99 | id: usize = 0, 100 | write_buffer: std.ArrayListUnmanaged(u8) = .{}, 101 | 102 | // TODO: Handle string ids 103 | callback_map: std.AutoHashMapUnmanaged(usize, StoredCallback) = .{}, 104 | 105 | _resdata: *anyopaque = undefined, 106 | 107 | pub fn init( 108 | allocator: std.mem.Allocator, 109 | reader: ReaderType, 110 | writer: WriterType, 111 | context: *ContextType, 112 | ) Self { 113 | return .{ 114 | .allocator = allocator, 115 | .reader = reader, 116 | .writer = writer, 117 | .context = context, 118 | }; 119 | } 120 | 121 | pub fn send( 122 | conn: *Self, 123 | value: anytype, 124 | ) !void { 125 | conn.write_buffer.clearRetainingCapacity(); 126 | try std.json.stringify(value, .{}, conn.write_buffer.writer(conn.allocator)); 127 | 128 | if (@hasDecl(ContextType, "dataSend")) try ContextType.dataSend(conn, conn.write_buffer.items); 129 | 130 | try Header.encode(.{ 131 | .content_length = conn.write_buffer.items.len, 132 | }, conn.writer); 133 | try conn.writer.writeAll(conn.write_buffer.items); 134 | } 135 | 136 | pub fn notify( 137 | conn: *Self, 138 | comptime method: []const u8, 139 | params: Params(method), 140 | ) !void { 141 | if (comptime messageKind(method) != .notification) 142 | @compileError("Cannot send request as notification"); 143 | 144 | if (@hasDecl(ContextType, "lspSendPre")) 145 | try ContextType.lspSendPre(conn, method, .notification, null, params); 146 | 147 | try conn.send(.{ 148 | .jsonrpc = "2.0", 149 | .method = method, 150 | .params = params, 151 | }); 152 | 153 | if (@hasDecl(ContextType, "lspSendPost")) 154 | try ContextType.lspSendPost(conn, method, .notification, null, params); 155 | } 156 | 157 | pub fn request( 158 | conn: *Self, 159 | comptime method: []const u8, 160 | params: Params(method), 161 | callback: RequestCallback(Self, method), 162 | ) !void { 163 | if (comptime messageKind(method) != .request) 164 | @compileError("Cannot send notification as request"); 165 | 166 | if (@hasDecl(ContextType, "lspSendPre")) 167 | try ContextType.lspSendPre(conn, method, .request, .{ .integer = @intCast(conn.id) }, params); 168 | 169 | try conn.send(.{ 170 | .jsonrpc = "2.0", 171 | .id = conn.id, 172 | .method = method, 173 | .params = switch (@TypeOf(params)) { 174 | void => .{}, 175 | ?void => null, 176 | else => params, 177 | }, 178 | }); 179 | 180 | try conn.callback_map.put(conn.allocator, conn.id, callback.store()); 181 | 182 | conn.id +%= 1; 183 | 184 | if (@hasDecl(ContextType, "lspSendPost")) 185 | try ContextType.lspSendPost(conn, method, .request, .{ .integer = @intCast(conn.id -% 1) }, params); 186 | } 187 | 188 | pub fn requestSync( 189 | conn: *Self, 190 | arena: std.mem.Allocator, 191 | comptime method: []const u8, 192 | params: Params(method), 193 | ) !Result(method) { 194 | var resdata: Result(method) = undefined; 195 | conn._resdata = &resdata; 196 | 197 | const cb = struct { 198 | pub fn res(conn_: *Self, result: Result(method)) !void { 199 | @as(*Result(method), @ptrCast(@alignCast(conn_._resdata))).* = result; 200 | } 201 | 202 | pub fn err(_: *Self, resperr: types.ResponseError) !void { 203 | return switch (resperr.code) { 204 | @intFromEnum(types.ErrorCodes.ParseError) => error.ParseError, 205 | @intFromEnum(types.ErrorCodes.InvalidRequest) => error.InvalidRequest, 206 | @intFromEnum(types.ErrorCodes.MethodNotFound) => error.MethodNotFound, 207 | @intFromEnum(types.ErrorCodes.InvalidParams) => error.InvalidParams, 208 | @intFromEnum(types.ErrorCodes.InternalError) => error.InternalError, 209 | @intFromEnum(types.ErrorCodes.ServerNotInitialized) => error.ServerNotInitialized, 210 | @intFromEnum(types.ErrorCodes.UnknownErrorCode) => error.UnknownErrorCode, 211 | else => error.InternalError, 212 | }; 213 | } 214 | }; 215 | 216 | try conn.request(method, params, .{ .onResponse = cb.res, .onError = cb.err }); 217 | try conn.acceptUntilResponse(arena); 218 | 219 | return resdata; 220 | } 221 | 222 | pub fn respond( 223 | conn: *Self, 224 | comptime method: []const u8, 225 | id: types.RequestId, 226 | result: Result(method), 227 | ) !void { 228 | if (@hasDecl(ContextType, "lspSendPre")) 229 | try ContextType.lspSendPre(conn, method, .response, id, result); 230 | 231 | try conn.send(.{ 232 | .jsonrpc = "2.0", 233 | .id = id, 234 | .result = if (@TypeOf(result) == ?void) null else result, 235 | }); 236 | 237 | if (@hasDecl(ContextType, "lspSendPost")) 238 | try ContextType.lspSendPost(conn, method, .response, id, result); 239 | } 240 | 241 | pub fn respondError( 242 | conn: *Self, 243 | arena: std.mem.Allocator, 244 | id: types.RequestId, 245 | err: anyerror, 246 | error_return_trace: ?*std.builtin.StackTrace, 247 | ) !void { 248 | const error_code: types.ErrorCodes = switch (err) { 249 | error.ParseError => .ParseError, 250 | error.InvalidRequest => .InvalidRequest, 251 | error.MethodNotFound => .MethodNotFound, 252 | error.InvalidParams => .InvalidParams, 253 | error.InternalError => .InternalError, 254 | 255 | error.ServerNotInitialized => .ServerNotInitialized, 256 | error.UnknownErrorCode => .UnknownErrorCode, 257 | 258 | else => .InternalError, 259 | }; 260 | 261 | log.err("{s}", .{@errorName(err)}); 262 | if (error_return_trace) |trace| { 263 | std.debug.dumpStackTrace(trace.*); 264 | } 265 | 266 | try conn.send(.{ 267 | .jsonrpc = "2.0", 268 | .id = id, 269 | .@"error" = types.ResponseError{ 270 | .code = @intFromEnum(error_code), 271 | .message = if (error_return_trace) |ert| 272 | try std.fmt.allocPrint(arena, "{s}: {any}", .{ @errorName(err), ert }) 273 | else 274 | try std.fmt.allocPrint(arena, "{s}: No error return trace available", .{@errorName(err)}), 275 | }, 276 | }); 277 | } 278 | 279 | pub fn accept(conn: *Self, arena: std.mem.Allocator) !void { 280 | const header = try Header.decode(arena, conn.reader); 281 | 282 | const data = try arena.alloc(u8, header.content_length); 283 | _ = try conn.reader.readAll(data); 284 | 285 | if (@hasDecl(ContextType, "dataRecv")) try ContextType.dataRecv(conn, data); 286 | 287 | var root = try std.json.parseFromSliceLeaky(std.json.Value, arena, data, .{}); 288 | 289 | // There are three cases at this point: 290 | // 1. We have a request (id + method) 291 | // 2. We have a response (id) 292 | // 3. We have a notification (method) 293 | 294 | const maybe_id = root.object.get("id"); 295 | const maybe_method = root.object.get("method"); 296 | 297 | if (maybe_id != null and maybe_method != null) { 298 | const id = try std.json.parseFromValueLeaky(types.RequestId, arena, maybe_id.?, .{}); 299 | const method = maybe_method.?.string; 300 | 301 | inline for (types.request_metadata) |req| { 302 | if (@hasDecl(ContextType, req.method)) { 303 | if (std.mem.eql(u8, req.method, method)) { 304 | @setEvalBranchQuota(100_000); 305 | const value = switch (Params(req.method)) { 306 | void, ?void => {}, 307 | else => try std.json.parseFromValueLeaky( 308 | Params(req.method), 309 | arena, 310 | root.object.get("params").?, 311 | .{ .ignore_unknown_fields = true }, 312 | ), 313 | }; 314 | if (@hasDecl(ContextType, "lspRecvPre")) try ContextType.lspRecvPre(conn, req.method, .request, id, value); 315 | try conn.respond(req.method, id, @field(ContextType, req.method)(conn, id, value) catch |err| { 316 | try conn.respondError(arena, id, err, @errorReturnTrace()); 317 | if (@hasDecl(ContextType, "lspRecvPost")) try ContextType.lspRecvPost(conn, req.method, .request, id, value); 318 | return; 319 | }); 320 | if (@hasDecl(ContextType, "lspRecvPost")) try ContextType.lspRecvPost(conn, req.method, .request, id, value); 321 | return; 322 | } 323 | } 324 | } 325 | 326 | // TODO: Are ids shared between server and client or not? If not, we can remove the line below 327 | conn.id +%= 1; 328 | 329 | log.warn("Request not handled: {s}", .{method}); 330 | try conn.send(.{ 331 | .jsonrpc = "2.0", 332 | .id = id, 333 | .@"error" = .{ .code = -32601, .message = "NotImplemented" }, 334 | }); 335 | } else if (maybe_id) |id_raw| { 336 | @setEvalBranchQuota(100_000); 337 | 338 | // TODO: Handle errors 339 | const id = try std.json.parseFromValueLeaky(types.RequestId, arena, id_raw, .{}); 340 | const iid: usize = @intCast(id.integer); 341 | 342 | const entry = conn.callback_map.fetchRemove(iid) orelse @panic("nothing!"); 343 | inline for (types.request_metadata) |req| { 344 | if (std.mem.eql(u8, req.method, entry.value.method)) { 345 | const value = switch (Result(req.method)) { 346 | void, ?void => {}, 347 | else => try std.json.parseFromValueLeaky( 348 | Result(req.method), 349 | arena, 350 | root.object.get("result") orelse { 351 | const response_error = try std.json.parseFromValueLeaky(types.ResponseError, arena, root.object.get("error").?, .{}); 352 | try (RequestCallback(Self, req.method).unstore(entry.value).onError(conn, response_error)); 353 | return; 354 | }, 355 | .{ .ignore_unknown_fields = true }, 356 | ), 357 | }; 358 | if (@hasDecl(ContextType, "lspRecvPre")) try ContextType.lspRecvPre(conn, req.method, .response, id, value); 359 | try (RequestCallback(Self, req.method).unstore(entry.value).onResponse(conn, value)); 360 | if (@hasDecl(ContextType, "lspRecvPost")) try ContextType.lspRecvPost(conn, req.method, .response, id, value); 361 | return; 362 | } 363 | } 364 | 365 | log.warn("Received unhandled response: {d}", .{iid}); 366 | } else if (maybe_method) |method| { 367 | inline for (types.notification_metadata) |notif| { 368 | if (@hasDecl(ContextType, notif.method)) { 369 | if (std.mem.eql(u8, notif.method, method.string)) { 370 | const value = switch (Params(notif.method)) { 371 | void, ?void => {}, 372 | else => try std.json.parseFromValueLeaky( 373 | Params(notif.method), 374 | arena, 375 | root.object.get("params").?, 376 | .{ .ignore_unknown_fields = true }, 377 | ), 378 | }; 379 | if (@hasDecl(ContextType, "lspRecvPre")) try ContextType.lspRecvPre(conn, notif.method, .notification, null, value); 380 | try @field(ContextType, notif.method)(conn, value); 381 | if (@hasDecl(ContextType, "lspRecvPost")) try ContextType.lspRecvPost(conn, notif.method, .notification, null, value); 382 | return; 383 | } 384 | } 385 | } 386 | 387 | log.warn("Notification not handled: {s}", .{method.string}); 388 | } else { 389 | @panic("Invalid JSON-RPC message."); 390 | } 391 | } 392 | 393 | pub fn acceptUntilResponse(conn: *Self, arena: std.mem.Allocator) !void { 394 | const initial_size = conn.callback_map.size; 395 | while (true) { 396 | try conn.accept(arena); 397 | if (initial_size != conn.callback_map.size) return; 398 | } 399 | } 400 | }; 401 | } 402 | --------------------------------------------------------------------------------