Add fallback to old behavior on diff failure

This commit is contained in:
Jeffery Stager 2022-08-13 17:16:53 -04:00
parent 4e33e1d61f
commit 254353a9f4
2 changed files with 88 additions and 28 deletions

View File

@ -2214,9 +2214,25 @@ 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); var edits = diff.edits(server.allocator, handle.document.text, stdout_bytes) catch {
defer edits.deinit(); // If there was an error trying to diff the text, return the formatted response
defer for (edits.items) |item| item.newText.deinit(); // as the new text for the entire range of the document
return try send(writer, server.arena.allocator(), types.Response{
.id = id,
.result = .{
.TextEdits = &[1]types.TextEdit{
.{
.range = try offsets.documentRange(handle.document, server.offset_encoding),
.newText = stdout_bytes,
},
},
},
});
};
defer {
for (edits.items) |item| item.newText.deinit();
edits.deinit();
}
var text_edits = try std var text_edits = try std
.ArrayList(types.TextEdit) .ArrayList(types.TextEdit)
@ -2234,18 +2250,11 @@ fn formattingHandler(server: *Server, writer: anytype, id: types.RequestId, req:
.TextEdits = text_edits.items, .TextEdits = text_edits.items,
}; };
return try send(writer, server.arena.allocator(), types.Response{ return try send(
.id = id, writer,
.result = result, server.arena.allocator(),
// .result = .{ types.Response{ .id = id, .result = result },
// .TextEdits = &[1]types.TextEdit{ );
// .{
// .range = try offsets.documentRange(handle.document, server.offset_encoding),
// .newText = stdout_bytes,
// },
// },
// },
});
}, },
else => {}, else => {},
} }

View File

@ -1,14 +1,18 @@
const std = @import("std"); const std = @import("std");
const types = @import("types.zig"); const types = @import("types.zig");
// pub const Position = struct { line: usize, character: usize }; // This is essentially the same as `types.TextEdit`, but we use an
// pub const Range = struct { start: Position, end: Position }; // ArrayList(u8) here to be able to clean up the memory later on
pub const Edit = struct { pub const Edit = struct {
range: types.Range, range: types.Range,
newText: std.ArrayList(u8), newText: std.ArrayList(u8),
}; };
// Whether the `Change` is an addition, deletion, or no change from the
// original string to the new string
const Operation = enum { Deletion, Addition, Nothing }; const Operation = enum { Deletion, Addition, Nothing };
/// A single character difference between two strings
const Change = struct { const Change = struct {
operation: Operation, operation: Operation,
pos: usize, pos: usize,
@ -68,18 +72,45 @@ fn trim_input(a_out: *[]const u8, b_out: *[]const u8) usize {
}) {} }) {}
end -= 1; end -= 1;
// Sanity check to make sure our range isn't negative var a_start = start;
if (start < a.len - end and start < b.len - end) { var a_end = a.len - end;
a_out.* = a[start .. a.len - end]; var b_start = start;
b_out.* = b[start .. b.len - end]; var b_end = b.len - end;
return start;
// In certain situations, the trimmed range can be "negative" where
// `a_start` ends up being after `a_end` in the byte stream. If you
// consider the following inputs:
// a: "xx gg xx"
// b: "xx gg xx"
//
// This will lead to the following calculations:
// a_start: 4
// a_end: 4
// b_start: 4
// b_end: 2
//
// In negative range situations, we add the absolute value of the
// the negative range's length (`b_start - b_end` in this case) to the
// other range's length (a_end + (b_start - b_end)), and then set the
// negative range end to the negative range start (b_end = b_start)
if (a_start > a_end) {
const difference = a_start - a_end;
a_end = a_start;
b_end += difference;
}
if (b_start > b_end) {
const difference = b_start - b_end;
b_end = b_start;
a_end += difference;
} }
a_out.* = a_out.*; a_out.* = a[a_start..a_end];
b_out.* = b_out.*; b_out.* = b[b_start..b_end];
return 0;
return start;
} }
/// A 2D array that is addressable as a[row, col]
pub const Array2D = struct { pub const Array2D = struct {
const Self = @This(); const Self = @This();
@ -112,6 +143,7 @@ pub const Array2D = struct {
} }
}; };
/// Build a Longest Common Subsequence table
fn calculate_lcs( fn calculate_lcs(
lcs: *Array2D, lcs: *Array2D,
astr: []const u8, astr: []const u8,
@ -122,6 +154,13 @@ fn calculate_lcs(
std.mem.set(usize, lcs.data[0 .. rows * cols], 0); std.mem.set(usize, lcs.data[0 .. rows * cols], 0);
// This approach is a dynamic programming technique to calculate the
// longest common subsequence between two strings, `a` and `b`. We start
// at 1 for `i` and `j` because the first column and first row are always
// set to zero
//
// You can find more information about this at the following url:
// https://en.wikipedia.org/wiki/Longest_common_subsequence_problem
var i: usize = 1; var i: usize = 1;
while (i < rows) : (i += 1) { while (i < rows) : (i += 1) {
var j: usize = 1; var j: usize = 1;
@ -146,6 +185,8 @@ pub fn get_changes(
b_trim: []const u8, b_trim: []const u8,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
) !std.ArrayList(Edit) { ) !std.ArrayList(Edit) {
// First we get a list of changes between strings at the character level:
// "addition", "deletion", and "no change" for each character
var changes = try std.ArrayList(Change).initCapacity(allocator, a_trim.len); var changes = try std.ArrayList(Change).initCapacity(allocator, a_trim.len);
defer changes.deinit(); defer changes.deinit();
try recur_changes( try recur_changes(
@ -157,6 +198,9 @@ pub fn get_changes(
@intCast(i64, b_trim.len), @intCast(i64, b_trim.len),
); );
// We want to group runs of deletions and additions, and separate them by
// runs of `.Nothing` changes. This will allow us to calculate the
// `TextEdit` ranges
var groups = std.ArrayList([]Change).init(allocator); var groups = std.ArrayList([]Change).init(allocator);
defer groups.deinit(); defer groups.deinit();
var active_change: ?[]Change = null; var active_change: ?[]Change = null;
@ -181,6 +225,8 @@ pub fn get_changes(
try groups.append(ac.*); try groups.append(ac.*);
} }
// The LCS algorithm works "in reverse", so we're putting everything back
// in ascending order
var a_lines = std.mem.split(u8, a, "\n"); var a_lines = std.mem.split(u8, a, "\n");
std.mem.reverse([]Change, groups.items); std.mem.reverse([]Change, groups.items);
for (groups.items) |group| std.mem.reverse(Change, group); for (groups.items) |group| std.mem.reverse(Change, group);
@ -188,6 +234,7 @@ pub fn get_changes(
var edit_results = std.ArrayList(Edit).init(allocator); var edit_results = std.ArrayList(Edit).init(allocator);
errdefer edit_results.deinit(); errdefer edit_results.deinit();
// Convert our grouped changes into `Edit`s
for (groups.items) |group| { for (groups.items) |group| {
var range_start = group[0].pos; var range_start = group[0].pos;
var range_len: usize = 0; var range_len: usize = 0;
@ -224,6 +271,10 @@ fn recur_changes(
i: i64, i: i64,
j: i64, j: i64,
) anyerror!void { ) anyerror!void {
// This function recursively works backwards through the LCS table in
// order to figure out what kind of changes took place to transform `a`
// into `b`
const ii = @intCast(usize, i); const ii = @intCast(usize, i);
const jj = @intCast(usize, j); const jj = @intCast(usize, j);
@ -282,15 +333,15 @@ fn char_pos_to_range(
} }
if (result_start_pos == null) return error.InvalidRange; if (result_start_pos == null) return error.InvalidRange;
// If we did not find an end position, it is outside the range of the
// string for some reason so clamp it to the string end position
if (result_end_pos == null) { if (result_end_pos == null) {
result_end_pos = types.Position{ result_end_pos = types.Position{
.line = @intCast(i64, line_pos), .line = @intCast(i64, line_pos),
.character = @intCast(i64, char_pos), .character = @intCast(i64, char_pos),
}; };
} }
if (result_start_pos == null or result_end_pos == null) {
return error.InvalidRange;
}
return types.Range{ return types.Range{
.start = result_start_pos.?, .start = result_start_pos.?,