refactor: add diffz support and use it for gutter diffs
This commit is contained in:
parent
1e02d978de
commit
cf28e27a81
7 changed files with 464 additions and 268 deletions
|
|
@ -297,6 +297,11 @@ pub fn build_exe(
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const diffz_dep = b.dependency("diffz", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
const fuzzig_dep = b.dependency("fuzzig", .{
|
const fuzzig_dep = b.dependency("fuzzig", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
|
@ -601,6 +606,7 @@ pub fn build_exe(
|
||||||
.{ .name = "Buffer", .module = Buffer_mod },
|
.{ .name = "Buffer", .module = Buffer_mod },
|
||||||
.{ .name = "tracy", .module = tracy_mod },
|
.{ .name = "tracy", .module = tracy_mod },
|
||||||
.{ .name = "dizzy", .module = dizzy_dep.module("dizzy") },
|
.{ .name = "dizzy", .module = dizzy_dep.module("dizzy") },
|
||||||
|
.{ .name = "diffz", .module = diffz_dep.module("diffz") },
|
||||||
.{ .name = "log", .module = log_mod },
|
.{ .name = "log", .module = log_mod },
|
||||||
.{ .name = "cbor", .module = cbor_mod },
|
.{ .name = "cbor", .module = cbor_mod },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@
|
||||||
.hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP",
|
.hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
|
.diffz = .{
|
||||||
|
.url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb",
|
||||||
|
.hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"include",
|
"include",
|
||||||
|
|
|
||||||
266
src/diff.zig
266
src/diff.zig
|
|
@ -1,10 +1,5 @@
|
||||||
const std = @import("std");
|
pub const dizzy = @import("dizzy.zig");
|
||||||
const tp = @import("thespian");
|
pub const diffz = @import("diffz.zig");
|
||||||
const dizzy = @import("dizzy");
|
|
||||||
const Buffer = @import("Buffer");
|
|
||||||
const tracy = @import("tracy");
|
|
||||||
|
|
||||||
const module_name = @typeName(@This());
|
|
||||||
|
|
||||||
pub const Kind = enum { insert, delete };
|
pub const Kind = enum { insert, delete };
|
||||||
pub const Diff = struct {
|
pub const Diff = struct {
|
||||||
|
|
@ -22,260 +17,3 @@ pub const Edit = struct {
|
||||||
end: usize,
|
end: usize,
|
||||||
bytes: []const u8,
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
179
src/diffz.zig
Normal file
179
src/diffz.zig
Normal file
|
|
@ -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..];
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/dizzy.zig
Normal file
269
src/dizzy.zig
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -5156,7 +5156,7 @@ pub const Editor = struct {
|
||||||
const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" });
|
const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" });
|
||||||
defer frame.deinit();
|
defer frame.deinit();
|
||||||
const diff = @import("diff");
|
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);
|
defer self.allocator.free(edits);
|
||||||
for (edits) |edit|
|
for (edits) |edit|
|
||||||
syntax_process_edit(syn, edit);
|
syntax_process_edit(syn, edit);
|
||||||
|
|
@ -6087,7 +6087,7 @@ pub const Editor = struct {
|
||||||
var last_end_row: usize = 0;
|
var last_end_row: usize = 0;
|
||||||
var last_end_col_pos: 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);
|
defer self.allocator.free(diffs);
|
||||||
var first = true;
|
var first = true;
|
||||||
for (diffs) |diff| {
|
for (diffs) |diff| {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ symbols: bool,
|
||||||
width: usize = 4,
|
width: usize = 4,
|
||||||
editor: *ed.Editor,
|
editor: *ed.Editor,
|
||||||
editor_widget: ?*const Widget = null,
|
editor_widget: ?*const Widget = null,
|
||||||
diff_: diff.AsyncDiffer,
|
diff_: diff.diffz.AsyncDiffer,
|
||||||
diff_symbols: std.ArrayList(Symbol),
|
diff_symbols: std.ArrayList(Symbol),
|
||||||
|
|
||||||
const Self = @This();
|
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,
|
.highlight = tui.config().highlight_current_line_gutter,
|
||||||
.symbols = tui.config().gutter_symbols,
|
.symbols = tui.config().gutter_symbols,
|
||||||
.editor = editor,
|
.editor = editor,
|
||||||
.diff_ = try diff.create(),
|
.diff_ = try diff.diffz.create(),
|
||||||
.diff_symbols = .empty,
|
.diff_symbols = .empty,
|
||||||
};
|
};
|
||||||
try tui.message_filters().add(MessageFilter.bind(self, filter_receive));
|
try tui.message_filters().add(MessageFilter.bind(self, filter_receive));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue