feat: lots more work on LSP support (still WIP)
This commit is contained in:
parent
f460490510
commit
1406052c06
8 changed files with 472 additions and 182 deletions
|
@ -20,8 +20,8 @@
|
|||
.hash = "1220a7cf5f59b61257993bc5b02991ffc523d103f66842fa8d8ab5c9fdba52799340",
|
||||
},
|
||||
.thespian = .{
|
||||
.url = "https://github.com/neurocyte/thespian/archive/d20e071a4d50b92bce196040b151988f96985c87.tar.gz",
|
||||
.hash = "12209e9d22ddb9ef7ff18e8781234fbb149c297cf23dda366269b0bbee437a2b8f32",
|
||||
.url = "https://github.com/neurocyte/thespian/archive/0559a75dbbe5fb372f3799f02d35a4b86ffdbc26.tar.gz",
|
||||
.hash = "1220d09aa407fc93e8c133e71bc92e9270c2bb8881bd592f92d5438837ce126b1bc8",
|
||||
},
|
||||
.themes = .{
|
||||
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-69be8cd05fddcbc2a3ca2dec4abe6b8d07ed65b1/flow-themes.tar.gz",
|
||||
|
|
343
src/LSP.zig
Normal file
343
src/LSP.zig
Normal file
|
@ -0,0 +1,343 @@
|
|||
const std = @import("std");
|
||||
const tp = @import("thespian");
|
||||
const cbor = @import("cbor");
|
||||
const root = @import("root");
|
||||
const tracy = @import("tracy");
|
||||
|
||||
a: std.mem.Allocator,
|
||||
pid: ?tp.pid,
|
||||
|
||||
const Self = @This();
|
||||
const module_name = @typeName(Self);
|
||||
const sp_tag = "LSP";
|
||||
const debug_lsp = true;
|
||||
pub const Error = error{ OutOfMemory, Exit };
|
||||
|
||||
pub fn open(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!Self {
|
||||
return .{ .a = a, .pid = try Process.create(a, cmd, tag) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.pid) |pid| {
|
||||
pid.send(.{"close"}) catch {};
|
||||
self.pid = null;
|
||||
pid.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_request(self: Self, a: std.mem.Allocator, method: []const u8, m: anytype) error{Exit}!tp.message {
|
||||
// const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".send_request" });
|
||||
// defer frame.deinit();
|
||||
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
|
||||
var cb = std.ArrayList(u8).init(self.a);
|
||||
defer cb.deinit();
|
||||
cbor.writeValue(cb.writer(), m) catch |e| return tp.exit_error(e);
|
||||
return pid.call(a, .{ "REQ", method, cb.items }) catch |e| return tp.exit_error(e);
|
||||
}
|
||||
|
||||
pub fn send_notification(self: Self, method: []const u8, m: anytype) tp.result {
|
||||
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
|
||||
var cb = std.ArrayList(u8).init(self.a);
|
||||
defer cb.deinit();
|
||||
cbor.writeValue(cb.writer(), m) catch |e| return tp.exit_error(e);
|
||||
return pid.send(.{ "NTFY", method, cb.items });
|
||||
}
|
||||
|
||||
pub fn close(self: *Self) void {
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
const Process = struct {
|
||||
a: std.mem.Allocator,
|
||||
cmd: tp.message,
|
||||
receiver: Receiver,
|
||||
sp: ?tp.subprocess = null,
|
||||
recv_buf: std.ArrayList(u8),
|
||||
parent: tp.pid,
|
||||
tag: [:0]const u8,
|
||||
log_file: ?std.fs.File = null,
|
||||
next_id: i32 = 0,
|
||||
requests: std.AutoHashMap(i32, tp.pid),
|
||||
|
||||
const Receiver = tp.Receiver(*Process);
|
||||
|
||||
pub fn create(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!tp.pid {
|
||||
const self = try a.create(Process);
|
||||
self.* = .{
|
||||
.a = a,
|
||||
.cmd = try cmd.clone(a),
|
||||
.receiver = Receiver.init(receive, self),
|
||||
.recv_buf = std.ArrayList(u8).init(a),
|
||||
.parent = tp.self_pid().clone(),
|
||||
.tag = try a.dupeZ(u8, tag),
|
||||
.requests = std.AutoHashMap(i32, tp.pid).init(a),
|
||||
};
|
||||
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
|
||||
}
|
||||
|
||||
fn deinit(self: *Process) void {
|
||||
var i = self.requests.iterator();
|
||||
while (i.next()) |req| req.value_ptr.deinit();
|
||||
self.recv_buf.deinit();
|
||||
self.a.free(self.cmd.buf);
|
||||
if (self.log_file) |file| file.close();
|
||||
self.close() catch {};
|
||||
}
|
||||
|
||||
fn close(self: *Process) tp.result {
|
||||
if (self.sp) |*sp| {
|
||||
defer self.sp = null;
|
||||
try sp.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn start(self: *Process) tp.result {
|
||||
const frame = tracy.initZone(@src(), .{ .name = module_name ++ " start" });
|
||||
defer frame.deinit();
|
||||
_ = tp.set_trap(true);
|
||||
self.sp = tp.subprocess.init(self.a, self.cmd, sp_tag, .Pipe) catch |e| return tp.exit_error(e);
|
||||
tp.receive(&self.receiver);
|
||||
|
||||
var log_file_path = std.ArrayList(u8).init(self.a);
|
||||
defer log_file_path.deinit();
|
||||
const cache_dir = root.get_cache_dir() catch |e| return tp.exit_error(e);
|
||||
log_file_path.writer().print("{s}/lsp-{s}.log", .{ cache_dir, self.tag }) catch |e| return tp.exit_error(e);
|
||||
self.log_file = std.fs.createFileAbsolute(log_file_path.items, .{ .truncate = true }) catch |e| return tp.exit_error(e);
|
||||
}
|
||||
|
||||
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
|
||||
const frame = tracy.initZone(@src(), .{ .name = module_name });
|
||||
defer frame.deinit();
|
||||
errdefer self.deinit();
|
||||
var method: []u8 = "";
|
||||
var bytes: []u8 = "";
|
||||
|
||||
if (try m.match(.{ "REQ", tp.extract(&method), tp.extract(&bytes) })) {
|
||||
self.send_request(from, method, bytes) catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{ "NTFY", tp.extract(&method), tp.extract(&bytes) })) {
|
||||
self.send_notification(method, bytes) catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{"close"})) {
|
||||
try self.close();
|
||||
} else if (try m.match(.{ sp_tag, "stdout", tp.extract(&bytes) })) {
|
||||
self.handle_output(bytes) catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{ sp_tag, "term", tp.more })) {
|
||||
self.handle_terminated() catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{ sp_tag, "stderr", tp.extract(&bytes) })) {
|
||||
self.write_log("{s}\n", .{bytes});
|
||||
} else if (try m.match(.{ "exit", "normal" })) {
|
||||
return tp.exit_normal();
|
||||
} else {
|
||||
const e = tp.unexpected(m);
|
||||
self.write_log("{s}\n", .{tp.error_text()});
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_lsp_message(self: *Process, cb: []const u8) !void {
|
||||
var iter = cb;
|
||||
|
||||
const MsgMembers = struct {
|
||||
id: ?i32 = null,
|
||||
method: ?[]const u8 = null,
|
||||
params: ?[]const u8 = null,
|
||||
result: ?[]const u8 = null,
|
||||
@"error": ?[]const u8 = null,
|
||||
};
|
||||
var values: MsgMembers = .{};
|
||||
|
||||
var len = try cbor.decodeMapHeader(&iter);
|
||||
while (len > 0) : (len -= 1) {
|
||||
var field_name: []const u8 = undefined;
|
||||
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
|
||||
if (std.mem.eql(u8, field_name, "id")) {
|
||||
if (!(try cbor.matchValue(&iter, cbor.extract(&values.id)))) return error.InvalidMessageField;
|
||||
} else if (std.mem.eql(u8, field_name, "method")) {
|
||||
if (!(try cbor.matchValue(&iter, cbor.extract(&values.method)))) return error.InvalidMessageField;
|
||||
} else if (std.mem.eql(u8, field_name, "params")) {
|
||||
var value: []const u8 = undefined;
|
||||
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&value)))) return error.InvalidMessageField;
|
||||
values.params = value;
|
||||
} else if (std.mem.eql(u8, field_name, "result")) {
|
||||
var value: []const u8 = undefined;
|
||||
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&value)))) return error.InvalidMessageField;
|
||||
values.result = value;
|
||||
} else if (std.mem.eql(u8, field_name, "error")) {
|
||||
var value: []const u8 = undefined;
|
||||
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&value)))) return error.InvalidMessageField;
|
||||
values.@"error" = value;
|
||||
} else {
|
||||
try cbor.skipValue(&iter);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.id) |id| {
|
||||
return if (values.method) |method| // Request messages have a method
|
||||
self.receive_lsp_request(id, method, values.params)
|
||||
else // Everything else is a Response message
|
||||
self.receive_lsp_response(id, values.result, values.@"error");
|
||||
} else { // Notification message has no ID
|
||||
return if (values.method) |method|
|
||||
self.receive_lsp_notification(method, values.params)
|
||||
else
|
||||
error.InvalidMessage;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_output(self: *Process, bytes: []u8) !void {
|
||||
try self.recv_buf.appendSlice(bytes);
|
||||
self.write_log("### RECV:\n{s}\n###\n", .{bytes});
|
||||
try self.frame_message_recv();
|
||||
}
|
||||
|
||||
fn handle_terminated(self: *Process) !void {
|
||||
self.write_log("terminated\n", .{});
|
||||
try self.parent.send(.{ self.tag, "done" });
|
||||
}
|
||||
|
||||
fn send_request(self: *Process, from: tp.pid_ref, method: []const u8, params_cb: []const u8) !void {
|
||||
const sp = if (self.sp) |*sp| sp else return error.Closed;
|
||||
|
||||
const id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
var msg = std.ArrayList(u8).init(self.a);
|
||||
defer msg.deinit();
|
||||
const msg_writer = msg.writer();
|
||||
try cbor.writeMapHeader(msg_writer, 4);
|
||||
try cbor.writeValue(msg_writer, "jsonrpc");
|
||||
try cbor.writeValue(msg_writer, "2.0");
|
||||
try cbor.writeValue(msg_writer, "id");
|
||||
try cbor.writeValue(msg_writer, id);
|
||||
try cbor.writeValue(msg_writer, "method");
|
||||
try cbor.writeValue(msg_writer, method);
|
||||
try cbor.writeValue(msg_writer, "params");
|
||||
_ = try msg_writer.write(params_cb);
|
||||
|
||||
const json = try cbor.toJsonPrettyAlloc(self.a, msg.items);
|
||||
defer self.a.free(json);
|
||||
var output = std.ArrayList(u8).init(self.a);
|
||||
defer output.deinit();
|
||||
const writer = output.writer();
|
||||
try writer.print("Content-Length: {d}\r\n\r\n", .{json.len});
|
||||
_ = try writer.write(json);
|
||||
|
||||
try sp.send(output.items);
|
||||
self.write_log("### SEND request:\n{s}\n###\n", .{output.items});
|
||||
try self.requests.put(id, from.clone());
|
||||
}
|
||||
|
||||
fn send_notification(self: *Process, method: []const u8, params_cb: []const u8) !void {
|
||||
const sp = if (self.sp) |*sp| sp else return error.Closed;
|
||||
|
||||
var msg = std.ArrayList(u8).init(self.a);
|
||||
defer msg.deinit();
|
||||
const msg_writer = msg.writer();
|
||||
try cbor.writeMapHeader(msg_writer, 3);
|
||||
try cbor.writeValue(msg_writer, "jsonrpc");
|
||||
try cbor.writeValue(msg_writer, "2.0");
|
||||
try cbor.writeValue(msg_writer, "method");
|
||||
try cbor.writeValue(msg_writer, method);
|
||||
try cbor.writeValue(msg_writer, "params");
|
||||
_ = try msg_writer.write(params_cb);
|
||||
|
||||
const json = try cbor.toJsonPrettyAlloc(self.a, msg.items);
|
||||
defer self.a.free(json);
|
||||
var output = std.ArrayList(u8).init(self.a);
|
||||
defer output.deinit();
|
||||
const writer = output.writer();
|
||||
try writer.print("Content-Length: {d}\r\n\r\n", .{json.len});
|
||||
_ = try writer.write(json);
|
||||
|
||||
try sp.send(output.items);
|
||||
self.write_log("### SEND notification:\n{s}\n###\n", .{output.items});
|
||||
}
|
||||
|
||||
fn frame_message_recv(self: *Process) !void {
|
||||
const sep = "\r\n\r\n";
|
||||
const headers_end = std.mem.indexOf(u8, self.recv_buf.items, sep) orelse return;
|
||||
const headers_data = self.recv_buf.items[0..headers_end];
|
||||
const headers = try Headers.parse(headers_data);
|
||||
if (self.recv_buf.items.len - (headers_end + sep.len) < headers.content_length) return;
|
||||
const buf = try self.recv_buf.toOwnedSlice();
|
||||
const data = buf[headers_end + sep.len .. headers_end + sep.len + headers.content_length];
|
||||
const rest = buf[headers_end + sep.len + headers.content_length ..];
|
||||
defer self.a.free(buf);
|
||||
if (rest.len > 0) try self.recv_buf.appendSlice(rest);
|
||||
const message = .{ .body = data[0..headers.content_length] };
|
||||
const cb = try cbor.fromJsonAlloc(self.a, message.body);
|
||||
defer self.a.free(cb);
|
||||
return self.receive_lsp_message(cb);
|
||||
}
|
||||
|
||||
fn receive_lsp_request(self: *Process, id: i32, method: []const u8, params: ?[]const u8) !void {
|
||||
const json = if (params) |p| try cbor.toJsonPrettyAlloc(self.a, p) else null;
|
||||
defer if (json) |p| self.a.free(p);
|
||||
self.write_log("### RECV req: {d}\nmethod: {s}\n{s}\n###\n", .{ id, method, json orelse "no params" });
|
||||
}
|
||||
|
||||
fn receive_lsp_response(self: *Process, id: i32, result: ?[]const u8, err: ?[]const u8) !void {
|
||||
const json = if (result) |p| try cbor.toJsonPrettyAlloc(self.a, p) else null;
|
||||
defer if (json) |p| self.a.free(p);
|
||||
const json_err = if (err) |p| try cbor.toJsonPrettyAlloc(self.a, p) else null;
|
||||
defer if (json_err) |p| self.a.free(p);
|
||||
self.write_log("### RECV rsp: {d} {s}\n{s}\n###\n", .{ id, if (json_err) |_| "error" else "response", json_err orelse json orelse "no result" });
|
||||
const from = self.requests.get(id) orelse return;
|
||||
var msg = std.ArrayList(u8).init(self.a);
|
||||
defer msg.deinit();
|
||||
const writer = msg.writer();
|
||||
try cbor.writeArrayHeader(writer, 2);
|
||||
if (err) |err_| {
|
||||
try cbor.writeValue(writer, "error");
|
||||
try cbor.writeValue(writer, err_);
|
||||
} else if (result) |result_| {
|
||||
try cbor.writeValue(writer, "result");
|
||||
try cbor.writeValue(writer, result_);
|
||||
}
|
||||
try from.send_raw(.{ .buf = msg.items });
|
||||
}
|
||||
|
||||
fn receive_lsp_notification(self: *Process, method: []const u8, params: ?[]const u8) !void {
|
||||
const json = if (params) |p| try cbor.toJsonPrettyAlloc(self.a, p) else null;
|
||||
defer if (json) |p| self.a.free(p);
|
||||
self.write_log("### RECV notify:\nmethod: {s}\n{s}\n###\n", .{ method, json orelse "no params" });
|
||||
}
|
||||
|
||||
fn write_log(self: *Process, comptime format: []const u8, args: anytype) void {
|
||||
if (!debug_lsp) return;
|
||||
const file = self.log_file orelse return;
|
||||
file.writer().print(format, args) catch {};
|
||||
}
|
||||
};
|
||||
|
||||
const Headers = struct {
|
||||
content_length: usize = 0,
|
||||
content_type: ?[]const u8 = null,
|
||||
|
||||
fn parse(buf_: []const u8) !Headers {
|
||||
var buf = buf_;
|
||||
var ret: Headers = .{};
|
||||
while (true) {
|
||||
const sep = std.mem.indexOf(u8, buf, ":") orelse return error.InvalidSyntax;
|
||||
const name = buf[0..sep];
|
||||
const end = std.mem.indexOf(u8, buf, "\r\n") orelse buf.len;
|
||||
const vstart = if (buf.len > sep + 1)
|
||||
if (buf[sep + 1] == ' ')
|
||||
sep + 2
|
||||
else
|
||||
sep + 1
|
||||
else
|
||||
sep + 1;
|
||||
const value = buf[vstart..end];
|
||||
try ret.parse_one(name, value);
|
||||
buf = if (end < buf.len - 2) buf[end + 2 ..] else return ret;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_one(self: *Headers, name: []const u8, value: []const u8) !void {
|
||||
if (std.mem.eql(u8, "Content-Length", name)) {
|
||||
self.content_length = try std.fmt.parseInt(@TypeOf(self.content_length), value, 10);
|
||||
} else if (std.mem.eql(u8, "Content-Type", name)) {
|
||||
self.content_type = value;
|
||||
}
|
||||
}
|
||||
};
|
159
src/Lsp.zig
159
src/Lsp.zig
|
@ -1,159 +0,0 @@
|
|||
const std = @import("std");
|
||||
const tp = @import("thespian");
|
||||
const cbor = @import("cbor");
|
||||
const log = @import("log");
|
||||
|
||||
pid: ?tp.pid,
|
||||
|
||||
const Self = @This();
|
||||
const module_name = @typeName(Self);
|
||||
const sp_tag = "LSP";
|
||||
pub const Error = error{ OutOfMemory, Exit };
|
||||
|
||||
pub fn open(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!Self {
|
||||
return .{ .pid = try Process.create(a, cmd, tag) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
if (self.pid) |pid| {
|
||||
pid.send(.{"close"}) catch {};
|
||||
self.pid = null;
|
||||
pid.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(self: *Self, message: []const u8) tp.result {
|
||||
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
|
||||
try pid.send(.{ "M", message });
|
||||
}
|
||||
|
||||
pub fn close(self: *Self) void {
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
const Process = struct {
|
||||
a: std.mem.Allocator,
|
||||
cmd: tp.message,
|
||||
receiver: Receiver,
|
||||
sp: ?tp.subprocess = null,
|
||||
recv_buf: std.ArrayList(u8),
|
||||
parent: tp.pid,
|
||||
tag: [:0]const u8,
|
||||
logger: log.Logger,
|
||||
|
||||
const Receiver = tp.Receiver(*Process);
|
||||
|
||||
pub fn create(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!tp.pid {
|
||||
const self = try a.create(Process);
|
||||
self.* = .{
|
||||
.a = a,
|
||||
.cmd = try cmd.clone(a),
|
||||
.receiver = Receiver.init(receive, self),
|
||||
.recv_buf = std.ArrayList(u8).init(a),
|
||||
.parent = tp.self_pid().clone(),
|
||||
.tag = try a.dupeZ(u8, tag),
|
||||
.logger = log.logger(module_name),
|
||||
};
|
||||
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
|
||||
}
|
||||
|
||||
fn deinit(self: *Process) void {
|
||||
self.recv_buf.deinit();
|
||||
self.logger.deinit();
|
||||
self.a.free(self.cmd.buf);
|
||||
self.close() catch {};
|
||||
}
|
||||
|
||||
fn close(self: *Process) tp.result {
|
||||
if (self.sp) |*sp| {
|
||||
defer self.sp = null;
|
||||
try sp.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn start(self: *Process) tp.result {
|
||||
_ = tp.set_trap(true);
|
||||
self.sp = tp.subprocess.init(self.a, self.cmd, sp_tag, .Pipe) catch |e| return tp.exit_error(e);
|
||||
tp.receive(&self.receiver);
|
||||
}
|
||||
|
||||
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
|
||||
errdefer self.deinit();
|
||||
var bytes: []u8 = "";
|
||||
|
||||
if (try m.match(.{ "S", tp.extract(&bytes) })) {
|
||||
const sp = if (self.sp) |sp| sp else return tp.exit_error(error.Closed);
|
||||
try sp.send(bytes);
|
||||
} else if (try m.match(.{"close"})) {
|
||||
try self.close();
|
||||
} else if (try m.match(.{ sp_tag, "stdout", tp.extract(&bytes) })) {
|
||||
self.handle_output(bytes) catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{ sp_tag, "term", tp.more })) {
|
||||
self.handle_terminated() catch |e| return tp.exit_error(e);
|
||||
} else if (try m.match(.{ sp_tag, "stderr", tp.extract(&bytes) })) {
|
||||
self.logger.print("ERR: {s}", .{bytes});
|
||||
} else if (try m.match(.{ "exit", "normal" })) {
|
||||
return tp.exit_normal();
|
||||
} else {
|
||||
self.logger.err("receive", tp.unexpected(m));
|
||||
return tp.unexpected(m);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_output(self: *Process, bytes: []u8) !void {
|
||||
try self.recv_buf.appendSlice(bytes);
|
||||
self.logger.print("{s}", .{bytes});
|
||||
const message = try self.frame_message() orelse return;
|
||||
_ = message;
|
||||
}
|
||||
|
||||
fn handle_terminated(self: *Process) !void {
|
||||
self.logger.print("done", .{});
|
||||
try self.parent.send(.{ self.tag, "done" });
|
||||
}
|
||||
|
||||
fn frame_message(self: *Process) !?Message {
|
||||
const end = std.mem.indexOf(u8, self.recv_buf.items, "\r\n\r\n") orelse return null;
|
||||
const headers = try Headers.parse(self.recv_buf.items[0..end]);
|
||||
const body = self.recv_buf.items[end + 2 ..];
|
||||
if (body.len < headers.content_length) return null;
|
||||
return .{ .body = body };
|
||||
}
|
||||
};
|
||||
|
||||
const Message = struct {
|
||||
body: []const u8,
|
||||
};
|
||||
|
||||
const Headers = struct {
|
||||
content_length: usize = 0,
|
||||
content_type: ?[]const u8 = null,
|
||||
|
||||
fn parse(buf_: []const u8) !Headers {
|
||||
var buf = buf_;
|
||||
var ret: Headers = .{};
|
||||
while (true) {
|
||||
const sep = std.mem.indexOf(u8, buf, ":") orelse return error.InvalidSyntax;
|
||||
const name = buf[0..sep];
|
||||
const end = std.mem.indexOf(u8, buf, "\r\n") orelse buf.len;
|
||||
const vstart = if (buf.len > sep + 1)
|
||||
if (buf[sep + 1] == ' ')
|
||||
sep + 2
|
||||
else
|
||||
sep + 1
|
||||
else
|
||||
sep + 1;
|
||||
const value = buf[vstart..end];
|
||||
try ret.parse_one(name, value);
|
||||
buf = if (end < buf.len - 2) buf[end + 2 ..] else return ret;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_one(self: *Headers, name: []const u8, value: []const u8) !void {
|
||||
if (std.mem.eql(u8, "Content-Length", name)) {
|
||||
self.content_length = try std.fmt.parseInt(@TypeOf(self.content_length), value, 10);
|
||||
} else if (std.mem.eql(u8, "Content-Type", name)) {
|
||||
self.content_type = value;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,13 +1,16 @@
|
|||
const std = @import("std");
|
||||
const tp = @import("thespian");
|
||||
const cbor = @import("cbor");
|
||||
const root = @import("root");
|
||||
|
||||
const Lsp = @import("Lsp.zig");
|
||||
const LSP = @import("LSP.zig");
|
||||
|
||||
a: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
files: std.ArrayList(File),
|
||||
open_time: i64,
|
||||
lsp: ?Lsp = null,
|
||||
lsp: ?LSP = null,
|
||||
lsp_name: [:0]const u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
|
@ -22,6 +25,7 @@ pub fn init(a: std.mem.Allocator, name: []const u8) error{OutOfMemory}!Self {
|
|||
.name = try a.dupe(u8, name),
|
||||
.files = std.ArrayList(File).init(a),
|
||||
.open_time = std.time.milliTimestamp(),
|
||||
.lsp_name = "zls",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -32,12 +36,45 @@ pub fn deinit(self: *Self) void {
|
|||
self.a.free(self.name);
|
||||
}
|
||||
|
||||
fn get_lsp(self: *Self) !Lsp {
|
||||
fn get_lsp(self: *Self) !LSP {
|
||||
if (self.lsp) |lsp| return lsp;
|
||||
self.lsp = try Lsp.open(self.a, tp.message.fmt(.{"zls"}), "LSP");
|
||||
self.lsp = try LSP.open(self.a, tp.message.fmt(.{self.lsp_name}), self.lsp_name);
|
||||
const uri = try self.make_URI(null);
|
||||
defer self.a.free(uri);
|
||||
const response = try self.lsp.?.send_request(self.a, "initialize", .{
|
||||
.processId = std.os.linux.getpid(),
|
||||
.rootUri = uri,
|
||||
.clientInfo = .{ .name = root.application_name },
|
||||
.capabilities = .{
|
||||
.workspace = .{
|
||||
.applyEdit = true,
|
||||
.codeLens = .{ .refreshSupport = true },
|
||||
.configuration = true,
|
||||
.diagnostics = .{ .refreshSupport = true },
|
||||
.fileOperations = .{
|
||||
.didCreate = true,
|
||||
.didDelete = true,
|
||||
.didRename = true,
|
||||
.willCreate = true,
|
||||
.willDelete = true,
|
||||
.willRename = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
defer self.a.free(response.buf);
|
||||
return self.lsp.?;
|
||||
}
|
||||
|
||||
fn make_URI(self: *Self, file_path: ?[]const u8) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(self.a);
|
||||
if (file_path) |path|
|
||||
try buf.writer().print("file:/{s}/{s}", .{ self.name, path })
|
||||
else
|
||||
try buf.writer().print("file:/{s}", .{self.name});
|
||||
return buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn add_file(self: *Self, path: []const u8, mtime: i128) error{OutOfMemory}!void {
|
||||
(try self.files.addOne()).* = .{ .path = try self.a.dupe(u8, path), .mtime = mtime };
|
||||
}
|
||||
|
@ -73,12 +110,30 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co
|
|||
return i;
|
||||
}
|
||||
|
||||
pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type: []const u8, row: usize, col: usize) tp.result {
|
||||
pub fn did_open(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type: []const u8, version: usize, text: []const u8) tp.result {
|
||||
const lsp = self.get_lsp() catch |e| return tp.exit_error(e);
|
||||
const uri = self.make_URI(file_path) catch |e| return tp.exit_error(e);
|
||||
defer self.a.free(uri);
|
||||
const response = try lsp.send_request(self.a, "textDocument/didOpen", .{
|
||||
.textDocument = .{
|
||||
.uri = uri,
|
||||
.languageId = file_type,
|
||||
.version = version,
|
||||
.text = text,
|
||||
},
|
||||
});
|
||||
defer self.a.free(response.buf);
|
||||
_ = from;
|
||||
}
|
||||
|
||||
pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) tp.result {
|
||||
const lsp = self.get_lsp() catch |e| return tp.exit_error(e);
|
||||
const uri = self.make_URI(file_path) catch |e| return tp.exit_error(e);
|
||||
defer self.a.free(uri);
|
||||
const response = try lsp.send_request(self.a, "textDocument/definition", .{
|
||||
.textDocument = .{ .uri = uri },
|
||||
.position = .{ .line = row, .character = col },
|
||||
});
|
||||
defer self.a.free(response.buf);
|
||||
_ = from;
|
||||
_ = file_path;
|
||||
_ = file_type;
|
||||
_ = row;
|
||||
_ = col;
|
||||
_ = lsp;
|
||||
}
|
||||
|
|
25
src/main.zig
25
src/main.zig
|
@ -286,6 +286,31 @@ fn get_app_config_dir(appname: []const u8) ![]const u8 {
|
|||
return config_dir;
|
||||
}
|
||||
|
||||
pub fn get_cache_dir() ![]const u8 {
|
||||
return get_app_cache_dir(application_name);
|
||||
}
|
||||
|
||||
fn get_app_cache_dir(appname: []const u8) ![]const u8 {
|
||||
const local = struct {
|
||||
var cache_dir_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
||||
var cache_dir: ?[]const u8 = null;
|
||||
};
|
||||
const cache_dir = if (local.cache_dir) |dir|
|
||||
dir
|
||||
else if (std.posix.getenv("XDG_CACHE_HOME")) |xdg|
|
||||
try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/{s}", .{ xdg, appname })
|
||||
else if (std.posix.getenv("HOME")) |home|
|
||||
try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/.cache/{s}", .{ home, appname })
|
||||
else
|
||||
return error.AppCacheDirUnavailable;
|
||||
local.cache_dir = cache_dir;
|
||||
std.fs.makeDirAbsolute(cache_dir) catch |e| switch (e) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => return e,
|
||||
};
|
||||
return cache_dir;
|
||||
}
|
||||
|
||||
fn get_app_config_file_name(appname: []const u8) ![]const u8 {
|
||||
const local = struct {
|
||||
var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined;
|
||||
|
|
|
@ -57,11 +57,18 @@ pub fn query_recent_files(max: usize, query: []const u8) tp.result {
|
|||
return (try get()).pid.send(.{ "query_recent_files", project, max, query });
|
||||
}
|
||||
|
||||
pub fn goto_definition(file_path: []const u8, file_type: []const u8, row: usize, col: usize) tp.result {
|
||||
pub fn did_open(file_path: []const u8, file_type: []const u8, version: usize, text: []const u8) tp.result {
|
||||
const project = tp.env.get().str("project");
|
||||
if (project.len == 0)
|
||||
return tp.exit("No project");
|
||||
return (try get()).pid.send(.{ "goto_definition", project, file_path, file_type, row, col });
|
||||
return (try get()).pid.send(.{ "did_open", project, file_path, file_type, version, @intFromPtr(text.ptr), text.len });
|
||||
}
|
||||
|
||||
pub fn goto_definition(file_path: []const u8, row: usize, col: usize) tp.result {
|
||||
const project = tp.env.get().str("project");
|
||||
if (project.len == 0)
|
||||
return tp.exit("No project");
|
||||
return (try get()).pid.send(.{ "goto_definition", project, file_path, row, col });
|
||||
}
|
||||
|
||||
const Process = struct {
|
||||
|
@ -117,6 +124,9 @@ const Process = struct {
|
|||
var max: usize = 0;
|
||||
var row: usize = 0;
|
||||
var col: usize = 0;
|
||||
var version: usize = 0;
|
||||
var text_ptr: usize = 0;
|
||||
var text_len: usize = 0;
|
||||
|
||||
if (try m.match(.{ "walk_tree_entry", tp.extract(&project_directory), tp.extract(&path), tp.extract(&high), tp.extract(&low) })) {
|
||||
const mtime = (@as(i128, @intCast(high)) << 64) | @as(i128, @intCast(low));
|
||||
|
@ -139,8 +149,10 @@ const Process = struct {
|
|||
self.request_recent_files(from, project_directory, max) catch |e| return from.send_raw(tp.exit_message(e));
|
||||
} else if (try m.match(.{ "query_recent_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) {
|
||||
self.query_recent_files(from, project_directory, max, query) catch |e| return from.send_raw(tp.exit_message(e));
|
||||
} else if (try m.match(.{ "goto_definition", tp.extract(&project_directory), tp.extract(&path), tp.extract(&file_type), tp.extract(&row), tp.extract(&col) })) {
|
||||
self.goto_definition(from, project_directory, path, file_type, row, col) catch |e| return from.send_raw(tp.exit_message(e));
|
||||
} else if (try m.match(.{ "did_open", tp.extract(&project_directory), tp.extract(&path), tp.extract(&file_type), tp.extract(&version), tp.extract(&text_ptr), tp.extract(&text_len) })) {
|
||||
self.did_open(from, project_directory, path, file_type, version, @as([*]const u8, @ptrFromInt(text_ptr))[0..text_len]) catch |e| return from.send_raw(tp.exit_message(e));
|
||||
} else if (try m.match(.{ "goto_definition", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
|
||||
self.goto_definition(from, project_directory, path, row, col) catch |e| return from.send_raw(tp.exit_message(e));
|
||||
} else if (try m.match(.{"shutdown"})) {
|
||||
if (self.walker) |pid| pid.send(.{"stop"}) catch {};
|
||||
try from.send(.{ "project_manager", "shutdown" });
|
||||
|
@ -177,9 +189,18 @@ const Process = struct {
|
|||
// self.logger.print("queried: {s} for {s} match {d} in {d} ms", .{ project_directory, query, matched, std.time.milliTimestamp() - start_time });
|
||||
}
|
||||
|
||||
fn goto_definition(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, file_type: []const u8, row: usize, col: usize) tp.result {
|
||||
fn did_open(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, file_type: []const u8, version: usize, text: []const u8) tp.result {
|
||||
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_open" });
|
||||
defer frame.deinit();
|
||||
const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project");
|
||||
return project.goto_definition(from, file_path, file_type, row, col);
|
||||
return project.did_open(from, file_path, file_type, version, text);
|
||||
}
|
||||
|
||||
fn goto_definition(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) tp.result {
|
||||
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".goto_definition" });
|
||||
defer frame.deinit();
|
||||
const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project");
|
||||
return project.goto_definition(from, file_path, row, col);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -164,6 +164,7 @@ pub const Editor = struct {
|
|||
|
||||
file_path: ?[]const u8,
|
||||
buffer: ?*Buffer,
|
||||
lsp_version: usize = 0,
|
||||
|
||||
cursels: CurSel.List,
|
||||
cursels_saved: CurSel.List,
|
||||
|
@ -356,15 +357,18 @@ pub const Editor = struct {
|
|||
if (self.buffer) |_| try self.close();
|
||||
self.buffer = new_buf;
|
||||
|
||||
var content = std.ArrayList(u8).init(self.a);
|
||||
defer content.deinit();
|
||||
try new_buf.root.store(content.writer());
|
||||
self.syntax = syntax: {
|
||||
var content = std.ArrayList(u8).init(self.a);
|
||||
defer content.deinit();
|
||||
try new_buf.root.store(content.writer());
|
||||
const lang_override = tp.env.get().str("language");
|
||||
if (lang_override.len > 0)
|
||||
break :syntax syntax.create_file_type(self.a, content.items, lang_override) catch null;
|
||||
break :syntax syntax.create_guess_file_type(self.a, content.items, self.file_path) catch null;
|
||||
};
|
||||
// TODO: fix and enable
|
||||
// if (self.syntax) |syn|
|
||||
// project_manager.did_open(file_path, syn.file_type.name, self.lsp_version, try content.toOwnedSlice()) catch {};
|
||||
|
||||
const ftn = if (self.syntax) |syn| syn.file_type.name else "text";
|
||||
const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹";
|
||||
|
@ -3151,8 +3155,7 @@ pub const Editor = struct {
|
|||
pub fn goto_definition(self: *Self, _: command.Context) tp.result {
|
||||
const file_path = self.file_path orelse return;
|
||||
const primary = self.get_primary();
|
||||
const file_type = (self.syntax orelse return).file_type.name;
|
||||
return project_manager.goto_definition(file_path, file_type, primary.cursor.row, primary.cursor.col);
|
||||
return project_manager.goto_definition(file_path, primary.cursor.row, primary.cursor.col);
|
||||
}
|
||||
|
||||
pub fn select(self: *Self, ctx: command.Context) tp.result {
|
||||
|
|
|
@ -202,6 +202,8 @@ fn listen_sigwinch(self: *Self) tp.result {
|
|||
}
|
||||
|
||||
fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result {
|
||||
const frame = tracy.initZone(@src(), .{ .name = "tui" });
|
||||
defer frame.deinit();
|
||||
instance_ = self;
|
||||
defer instance_ = null;
|
||||
errdefer self.deinit();
|
||||
|
|
Loading…
Add table
Reference in a new issue