From a0ff26cc8f89cb8329054e75c444072cd91cec2d Mon Sep 17 00:00:00 2001 From: SuperAuguste Date: Fri, 24 Apr 2020 18:19:03 -0400 Subject: [PATCH] init --- .gitignore | 2 + README.md | 21 ++++ build.zig | 26 +++++ src/analysis.zig | 45 ++++++++ src/main.zig | 275 +++++++++++++++++++++++++++++++++++++++++++++++ src/uri.zig | 45 ++++++++ 6 files changed, 414 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 src/analysis.zig create mode 100644 src/main.zig create mode 100644 src/uri.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6e6242 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Zig magic stuff +/zig-cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..e11ef23 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# zls - Zig Language Server + +Zig Language Server (or `zls` for short) is a - you guessed it - language server for Zig! Based off of the code written by the great `andersfr`. + +## Installation + +```bash +git clone https://github.com/SuperAuguste/zls +cd zls +zig build +``` + +Then, you can use the `zls` executable in an editor of your choice that has a Zig language server client! + +## Usage + +**Please, I beg you, please don't use this unless you're developing or testing it!** + +### VSCode + +Install the `zig-lsc` extension from [here](https://github.com/SuperAuguste/zig-lsc). diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..41fd4e9 --- /dev/null +++ b/build.zig @@ -0,0 +1,26 @@ +const builtin = @import("builtin"); +const Builder = @import("std").build.Builder; + +pub fn build(b: *Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("zls", "src/main.zig"); + + exe.setTarget(target); + exe.setBuildMode(mode); + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/src/analysis.zig b/src/analysis.zig new file mode 100644 index 0000000..0782a06 --- /dev/null +++ b/src/analysis.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +/// REALLY BAD CODE, PLEASE DON'T USE THIS!!!!!!! (only for testing) +pub fn getFunctionByName(tree: *std.zig.ast.Tree, name: []const u8) ?*std.zig.ast.Node.FnProto { + + var decls = tree.root_node.decls.iterator(0); + while (decls.next()) |decl_ptr| { + + var decl = decl_ptr.*; + switch (decl.id) { + .FnProto => { + const func = decl.cast(std.zig.ast.Node.FnProto).?; + if (std.mem.eql(u8, tree.tokenSlice(func.name_token.?), name)) return func; + }, + else => {} + } + + } + + return null; + +} + +/// Gets a function's doc comments, caller must free memory when a value is returned +/// Like: +///```zig +///var comments = getFunctionDocComments(allocator, tree, func); +///defer if (comments) |comments_pointer| allocator.free(comments_pointer); +///``` +pub fn getFunctionDocComments(allocator: *std.mem.Allocator, tree: *std.zig.ast.Tree, func: *std.zig.ast.Node.FnProto) !?[]const u8 { + + if (func.doc_comments) |doc_comments| { + var doc_it = doc_comments.lines.iterator(0); + var lines = std.ArrayList([]const u8).init(allocator); + + while (doc_it.next()) |doc_comment| { + _ = try lines.append(std.fmt.trim(tree.tokenSlice(doc_comment.*)[3..])); + } + + return try std.mem.join(allocator, "\n", lines.toOwnedSlice()); + } else { + return null; + } + +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..e97b3d1 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,275 @@ +const std = @import("std"); +const Uri = @import("uri.zig"); + +// Code is largely based off of https://github.com/andersfr/zig-lsp/blob/master/server.zig + +var stdout: std.fs.File.OutStream = undefined; +var allocator: *std.mem.Allocator = undefined; + +const initialize_response = \\,"result":{"capabilities":{"signatureHelpProvider":{"triggerCharacters":["(",","]},"textDocumentSync":1,"completionProvider":{"resolveProvider":false,"triggerCharacters":[".",":"]},"documentHighlightProvider":false,"codeActionProvider":false,"workspace":{"workspaceFolders":{"supported":true}}}}} +; + +const not_implemented_response = \\,"error":{"code":-32601,"message":"NotImplemented"}} +; + +const null_result_response = \\,"result":null} +; +const empty_result_response = \\,"result":{}} +; +const empty_array_response = \\,"result":[]} +; +const edit_not_applied_response = \\,"result":{"applied":false,"failureReason":"feature not implemented"}} +; +const no_completions_response = \\,"result":{"isIncomplete":false,"items":[]}} +; + +pub fn log(comptime fmt: []const u8, args: var) !void { + // Don't need much memory for log messages. This is a bad approach, but it's quick and easy and I wrote this code in ~1 minute. + var buffer: []u8 = try allocator.alloc(u8, 100); + defer allocator.free(buffer); + var bstream = std.io.fixedBufferStream(buffer); + var stream = bstream.outStream(); + + _ = try stream.write( + \\{"jsonrpc":"2.0","method":"window/logMessage","params":{"type": 4, "message": " + ); + _ = try stream.print(fmt, args); + _ = try stream.write( + \\"}} + ); + + _ = try stdout.print("Content-Length: {}\r\n\r\n", .{bstream.pos}); + _ = try stdout.write(bstream.getWritten()); +} + +pub fn respondGeneric(id: i64, response: []const u8) !void { + const id_digits = blk: { + if (id == 0) break :blk 1; + var digits: usize = 1; + var value = @divTrunc(id, 10); + while (value != 0) : (value = @divTrunc(value, 10)) { + digits += 1; + } + break :blk digits; + }; + + _ = try stdout.print("Content-Length: {}\r\n\r\n{}\"jsonrpc\":\"2.0\",\"id\":{}", .{response.len + id_digits + 22, "{", id}); + _ = try stdout.write(response); +} + +pub fn processSource(uri: []const u8, source: []const u8) !void { + + try log("An error, cool", .{}); + + const tree = try std.zig.parse(allocator, source); + defer tree.deinit(); + + var buffer: []u8 = try allocator.alloc(u8, 4096); + defer allocator.free(buffer); + // var buffer = try std.ArrayListSentineled(u8, 0).initSize(allocator, 0); + // defer buffer.deinit(); + var bstream = std.io.fixedBufferStream(buffer); + var stream = bstream.outStream(); + + _ = try stream.write( + \\{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri": + ); + _ = try stream.print("\"{}\",\"diagnostics\":[", .{uri}); + + if (tree.errors.len > 0) { + var index: usize = 0; + while (index < tree.errors.len) : (index += 1) { + + const err = tree.errors.at(index); + const loc = tree.tokenLocation(0, err.loc()); + + _ = try stream.write( + \\{"range":{"start":{ + ); + _ = try stream.print("\"line\":{},\"character\":{}", .{loc.line, loc.column}); + _ = try stream.write( + \\},"end":{ + ); + _ = try stream.print("\"line\":{},\"character\":{}", .{loc.line, loc.column}); + _ = try stream.write( + \\}},"severity":1,"source":"zig-lsp","message":" + ); + _ = try tree.renderError(err, stream); + _ = try stream.print("\",\"code\":\"{}\"", .{@tagName(err.*)}); + _ = try stream.write( + \\,"relatedInformation":[]} + ); + if (index != tree.errors.len - 1) { + _ = try stream.writeByte(','); + } + + } + } + + _ = try stream.write( + \\]}} + ); + + _ = try stdout.print("Content-Length: {}\r\n\r\n", .{bstream.pos}); + _ = try stdout.write(bstream.getWritten()); + +} + +// pub fn signature + +pub fn processJsonRpc(json: []const u8) !void { + + var parser = std.json.Parser.init(allocator, false); + var tree = try parser.parse(json); + defer tree.deinit(); + + const root = tree.root; + + const method = root.Object.getValue("method").?.String; + const id = if (root.Object.getValue("id")) |id| id.Integer else 0; + + const params = root.Object.getValue("params").?.Object; + + // Core + if (std.mem.eql(u8, method, "initialize")) { + try respondGeneric(id, initialize_response); + } else if (std.mem.eql(u8, method, "initialized")) { + // noop + } else if (std.mem.eql(u8, method, "$/cancelRequest")) { + // noop + } + // File changes + else if (std.mem.eql(u8, method, "textDocument/didOpen")) { + const document = params.getValue("textDocument").?.Object; + const uri = document.getValue("uri").?.String; + const text = document.getValue("text").?.String; + + try processSource(uri, text); + } else if (std.mem.eql(u8, method, "textDocument/didChange")) { + const document = params.getValue("textDocument").?.Object; + const uri = document.getValue("uri").?.String; + const text = params.getValue("contentChanges").?.Array.items[0].Object.getValue("text").?.String; + + try processSource(uri, text); + } else if (std.mem.eql(u8, method, "textDocument/didSave")) { + // noop + } else if (std.mem.eql(u8, method, "textDocument/didClose")) { + // noop + } + // Autocomplete / Signatures + else if (std.mem.eql(u8, method, "textDocument/completion")) { + try respondGeneric(id, no_completions_response); + } else if (std.mem.eql(u8, method, "textDocument/signatureHelp")) { + try respondGeneric(id, + \\,"result":{"signatures":[{ + \\"label": "nameOfFunction(aNumber: u8)", + \\"documentation": {"kind": "markdown", "value": "Description of the function in **Markdown**!"}, + \\"parameters": [ + \\{"label": [15, 27], "documentation": {"kind": "markdown", "value": "An argument"}} + \\] + \\}]}} + ); + } else if (root.Object.getValue("id")) |_| { + try log("Method with return value not implemented: {}", .{method}); + try respondGeneric(id, not_implemented_response); + } else { + try log("Method without return value not implemented: {}", .{method}); + } + +} + +pub fn main() anyerror!void { + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + allocator = &arena.allocator; + + var buffer = std.ArrayList(u8).init(allocator); + defer buffer.deinit(); + + try buffer.resize(4096); + + const stdin = std.io.getStdIn().inStream(); + stdout = std.io.getStdOut().outStream(); + + var offset: usize = 0; + var bytes_read: usize = 0; + + var index: usize = 0; + var content_len: usize = 0; + + stdin_poll: while (true) { + + // var bytes = stdin.read(buffer.items[0..6]) catch return; + + if (offset >= 16 and std.mem.eql(u8, "Content-Length: ", buffer.items[0..16])) { + + index = 16; + while (index <= offset + 10) : (index += 1) { + const c = buffer.items[index]; + if (c >= '0' and c <= '9') { + content_len = content_len * 10 + (c - '0'); + } if (c == '\r' and buffer.items[index + 1] == '\n') { + index += 2; + break; + } + } + + // buffer.items[offset] = try stdin.readByte();= + if (buffer.items[index] == '\r') { + index += 2; + if (buffer.items.len < index + content_len) { + try buffer.resize(index + content_len); + } + + body_poll: while (offset < content_len + index) { + bytes_read = stdin.read(buffer.items[offset .. index + content_len]) catch return; + if (bytes_read == 0) { + try log("0 bytes written; exiting!", .{}); + return; + } + + offset += bytes_read; + } + + try processJsonRpc(buffer.items[index .. index + content_len]); + + offset = 0; + content_len = 0; + } else { + try log("\\r not found", .{}); + } + + } else if (offset >= 16) { + try log("Offset is greater than 16!", .{}); + return; + } + + if (offset < 16) { + bytes_read = stdin.read(buffer.items[offset..25]) catch return; + } else { + if (offset == buffer.items.len) { + try buffer.resize(buffer.items.len * 2); + } + if (index + content_len > buffer.items.len) { + bytes_read = stdin.read(buffer.items[offset..buffer.items.len]) catch { + try log("Error reading!", .{}); + return; + }; + } else { + bytes_read = stdin.read(buffer.items[offset .. index + content_len]) catch { + try log("Error reading!", .{}); + return; + }; + } + } + + if (bytes_read == 0) { + try log("0 bytes written; exiting!", .{}); + return; + } + + offset += bytes_read; + + } +} diff --git a/src/uri.zig b/src/uri.zig new file mode 100644 index 0000000..971c866 --- /dev/null +++ b/src/uri.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +// Original code: https://github.com/andersfr/zig-lsp/blob/master/uri.zig + +fn parseHex(c: u8) !u8 { + return switch(c) { + '0'...'9' => c - '0', + 'a'...'f' => c - 'a' + 10, + 'A'...'F' => c - 'A' + 10, + else => return error.UriBadHexChar, + }; +} + +/// Caller should free memory +pub fn parse(allocator: *std.mem.Allocator, str: []const u8) ![]u8 { + if (str.len < 7 or !std.mem.eql(u8, "file://", str[0..7])) return error.UriBadScheme; + + var uri = try allocator.alloc(u8, str.len - (if (std.fs.path.sep == '\\') 8 else 7)); + errdefer allocator.free(uri); + + const path = if (std.fs.path.sep == '\\') str[8..] else str[7..]; + + var i: usize = 0; + var j: usize = 0; + var e: usize = path.len; + while (j < e) : (i += 1) { + if (path[j] == '%') { + if (j + 2 >= e) return error.UriBadEscape; + const upper = try parseHex(path[j + 1]); + const lower = try parseHex(path[j + 2]); + uri[i] = (upper << 4) + lower; + j += 3; + } else { + uri[i] = if (path[j] == '/') std.fs.path.sep else path[j]; + j += 1; + } + } + + // Remove trailing separator + if (i > 0 and uri[i - 1] == std.fs.path.sep) { + i -= 1; + } + + return allocator.shrink(uri, i); +}