diff --git a/build.zig b/build.zig index 310531b..023f96f 100644 --- a/build.zig +++ b/build.zig @@ -143,8 +143,10 @@ pub fn build(b: *std.Build) void { .{ .name = "log", .module = log_mod }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "Buffer", .module = Buffer_mod }, .{ .name = "tracy", .module = tracy_mod }, .{ .name = "syntax", .module = syntax_dep.module("syntax") }, + .{ .name = "dizzy", .module = dizzy_dep.module("dizzy") }, }, }); diff --git a/src/LSP.zig b/src/LSP.zig index 0bc1912..1d9e14f 100644 --- a/src/LSP.zig +++ b/src/LSP.zig @@ -35,7 +35,11 @@ pub fn send_notification(self: Self, method: []const u8, m: anytype) tp.result { var cb = std.ArrayList(u8).init(self.a); defer cb.deinit(); cbor.writeValue(cb.writer(), m) catch |e| return tp.exit_error(e); - return self.pid.send(.{ "NTFY", method, cb.items }); + return self.send_notification_raw(method, cb.items); +} + +pub fn send_notification_raw(self: Self, method: []const u8, cb: []const u8) tp.result { + return self.pid.send(.{ "NTFY", method, cb }); } pub fn close(self: *Self) void { diff --git a/src/Project.zig b/src/Project.zig index 20a34eb..2499b09 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -2,6 +2,8 @@ const std = @import("std"); const tp = @import("thespian"); const cbor = @import("cbor"); const root = @import("root"); +const dizzy = @import("dizzy"); +const Buffer = @import("Buffer"); const builtin = @import("builtin"); const LSP = @import("LSP.zig"); @@ -185,6 +187,129 @@ pub fn did_open(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type: }); } +pub fn did_change(self: *Self, file_path: []const u8, version: usize, root_dst_addr: usize, root_src_addr: 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 root_dst: Buffer.Root = if (root_dst_addr == 0) return else @ptrFromInt(root_dst_addr); + const root_src: Buffer.Root = if (root_src_addr == 0) return else @ptrFromInt(root_src_addr); + + var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; + var dst = std.ArrayList(u8).init(self.a); + var src = std.ArrayList(u8).init(self.a); + var scratch = std.ArrayListUnmanaged(u32){}; + var edits_cb = std.ArrayList(u8).init(self.a); + const writer = edits_cb.writer(); + + defer { + edits_cb.deinit(); + dst.deinit(); + src.deinit(); + scratch.deinit(self.a); + dizzy_edits.deinit(self.a); + } + + try root_dst.store(dst.writer()); + try root_src.store(src.writer()); + + const scratch_len = 4 * (dst.items.len + src.items.len) + 2; + try scratch.ensureTotalCapacity(self.a, scratch_len); + scratch.items.len = scratch_len; + + try dizzy.PrimitiveSliceDiffer(u8).diff(self.a, &dizzy_edits, src.items, dst.items, scratch.items); + + var lines_dst: usize = 0; + var pos_src: usize = 0; + var pos_dst: usize = 0; + var last_offset: usize = 0; + var edits_count: usize = 0; + + for (dizzy_edits.items) |dizzy_edit| { + switch (dizzy_edit.kind) { + .equal => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += dist; + pos_dst += dist; + scan_char(src.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', &last_offset); + }, + .insert => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += 0; + pos_dst += dist; + const line_start_dst: usize = lines_dst; + scan_char(dst.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null); + try cbor.writeValue(writer, .{ + .range = .{ + .start = .{ .line = line_start_dst, .character = last_offset }, + .end = .{ .line = line_start_dst, .character = last_offset }, + }, + .text = dst.items[dizzy_edit.range.start..dizzy_edit.range.end], + }); + edits_count += 1; + }, + .delete => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += dist; + pos_dst += 0; + var line_end_dst: usize = lines_dst; + var offset_end_dst: usize = last_offset; + scan_char(src.items[dizzy_edit.range.start..dizzy_edit.range.end], &line_end_dst, '\n', &offset_end_dst); + if (lines_dst == line_end_dst) offset_end_dst = last_offset + dist; + try cbor.writeValue(writer, .{ + .range = .{ + .start = .{ .line = lines_dst, .character = last_offset }, + .end = .{ .line = line_end_dst, .character = offset_end_dst }, + }, + .text = "", + }); + edits_count += 1; + }, + } + } + + var msg = std.ArrayList(u8).init(self.a); + defer msg.deinit(); + const msg_writer = msg.writer(); + try cbor.writeMapHeader(msg_writer, 2); + try cbor.writeValue(msg_writer, "textDocument"); + try cbor.writeValue(msg_writer, .{ .uri = uri, .version = version }); + try cbor.writeValue(msg_writer, "contentChanges"); + try cbor.writeArrayHeader(msg_writer, edits_count); + _ = try msg_writer.write(edits_cb.items); + + try lsp.send_notification_raw("textDocument/didChange", msg.items); +} + +fn scan_char(chars: []const u8, lines: *usize, char: u8, last_offset: ?*usize) void { + var pos = chars; + while (pos.len > 0) { + if (pos[0] == char) { + if (last_offset) |off| off.* = pos.len - 1; + lines.* += 1; + } + pos = pos[1..]; + } +} + +pub fn did_save(self: *Self, file_path: []const u8) tp.result { + 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); + try lsp.send_notification("textDocument/didSave", .{ + .textDocument = .{ .uri = uri }, + }); +} + +pub fn did_close(self: *Self, file_path: []const u8) tp.result { + 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); + try lsp.send_notification("textDocument/didClose", .{ + .textDocument = .{ .uri = uri }, + }); +} + 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); diff --git a/src/project_manager.zig b/src/project_manager.zig index 76a3a4b..7fc6526 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -67,6 +67,27 @@ pub fn did_open(file_path: []const u8, file_type: *const FileType, version: usiz return (try get()).pid.send(.{ "did_open", project, file_path, file_type.name, file_type.language_server, version, text_ptr, text.len }); } +pub fn did_change(file_path: []const u8, version: usize, root_dst: usize, root_src: usize) tp.result { + const project = tp.env.get().str("project"); + if (project.len == 0) + return tp.exit("No project"); + return (try get()).pid.send(.{ "did_change", project, file_path, version, root_dst, root_src }); +} + +pub fn did_save(file_path: []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_save", project, file_path }); +} + +pub fn did_close(file_path: []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_close", project, file_path }); +} + 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) @@ -146,6 +167,9 @@ const Process = struct { var text_ptr: usize = 0; var text_len: usize = 0; + var root_dst: usize = 0; + var root_src: 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)); if (self.projects.get(project_directory)) |project| @@ -173,6 +197,12 @@ const Process = struct { } 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(from, project_directory, path, file_type, language_server, version, text) catch |e| return from.forward_error(e); + } else if (try m.match(.{ "did_change", tp.extract(&project_directory), tp.extract(&path), tp.extract(&version), tp.extract(&root_dst), tp.extract(&root_src) })) { + self.did_change(project_directory, path, version, root_dst, root_src) catch |e| return from.forward_error(e); + } else if (try m.match(.{ "did_save", tp.extract(&project_directory), tp.extract(&path) })) { + self.did_save(project_directory, path) catch |e| return from.forward_error(e); + } else if (try m.match(.{ "did_close", tp.extract(&project_directory), tp.extract(&path) })) { + self.did_close(project_directory, path) 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.forward_error(e); } else if (try m.match(.{ "get_mru_position", tp.extract(&project_directory), tp.extract(&path) })) { @@ -221,6 +251,27 @@ const Process = struct { return project.did_open(from, file_path, file_type, language_server, version, text); } + fn did_change(self: *Process, project_directory: []const u8, file_path: []const u8, version: usize, root_dst: usize, root_src: usize) tp.result { + const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_change" }); + defer frame.deinit(); + const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project"); + return project.did_change(file_path, version, root_dst, root_src) catch |e| tp.exit_error(e); + } + + fn did_save(self: *Process, project_directory: []const u8, file_path: []const u8) tp.result { + const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_save" }); + defer frame.deinit(); + const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project"); + return project.did_save(file_path); + } + + fn did_close(self: *Process, project_directory: []const u8, file_path: []const u8) tp.result { + const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_close" }); + defer frame.deinit(); + const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project"); + return project.did_close(file_path); + } + 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(); diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f8ac4ed..2e3e339 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -385,6 +385,8 @@ pub const Editor = struct { self.plane.home(); self.plane.context().cursor_disable() catch {}; _ = try self.handlers.msg(.{ "E", "close" }); + if (self.syntax) |_| if (self.file_path) |file_path| + project_manager.did_close(file_path) catch {}; } fn save(self: *Self) !void { @@ -973,8 +975,10 @@ pub const Editor = struct { const dirty = if (self.buffer) |buf| buf.is_dirty() else false; const root: ?Buffer.Root = self.buf_root() catch null; - if (token_from(self.last.root) != token_from(root)) + if (token_from(self.last.root) != token_from(root)) { try self.send_editor_update(self.last.root, root); + self.lsp_version += 1; + } if (self.last.dirty != dirty) try self.send_editor_dirty(dirty); @@ -1069,6 +1073,7 @@ pub const Editor = struct { fn send_editor_save(self: *const Self, file_path: []const u8) !void { _ = try self.handlers.msg(.{ "E", "save", file_path }); + if (self.syntax) |_| project_manager.did_save(file_path) catch {}; } fn send_editor_dirty(self: *const Self, file_dirty: bool) !void { @@ -1081,6 +1086,8 @@ pub const Editor = struct { fn send_editor_update(self: *const Self, old_root: ?Buffer.Root, new_root: ?Buffer.Root) !void { _ = try self.handlers.msg(.{ "E", "update", token_from(new_root), token_from(old_root) }); + if (self.syntax) |_| if (self.file_path) |file_path| + project_manager.did_change(file_path, self.lsp_version, token_from(new_root), token_from(old_root)) catch {}; } fn clamp_abs(self: *Self, abs: bool) void {