From 0e4f4c1e047f2f9d2f33afe8da1137532ad22d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20H=C3=A4hne?= Date: Wed, 7 Apr 2021 15:10:18 +0200 Subject: [PATCH] Improve testing infrastructure It should now be a bit easier to set up a test and see how it failed. --- src/analysis.zig | 29 ++--- src/main.zig | 136 ++++++++++++------------ tests/sessions.zig | 257 ++++++++++++++++++++++++++------------------- 3 files changed, 232 insertions(+), 190 deletions(-) diff --git a/src/analysis.zig b/src/analysis.zig index 3c20203..85a81bd 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -6,6 +6,17 @@ const offsets = @import("offsets.zig"); const log = std.log.scoped(.analysis); usingnamespace @import("ast.zig"); +var using_trail: std.ArrayList([*]const u8) = undefined; +var resolve_trail: std.ArrayList(NodeWithHandle) = undefined; +pub fn init(allocator: *std.mem.Allocator) void { + using_trail = std.ArrayList([*]const u8).init(allocator); + resolve_trail = std.ArrayList(NodeWithHandle).init(allocator); +} +pub fn deinit() void { + using_trail.deinit(); + resolve_trail.deinit(); +} + /// Gets a declaration's doc comments, caller must free memory when a value is returned /// Like: ///```zig @@ -681,19 +692,16 @@ pub fn resolveTypeOfNodeInternal( node_handle: NodeWithHandle, bound_type_params: *BoundTypeParams, ) error{OutOfMemory}!?TypeWithHandle { - const state = struct { - var resolve_trail = std.ArrayListUnmanaged(NodeWithHandle){}; - }; // If we were asked to resolve this node before, // it is self-referential and we cannot resolve it. - for (state.resolve_trail.items) |i| { + for (resolve_trail.items) |i| { if (std.meta.eql(i, node_handle)) return null; } // We use the backing allocator here because the ArrayList expects its // allocated memory to persist while it is empty. - try state.resolve_trail.append(arena.child_allocator, node_handle); - defer _ = state.resolve_trail.pop(); + try resolve_trail.append(node_handle); + defer _ = resolve_trail.pop(); const node = node_handle.node; const handle = node_handle.handle; @@ -2259,17 +2267,14 @@ fn resolveUse( symbol: []const u8, handle: *DocumentStore.Handle, ) error{OutOfMemory}!?DeclWithHandle { - const state = struct { - var using_trail = std.ArrayListUnmanaged([*]const u8){}; - }; // If we were asked to resolve this symbol before, // it is self-referential and we cannot resolve it. - if (std.mem.indexOfScalar([*]const u8, state.using_trail.items, symbol.ptr) != null) + if (std.mem.indexOfScalar([*]const u8, using_trail.items, symbol.ptr) != null) return null; // We use the backing allocator here because the ArrayList expects its // allocated memory to persist while it is empty. - try state.using_trail.append(arena.child_allocator, symbol.ptr); - defer _ = state.using_trail.pop(); + try using_trail.append(symbol.ptr); + defer _ = using_trail.pop(); for (uses) |use| { const expr = .{ .node = handle.tree.nodes.items(.data)[use.*].lhs, .handle = handle }; diff --git a/src/main.zig b/src/main.zig index ce29140..fb633fe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -38,7 +38,7 @@ pub fn log( } // After shutdown, pipe output to stderr if (!keep_running) { - std.debug.print("[{s}-{s}] " ++ format, .{ @tagName(message_level), @tagName(scope) } ++ args); + std.debug.print("[{s}-{s}] " ++ format ++ "\n", .{ @tagName(message_level), @tagName(scope) } ++ args); return; } @@ -619,37 +619,32 @@ fn hoverSymbol( const tree = handle.tree; const hover_kind: types.MarkupContent.Kind = if (client_capabilities.hover_supports_md) .Markdown else .PlainText; - const md_string = switch (decl_handle.decl.*) { - .ast_node => |node| ast_node: { + var doc_str: ?[]const u8 = null; + + const def_str = switch (decl_handle.decl.*) { + .ast_node => |node| def: { if (try analysis.resolveVarDeclAlias(&document_store, arena, .{ .node = node, .handle = handle })) |result| { return try hoverSymbol(id, arena, result); } - - const doc_str = if (try analysis.getDocComments(&arena.allocator, tree, node, hover_kind)) |str| - str - else - ""; + doc_str = try analysis.getDocComments(&arena.allocator, tree, node, hover_kind); var buf: [1]std.zig.ast.Node.Index = undefined; - const signature_str = if (analysis.varDecl(tree, node)) |var_decl| blk: { - break :blk analysis.getVariableSignature(tree, var_decl); - } else if (analysis.fnProto(tree, node, &buf)) |fn_proto| blk: { - break :blk analysis.getFunctionSignature(tree, fn_proto); - } else if (analysis.containerField(tree, node)) |field| blk: { - break :blk analysis.getContainerFieldSignature(tree, field); - } else analysis.nodeToString(tree, node) orelse - return try respondGeneric(id, null_result_response); - break :ast_node if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```\n{s}", .{ signature_str, doc_str }) - else - try std.fmt.allocPrint(&arena.allocator, "{s}\n{s}", .{ signature_str, doc_str }); + if (analysis.varDecl(tree, node)) |var_decl| { + break :def analysis.getVariableSignature(tree, var_decl); + } else if (analysis.fnProto(tree, node, &buf)) |fn_proto| { + break :def analysis.getFunctionSignature(tree, fn_proto); + } else if (analysis.containerField(tree, node)) |field| { + break :def analysis.getContainerFieldSignature(tree, field); + } else { + break :def analysis.nodeToString(tree, node) orelse + return try respondGeneric(id, null_result_response); + } }, - .param_decl => |param| param_decl: { - const doc_str = if (param.first_doc_comment) |doc_comments| - try analysis.collectDocComments(&arena.allocator, handle.tree, doc_comments, hover_kind) - else - ""; + .param_decl => |param| def: { + if (param.first_doc_comment) |doc_comments| { + doc_str = try analysis.collectDocComments(&arena.allocator, handle.tree, doc_comments, hover_kind); + } const first_token = param.first_doc_comment orelse param.comptime_noalias orelse @@ -659,42 +654,35 @@ fn hoverSymbol( const start = offsets.tokenLocation(tree, first_token).start; const end = offsets.tokenLocation(tree, last_token).end; - const signature_str = tree.source[start..end]; - break :param_decl if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```\n{s}", .{ signature_str, doc_str }) - else - try std.fmt.allocPrint(&arena.allocator, "{s}\n{s}", .{ signature_str, doc_str }); - }, - .pointer_payload => |payload| if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{tree.tokenSlice(payload.name)}) - else - try std.fmt.allocPrint(&arena.allocator, "{s}", .{tree.tokenSlice(payload.name)}), - .array_payload => |payload| if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{handle.tree.tokenSlice(payload.identifier)}) - else - try std.fmt.allocPrint(&arena.allocator, "{s}", .{handle.tree.tokenSlice(payload.identifier)}), - .array_index => |payload| if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{handle.tree.tokenSlice(payload)}) - else - try std.fmt.allocPrint(&arena.allocator, "{s}", .{handle.tree.tokenSlice(payload)}), - .switch_payload => |payload| if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{tree.tokenSlice(payload.node)}) - else - try std.fmt.allocPrint(&arena.allocator, "{s}", .{tree.tokenSlice(payload.node)}), - .label_decl => |label_decl| block: { - const source = tree.tokenSlice(label_decl); - break :block if (hover_kind == .Markdown) - try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{source}) - else - try std.fmt.allocPrint(&arena.allocator, "```{s}```", .{source}); + break :def tree.source[start..end]; }, + .pointer_payload => |payload| tree.tokenSlice(payload.name), + .array_payload => |payload| handle.tree.tokenSlice(payload.identifier), + .array_index => |payload| handle.tree.tokenSlice(payload), + .switch_payload => |payload| tree.tokenSlice(payload.node), + .label_decl => |label_decl| tree.tokenSlice(label_decl), }; + var hover_text: []const u8 = undefined; + if (hover_kind == .Markdown) { + hover_text = + if (doc_str) |doc| + try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```\n{s}", .{ def_str, doc }) + else + try std.fmt.allocPrint(&arena.allocator, "```zig\n{s}\n```", .{def_str}); + } else { + hover_text = + if (doc_str) |doc| + try std.fmt.allocPrint(&arena.allocator, "{s}\n{s}", .{ def_str, doc }) + else + def_str; + } + try send(arena, types.Response{ .id = id, .result = .{ .Hover = .{ - .contents = .{ .value = md_string }, + .contents = .{ .value = hover_text }, }, }, }); @@ -739,11 +727,13 @@ fn hoverDefinitionBuiltin(arena: *std.heap.ArenaAllocator, id: types.RequestId, .id = id, .result = .{ .Hover = .{ - .contents = .{ .value = try std.fmt.allocPrint( - &arena.allocator, - "```zig\n{s}\n```\n{s}", - .{ builtin.signature, builtin.documentation }, - ) }, + .contents = .{ + .value = try std.fmt.allocPrint( + &arena.allocator, + "```zig\n{s}\n```\n{s}", + .{ builtin.signature, builtin.documentation }, + ), + }, }, }, }); @@ -1161,6 +1151,7 @@ fn completeError( ) !void { const completions = try document_store.errorCompletionItems(arena, handle); truncateCompletions(completions, config.max_detail_length); + logger.debug("Completing error:", .{}); try send(arena, types.Response{ .id = id, @@ -1348,7 +1339,8 @@ fn openDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req const handle = try document_store.openDocument(req.params.textDocument.uri, req.params.textDocument.text); try publishDiagnostics(arena, handle.*, config); - try semanticTokensFullHandler(arena, id, .{ .params = .{ .textDocument = .{ .uri = req.params.textDocument.uri } } }, config); + if (client_capabilities.supports_semantic_tokens) + try semanticTokensFullHandler(arena, id, .{ .params = .{ .textDocument = .{ .uri = req.params.textDocument.uri } } }, config); } fn changeDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.ChangeDocument, config: Config) !void { @@ -1374,10 +1366,10 @@ fn closeDocumentHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, re } fn semanticTokensFullHandler(arena: *std.heap.ArenaAllocator, id: types.RequestId, req: requests.SemanticTokensFull, config: Config) (error{OutOfMemory} || std.fs.File.WriteError)!void { - if (config.enable_semantic_tokens and client_capabilities.supports_semantic_tokens) { + if (config.enable_semantic_tokens) blk: { const handle = document_store.getHandle(req.params.textDocument.uri) orelse { logger.warn("Trying to get semantic tokens of non existent document {s}", .{req.params.textDocument.uri}); - return try respondGeneric(id, no_semantic_tokens_response); + break :blk; }; const token_array = try semantic_tokens.writeAllSemanticTokens(arena, &document_store, handle, offset_encoding); @@ -1388,6 +1380,7 @@ fn semanticTokensFullHandler(arena: *std.heap.ArenaAllocator, id: types.RequestI .result = .{ .SemanticTokensFull = .{ .data = token_array } }, }); } + return try respondGeneric(id, no_semantic_tokens_response); } fn completionHandler( @@ -1444,11 +1437,13 @@ fn signatureHelpHandler( )) |sig_info| { return try send(arena, types.Response{ .id = id, - .result = .{ .SignatureHelp = .{ - .signatures = &[1]types.SignatureInformation{sig_info}, - .activeSignature = 0, - .activeParameter = sig_info.activeParameter, - } }, + .result = .{ + .SignatureHelp = .{ + .signatures = &[1]types.SignatureInformation{sig_info}, + .activeSignature = 0, + .activeParameter = sig_info.activeParameter, + }, + }, }); } return try respondGeneric(id, no_signatures_response); @@ -1717,12 +1712,16 @@ var gpa_state = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = stack_ pub fn main() anyerror!void { defer _ = gpa_state.deinit(); + defer keep_running = false; allocator = &gpa_state.allocator; + analysis.init(allocator); + defer analysis.deinit(); + // Check arguments. var args_it = std.process.args(); defer args_it.deinit(); - const prog_name = try args_it.next(allocator) orelse unreachable; + const prog_name = try args_it.next(allocator) orelse @panic("Could not find self argument"); allocator.free(prog_name); while (args_it.next(allocator)) |maybe_arg| { @@ -1732,7 +1731,6 @@ pub fn main() anyerror!void { actual_log_level = .debug; std.debug.print("Enabled debug logging\n", .{}); } else if (std.mem.eql(u8, arg, "config")) { - keep_running = false; try setup.wizard(allocator); return; } else { diff --git a/tests/sessions.zig b/tests/sessions.zig index feeb167..61b6e0a 100644 --- a/tests/sessions.zig +++ b/tests/sessions.zig @@ -4,72 +4,142 @@ const headerPkg = @import("header"); const suffix = if (std.builtin.os.tag == .windows) ".exe" else ""; const allocator = std.heap.page_allocator; -const initialize_message = - \\{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":6896,"clientInfo":{"name":"vscode","version":"1.46.1"},"rootPath":null,"rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional"},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"complexDiagnosticCodeSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true}},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]}},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["comment","keyword","number","regexp","operator","namespace","type","struct","class","interface","enum","typeParameter","function","member","macro","variable","parameter","property","label"],"tokenModifiers":["declaration","documentation","static","abstract","deprecated","readonly"]}},"window":{"workDoneProgress":true}},"trace":"off","workspaceFolders":[{"uri":"file://./tests", "name":"root"}]}} +const initialize_msg = + \\{"processId":6896,"clientInfo":{"name":"vscode","version":"1.46.1"},"rootPath":null,"rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional"},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"complexDiagnosticCodeSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true}},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]}},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["comment","keyword","number","regexp","operator","namespace","type","struct","class","interface","enum","typeParameter","function","member","macro","variable","parameter","property","label"],"tokenModifiers":["declaration","documentation","static","abstract","deprecated","readonly"]}},"window":{"workDoneProgress":true}},"trace":"off","workspaceFolders":[{"uri":"file://./tests", "name":"root"}]} +; +const initialize_msg_offs = + \\{"processId":6896,"clientInfo":{"name":"vscode","version":"1.46.1"},"rootPath":null,"rootUri":null,"capabilities":{"offsetEncoding":["utf-16", "utf-8"],"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional"},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"complexDiagnosticCodeSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true}},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]}},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["comment","keyword","number","regexp","operator","namespace","type","struct","class","interface","enum","typeParameter","function","member","macro","variable","parameter","property","label"],"tokenModifiers":["declaration","documentation","static","abstract","deprecated","readonly"]}},"window":{"workDoneProgress":true}},"trace":"off","workspaceFolders":null} ; -const initialized_message = - \\{"jsonrpc":"2.0","method":"initialized","params":{}} -; +const Server = struct { + process: *std.ChildProcess, + request_id: u32 = 1, -const shutdown_message = - \\{"jsonrpc":"2.0", "id":"STDWN", "method":"shutdown","params":{}} -; + fn start(initialization: []const u8, expect: ?[]const u8) !Server { + var server = Server{ .process = try startZls() }; -fn sendRequest(req: []const u8, process: *std.ChildProcess) !void { - try process.stdin.?.writer().print("Content-Length: {}\r\n\r\n", .{req.len}); - try process.stdin.?.writeAll(req); -} - -fn readResponses(process: *std.ChildProcess, expected_responses: anytype) !void { - var seen = std.mem.zeroes([expected_responses.len]bool); - while (true) { - const header = headerPkg.readRequestHeader(allocator, process.stdout.?.reader()) catch |err| { - switch (err) { - error.EndOfStream => break, - else => return err, - } - }; - defer header.deinit(allocator); - - var stdout_mem = try allocator.alloc(u8, header.content_length); - defer allocator.free(stdout_mem); - - const stdout_bytes = stdout_mem[0..try process.stdout.?.reader().readAll(stdout_mem)]; - inline for (expected_responses) |resp, idx| { - if (std.mem.eql(u8, resp, stdout_bytes)) { - if (seen[idx]) @panic("Expected response already received."); - seen[idx] = true; - } - } - std.debug.print("GOT MESSAGE: {s}\n", .{stdout_bytes}); + try server.request("initialize", initialization, expect); + try server.request("initialized", "{}", null); + return server; } - comptime var idx = 0; - inline while (idx < expected_responses.len) : (idx += 1) { - if (!seen[idx]) { - std.debug.print("Response `{s}` not received.", .{expected_responses[idx]}); - return error.ExpectedResponse; + fn request( + self: *Server, + method: []const u8, + params: []const u8, + expect: ?[]const u8, + ) !void { + self.request_id += 1; + const req = try std.fmt.allocPrint(allocator, + \\{{"jsonrpc":"2.0","id":{},"method":"{s}","params":{s}}} + , .{ self.request_id, method, params }); + + const to_server = self.process.stdin.?.writer(); + try to_server.print("Content-Length: {}\r\n\r\n", .{req.len}); + try to_server.writeAll(req); + + const expected = expect orelse return; + var from_server = self.process.stdout.?.reader(); + + while (true) { + const header = headerPkg.readRequestHeader(allocator, from_server) catch |err| { + switch (err) { + error.EndOfStream => break, + else => return err, + } + }; + defer header.deinit(allocator); + var resonse_bytes = try allocator.alloc(u8, header.content_length); + defer allocator.free(resonse_bytes); + if ((try from_server.readAll(resonse_bytes)) != header.content_length) { + return error.InvalidResponse; + } + // std.debug.print("{s}\n", .{resonse_bytes}); + + const json_fmt = "{\"jsonrpc\":\"2.0\",\"id\":"; + if (!std.mem.startsWith(u8, resonse_bytes, json_fmt)) { + try extractError(resonse_bytes); + continue; + } + + const rest = resonse_bytes[json_fmt.len..]; + const id_end = std.mem.indexOfScalar(u8, rest, ',') orelse return error.InvalidResponse; + + const id = try std.fmt.parseInt(u32, rest[0..id_end], 10); + + if (id != self.request_id) { + continue; + } + + const result = ",\"result\":"; + const msg = rest[id_end + result.len .. rest.len - 1]; + + if (std.mem.eql(u8, msg, expected)) { + return; + } else { + const mismatch = std.mem.indexOfDiff(u8, expected, msg) orelse 0; + std.debug.print("==> Expected:\n{s}\n==> Got: (Mismatch in position {})\n{s}\n", .{ expected, mismatch, msg }); + return error.InvalidResponse; + } + } + } + fn extractError(msg: []const u8) !void { + const log_request = + \\"method":"window/logMessage","params":{"type": + ; + if (std.mem.indexOf(u8, msg, log_request)) |log_msg| { + const rest = msg[log_msg + log_request.len ..]; + const level = rest[0]; + if (level <= '2') { + std.debug.print("{s}\n", .{rest[13 .. rest.len - 3]}); + if (level <= '1') { + return error.ServerError; + } + } } } -} + fn shutdown(self: *Server) void { + self.request("shutdown", "{}", null) catch @panic("Could not send shutdown request"); + waitNoError(self.process) catch |err| @panic("Server error"); + self.process.deinit(); + } +}; fn startZls() !*std.ChildProcess { + std.debug.print("\n", .{}); + var process = try std.ChildProcess.init(&[_][]const u8{"zig-cache/bin/zls" ++ suffix}, allocator); process.stdin_behavior = .Pipe; process.stdout_behavior = .Pipe; - process.stderr_behavior = std.ChildProcess.StdIo.Inherit; + process.stderr_behavior = .Pipe; //std.ChildProcess.StdIo.Inherit; process.spawn() catch |err| { - std.log.debug("Failed to spawn zls process, error: {}\n", .{err}); + std.debug.print("Failed to spawn zls process, error: {}\n", .{err}); return err; }; return process; } - fn waitNoError(process: *std.ChildProcess) !void { + const stderr = std.io.getStdErr().writer(); + const err_in = process.stderr.?.reader(); + var buf: [4096]u8 = undefined; + while (true) { + const line = err_in.readUntilDelimiterOrEof(&buf, '\n') catch |err| switch (err) { + error.StreamTooLong => { + std.debug.print("skipping very long line\n", .{}); + continue; + }, + else => return err, + } orelse break; + + if (std.mem.startsWith(u8, line, "[debug")) continue; + + try stderr.writeAll(line); + try stderr.writeByte('\n'); + } const result = try process.wait(); + switch (result) { .Exited => |code| if (code == 0) { return; @@ -79,80 +149,49 @@ fn waitNoError(process: *std.ChildProcess) !void { return error.ShutdownWithError; } -fn consumeOutputAndWait(process: *std.ChildProcess, expected_responses: anytype) !void { - process.stdin.?.close(); - process.stdin = null; - try readResponses(process, expected_responses); - try waitNoError(process); - process.deinit(); -} - test "Open file, ask for semantic tokens" { - var process = try startZls(); - try sendRequest(initialize_message, process); - try sendRequest(initialized_message, process); + var server = try Server.start(initialize_msg, null); + defer server.shutdown(); - const new_file_req = - \\{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://./tests/test.zig","languageId":"zig","version":420,"text":"const std = @import(\"std\");"}}} - ; - try sendRequest(new_file_req, process); - const sem_toks_req = - \\{"jsonrpc":"2.0","id":2,"method":"textDocument/semanticTokens/full","params":{"textDocument":{"uri":"file://./tests/test.zig"}}} - ; - try sendRequest(sem_toks_req, process); - try sendRequest(shutdown_message, process); - try consumeOutputAndWait(process, .{ - \\{"jsonrpc":"2.0","id":0,"result":{"data":[0,0,5,7,0,0,6,3,0,33,0,4,1,11,0,0,2,7,12,0,0,8,5,9,0]}} - }); + try server.request("textDocument/didOpen", + \\{"textDocument":{"uri":"file://./tests/test.zig","languageId":"zig","version":420,"text":"const std = @import(\"std\");"}} + , null); + try server.request("textDocument/semanticTokens/full", + \\{"textDocument":{"uri":"file://./tests/test.zig"}} + , + \\{"data":[0,0,5,7,0,0,6,3,0,33,0,4,1,11,0,0,2,7,12,0,0,8,5,9,0]} + ); } -test "Requesting a completion in an empty file" { - var process = try startZls(); - try sendRequest(initialize_message, process); - try sendRequest(initialized_message, process); +test "Request completion in an empty file" { + var server = try Server.start(initialize_msg, null); + defer server.shutdown(); - const new_file_req = + try server.request("textDocument/didOpen", \\{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":""}}} - ; - try sendRequest(new_file_req, process); - const completion_req = - \\{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":0,"character":0}}} - ; - try sendRequest(completion_req, process); - try sendRequest(shutdown_message, process); - try consumeOutputAndWait(process, .{}); + , null); + try server.request("textDocument/completion", + \\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":0,"character":0}} + , null); } -test "Requesting a completion with no trailing whitespace" { - var process = try startZls(); - try sendRequest(initialize_message, process); - try sendRequest(initialized_message, process); +test "Request completion with no trailing whitespace" { + var server = try Server.start(initialize_msg, null); + defer server.shutdown(); - const new_file_req = - \\{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"const std = @import(\"std\");\nc"}}} - ; - try sendRequest(new_file_req, process); - const completion_req = - \\{"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":1}}} - ; - try sendRequest(completion_req, process); - try sendRequest(shutdown_message, process); - try consumeOutputAndWait(process, .{ - \\{"jsonrpc":"2.0","id":2,"result":{"isIncomplete":false,"items":[{"label":"std","kind":21,"textEdit":null,"filterText":null,"insertText":"std","insertTextFormat":1,"detail":"const std = @import(\"std\")","documentation":null}]}} - }); + try server.request("textDocument/didOpen", + \\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"const std = @import(\"std\");\nc"}} + , null); + try server.request("textDocument/completion", + \\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":1}} + , + \\{"isIncomplete":false,"items":[{"label":"std","kind":21,"textEdit":null,"filterText":null,"insertText":"std","insertTextFormat":1,"detail":"const std = @import(\"std\")","documentation":null}]} + ); } -const initialize_message_offs = - \\{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":6896,"clientInfo":{"name":"vscode","version":"1.46.1"},"rootPath":null,"rootUri":null,"capabilities":{"offsetEncoding":["utf-16", "utf-8"],"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional"},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]}},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"complexDiagnosticCodeSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true}},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]}},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["comment","keyword","number","regexp","operator","namespace","type","struct","class","interface","enum","typeParameter","function","member","macro","variable","parameter","property","label"],"tokenModifiers":["declaration","documentation","static","abstract","deprecated","readonly"]}},"window":{"workDoneProgress":true}},"trace":"off","workspaceFolders":null}} -; - -test "Requesting utf-8 offset encoding" { - var process = try startZls(); - try sendRequest(initialize_message_offs, process); - try sendRequest(initialized_message, process); - - try sendRequest(shutdown_message, process); - try consumeOutputAndWait(process, .{ - \\{"jsonrpc":"2.0","id":0,"result":{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["("],"retriggerCharacters":[","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"documentHighlightProvider":false,"hoverProvider":true,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":false,"referencesProvider":true,"documentSymbolProvider":true,"colorProvider":false,"documentFormattingProvider":true,"documentRangeFormattingProvider":false,"foldingRangeProvider":false,"selectionRangeProvider":false,"workspaceSymbolProvider":false,"rangeProvider":false,"documentProvider":true,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"semanticTokensProvider":{"full":true,"range":false,"legend":{"tokenTypes":["type","parameter","variable","enumMember","field","errorTag","function","keyword","comment","string","number","operator","builtin","label","keywordLiteral"],"tokenModifiers":["namespace","struct","enum","union","opaque","declaration","async","documentation","generic"]}}},"serverInfo":{"name":"zls","version":"0.1.0"}}} - }); +test "Request utf-8 offset encoding" { + var server = try Server.start(initialize_msg_offs, + \\{"offsetEncoding":"utf-8","capabilities":{"signatureHelpProvider":{"triggerCharacters":["("],"retriggerCharacters":[","]},"textDocumentSync":1,"renameProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":","@"]},"documentHighlightProvider":false,"hoverProvider":true,"codeActionProvider":false,"declarationProvider":true,"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":false,"referencesProvider":true,"documentSymbolProvider":true,"colorProvider":false,"documentFormattingProvider":true,"documentRangeFormattingProvider":false,"foldingRangeProvider":false,"selectionRangeProvider":false,"workspaceSymbolProvider":false,"rangeProvider":false,"documentProvider":true,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"semanticTokensProvider":{"full":true,"range":false,"legend":{"tokenTypes":["type","parameter","variable","enumMember","field","errorTag","function","keyword","comment","string","number","operator","builtin","label","keywordLiteral"],"tokenModifiers":["namespace","struct","enum","union","opaque","declaration","async","documentation","generic"]}}},"serverInfo":{"name":"zls","version":"0.1.0"}} + ); + server.shutdown(); }