Working diff for formatting

This commit is contained in:
Jeffery Stager 2022-08-10 22:47:33 -04:00
parent 8847ed54f9
commit 4e33e1d61f
2 changed files with 329 additions and 8 deletions

View File

@ -18,6 +18,7 @@ const Ast = std.zig.Ast;
const tracy = @import("tracy.zig"); const tracy = @import("tracy.zig");
const uri_utils = @import("uri.zig"); const uri_utils = @import("uri.zig");
const data = @import("data/data.zig"); const data = @import("data/data.zig");
const diff = @import("diff.zig");
// Server fields // Server fields
@ -2213,16 +2214,37 @@ fn formattingHandler(server: *Server, writer: anytype, id: types.RequestId, req:
.Exited => |code| if (code == 0) { .Exited => |code| if (code == 0) {
if (std.mem.eql(u8, handle.document.text, stdout_bytes)) return try respondGeneric(writer, id, null_result_response); if (std.mem.eql(u8, handle.document.text, stdout_bytes)) return try respondGeneric(writer, id, null_result_response);
var edits = try diff.edits(server.allocator, handle.document.text, stdout_bytes);
defer edits.deinit();
defer for (edits.items) |item| item.newText.deinit();
var text_edits = try std
.ArrayList(types.TextEdit)
.initCapacity(server.allocator, edits.items.len);
defer text_edits.deinit();
for (edits.items) |edit| {
try text_edits.append(.{
.range = edit.range,
.newText = edit.newText.items,
});
}
const result = types.ResponseParams{
.TextEdits = text_edits.items,
};
return try send(writer, server.arena.allocator(), types.Response{ return try send(writer, server.arena.allocator(), types.Response{
.id = id, .id = id,
.result = .{ .result = result,
.TextEdits = &[1]types.TextEdit{ // .result = .{
.{ // .TextEdits = &[1]types.TextEdit{
.range = try offsets.documentRange(handle.document, server.offset_encoding), // .{
.newText = stdout_bytes, // .range = try offsets.documentRange(handle.document, server.offset_encoding),
}, // .newText = stdout_bytes,
}, // },
}, // },
// },
}); });
}, },
else => {}, else => {},

299
src/diff.zig Normal file
View File

@ -0,0 +1,299 @@
const std = @import("std");
const types = @import("types.zig");
// pub const Position = struct { line: usize, character: usize };
// pub const Range = struct { start: Position, end: Position };
pub const Edit = struct {
range: types.Range,
newText: std.ArrayList(u8),
};
const Operation = enum { Deletion, Addition, Nothing };
const Change = struct {
operation: Operation,
pos: usize,
value: ?u8,
};
/// Given two input strings, `a` and `b`, return a list of Edits that
/// describe the changes from `a` to `b`
pub fn edits(
allocator: std.mem.Allocator,
a: []const u8,
b: []const u8,
) !std.ArrayList(Edit) {
// Given the input strings A and B, we skip over the first N characters
// where A[0..N] == B[0..N]. We want to trim the start (and end) of the
// strings that have the same text. This decreases the size of the LCS
// table and makes the diff comparison more efficient
var a_trim: []const u8 = a;
var b_trim: []const u8 = b;
const a_trim_offset = trim_input(&a_trim, &b_trim);
_ = a_trim_offset;
const rows = a_trim.len + 1;
const cols = b_trim.len + 1;
var lcs = try Array2D.new(allocator, rows, cols);
defer lcs.deinit();
calculate_lcs(&lcs, a_trim, b_trim);
return try get_changes(
&lcs,
a,
a_trim_offset,
a_trim,
b_trim,
allocator,
);
}
fn trim_input(a_out: *[]const u8, b_out: *[]const u8) usize {
if (a_out.len == 0 or b_out.len == 0) return 0;
var a: []const u8 = a_out.*;
var b: []const u8 = b_out.*;
// Trim the beginning of the string
var start: usize = 0;
while (start < a.len and start < b.len and a[start] == b[start]) : ({
start += 1;
}) {}
// Trim the end of the string
var end: usize = 1;
while (end < a.len and end < b.len and a[a.len - end] == b[b.len - end]) : ({
end += 1;
}) {}
end -= 1;
// Sanity check to make sure our range isn't negative
if (start < a.len - end and start < b.len - end) {
a_out.* = a[start .. a.len - end];
b_out.* = b[start .. b.len - end];
return start;
}
a_out.* = a_out.*;
b_out.* = b_out.*;
return 0;
}
pub const Array2D = struct {
const Self = @This();
data: [*]usize,
allocator: std.mem.Allocator,
rows: usize,
cols: usize,
pub fn new(
allocator: std.mem.Allocator,
rows: usize,
cols: usize,
) !Self {
const data = try allocator.alloc(usize, rows * cols);
return Self{
.data = data.ptr,
.allocator = allocator,
.rows = rows,
.cols = cols,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.data[0 .. self.rows * self.cols]);
}
pub fn get(self: *Self, row: usize, col: usize) *usize {
return @ptrCast(*usize, self.data + (row * self.cols) + col);
}
};
fn calculate_lcs(
lcs: *Array2D,
astr: []const u8,
bstr: []const u8,
) void {
const rows = astr.len + 1;
const cols = bstr.len + 1;
std.mem.set(usize, lcs.data[0 .. rows * cols], 0);
var i: usize = 1;
while (i < rows) : (i += 1) {
var j: usize = 1;
while (j < cols) : (j += 1) {
if (astr[i - 1] == bstr[j - 1]) {
lcs.get(i, j).* = lcs.get(i - 1, j - 1).* + 1;
} else {
lcs.get(i, j).* = std.math.max(
lcs.get(i - 1, j).*,
lcs.get(i, j - 1).*,
);
}
}
}
}
pub fn get_changes(
lcs: *Array2D,
a: []const u8,
a_trim_offset: usize,
a_trim: []const u8,
b_trim: []const u8,
allocator: std.mem.Allocator,
) !std.ArrayList(Edit) {
var changes = try std.ArrayList(Change).initCapacity(allocator, a_trim.len);
defer changes.deinit();
try recur_changes(
lcs,
&changes,
a_trim,
b_trim,
@intCast(i64, a_trim.len),
@intCast(i64, b_trim.len),
);
var groups = std.ArrayList([]Change).init(allocator);
defer groups.deinit();
var active_change: ?[]Change = null;
for (changes.items) |ch, i| {
switch (ch.operation) {
.Addition, .Deletion => {
if (active_change == null) {
active_change = changes.items[i..];
}
},
.Nothing => {
if (active_change) |*ac| {
ac.* = ac.*[0..(i - (changes.items.len - ac.*.len))];
try groups.append(ac.*);
active_change = null;
}
},
}
}
if (active_change) |*ac| {
ac.* = ac.*[0..(changes.items.len - (changes.items.len - ac.*.len))];
try groups.append(ac.*);
}
var a_lines = std.mem.split(u8, a, "\n");
std.mem.reverse([]Change, groups.items);
for (groups.items) |group| std.mem.reverse(Change, group);
var edit_results = std.ArrayList(Edit).init(allocator);
errdefer edit_results.deinit();
for (groups.items) |group| {
var range_start = group[0].pos;
var range_len: usize = 0;
var newText = std.ArrayList(u8).init(allocator);
_ = range_start;
_ = range_len;
for (group) |ch| {
switch (ch.operation) {
.Addition => try newText.append(ch.value.?),
.Deletion => range_len += 1,
else => {},
}
}
var range = try char_pos_to_range(
&a_lines,
a_trim_offset + range_start,
a_trim_offset + range_start + range_len,
);
a_lines.reset();
try edit_results.append(Edit{
.range = range,
.newText = newText,
});
}
return edit_results;
}
fn recur_changes(
lcs: *Array2D,
changes: *std.ArrayList(Change),
a: []const u8,
b: []const u8,
i: i64,
j: i64,
) anyerror!void {
const ii = @intCast(usize, i);
const jj = @intCast(usize, j);
if (i > 0 and j > 0 and a[ii - 1] == b[jj - 1]) {
try changes.append(.{
.operation = .Nothing,
.pos = ii - 1,
.value = null,
});
try recur_changes(lcs, changes, a, b, i - 1, j - 1);
} else if (j > 0 and (i == 0 or lcs.get(ii, jj - 1).* >= lcs.get(ii - 1, jj).*)) {
try changes.append(.{
.operation = .Addition,
.pos = ii,
.value = b[jj - 1],
});
try recur_changes(lcs, changes, a, b, i, j - 1);
} else if (i > 0 and (j == 0 or lcs.get(ii, jj - 1).* < lcs.get(ii - 1, jj).*)) {
try changes.append(.{
.operation = .Deletion,
.pos = ii - 1,
.value = a[ii - 1],
});
try recur_changes(lcs, changes, a, b, i - 1, j);
}
}
/// Accept a range that is solely based on buffer/character position and
/// convert it to line number & character position range
fn char_pos_to_range(
lines: *std.mem.SplitIterator(u8),
start: usize,
end: usize,
) !types.Range {
var char_pos: usize = 0;
var line_pos: usize = 0;
var result_start_pos: ?types.Position = null;
var result_end_pos: ?types.Position = null;
while (lines.next()) |line| : ({
char_pos += line.len + 1;
line_pos += 1;
}) {
if (start >= char_pos and start <= char_pos + line.len) {
result_start_pos = .{
.line = @intCast(i64, line_pos),
.character = @intCast(i64, start - char_pos),
};
}
if (end >= char_pos and end <= char_pos + line.len) {
result_end_pos = .{
.line = @intCast(i64, line_pos),
.character = @intCast(i64, end - char_pos),
};
}
}
if (result_start_pos == null) return error.InvalidRange;
if (result_end_pos == null) {
result_end_pos = types.Position{
.line = @intCast(i64, line_pos),
.character = @intCast(i64, char_pos),
};
}
if (result_start_pos == null or result_end_pos == null) {
return error.InvalidRange;
}
return types.Range{
.start = result_start_pos.?,
.end = result_end_pos.?,
};
}