init
This commit is contained in:
commit
a0ff26cc8f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Zig magic stuff
|
||||
/zig-cache
|
21
README.md
Normal file
21
README.md
Normal file
@ -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).
|
26
build.zig
Normal file
26
build.zig
Normal file
@ -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);
|
||||
}
|
45
src/analysis.zig
Normal file
45
src/analysis.zig
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
275
src/main.zig
Normal file
275
src/main.zig
Normal file
@ -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;
|
||||
|
||||
}
|
||||
}
|
45
src/uri.zig
Normal file
45
src/uri.zig
Normal file
@ -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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user