diff --git a/build.zig b/build.zig index 9764fd5..04b8cd5 100644 --- a/build.zig +++ b/build.zig @@ -297,6 +297,11 @@ pub fn build_exe( .optimize = optimize, }); + const diffz_dep = b.dependency("diffz", .{ + .target = target, + .optimize = optimize, + }); + const fuzzig_dep = b.dependency("fuzzig", .{ .target = target, .optimize = optimize, @@ -601,6 +606,7 @@ pub fn build_exe( .{ .name = "Buffer", .module = Buffer_mod }, .{ .name = "tracy", .module = tracy_mod }, .{ .name = "dizzy", .module = dizzy_dep.module("dizzy") }, + .{ .name = "diffz", .module = diffz_dep.module("diffz") }, .{ .name = "log", .module = log_mod }, .{ .name = "cbor", .module = cbor_mod }, }, diff --git a/build.zig.zon b/build.zig.zon index 6d5fdc1..dfc143b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -42,6 +42,10 @@ .hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP", .lazy = true, }, + .diffz = .{ + .url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb", + .hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ", + }, }, .paths = .{ "include", diff --git a/src/diff.zig b/src/diff.zig index ffded8c..76f7b9b 100644 --- a/src/diff.zig +++ b/src/diff.zig @@ -1,10 +1,5 @@ -const std = @import("std"); -const tp = @import("thespian"); -const dizzy = @import("dizzy"); -const Buffer = @import("Buffer"); -const tracy = @import("tracy"); - -const module_name = @typeName(@This()); +pub const dizzy = @import("dizzy.zig"); +pub const diffz = @import("diffz.zig"); pub const Kind = enum { insert, delete }; pub const Diff = struct { @@ -22,260 +17,3 @@ pub const Edit = struct { end: usize, bytes: []const u8, }; - -pub fn create() !AsyncDiffer { - return .{ .pid = try Process.create() }; -} - -pub const AsyncDiffer = struct { - pid: ?tp.pid, - - pub fn deinit(self: *@This()) void { - if (self.pid) |pid| { - pid.send(.{"shutdown"}) catch {}; - pid.deinit(); - self.pid = null; - } - } - - fn text_from_root(root: Buffer.Root, eol_mode: Buffer.EolMode) ![]const u8 { - var text: std.Io.Writer.Allocating = .init(std.heap.c_allocator); - defer text.deinit(); - try root.store(&text.writer, eol_mode); - return text.toOwnedSlice(); - } - - pub const CallBack = fn (from: tp.pid_ref, edits: []Diff) void; - - pub fn diff_buffer(self: @This(), cb: *const CallBack, buffer: *const Buffer) tp.result { - const eol_mode = buffer.file_eol_mode; - const text_dst = text_from_root(buffer.root, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); - errdefer std.heap.c_allocator.free(text_dst); - const text_src = if (buffer.get_vcs_content()) |vcs_content| - std.heap.c_allocator.dupe(u8, vcs_content) catch |e| return tp.exit_error(e, @errorReturnTrace()) - else - text_from_root(buffer.last_save orelse return, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); - errdefer std.heap.c_allocator.free(text_src); - const text_dst_ptr: usize = if (text_dst.len > 0) @intFromPtr(text_dst.ptr) else 0; - const text_src_ptr: usize = if (text_src.len > 0) @intFromPtr(text_src.ptr) else 0; - if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), text_dst_ptr, text_dst.len, text_src_ptr, text_src.len }); - } -}; - -const Process = struct { - receiver: Receiver, - - const Receiver = tp.Receiver(*Process); - const allocator = std.heap.c_allocator; - - pub fn create() !tp.pid { - const self = try allocator.create(Process); - errdefer allocator.destroy(self); - self.* = .{ - .receiver = Receiver.init(Process.receive, self), - }; - return tp.spawn_link(allocator, self, Process.start, module_name); - } - - fn start(self: *Process) tp.result { - errdefer self.deinit(); - tp.receive(&self.receiver); - } - - fn deinit(self: *Process) void { - allocator.destroy(self); - } - - fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { - errdefer self.deinit(); - - var cb: usize = 0; - var text_dst_ptr: usize = 0; - var text_dst_len: usize = 0; - var text_src_ptr: usize = 0; - var text_src_len: usize = 0; - - return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&text_dst_ptr), tp.extract(&text_dst_len), tp.extract(&text_src_ptr), tp.extract(&text_src_len) })) blk: { - const text_dst = if (text_dst_len > 0) @as([*]const u8, @ptrFromInt(text_dst_ptr))[0..text_dst_len] else ""; - const text_src = if (text_src_len > 0) @as([*]const u8, @ptrFromInt(text_src_ptr))[0..text_src_len] else ""; - break :blk do_diff_async(from, cb, text_dst, text_src) catch |e| tp.exit_error(e, @errorReturnTrace()); - } else if (try m.match(.{"shutdown"})) - tp.exit_normal(); - } - - fn do_diff_async(from_: tp.pid_ref, cb_addr: usize, text_dst: []const u8, text_src: []const u8) !void { - defer std.heap.c_allocator.free(text_dst); - defer std.heap.c_allocator.free(text_src); - const cb_: *AsyncDiffer.CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr); - - var arena_ = std.heap.ArenaAllocator.init(allocator); - defer arena_.deinit(); - const arena = arena_.allocator(); - - const edits = try diff(arena, text_dst, text_src); - cb_(from_, edits); - } -}; - -pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) ![]Diff { - var arena_ = std.heap.ArenaAllocator.init(allocator); - defer arena_.deinit(); - const arena = arena_.allocator(); - const frame = tracy.initZone(@src(), .{ .name = "diff" }); - defer frame.deinit(); - - var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; - var scratch = std.ArrayListUnmanaged(u32){}; - var diffs: std.ArrayList(Diff) = .empty; - errdefer diffs.deinit(allocator); - - const scratch_len = 4 * (dst.len + src.len) + 2; - try scratch.ensureTotalCapacity(arena, scratch_len); - scratch.items.len = scratch_len; - - try dizzy.PrimitiveSliceDiffer(u8).diff(arena, &dizzy_edits, src, dst, scratch.items); - - if (dizzy_edits.items.len > 2) - try diffs.ensureTotalCapacity(allocator, (dizzy_edits.items.len - 1) / 2); - - var lines_dst: usize = 0; - var pos_src: usize = 0; - var pos_dst: usize = 0; - var last_offset: 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[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[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null); - (try diffs.addOne(allocator)).* = .{ - .kind = .insert, - .line = line_start_dst, - .offset = last_offset, - .start = dizzy_edit.range.start, - .end = dizzy_edit.range.end, - .bytes = dst[dizzy_edit.range.start..dizzy_edit.range.end], - }; - }, - .delete => { - const dist = dizzy_edit.range.end - dizzy_edit.range.start; - pos_src += dist; - pos_dst += 0; - (try diffs.addOne(allocator)).* = .{ - .kind = .delete, - .line = lines_dst, - .offset = last_offset, - .start = dizzy_edit.range.start, - .end = dizzy_edit.range.end, - .bytes = src[dizzy_edit.range.start..dizzy_edit.range.end], - }; - }, - } - } - return diffs.toOwnedSlice(allocator); -} - -pub fn get_edits(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) ![]Edit { - var arena_ = std.heap.ArenaAllocator.init(allocator); - defer arena_.deinit(); - const arena = arena_.allocator(); - const frame = tracy.initZone(@src(), .{ .name = "diff" }); - defer frame.deinit(); - - var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; - var scratch = std.ArrayListUnmanaged(u32){}; - var edits = std.ArrayList(Edit).init(allocator); - - const scratch_len = 4 * (dst.len + src.len) + 2; - try scratch.ensureTotalCapacity(arena, scratch_len); - scratch.items.len = scratch_len; - - try dizzy.PrimitiveSliceDiffer(u8).diff(arena, &dizzy_edits, src, dst, scratch.items); - - if (dizzy_edits.items.len > 2) - try edits.ensureTotalCapacity((dizzy_edits.items.len - 1) / 2); - - var pos: usize = 0; - - for (dizzy_edits.items) |dizzy_edit| { - switch (dizzy_edit.kind) { - .equal => { - const dist = dizzy_edit.range.end - dizzy_edit.range.start; - pos += dist; - }, - .insert => { - (try edits.addOne()).* = .{ - .kind = .insert, - .start = pos, - .end = pos, - .bytes = dst[dizzy_edit.range.start..dizzy_edit.range.end], - }; - const dist = dizzy_edit.range.end - dizzy_edit.range.start; - pos += dist; - }, - .delete => { - const dist = dizzy_edit.range.end - dizzy_edit.range.start; - pos += 0; - (try edits.addOne()).* = .{ - .kind = .delete, - .start = pos, - .end = pos + dist, - .bytes = "", - }; - }, - } - } - return edits.toOwnedSlice(); -} - -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 assert_edits_valid(allocator: std.mem.Allocator, dst: []const u8, src: []const u8, edits: []Edit) void { - const frame = tracy.initZone(@src(), .{ .name = "diff validate" }); - defer frame.deinit(); - var result = std.ArrayListUnmanaged(u8){}; - var tmp = std.ArrayListUnmanaged(u8){}; - defer result.deinit(allocator); - defer tmp.deinit(allocator); - result.appendSlice(allocator, src) catch @panic("assert_edits_valid OOM"); - - for (edits) |edit| { - tmp.clearRetainingCapacity(); - tmp.appendSlice(allocator, result.items[0..edit.start]) catch @panic("assert_edits_valid OOM"); - tmp.appendSlice(allocator, edit.bytes) catch @panic("assert_edits_valid OOM"); - tmp.appendSlice(allocator, result.items[edit.end..]) catch @panic("assert_edits_valid OOM"); - result.clearRetainingCapacity(); - result.appendSlice(allocator, tmp.items) catch @panic("assert_edits_valid OOM"); - } - - if (!std.mem.eql(u8, dst, result.items)) { - write_file(src, "bad_diff_src") catch @panic("invalid edits write failed"); - write_file(dst, "bad_diff_dst") catch @panic("invalid edits write failed"); - write_file(result.items, "bad_diff_result") catch @panic("invalid edits write failed"); - @panic("invalid edits"); - } -} - -fn write_file(data: []const u8, file_name: []const u8) !void { - var file = try std.fs.cwd().createFile(file_name, .{ .truncate = true }); - defer file.close(); - return file.writeAll(data); -} diff --git a/src/diffz.zig b/src/diffz.zig new file mode 100644 index 0000000..dabb199 --- /dev/null +++ b/src/diffz.zig @@ -0,0 +1,179 @@ +const std = @import("std"); +const tp = @import("thespian"); +const diffz = @import("diffz"); +const Buffer = @import("Buffer"); +const tracy = @import("tracy"); + +const module_name = @typeName(@This()); + +const diff_ = @import("diff.zig"); +const Kind = diff_.Kind; +const Diff = diff_.Diff; +const Edit = diff_.Edit; + +pub fn create() !AsyncDiffer { + return .{ .pid = try Process.create() }; +} + +pub const AsyncDiffer = struct { + pid: ?tp.pid, + + pub fn deinit(self: *@This()) void { + if (self.pid) |pid| { + pid.send(.{"shutdown"}) catch {}; + pid.deinit(); + self.pid = null; + } + } + + fn text_from_root(root: Buffer.Root, eol_mode: Buffer.EolMode) ![]const u8 { + var text: std.Io.Writer.Allocating = .init(std.heap.c_allocator); + defer text.deinit(); + try root.store(&text.writer, eol_mode); + return text.toOwnedSlice(); + } + + pub const CallBack = fn (from: tp.pid_ref, edits: []Diff) void; + + pub fn diff_buffer(self: @This(), cb: *const CallBack, buffer: *const Buffer) tp.result { + const eol_mode = buffer.file_eol_mode; + const text_dst = text_from_root(buffer.root, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); + errdefer std.heap.c_allocator.free(text_dst); + const text_src = if (buffer.get_vcs_content()) |vcs_content| + std.heap.c_allocator.dupe(u8, vcs_content) catch |e| return tp.exit_error(e, @errorReturnTrace()) + else + text_from_root(buffer.last_save orelse return, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); + errdefer std.heap.c_allocator.free(text_src); + const text_dst_ptr: usize = if (text_dst.len > 0) @intFromPtr(text_dst.ptr) else 0; + const text_src_ptr: usize = if (text_src.len > 0) @intFromPtr(text_src.ptr) else 0; + if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), text_dst_ptr, text_dst.len, text_src_ptr, text_src.len }); + } +}; + +const Process = struct { + receiver: Receiver, + + const Receiver = tp.Receiver(*Process); + const allocator = std.heap.c_allocator; + + pub fn create() !tp.pid { + const self = try allocator.create(Process); + errdefer allocator.destroy(self); + self.* = .{ + .receiver = Receiver.init(Process.receive, self), + }; + return tp.spawn_link(allocator, self, Process.start, module_name); + } + + fn start(self: *Process) tp.result { + errdefer self.deinit(); + tp.receive(&self.receiver); + } + + fn deinit(self: *Process) void { + allocator.destroy(self); + } + + fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + + var cb: usize = 0; + var text_dst_ptr: usize = 0; + var text_dst_len: usize = 0; + var text_src_ptr: usize = 0; + var text_src_len: usize = 0; + + return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&text_dst_ptr), tp.extract(&text_dst_len), tp.extract(&text_src_ptr), tp.extract(&text_src_len) })) blk: { + const text_dst = if (text_dst_len > 0) @as([*]const u8, @ptrFromInt(text_dst_ptr))[0..text_dst_len] else ""; + const text_src = if (text_src_len > 0) @as([*]const u8, @ptrFromInt(text_src_ptr))[0..text_src_len] else ""; + break :blk do_diff_async(from, cb, text_dst, text_src) catch |e| tp.exit_error(e, @errorReturnTrace()); + } else if (try m.match(.{"shutdown"})) + tp.exit_normal(); + } + + fn do_diff_async(from_: tp.pid_ref, cb_addr: usize, text_dst: []const u8, text_src: []const u8) !void { + defer std.heap.c_allocator.free(text_dst); + defer std.heap.c_allocator.free(text_src); + const cb_: *AsyncDiffer.CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr); + + var arena_ = std.heap.ArenaAllocator.init(allocator); + defer arena_.deinit(); + const arena = arena_.allocator(); + + const edits = try diff(arena, text_dst, text_src); + cb_(from_, edits); + } +}; + +pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) error{OutOfMemory}![]Diff { + var arena_ = std.heap.ArenaAllocator.init(allocator); + defer arena_.deinit(); + const arena = arena_.allocator(); + const frame = tracy.initZone(@src(), .{ .name = "diff" }); + defer frame.deinit(); + + var diffs: std.ArrayList(Diff) = .empty; + errdefer diffs.deinit(allocator); + + const dmp = diffz.default; + const diff_list = try diffz.diff(&dmp, arena, src, dst, true); + + if (diff_list.items.len > 2) + try diffs.ensureTotalCapacity(allocator, (diff_list.items.len - 1) / 2); + + var lines_dst: usize = 0; + var pos_src: usize = 0; + var pos_dst: usize = 0; + var last_offset: usize = 0; + + for (diff_list.items) |diffz_diff| { + switch (diffz_diff.operation) { + .equal => { + const dist = diffz_diff.text.len; + pos_src += dist; + pos_dst += dist; + scan_char(diffz_diff.text, &lines_dst, '\n', &last_offset); + }, + .insert => { + const dist = diffz_diff.text.len; + pos_src += 0; + pos_dst += dist; + const line_start_dst: usize = lines_dst; + scan_char(diffz_diff.text, &lines_dst, '\n', null); + (try diffs.addOne(allocator)).* = .{ + .kind = .insert, + .line = line_start_dst, + .offset = last_offset, + .start = line_start_dst + last_offset, + .end = line_start_dst + last_offset + dist, + .bytes = diffz_diff.text, + }; + }, + .delete => { + const dist = diffz_diff.text.len; + pos_src += dist; + pos_dst += 0; + (try diffs.addOne(allocator)).* = .{ + .kind = .delete, + .line = lines_dst, + .offset = last_offset, + .start = lines_dst + last_offset, + .end = lines_dst + last_offset + dist, + .bytes = diffz_diff.text, + }; + }, + } + } + return diffs.toOwnedSlice(allocator); +} + +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..]; + } +} diff --git a/src/dizzy.zig b/src/dizzy.zig new file mode 100644 index 0000000..e8c9bd5 --- /dev/null +++ b/src/dizzy.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const tp = @import("thespian"); +const dizzy = @import("dizzy"); +const Buffer = @import("Buffer"); +const tracy = @import("tracy"); + +const module_name = @typeName(@This()); + +const diff_ = @import("diff.zig"); +const Kind = diff_.Kind; +const Diff = diff_.Diff; +const Edit = diff_.Edit; + +pub fn create() !AsyncDiffer { + return .{ .pid = try Process.create() }; +} + +pub const AsyncDiffer = struct { + pid: ?tp.pid, + + pub fn deinit(self: *@This()) void { + if (self.pid) |pid| { + pid.send(.{"shutdown"}) catch {}; + pid.deinit(); + self.pid = null; + } + } + + fn text_from_root(root: Buffer.Root, eol_mode: Buffer.EolMode) ![]const u8 { + var text: std.Io.Writer.Allocating = .init(std.heap.c_allocator); + defer text.deinit(); + try root.store(&text.writer, eol_mode); + return text.toOwnedSlice(); + } + + pub const CallBack = fn (from: tp.pid_ref, edits: []Diff) void; + + pub fn diff_buffer(self: @This(), cb: *const CallBack, buffer: *const Buffer) tp.result { + const eol_mode = buffer.file_eol_mode; + const text_dst = text_from_root(buffer.root, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); + errdefer std.heap.c_allocator.free(text_dst); + const text_src = if (buffer.get_vcs_content()) |vcs_content| + std.heap.c_allocator.dupe(u8, vcs_content) catch |e| return tp.exit_error(e, @errorReturnTrace()) + else + text_from_root(buffer.last_save orelse return, eol_mode) catch |e| return tp.exit_error(e, @errorReturnTrace()); + errdefer std.heap.c_allocator.free(text_src); + const text_dst_ptr: usize = if (text_dst.len > 0) @intFromPtr(text_dst.ptr) else 0; + const text_src_ptr: usize = if (text_src.len > 0) @intFromPtr(text_src.ptr) else 0; + if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), text_dst_ptr, text_dst.len, text_src_ptr, text_src.len }); + } +}; + +const Process = struct { + receiver: Receiver, + + const Receiver = tp.Receiver(*Process); + const allocator = std.heap.c_allocator; + + pub fn create() !tp.pid { + const self = try allocator.create(Process); + errdefer allocator.destroy(self); + self.* = .{ + .receiver = Receiver.init(Process.receive, self), + }; + return tp.spawn_link(allocator, self, Process.start, module_name); + } + + fn start(self: *Process) tp.result { + errdefer self.deinit(); + tp.receive(&self.receiver); + } + + fn deinit(self: *Process) void { + allocator.destroy(self); + } + + fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + + var cb: usize = 0; + var text_dst_ptr: usize = 0; + var text_dst_len: usize = 0; + var text_src_ptr: usize = 0; + var text_src_len: usize = 0; + + return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&text_dst_ptr), tp.extract(&text_dst_len), tp.extract(&text_src_ptr), tp.extract(&text_src_len) })) blk: { + const text_dst = if (text_dst_len > 0) @as([*]const u8, @ptrFromInt(text_dst_ptr))[0..text_dst_len] else ""; + const text_src = if (text_src_len > 0) @as([*]const u8, @ptrFromInt(text_src_ptr))[0..text_src_len] else ""; + break :blk do_diff_async(from, cb, text_dst, text_src) catch |e| tp.exit_error(e, @errorReturnTrace()); + } else if (try m.match(.{"shutdown"})) + tp.exit_normal(); + } + + fn do_diff_async(from_: tp.pid_ref, cb_addr: usize, text_dst: []const u8, text_src: []const u8) !void { + defer std.heap.c_allocator.free(text_dst); + defer std.heap.c_allocator.free(text_src); + const cb_: *AsyncDiffer.CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr); + + var arena_ = std.heap.ArenaAllocator.init(allocator); + defer arena_.deinit(); + const arena = arena_.allocator(); + + const edits = try diff(arena, text_dst, text_src); + cb_(from_, edits); + } +}; + +pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) ![]Diff { + var arena_ = std.heap.ArenaAllocator.init(allocator); + defer arena_.deinit(); + const arena = arena_.allocator(); + const frame = tracy.initZone(@src(), .{ .name = "diff" }); + defer frame.deinit(); + + var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; + var scratch = std.ArrayListUnmanaged(u32){}; + var diffs: std.ArrayList(Diff) = .empty; + errdefer diffs.deinit(allocator); + + const scratch_len = 4 * (dst.len + src.len) + 2; + try scratch.ensureTotalCapacity(arena, scratch_len); + scratch.items.len = scratch_len; + + try dizzy.PrimitiveSliceDiffer(u8).diff(arena, &dizzy_edits, src, dst, scratch.items); + + if (dizzy_edits.items.len > 2) + try diffs.ensureTotalCapacity(allocator, (dizzy_edits.items.len - 1) / 2); + + var lines_dst: usize = 0; + var pos_src: usize = 0; + var pos_dst: usize = 0; + var last_offset: 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[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[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null); + (try diffs.addOne(allocator)).* = .{ + .kind = .insert, + .line = line_start_dst, + .offset = last_offset, + .start = dizzy_edit.range.start, + .end = dizzy_edit.range.end, + .bytes = dst[dizzy_edit.range.start..dizzy_edit.range.end], + }; + }, + .delete => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += dist; + pos_dst += 0; + (try diffs.addOne(allocator)).* = .{ + .kind = .delete, + .line = lines_dst, + .offset = last_offset, + .start = dizzy_edit.range.start, + .end = dizzy_edit.range.end, + .bytes = src[dizzy_edit.range.start..dizzy_edit.range.end], + }; + }, + } + } + return diffs.toOwnedSlice(allocator); +} + +pub fn get_edits(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) ![]Edit { + var arena_ = std.heap.ArenaAllocator.init(allocator); + defer arena_.deinit(); + const arena = arena_.allocator(); + const frame = tracy.initZone(@src(), .{ .name = "diff" }); + defer frame.deinit(); + + var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; + var scratch = std.ArrayListUnmanaged(u32){}; + var edits = std.ArrayList(Edit).init(allocator); + + const scratch_len = 4 * (dst.len + src.len) + 2; + try scratch.ensureTotalCapacity(arena, scratch_len); + scratch.items.len = scratch_len; + + try dizzy.PrimitiveSliceDiffer(u8).diff(arena, &dizzy_edits, src, dst, scratch.items); + + if (dizzy_edits.items.len > 2) + try edits.ensureTotalCapacity((dizzy_edits.items.len - 1) / 2); + + var pos: usize = 0; + + for (dizzy_edits.items) |dizzy_edit| { + switch (dizzy_edit.kind) { + .equal => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos += dist; + }, + .insert => { + (try edits.addOne()).* = .{ + .kind = .insert, + .start = pos, + .end = pos, + .bytes = dst[dizzy_edit.range.start..dizzy_edit.range.end], + }; + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos += dist; + }, + .delete => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos += 0; + (try edits.addOne()).* = .{ + .kind = .delete, + .start = pos, + .end = pos + dist, + .bytes = "", + }; + }, + } + } + return edits.toOwnedSlice(); +} + +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 assert_edits_valid(allocator: std.mem.Allocator, dst: []const u8, src: []const u8, edits: []Edit) void { + const frame = tracy.initZone(@src(), .{ .name = "diff validate" }); + defer frame.deinit(); + var result = std.ArrayListUnmanaged(u8){}; + var tmp = std.ArrayListUnmanaged(u8){}; + defer result.deinit(allocator); + defer tmp.deinit(allocator); + result.appendSlice(allocator, src) catch @panic("assert_edits_valid OOM"); + + for (edits) |edit| { + tmp.clearRetainingCapacity(); + tmp.appendSlice(allocator, result.items[0..edit.start]) catch @panic("assert_edits_valid OOM"); + tmp.appendSlice(allocator, edit.bytes) catch @panic("assert_edits_valid OOM"); + tmp.appendSlice(allocator, result.items[edit.end..]) catch @panic("assert_edits_valid OOM"); + result.clearRetainingCapacity(); + result.appendSlice(allocator, tmp.items) catch @panic("assert_edits_valid OOM"); + } + + if (!std.mem.eql(u8, dst, result.items)) { + write_file(src, "bad_diff_src") catch @panic("invalid edits write failed"); + write_file(dst, "bad_diff_dst") catch @panic("invalid edits write failed"); + write_file(result.items, "bad_diff_result") catch @panic("invalid edits write failed"); + @panic("invalid edits"); + } +} + +fn write_file(data: []const u8, file_name: []const u8) !void { + var file = try std.fs.cwd().createFile(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(data); +} diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 63e34cc..b389557 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -5156,7 +5156,7 @@ pub const Editor = struct { const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" }); defer frame.deinit(); const diff = @import("diff"); - const edits = try diff.diff(self.allocator, content, old_content.written()); + const edits = try diff.dizzy.diff(self.allocator, content, old_content.written()); defer self.allocator.free(edits); for (edits) |edit| syntax_process_edit(syn, edit); @@ -6087,7 +6087,7 @@ pub const Editor = struct { var last_end_row: usize = 0; var last_end_col_pos: usize = 0; - const diffs = try @import("diff").diff(self.allocator, new_content, content); + const diffs = try @import("diff").dizzy.diff(self.allocator, new_content, content); defer self.allocator.free(diffs); var first = true; for (diffs) |diff| { diff --git a/src/tui/editor_gutter.zig b/src/tui/editor_gutter.zig index 7e655c1..44f120a 100644 --- a/src/tui/editor_gutter.zig +++ b/src/tui/editor_gutter.zig @@ -35,7 +35,7 @@ symbols: bool, width: usize = 4, editor: *ed.Editor, editor_widget: ?*const Widget = null, -diff_: diff.AsyncDiffer, +diff_: diff.diffz.AsyncDiffer, diff_symbols: std.ArrayList(Symbol), const Self = @This(); @@ -55,7 +55,7 @@ pub fn create(allocator: Allocator, parent: Widget, event_source: Widget, editor .highlight = tui.config().highlight_current_line_gutter, .symbols = tui.config().gutter_symbols, .editor = editor, - .diff_ = try diff.create(), + .diff_ = try diff.diffz.create(), .diff_symbols = .empty, }; try tui.message_filters().add(MessageFilter.bind(self, filter_receive));