feat: implement proper line diffing for diff gutter
This commit is contained in:
parent
ce9246374c
commit
1fbd09387e
3 changed files with 88 additions and 89 deletions
|
|
@ -17,3 +17,10 @@ pub const Edit = struct {
|
||||||
end: usize,
|
end: usize,
|
||||||
bytes: []const u8,
|
bytes: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const LineDiffKind = enum { insert, modify, delete };
|
||||||
|
pub const LineDiff = struct {
|
||||||
|
kind: LineDiffKind,
|
||||||
|
line: usize,
|
||||||
|
lines: usize,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ const tracy = @import("tracy");
|
||||||
const module_name = @typeName(@This());
|
const module_name = @typeName(@This());
|
||||||
|
|
||||||
const diff_ = @import("diff.zig");
|
const diff_ = @import("diff.zig");
|
||||||
const Kind = diff_.Kind;
|
const Diff = diff_.LineDiff;
|
||||||
const Diff = diff_.Diff;
|
const Kind = diff_.LineDiffKind;
|
||||||
const Edit = diff_.Edit;
|
|
||||||
|
|
||||||
pub fn create() !AsyncDiffer {
|
pub fn create() !AsyncDiffer {
|
||||||
return .{ .pid = try Process.create() };
|
return .{ .pid = try Process.create() };
|
||||||
|
|
@ -116,14 +115,13 @@ pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) erro
|
||||||
errdefer diffs.deinit(allocator);
|
errdefer diffs.deinit(allocator);
|
||||||
|
|
||||||
const dmp = diffz.default;
|
const dmp = diffz.default;
|
||||||
var diff_list = try diffz.diff(&dmp, arena, src, dst, true);
|
var diff_list = try diffz.diff(&dmp, arena, src, dst, false);
|
||||||
try diffz.diffCleanupSemantic(arena, &diff_list);
|
try diffz.diffCleanupSemantic(arena, &diff_list);
|
||||||
|
|
||||||
if (diff_list.items.len > 2)
|
if (diff_list.items.len > 2)
|
||||||
try diffs.ensureTotalCapacity(allocator, (diff_list.items.len - 1) / 2);
|
try diffs.ensureTotalCapacity(allocator, (diff_list.items.len - 1) / 2);
|
||||||
|
|
||||||
var lines_dst: usize = 0;
|
var lines_dst: usize = 0;
|
||||||
var pos_src: usize = 0;
|
|
||||||
var pos_dst: usize = 0;
|
var pos_dst: usize = 0;
|
||||||
var last_offset: usize = 0;
|
var last_offset: usize = 0;
|
||||||
|
|
||||||
|
|
@ -131,36 +129,47 @@ pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) erro
|
||||||
switch (diffz_diff.operation) {
|
switch (diffz_diff.operation) {
|
||||||
.equal => {
|
.equal => {
|
||||||
const dist = diffz_diff.text.len;
|
const dist = diffz_diff.text.len;
|
||||||
pos_src += dist;
|
|
||||||
pos_dst += dist;
|
pos_dst += dist;
|
||||||
scan_char(diffz_diff.text, &lines_dst, '\n', &last_offset);
|
scan_eol(diffz_diff.text, &lines_dst, &last_offset);
|
||||||
},
|
},
|
||||||
.insert => {
|
.insert => {
|
||||||
const dist = diffz_diff.text.len;
|
const dist = diffz_diff.text.len;
|
||||||
pos_src += 0;
|
|
||||||
pos_dst += dist;
|
pos_dst += dist;
|
||||||
const line_start_dst: usize = lines_dst;
|
const line_start_dst: usize = lines_dst;
|
||||||
scan_char(diffz_diff.text, &lines_dst, '\n', null);
|
scan_eol(diffz_diff.text, &lines_dst, &last_offset);
|
||||||
(try diffs.addOne(allocator)).* = .{
|
(try diffs.addOne(allocator)).* = .{
|
||||||
.kind = .insert,
|
.kind = .insert,
|
||||||
.line = line_start_dst,
|
.line = line_start_dst,
|
||||||
.offset = last_offset,
|
.lines = lines_dst - line_start_dst,
|
||||||
.start = line_start_dst + last_offset,
|
};
|
||||||
.end = line_start_dst + last_offset + dist,
|
if (last_offset > 0)
|
||||||
.bytes = diffz_diff.text,
|
(try diffs.addOne(allocator)).* = .{
|
||||||
|
.kind = .modify,
|
||||||
|
.line = line_start_dst,
|
||||||
|
.lines = 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.delete => {
|
.delete => {
|
||||||
const dist = diffz_diff.text.len;
|
|
||||||
pos_src += dist;
|
|
||||||
pos_dst += 0;
|
pos_dst += 0;
|
||||||
|
var lines: usize = 0;
|
||||||
|
var diff_offset: usize = 0;
|
||||||
|
scan_eol(diffz_diff.text, &lines, &diff_offset);
|
||||||
|
(try diffs.addOne(allocator)).* = .{
|
||||||
|
.kind = .modify,
|
||||||
|
.line = lines_dst,
|
||||||
|
.lines = 1,
|
||||||
|
};
|
||||||
|
if (lines > 0)
|
||||||
(try diffs.addOne(allocator)).* = .{
|
(try diffs.addOne(allocator)).* = .{
|
||||||
.kind = .delete,
|
.kind = .delete,
|
||||||
.line = lines_dst,
|
.line = lines_dst,
|
||||||
.offset = last_offset,
|
.lines = lines,
|
||||||
.start = lines_dst + last_offset,
|
};
|
||||||
.end = lines_dst + last_offset + dist,
|
if (lines > 0 and diff_offset > 0)
|
||||||
.bytes = diffz_diff.text,
|
(try diffs.addOne(allocator)).* = .{
|
||||||
|
.kind = .modify,
|
||||||
|
.line = lines_dst,
|
||||||
|
.lines = 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +177,12 @@ pub fn diff(allocator: std.mem.Allocator, dst: []const u8, src: []const u8) erro
|
||||||
return diffs.toOwnedSlice(allocator);
|
return diffs.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_char(chars: []const u8, lines: *usize, char: u8, last_offset: ?*usize) void {
|
fn scan_eol(chars: []const u8, lines: *usize, remain: *usize) void {
|
||||||
var pos = chars;
|
var pos = chars;
|
||||||
|
remain.* += pos.len;
|
||||||
while (pos.len > 0) {
|
while (pos.len > 0) {
|
||||||
if (pos[0] == char) {
|
if (pos[0] == '\n') {
|
||||||
if (last_offset) |off| off.* = pos.len - 1;
|
remain.* = pos.len - 1;
|
||||||
lines.* += 1;
|
lines.* += 1;
|
||||||
}
|
}
|
||||||
pos = pos[1..];
|
pos = pos[1..];
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const tp = @import("thespian");
|
const tp = @import("thespian");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
const diff = @import("diff");
|
const Diff = @import("diff").LineDiff;
|
||||||
|
const Kind = @import("diff").LineDiffKind;
|
||||||
|
const diffz = @import("diff").diffz;
|
||||||
const cbor = @import("cbor");
|
const cbor = @import("cbor");
|
||||||
const root = @import("soft_root").root;
|
const root = @import("soft_root").root;
|
||||||
|
|
||||||
|
|
@ -35,14 +37,11 @@ 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.diffz.AsyncDiffer,
|
differ: diffz.AsyncDiffer,
|
||||||
diff_symbols: std.ArrayList(Symbol),
|
diff_symbols: std.ArrayList(Diff),
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
const Kind = enum { insert, modified, delete };
|
|
||||||
const Symbol = struct { kind: Kind, line: usize };
|
|
||||||
|
|
||||||
pub fn create(allocator: Allocator, parent: Widget, event_source: Widget, editor: *ed.Editor) !Widget {
|
pub fn create(allocator: Allocator, parent: Widget, event_source: Widget, editor: *ed.Editor) !Widget {
|
||||||
const self = try allocator.create(Self);
|
const self = try allocator.create(Self);
|
||||||
errdefer allocator.destroy(self);
|
errdefer allocator.destroy(self);
|
||||||
|
|
@ -55,7 +54,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.diffz.create(),
|
.differ = try 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));
|
||||||
|
|
@ -248,7 +247,7 @@ inline fn render_line_highlight(self: *Self, pos: usize, theme: *const Widget.Th
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Symbol, pos: usize, linenum_: usize, theme: *const Widget.Theme) void {
|
inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Diff, pos: usize, linenum_: usize, theme: *const Widget.Theme) void {
|
||||||
const linenum = linenum_ - 1;
|
const linenum = linenum_ - 1;
|
||||||
if (diff_symbols.len == 0) return;
|
if (diff_symbols.len == 0) return;
|
||||||
while ((diff_symbols.*)[0].line < linenum) {
|
while ((diff_symbols.*)[0].line < linenum) {
|
||||||
|
|
@ -258,29 +257,34 @@ inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Symbol, pos: usize,
|
||||||
|
|
||||||
if ((diff_symbols.*)[0].line > linenum) return;
|
if ((diff_symbols.*)[0].line > linenum) return;
|
||||||
|
|
||||||
const sym = (diff_symbols.*)[0];
|
while ((diff_symbols.*)[0].line == linenum) {
|
||||||
const kind: Kind = switch (sym.kind) {
|
self.render_diff((diff_symbols.*)[0], pos, theme);
|
||||||
.insert => .insert,
|
diff_symbols.* = (diff_symbols.*)[1..];
|
||||||
.modified => .insert, //TODO: we map .modified to .insert here because the diff algo is unstable
|
if (diff_symbols.len == 0) return;
|
||||||
.delete => .delete,
|
}
|
||||||
};
|
}
|
||||||
const char = switch (kind) {
|
|
||||||
|
inline fn render_diff(self: *Self, sym: Diff, pos: usize, theme: *const Widget.Theme) void {
|
||||||
|
const char = switch (sym.kind) {
|
||||||
.insert => "┃",
|
.insert => "┃",
|
||||||
.modified => "┋",
|
.modify => "┋",
|
||||||
.delete => "▔",
|
.delete => "▔",
|
||||||
};
|
};
|
||||||
|
|
||||||
self.plane.cursor_move_yx(@intCast(pos), @intCast(self.get_width() - 1)) catch return;
|
var lines = if (sym.kind == .delete) 1 else sym.lines;
|
||||||
|
while (lines > 0) : (lines -= 1) {
|
||||||
|
self.plane.cursor_move_yx(@intCast(pos + lines - 1), @intCast(self.get_width() - 1));
|
||||||
var cell = self.plane.cell_init();
|
var cell = self.plane.cell_init();
|
||||||
_ = self.plane.at_cursor_cell(&cell) catch return;
|
_ = self.plane.at_cursor_cell(&cell) catch return;
|
||||||
cell.set_style_fg(switch (kind) {
|
cell.set_style_fg(switch (sym.kind) {
|
||||||
.insert => theme.editor_gutter_added,
|
.insert => theme.editor_gutter_added,
|
||||||
.modified => theme.editor_gutter_modified,
|
.modify => theme.editor_gutter_modified,
|
||||||
.delete => theme.editor_gutter_deleted,
|
.delete => theme.editor_gutter_deleted,
|
||||||
});
|
});
|
||||||
_ = self.plane.cell_load(&cell, char) catch {};
|
_ = self.plane.cell_load(&cell, char) catch {};
|
||||||
_ = self.plane.putc(&cell) catch {};
|
_ = self.plane.putc(&cell) catch {};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_diagnostics(self: *Self, theme: *const Widget.Theme) void {
|
fn render_diagnostics(self: *Self, theme: *const Widget.Theme) void {
|
||||||
if (tui.config().inline_diagnostics)
|
if (tui.config().inline_diagnostics)
|
||||||
|
|
@ -365,28 +369,28 @@ fn diff_update(self: *Self) !void {
|
||||||
self.diff_symbols_clear();
|
self.diff_symbols_clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return self.diff_.diff_buffer(diff_result, self.editor.buffer orelse return);
|
return self.differ.diff_buffer(diff_result, self.editor.buffer orelse return);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_result(from: tp.pid_ref, edits: []diff.Diff) void {
|
fn diff_result(from: tp.pid_ref, edits: []Diff) void {
|
||||||
diff_result_send(from, edits) catch |e| @import("log").err(@typeName(Self), "diff", e);
|
diff_result_send(from, edits) catch |e| @import("log").err(@typeName(Self), "diff", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diff_result_send(from: tp.pid_ref, edits: []diff.Diff) !void {
|
fn diff_result_send(from: tp.pid_ref, edits: []Diff) !void {
|
||||||
var buf: std.Io.Writer.Allocating = .init(std.heap.c_allocator);
|
var buf: std.Io.Writer.Allocating = .init(std.heap.c_allocator);
|
||||||
defer buf.deinit();
|
defer buf.deinit();
|
||||||
try cbor.writeArrayHeader(&buf.writer, 2);
|
try cbor.writeArrayHeader(&buf.writer, 2);
|
||||||
try cbor.writeValue(&buf.writer, "DIFF");
|
try cbor.writeValue(&buf.writer, "DIFF");
|
||||||
try cbor.writeArrayHeader(&buf.writer, edits.len);
|
try cbor.writeArrayHeader(&buf.writer, edits.len);
|
||||||
for (edits) |edit| {
|
for (edits) |edit| {
|
||||||
try cbor.writeArrayHeader(&buf.writer, 4);
|
try cbor.writeArrayHeader(&buf.writer, 3);
|
||||||
try cbor.writeValue(&buf.writer, switch (edit.kind) {
|
try cbor.writeValue(&buf.writer, switch (edit.kind) {
|
||||||
.insert => "I",
|
.insert => "I",
|
||||||
|
.modify => "M",
|
||||||
.delete => "D",
|
.delete => "D",
|
||||||
});
|
});
|
||||||
try cbor.writeValue(&buf.writer, edit.line);
|
try cbor.writeValue(&buf.writer, edit.line);
|
||||||
try cbor.writeValue(&buf.writer, edit.offset);
|
try cbor.writeValue(&buf.writer, edit.lines);
|
||||||
try cbor.writeValue(&buf.writer, edit.bytes);
|
|
||||||
}
|
}
|
||||||
from.send_raw(tp.message{ .buf = buf.written() }) catch return;
|
from.send_raw(tp.message{ .buf = buf.written() }) catch return;
|
||||||
}
|
}
|
||||||
|
|
@ -397,47 +401,25 @@ pub fn process_diff(self: *Self, cb: []const u8) MessageFilter.Error!void {
|
||||||
var count = try cbor.decodeArrayHeader(&iter);
|
var count = try cbor.decodeArrayHeader(&iter);
|
||||||
while (count > 0) : (count -= 1) {
|
while (count > 0) : (count -= 1) {
|
||||||
var line: usize = undefined;
|
var line: usize = undefined;
|
||||||
var offset: usize = undefined;
|
var lines: usize = undefined;
|
||||||
var bytes: []const u8 = undefined;
|
if (try cbor.matchValue(&iter, .{ "I", cbor.extract(&line), cbor.extract(&lines) })) {
|
||||||
if (try cbor.matchValue(&iter, .{ "I", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
|
try self.process_edit(.insert, line, lines);
|
||||||
var pos: usize = 0;
|
|
||||||
var ln: usize = line;
|
|
||||||
while (std.mem.indexOfScalarPos(u8, bytes, pos, '\n')) |next| {
|
|
||||||
const end = if (next < bytes.len) next + 1 else next;
|
|
||||||
try self.process_edit(.insert, ln, offset, bytes[pos..end]);
|
|
||||||
pos = next + 1;
|
|
||||||
ln += 1;
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
try self.process_edit(.insert, ln, offset, bytes[pos..]);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (try cbor.matchValue(&iter, .{ "D", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
|
if (try cbor.matchValue(&iter, .{ "M", cbor.extract(&line), cbor.extract(&lines) })) {
|
||||||
try self.process_edit(.delete, line, offset, bytes);
|
try self.process_edit(.modify, line, lines);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (try cbor.matchValue(&iter, .{ "D", cbor.extract(&line), cbor.extract(&lines) })) {
|
||||||
|
try self.process_edit(.delete, line, lines);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_edit(self: *Self, kind: Kind, line: usize, offset: usize, bytes: []const u8) !void {
|
fn process_edit(self: *Self, kind: Kind, line: usize, lines: usize) !void {
|
||||||
const change = if (self.diff_symbols.items.len > 0) self.diff_symbols.items[self.diff_symbols.items.len - 1].line == line else false;
|
std.log.debug("edit: {} l:{d} n:{}", .{ kind, line, lines });
|
||||||
if (change) {
|
(try self.diff_symbols.addOne(self.allocator)).* = .{ .kind = kind, .line = line, .lines = lines };
|
||||||
self.diff_symbols.items[self.diff_symbols.items.len - 1].kind = .modified;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(try self.diff_symbols.addOne(self.allocator)).* = switch (kind) {
|
|
||||||
.insert => ret: {
|
|
||||||
if (offset > 0)
|
|
||||||
break :ret .{ .kind = .modified, .line = line };
|
|
||||||
if (bytes.len == 0)
|
|
||||||
return;
|
|
||||||
if (bytes[bytes.len - 1] == '\n')
|
|
||||||
break :ret .{ .kind = .insert, .line = line };
|
|
||||||
break :ret .{ .kind = .modified, .line = line };
|
|
||||||
},
|
|
||||||
.delete => .{ .kind = .delete, .line = line },
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_receive(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
|
pub fn filter_receive(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue