From d030dd09930b89d69fda46ff33d15354beef9140 Mon Sep 17 00:00:00 2001 From: Techatrix <19954306+Techatrix@users.noreply.github.com> Date: Fri, 30 Sep 2022 06:04:27 +0200 Subject: [PATCH] improve conformance to the lsp (#687) --- src/Server.zig | 109 +++++++++++++++++++++++++++++++++++++++---------- src/main.zig | 2 +- src/types.zig | 91 ++++++++++++++++++++++++++--------------- 3 files changed, 147 insertions(+), 55 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 2834aa1..b1aea5f 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -33,7 +33,16 @@ document_store: DocumentStore = undefined, builtin_completions: std.ArrayListUnmanaged(types.CompletionItem), client_capabilities: ClientCapabilities = .{}, offset_encoding: offsets.Encoding = .utf16, -keep_running: bool = true, +status: enum { + /// the server has not received a `initialize` request + uninitialized, + /// the server has recieved a `initialize` request and is awaiting the `initialized` notification + initializing, + /// the server has been initialized and is ready to received requests + initialized, + /// the server has been shutdown and can't handle any more requests + shutdown, +}, // Code was based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig @@ -87,6 +96,16 @@ fn send(writer: anytype, allocator: std.mem.Allocator, reqOrRes: anytype) !void try writer.writeAll(arr.items); } +pub fn sendErrorResponse(writer: anytype, allocator: std.mem.Allocator, code: types.ErrorCodes, message: []const u8) !void { + try send(writer, allocator, .{ + .@"error" = types.ResponseError{ + .code = @enumToInt(code), + .message = message, + .data = .Null, + }, + }); +} + fn respondGeneric(writer: anytype, id: types.RequestId, response: []const u8) !void { var buffered_writer = std.io.bufferedWriter(writer); const buf_writer = buffered_writer.writer(); @@ -105,7 +124,6 @@ fn respondGeneric(writer: anytype, id: types.RequestId, response: []const u8) !v break :blk digits; }, .String => |str_val| str_val.len + 2, - else => unreachable, }; // Numbers of character that will be printed from this string: len - 1 brackets @@ -115,7 +133,6 @@ fn respondGeneric(writer: anytype, id: types.RequestId, response: []const u8) !v switch (id) { .Integer => |int| try buf_writer.print("{}", .{int}), .String => |str| try buf_writer.print("\"{s}\"", .{str}), - else => unreachable, } try buf_writer.writeAll(response); @@ -1472,12 +1489,12 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: .id = id, .result = .{ .InitializeResult = .{ - .offsetEncoding = server.offset_encoding, .serverInfo = .{ .name = "zls", .version = "0.1.0", }, .capabilities = .{ + .positionEncoding = server.offset_encoding, .signatureHelpProvider = .{ .triggerCharacters = &.{"("}, .retriggerCharacters = &.{","}, @@ -1540,6 +1557,8 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: }, }); + server.status = .initializing; + if (req.params.capabilities.workspace) |workspace| { server.client_capabilities.supports_configuration = workspace.configuration.value; if (workspace.didChangeConfiguration != null and workspace.didChangeConfiguration.?.dynamicRegistration.value) { @@ -1547,7 +1566,7 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: } } - log.info("zls initialized", .{}); + log.info("zls initializing", .{}); log.info("{}", .{server.client_capabilities}); log.info("Using offset encoding: {s}", .{std.meta.tagName(server.offset_encoding)}); @@ -1566,6 +1585,47 @@ fn initializeHandler(server: *Server, writer: anytype, id: types.RequestId, req: } } +fn initializedHandler(server: *Server, writer: anytype, id: types.RequestId) !void { + _ = id; + + if (server.status != .initializing) { + std.log.warn("received a initialized notification but the server has not send a initialize request!", .{}); + } + + server.status = .initialized; + + if (server.client_capabilities.supports_configuration) + try server.requestConfiguration(writer); +} + +fn shutdownHandler(server: *Server, writer: anytype, id: types.RequestId) !void { + if (server.status != .initialized) { + return try sendErrorResponse( + writer, + server.arena.allocator(), + types.ErrorCodes.InvalidRequest, + "received a shutdown request but the server is not initialized!", + ); + } + + // Technically we should deinitialize first and send possible errors to the client + return try respondGeneric(writer, id, null_result_response); +} + +fn exitHandler(server: *Server, writer: anytype, id: types.RequestId) noreturn { + _ = writer; + _ = id; + log.info("Server exiting...", .{}); + // Technically we should deinitialize first and send possible errors to the client + + const error_code: u8 = switch (server.status) { + .uninitialized, .shutdown => 0, + else => 1, + }; + + std.os.exit(error_code); +} + fn registerCapability(server: *Server, writer: anytype, method: []const u8) !void { // NOTE: stage1 moment occurs if we dont do it like this :( // long live stage2's not broken anon structs @@ -1618,21 +1678,6 @@ fn requestConfiguration(server: *Server, writer: anytype) !void { }); } -fn initializedHandler(server: *Server, writer: anytype, id: types.RequestId) !void { - _ = id; - - if (server.client_capabilities.supports_configuration) - try server.requestConfiguration(writer); -} - -fn shutdownHandler(server: *Server, writer: anytype, id: types.RequestId) !void { - log.info("Server closing...", .{}); - - server.keep_running = false; - // Technically we should deinitialize first and send possible errors to the client - try respondGeneric(writer, id, null_result_response); -} - fn openDocumentHandler(server: *Server, writer: anytype, id: types.RequestId, req: requests.OpenDocument) !void { const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); @@ -2266,7 +2311,7 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void defer tree.deinit(); const id = if (tree.root.Object.get("id")) |id| switch (id) { - .Integer => |int| types.RequestId{ .Integer = int }, + .Integer => |int| types.RequestId{ .Integer = @intCast(i32, int) }, .String => |str| types.RequestId{ .String = str }, else => types.RequestId{ .Integer = 0 }, } else types.RequestId{ .Integer = 0 }; @@ -2326,6 +2371,26 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void const method = tree.root.Object.get("method").?.String; + switch (server.status) { + .uninitialized => blk: { + if (std.mem.eql(u8, method, "initialize")) break :blk; + if (std.mem.eql(u8, method, "exit")) break :blk; + + // ignore notifications + if (tree.root.Object.get("id") == null) break :blk; + + return try sendErrorResponse(writer, server.arena.allocator(), .ServerNotInitialized, "server received a request before being initialized!"); + }, + .initializing => blk: { + if (std.mem.eql(u8, method, "initialized")) break :blk; + if (std.mem.eql(u8, method, "$/progress")) break :blk; + + return try sendErrorResponse(writer, server.arena.allocator(), .InvalidRequest, "server received a request during initialization!"); + }, + .initialized => {}, + .shutdown => return try sendErrorResponse(writer, server.arena.allocator(), .InvalidRequest, "server received a request after shutdown!"), + } + const start_time = std.time.milliTimestamp(); defer { // makes `zig build test` look nice @@ -2341,6 +2406,7 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void .{"textDocument/willSave"}, .{ "initialize", requests.Initialize, initializeHandler }, .{ "shutdown", void, shutdownHandler }, + .{ "exit", void, exitHandler }, .{ "textDocument/didOpen", requests.OpenDocument, openDocumentHandler }, .{ "textDocument/didChange", requests.ChangeDocument, changeDocumentHandler }, .{ "textDocument/didSave", requests.SaveDocument, saveDocumentHandler }, @@ -2465,6 +2531,7 @@ pub fn init( .allocator = allocator, .document_store = document_store, .builtin_completions = builtin_completions, + .status = .uninitialized, }; } diff --git a/src/main.zig b/src/main.zig index 9beecab..4f74bf8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,7 +36,7 @@ pub fn log( fn loop(server: *Server) !void { var reader = std.io.getStdIn().reader(); - while (server.keep_running) { + while (true) { const headers = readRequestHeader(server.allocator, reader) catch |err| { logger.err("{s}; exiting!", .{@errorName(err)}); return; diff --git a/src/types.zig b/src/types.zig index 72faf5b..f2d264b 100644 --- a/src/types.zig +++ b/src/types.zig @@ -2,7 +2,7 @@ const std = @import("std"); const string = []const u8; // LSP types -// https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/ +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ pub const Position = struct { line: u32, @@ -22,13 +22,7 @@ pub const Location = struct { /// Id of a request pub const RequestId = union(enum) { String: string, - Integer: i64, - Float: f64, -}; - -/// Hover response -pub const Hover = struct { - contents: MarkupContent, + Integer: i32, }; /// Params of a response (result) @@ -38,7 +32,7 @@ pub const ResponseParams = union(enum) { Location: Location, Hover: Hover, DocumentSymbols: []DocumentSymbol, - SemanticTokensFull: struct { data: []const u32 }, + SemanticTokensFull: SemanticTokens, InlayHint: []InlayHint, TextEdits: []TextEdit, Locations: []Location, @@ -51,29 +45,6 @@ pub const ResponseParams = union(enum) { ApplyEdit: ApplyWorkspaceEditParams, }; -/// JSONRPC notifications -pub const Notification = struct { - pub const Params = union(enum) { - LogMessage: struct { - type: MessageType, - message: string, - }, - PublishDiagnostics: struct { - uri: string, - diagnostics: []Diagnostic, - }, - ShowMessage: struct { - type: MessageType, - message: string, - }, - }; - - jsonrpc: string = "2.0", - method: string, - params: Params, -}; - -/// JSONRPC response pub const Response = struct { jsonrpc: string = "2.0", id: RequestId, @@ -87,6 +58,52 @@ pub const Request = struct { params: ?ResponseParams, }; +pub const ResponseError = struct { + code: i32, + message: string, + data: std.json.Value, +}; + +pub const ErrorCodes = enum(i32) { + // Defined by JSON-RPC + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + + // JSON-RPC reserved error codes + ServerNotInitialized = -32002, + UnknownErrorCode = -3200, + + // LSP reserved error codes + RequestFailed = -32803, + ServerCancelled = -32802, + ContentModified = -32801, + RequestCancelled = -32800, +}; + +pub const Notification = struct { + jsonrpc: string = "2.0", + method: string, + params: NotificationParams, +}; + +pub const NotificationParams = union(enum) { + LogMessage: struct { + type: MessageType, + message: string, + }, + PublishDiagnostics: struct { + uri: string, + diagnostics: []Diagnostic, + }, + ShowMessage: struct { + type: MessageType, + message: string, + }, +}; + /// Type of a debug message pub const MessageType = enum(i64) { Error = 1, @@ -215,6 +232,14 @@ pub const InsertTextFormat = enum(i64) { } }; +pub const Hover = struct { + contents: MarkupContent, +}; + +pub const SemanticTokens = struct { + data: []const u32, +}; + pub const CompletionItem = struct { pub const Kind = enum(i64) { Text = 1, @@ -441,8 +466,8 @@ const TextDocumentSyncKind = enum(u32) { // Only includes options we set in our initialize result. const InitializeResult = struct { - offsetEncoding: PositionEncodingKind, capabilities: struct { + positionEncoding: PositionEncodingKind, signatureHelpProvider: struct { triggerCharacters: []const string, retriggerCharacters: []const string,