diff --git a/build.zig b/build.zig index d62a931..310531b 100644 --- a/build.zig +++ b/build.zig @@ -144,6 +144,7 @@ pub fn build(b: *std.Build) void { .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, .{ .name = "tracy", .module = tracy_mod }, + .{ .name = "syntax", .module = syntax_dep.module("syntax") }, }, }); diff --git a/help.md b/help.md index 95b0b20..8c1f094 100644 --- a/help.md +++ b/help.md @@ -241,6 +241,11 @@ cycle style of editing. - Ctrl-F10 => Toggle visible whitespace mode +### Language Server Commands + +- F12 => + Goto definition + ### Debugging Commands - F5, Ctrl-Shift-i => @@ -255,9 +260,6 @@ cycle style of editing. - F11, Ctrl-J, Alt-l => Toggle log view -- F12, Alt-i => - Toggle input view - - Ctrl-Shift-/ => Dump current widget tree to log view diff --git a/src/LSP.zig b/src/LSP.zig index baf2689..0bc1912 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -5,7 +5,7 @@ const root = @import("root"); const tracy = @import("tracy"); a: std.mem.Allocator, -pid: ?tp.pid, +pid: tp.pid, const Self = @This(); const module_name = @typeName(Self); @@ -13,34 +13,29 @@ const sp_tag = "child"; 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 open(a: std.mem.Allocator, cmd: tp.message) Error!Self { + return .{ .a = a, .pid = try Process.create(a, cmd) }; } pub fn deinit(self: *Self) void { - if (self.pid) |pid| { - pid.send(.{"close"}) catch {}; - self.pid = null; - pid.deinit(); - } + self.pid.send(.{"close"}) catch {}; + self.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); + return self.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 }); + return self.pid.send(.{ "NTFY", method, cb.items }); } pub fn close(self: *Self) void { @@ -62,7 +57,15 @@ const Process = struct { const Receiver = tp.Receiver(*Process); - pub fn create(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!tp.pid { + pub fn create(a: std.mem.Allocator, cmd: tp.message) Error!tp.pid { + var tag: []const u8 = undefined; + if (try cmd.match(.{tp.extract(&tag)})) { + // + } else if (try cmd.match(.{ tp.extract(&tag), tp.more })) { + // + } else { + return tp.exit("no LSP command"); + } const self = try a.create(Process); var sp_tag_ = std.ArrayList(u8).init(a); defer sp_tag_.deinit(); @@ -78,7 +81,7 @@ const Process = struct { .requests = std.AutoHashMap(i32, tp.pid).init(a), .sp_tag = try sp_tag_.toOwnedSliceSentinel(0), }; - return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e); + return tp.spawn_link(self.a, self, Process.start, self.tag) catch |e| tp.exit_error(e); } fn deinit(self: *Process) void { @@ -225,13 +228,16 @@ const Process = struct { try cbor.writeValue(msg_writer, "params"); _ = try msg_writer.write(params_cb); - const json = try cbor.toJsonPrettyAlloc(self.a, msg.items); + const json = try cbor.toJsonAlloc(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\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n", .{json.len}); + const terminator = "\r\n"; + const content_length = json.len + terminator.len; + try writer.print("Content-Length: {d}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n", .{content_length}); _ = try writer.write(json); + _ = try writer.write(terminator); try sp.send(output.items); self.write_log("### SEND request:\n{s}\n###\n", .{output.items}); @@ -241,6 +247,8 @@ const Process = struct { fn send_notification(self: *Process, method: []const u8, params_cb: []const u8) !void { const sp = if (self.sp) |*sp| sp else return error.Closed; + const have_params = !(cbor.match(params_cb, cbor.null_) catch false); + var msg = std.ArrayList(u8).init(self.a); defer msg.deinit(); const msg_writer = msg.writer(); @@ -250,15 +258,22 @@ const Process = struct { try cbor.writeValue(msg_writer, "method"); try cbor.writeValue(msg_writer, method); try cbor.writeValue(msg_writer, "params"); - _ = try msg_writer.write(params_cb); + if (have_params) { + _ = try msg_writer.write(params_cb); + } else { + try cbor.writeMapHeader(msg_writer, 0); + } - const json = try cbor.toJsonPrettyAlloc(self.a, msg.items); + const json = try cbor.toJsonAlloc(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}); + const terminator = "\r\n"; + const content_length = json.len + terminator.len; + try writer.print("Content-Length: {d}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n", .{content_length}); _ = try writer.write(json); + _ = try writer.write(terminator); try sp.send(output.items); self.write_log("### SEND notification:\n{s}\n###\n", .{output.items}); diff --git a/src/Project.zig b/src/Project.zig index edd8a8c..e3d7e85 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -9,8 +9,8 @@ a: std.mem.Allocator, name: []const u8, files: std.ArrayList(File), open_time: i64, -lsp: ?LSP = null, -lsp_name: [:0]const u8, +language_servers: std.StringHashMap(LSP), +file_language_server: std.StringHashMap(LSP), const Self = @This(); @@ -25,27 +25,44 @@ 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", + .language_servers = std.StringHashMap(LSP).init(a), + .file_language_server = std.StringHashMap(LSP).init(a), }; } pub fn deinit(self: *Self) void { + var i_ = self.file_language_server.iterator(); + while (i_.next()) |p| { + self.a.free(p.key_ptr.*); + } + var i = self.language_servers.iterator(); + while (i.next()) |p| { + self.a.free(p.key_ptr.*); + p.value_ptr.*.deinit(); + } for (self.files.items) |file| self.a.free(file.path); self.files.deinit(); - if (self.lsp) |*lsp| lsp.deinit(); self.a.free(self.name); } -fn get_lsp(self: *Self) !LSP { - if (self.lsp) |lsp| return lsp; - self.lsp = try LSP.open(self.a, tp.message.fmt(.{self.lsp_name}), self.lsp_name); +fn get_lsp(self: *Self, language_server: []const u8) !LSP { + if (self.language_servers.get(language_server)) |lsp| return lsp; + const lsp = try LSP.open(self.a, .{ .buf = language_server }); + try self.language_servers.put(try self.a.dupe(u8, language_server), lsp); const uri = try self.make_URI(null); defer self.a.free(uri); const basename_begin = std.mem.lastIndexOfScalar(u8, self.name, std.fs.path.sep); const basename = if (basename_begin) |begin| self.name[begin + 1 ..] else self.name; - const response = try self.send_lsp_init_request(self.name, basename, uri); + const response = try self.send_lsp_init_request(lsp, self.name, basename, uri); defer self.a.free(response.buf); - return self.lsp.?; + try lsp.send_notification("initialized", .{}); + return lsp; +} + +fn get_file_lsp(self: *Self, file_path: []const u8) !LSP { + const lsp = self.file_language_server.get(file_path) orelse return tp.exit("no language server"); + if (lsp.pid.expired()) return tp.exit("no language server"); + return lsp; } fn make_URI(self: *Self, file_path: ?[]const u8) ![]const u8 { @@ -92,9 +109,12 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co return i; } -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 { - _ = from; // autofix - const lsp = self.get_lsp() catch |e| return tp.exit_error(e); +pub fn did_open(self: *Self, file_path: []const u8, file_type: []const u8, language_server: []const u8, version: usize, text: []const u8) tp.result { + const lsp = self.get_lsp(language_server) catch |e| return tp.exit_error(e); + if (!self.file_language_server.contains(file_path)) { + const key = self.a.dupe(u8, file_path) catch |e| return tp.exit_error(e); + self.file_language_server.put(key, 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); try lsp.send_notification("textDocument/didOpen", .{ @@ -102,8 +122,8 @@ pub fn did_open(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type: }); } -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); +pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) !void { + const lsp = try self.get_file_lsp(file_path); 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", .{ @@ -111,11 +131,100 @@ pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row .position = .{ .line = row, .character = col }, }); defer self.a.free(response.buf); - try from.send_raw(response); + var link: []const u8 = undefined; + if (try response.match(.{ "child", tp.string, "result", tp.array })) { + if (try response.match(.{ tp.any, tp.any, tp.any, .{ tp.extract_cbor(&link), tp.more } })) { + try self.navigate_to_location_link(from, link); + } else if (try response.match(.{ tp.any, tp.any, tp.any, .{tp.extract_cbor(&link)} })) { + try self.navigate_to_location_link(from, link); + } + } else if (try response.match(.{ "child", tp.string, "result", tp.null_ })) { + return; + } else if (try response.match(.{ "child", tp.string, "result", tp.extract_cbor(&link) })) { + try self.navigate_to_location_link(from, link); + } } -fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename: []const u8, project_uri: []const u8) error{Exit}!tp.message { - return self.lsp.?.send_request(self.a, "initialize", .{ +fn navigate_to_location_link(self: *Self, from: tp.pid_ref, location_link: []const u8) !void { + var iter = location_link; + var targetUri: ?[]const u8 = null; + var targetRange: ?Range = null; + var targetSelectionRange: ?Range = null; + 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, "targetUri") or std.mem.eql(u8, field_name, "uri")) { + var value: []const u8 = undefined; + if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidMessageField; + targetUri = value; + } else if (std.mem.eql(u8, field_name, "targetRange") or std.mem.eql(u8, field_name, "range")) { + var range: []const u8 = undefined; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range)))) return error.InvalidMessageField; + targetRange = try read_range(range); + } else if (std.mem.eql(u8, field_name, "targetSelectionRange")) { + var range: []const u8 = undefined; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range)))) return error.InvalidMessageField; + targetSelectionRange = try read_range(range); + } else { + try cbor.skipValue(&iter); + } + } + if (targetUri == null or targetRange == null) return error.InvalidMessageField; + if (!std.mem.eql(u8, targetUri.?[0..7], "file://")) return error.InvalidTargetURI; + const file_path = try std.Uri.unescapeString(self.a, targetUri.?[7..]); + defer self.a.free(file_path); + try from.send(.{ "cmd", "navigate", .{ .file = file_path, .line = targetRange.?.start.line + 1, .column = targetRange.?.start.character + 1 } }); +} + +const Range = struct { start: Position, end: Position }; +fn read_range(range: []const u8) !Range { + var iter = range; + var start: ?Position = null; + var end: ?Position = null; + 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, "start")) { + var position: []const u8 = undefined; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&position)))) return error.InvalidMessageField; + start = try read_position(position); + } else if (std.mem.eql(u8, field_name, "end")) { + var position: []const u8 = undefined; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&position)))) return error.InvalidMessageField; + end = try read_position(position); + } else { + try cbor.skipValue(&iter); + } + } + if (start == null or end == null) return error.InvalidMessageField; + return .{ .start = start.?, .end = end.? }; +} + +const Position = struct { line: usize, character: usize }; +fn read_position(position: []const u8) !Position { + var iter = position; + var line: ?usize = 0; + var character: ?usize = 0; + 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, "line")) { + if (!(try cbor.matchValue(&iter, cbor.extract(&line)))) return error.InvalidMessageField; + } else if (std.mem.eql(u8, field_name, "character")) { + if (!(try cbor.matchValue(&iter, cbor.extract(&character)))) return error.InvalidMessageField; + } else { + try cbor.skipValue(&iter); + } + } + if (line == null or character == null) return error.InvalidMessageField; + return .{ .line = line.?, .character = character.? }; +} + +fn send_lsp_init_request(self: *Self, lsp: LSP, project_path: []const u8, project_basename: []const u8, project_uri: []const u8) error{Exit}!tp.message { + return lsp.send_request(self.a, "initialize", .{ .processId = std.os.linux.getpid(), .rootPath = project_path, .rootUri = project_uri, @@ -145,7 +254,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename .normalizesLineEndings = true, .changeAnnotationSupport = .{ .groupsOnLabel = true }, }, - .configuration = true, + // .configuration = true, .didChangeWatchedFiles = .{ .dynamicRegistration = true, .relativePatternSupport = true, @@ -160,7 +269,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename }, .codeLens = .{ .refreshSupport = true }, .executeCommand = .{ .dynamicRegistration = true }, - .didChangeConfiguration = .{ .dynamicRegistration = true }, + // .didChangeConfiguration = .{ .dynamicRegistration = true }, .workspaceFolders = true, .semanticTokens = .{ .refreshSupport = true }, .fileOperations = .{ @@ -402,7 +511,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename .parser = "marked", .version = "1.1.0", }, - .positionEncodings = .{"utf-16"}, + .positionEncodings = .{"utf-8"}, }, .notebookDocument = .{ .synchronization = .{ diff --git a/src/project_manager.zig b/src/project_manager.zig index ccb08c0..a5bc84c 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -3,6 +3,7 @@ const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); const tracy = @import("tracy"); +const FileType = @import("syntax").FileType; const Project = @import("Project.zig"); @@ -57,11 +58,12 @@ 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 did_open(file_path: []const u8, file_type: []const u8, version: usize, text: []const u8) tp.result { +pub fn did_open(file_path: []const u8, file_type: *const FileType, 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(.{ "did_open", project, file_path, file_type, version, @intFromPtr(text.ptr), text.len }); + const text_ptr: usize = if (text.len > 0) @intFromPtr(text.ptr) else 0; + return (try get()).pid.send(.{ "did_open", project, file_path, file_type.name, file_type.language_server, version, text_ptr, text.len }); } pub fn goto_definition(file_path: []const u8, row: usize, col: usize) tp.result { @@ -119,6 +121,7 @@ const Process = struct { var path: []const u8 = undefined; var query: []const u8 = undefined; var file_type: []const u8 = undefined; + var language_server: []const u8 = undefined; var high: i64 = 0; var low: i64 = 0; var max: usize = 0; @@ -144,15 +147,16 @@ const Process = struct { std.time.milliTimestamp() - project.open_time, }); } else if (try m.match(.{ "open", tp.extract(&project_directory) })) { - self.open(project_directory) catch |e| return from.send_raw(tp.exit_message(e)); + self.open(project_directory) catch |e| return from.forward_error(e); } else if (try m.match(.{ "request_recent_files", tp.extract(&project_directory), tp.extract(&max) })) { - self.request_recent_files(from, project_directory, max) catch |e| return from.send_raw(tp.exit_message(e)); + self.request_recent_files(from, project_directory, max) catch |e| return from.forward_error(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(.{ "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)); + self.query_recent_files(from, project_directory, max, query) catch |e| return from.forward_error(e); + } else if (try m.match(.{ "did_open", tp.extract(&project_directory), tp.extract(&path), tp.extract(&file_type), tp.extract_cbor(&language_server), tp.extract(&version), tp.extract(&text_ptr), tp.extract(&text_len) })) { + const text = if (text_len > 0) @as([*]const u8, @ptrFromInt(text_ptr))[0..text_len] else ""; + self.did_open(project_directory, path, file_type, language_server, version, text) catch |e| return from.forward_error(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)); + self.goto_definition(from, project_directory, path, row, col) catch |e| return from.forward_error(e); } else if (try m.match(.{"shutdown"})) { if (self.walker) |pid| pid.send(.{"stop"}) catch {}; try from.send(.{ "project_manager", "shutdown" }); @@ -189,18 +193,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 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 { + fn did_open(self: *Process, project_directory: []const u8, file_path: []const u8, file_type: []const u8, language_server: []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.did_open(from, file_path, file_type, version, text); + return project.did_open(file_path, file_type, language_server, 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); + return project.goto_definition(from, file_path, row, col) catch |e| tp.exit_error(e); } }; diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 37c4383..697c311 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -366,9 +366,8 @@ pub const Editor = struct { 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 {}; + if (self.syntax) |syn| + project_manager.did_open(file_path, syn.file_type, 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 "🖹";