Improve testing infrastructure

It should now be a bit easier to set up a test and see how it failed.
This commit is contained in:
Jonathan Hähne 2021-04-07 15:10:18 +02:00
parent b59db79a05
commit 0e4f4c1e04
3 changed files with 232 additions and 190 deletions

View File

@ -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 };

View File

@ -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 {

View File

@ -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();
}