diff --git a/build.zig.zon b/build.zig.zon index fcccd6a..3c7b427 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/src/LSP.zig b/src/LSP.zig new file mode 100644 index 0000000..7f19c47 --- /dev/null +++ b/src/LSP.zig @@ -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; + } + } +}; diff --git a/src/Lsp.zig b/src/Lsp.zig deleted file mode 100644 index d71f065..0000000 --- a/src/Lsp.zig +++ /dev/null @@ -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; - } - } -}; diff --git a/src/Project.zig b/src/Project.zig index d9486ed..c8f596b 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -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; } diff --git a/src/main.zig b/src/main.zig index a36b5d8..3bafc95 100644 --- a/src/main.zig +++ b/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; diff --git a/src/project_manager.zig b/src/project_manager.zig index 10830fe..ccb08c0 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -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); } }; diff --git a/src/tui/editor.zig b/src/tui/editor.zig index d72a048..b305e07 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -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 { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index df8ef23..c4097ba 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -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();