-
-
Notifications
You must be signed in to change notification settings - Fork 309
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
3 additions
and
274 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,272 +1 @@ | ||
const std = @import("std"); | ||
const headerPkg = @import("header"); | ||
const builtin = @import("builtin"); | ||
|
||
const suffix = if (builtin.os.tag == .windows) ".exe" else ""; | ||
const allocator = std.heap.page_allocator; | ||
|
||
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 Server = struct { | ||
process: std.ChildProcess, | ||
request_id: u32 = 1, | ||
|
||
fn start(initialization: []const u8, expect: ?[]const u8) !Server { | ||
var server = Server{ .process = try startZls() }; | ||
|
||
try server.request("initialize", initialization, expect); | ||
try server.request("initialized", "{}", null); | ||
return server; | ||
} | ||
|
||
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 response_bytes = try allocator.alloc(u8, header.content_length); | ||
defer allocator.free(response_bytes); | ||
if ((try from_server.readAll(response_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, response_bytes, json_fmt)) { | ||
try extractError(response_bytes); | ||
continue; | ||
} | ||
|
||
const rest = response_bytes[json_fmt.len..]; | ||
const id_end = std.mem.indexOfScalar(u8, rest, ',') orelse return error.InvalidResponse; | ||
|
||
const id = std.fmt.parseInt(u32, rest[0..id_end], 10) catch continue; | ||
|
||
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 { | ||
// FIXME this shutdown request fails with a broken pipe on stdin on the CI | ||
self.request("shutdown", "{}", null) catch {}; | ||
// waitNoError(self.process) catch @panic("Server error"); | ||
} | ||
}; | ||
|
||
fn startZls() !std.ChildProcess { | ||
var process = std.ChildProcess.init(&[_][]const u8{"zig-out/bin/zls" ++ suffix}, allocator); | ||
process.stdin_behavior = .Pipe; | ||
process.stdout_behavior = .Pipe; | ||
process.stderr_behavior = .Inherit; | ||
|
||
process.spawn() catch |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; | ||
}, | ||
else => {}, | ||
} | ||
return error.ShutdownWithError; | ||
} | ||
|
||
test "Open file, ask for semantic tokens" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
try server.request("textDocument/didOpen", | ||
\\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"const std = @import(\"std\");"}} | ||
, null); | ||
try server.request("textDocument/semanticTokens/full", | ||
\\{"textDocument":{"uri":"file:///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 "Request completion in an empty file" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
try server.request("textDocument/didOpen", | ||
\\{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":""}}} | ||
, null); | ||
try server.request("textDocument/completion", | ||
\\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":0,"character":0}} | ||
, null); | ||
} | ||
|
||
test "Request completion with no trailing whitespace" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
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","labelDetails":{"detail":"","description":"@import(\"std\")","sortText":null},"kind":21,"detail":"std","sortText":"1_std","filterText":null,"insertText":"std","insertTextFormat":1,"documentation":null}]} | ||
); | ||
} | ||
|
||
test "Encoded space in file name and usingnamespace on non-existing symbol" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
try server.request("textDocument/didOpen", | ||
\\{"textDocument":{"uri":"file:///%20test.zig","languageId":"zig","version":420,"text":"usingnamespace a.b;\nb."}} | ||
, null); | ||
try server.request("textDocument/completion", | ||
\\{"textDocument":{"uri":"file:///%20test.zig"}, "position":{"line":1,"character":2}} | ||
, | ||
\\{"isIncomplete":false,"items":[]} | ||
); | ||
} | ||
|
||
test "Self-referential definition" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
try server.request("textDocument/didOpen", | ||
\\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"const h = h(0);\nc"}} | ||
, null); | ||
try server.request("textDocument/completion", | ||
\\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":1}} | ||
, | ||
\\{"isIncomplete":false,"items":[{"label":"h","labelDetails":{"detail":"","description":"h(0)","sortText":null},"kind":21,"detail":"h","sortText":"1_h","filterText":null,"insertText":"h","insertTextFormat":1,"documentation":null}]} | ||
); | ||
} | ||
|
||
// This test as written depends on the configuration in the *host* machines zls.json, if `enable_snippets` is true then | ||
// the insert text is "w()" if it is false it is "w" | ||
// | ||
// test "Missing return type" { | ||
// var server = try Server.start(initialize_msg, null); | ||
// defer server.shutdown(); | ||
|
||
// try server.request("textDocument/didOpen", | ||
// \\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"fn w() {}\nc"}} | ||
// , null); | ||
// try server.request("textDocument/completion", | ||
// \\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":1}} | ||
// , | ||
// \\{"isIncomplete":false,"items":[{"label":"w","kind":3,"textEdit":null,"filterText":null,"insertText":"w","insertTextFormat":1,"detail":"fn","documentation":null}]} | ||
// ); | ||
// } | ||
|
||
test "Pointer and optional deref" { | ||
var server = try Server.start(initialize_msg, null); | ||
defer server.shutdown(); | ||
|
||
try server.request("textDocument/didOpen", | ||
\\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"var value: ?struct { data: i32 = 5 } = null;const ptr = &value;\nconst a = ptr.*.?."}} | ||
, null); | ||
try server.request("textDocument/completion", | ||
\\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":18}} | ||
, | ||
\\{"isIncomplete":false,"items":[{"label":"data","labelDetails":{"detail":"","description":"i32 ","sortText":null},"kind":5,"detail":"data","sortText":"3_data","filterText":null,"insertText":"data","insertTextFormat":1,"documentation":null}]} | ||
); | ||
} | ||
|
||
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":[".",":","@"],"completionItem":{"labelDetailsSupport":true}},"documentHighlightProvider":true,"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"]}},"inlayHintProvider":true},"serverInfo":{"name":"zls","version":"0.1.0"}} | ||
); | ||
defer server.shutdown(); | ||
} | ||
|
||
// not fixed yet! | ||
// test "Self-referential import" { | ||
// var server = try Server.start(initialize_msg, null); | ||
// defer server.shutdown(); | ||
// try server.request("textDocument/didOpen", | ||
// \\{"textDocument":{"uri":"file:///test.zig","languageId":"zig","version":420,"text":"const a = @import(\"test.zig\").a;\nc"}} | ||
// , null); | ||
// try server.request("textDocument/completion", | ||
// \\{"textDocument":{"uri":"file:///test.zig"}, "position":{"line":1,"character":1}} | ||
// , | ||
// \\{"isIncomplete":false,"items":[]} | ||
// ); | ||
// } | ||
// TODO Rewrite tests; the old session tests were extremely broken and unhelpful |