private static void ComputeDiffImpl(ReadOnlySpan <char> left, ReadOnlySpan <char> right, ImmutableArray <Diff> .Builder builder) { // Only text was added if (left.Length == 0) { builder.AddIfNotEmpty(new Diff(Operation.INSERT, right)); return; } // Only text was deleted if (right.Length == 0) { builder.AddIfNotEmpty(new Diff(Operation.DELETE, left)); return; } var longText = left.Length > right.Length ? left : right; var shortText = left.Length > right.Length ? right : left; int index = longText.IndexOf(shortText, StringComparison.Ordinal); // Shorter text is inside the longer text if (index != -1) { Operation op = (left.Length > right.Length) ? Operation.DELETE : Operation.INSERT; builder.AddIfNotEmpty(new Diff(op, longText.Slice(0, index))); builder.AddIfNotEmpty(new Diff(Operation.EQUAL, shortText)); builder.AddIfNotEmpty(new Diff(op, longText.Slice(index + shortText.Length))); return; } // Single character string. // After the previous speedup, the character can't be an equality. if (shortText.Length == 1) { builder.AddIfNotEmpty(new Diff(Operation.DELETE, left)); builder.AddIfNotEmpty(new Diff(Operation.INSERT, right)); return; } // Check to see if the problem can be split in two. var success = TryComputeHalfMatch(left, right, out var leftA, out var leftB, out var rightA, out var rightB, out var common); if (success) { ComputeDiff(leftA, rightA, builder); builder.AddIfNotEmpty(new Diff(Operation.EQUAL, common)); ComputeDiff(leftB, rightB, builder); return; } if (left.Length > 100 && right.Length > 100) { ComputeLineModeDiff(left, right, builder); return; } Bisect(left, right, builder); }
private static void ComputeDiff(ReadOnlySpan <char> left, ReadOnlySpan <char> right, ImmutableArray <Diff> .Builder builder) { // Check for equality if (left.SequenceEqual(right)) { builder.AddIfNotEmpty(new Diff(Operation.EQUAL, left)); return; } // Trim off common prefix int commonPrefixLength = ComputeCommonPrefix(left, right); var commonPrefix = left.Slice(0, commonPrefixLength); left = left.Slice(commonPrefixLength); right = right.Slice(commonPrefixLength); // Trim off common suffix int commonSuffixLength = ComputeCommonSuffix(left, right); var commonSuffix = left.Slice(left.Length - commonSuffixLength); left = left.Slice(0, left.Length - commonSuffixLength); right = right.Slice(0, right.Length - commonSuffixLength); // Compute the diff on the middle block. ComputeDiffImpl(left, right, builder); // Restore the prefix and suffix. if (commonPrefix.Length > 0) { builder.AddIfNotEmpty(new Diff(Operation.EQUAL, commonPrefix)); } if (commonSuffix.Length > 0) { builder.AddIfNotEmpty(new Diff(Operation.EQUAL, commonSuffix)); } CleanUpMerge(builder); }
private static void Bisect(ReadOnlySpan <char> text1, ReadOnlySpan <char> text2, ImmutableArray <Diff> .Builder builder) { // Cache the text lengths to prevent multiple calls. int text1_length = text1.Length; int text2_length = text2.Length; int max_d = (text1_length + text2_length + 1) / 2; int v_offset = max_d; int v_length = 2 * max_d; int[] v1 = new int[v_length]; int[] v2 = new int[v_length]; for (int x = 0; x < v_length; x++) { v1[x] = -1; v2[x] = -1; } v1[v_offset + 1] = 0; v2[v_offset + 1] = 0; int delta = text1_length - text2_length; // If the total number of characters is odd, then the front path will // collide with the reverse path. bool front = (delta % 2 != 0); // Offsets for start and end of k loop. // Prevents mapping of space beyond the grid. int k1start = 0; int k1end = 0; int k2start = 0; int k2end = 0; for (int d = 0; d < max_d; d++) { // Walk the front path one step. for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { int k1_offset = v_offset + k1; int x1; if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { x1 = v1[k1_offset + 1]; } else { x1 = v1[k1_offset - 1] + 1; } int y1 = x1 - k1; while (x1 < text1_length && y1 < text2_length && text1[x1] == text2[y1]) { x1++; y1++; } v1[k1_offset] = x1; if (x1 > text1_length) { // Ran off the right of the graph. k1end += 2; } else if (y1 > text2_length) { // Ran off the bottom of the graph. k1start += 2; } else if (front) { int k2_offset = v_offset + delta - k1; if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { // Mirror x2 onto top-left coordinate system. int x2 = text1_length - v2[k2_offset]; if (x1 >= x2) { // Overlap detected. BisectSplit(text1, text2, x1, y1, builder); return; } } } } // Walk the reverse path one step. for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { int k2_offset = v_offset + k2; int x2; if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { x2 = v2[k2_offset + 1]; } else { x2 = v2[k2_offset - 1] + 1; } int y2 = x2 - k2; while (x2 < text1_length && y2 < text2_length && text1[text1_length - x2 - 1] == text2[text2_length - y2 - 1]) { x2++; y2++; } v2[k2_offset] = x2; if (x2 > text1_length) { // Ran off the left of the graph. k2end += 2; } else if (y2 > text2_length) { // Ran off the top of the graph. k2start += 2; } else if (!front) { int k1_offset = v_offset + delta - k2; if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { int x1 = v1[k1_offset]; int y1 = v_offset + x1 - k1_offset; // Mirror x2 onto top-left coordinate system. x2 = text1_length - v2[k2_offset]; if (x1 >= x2) { // Overlap detected. BisectSplit(text1, text2, x1, y1, builder); return; } } } } } // number of diffs equals number of characters, no commonality at all. builder.AddIfNotEmpty(new Diff(Operation.DELETE, text1)); builder.AddIfNotEmpty(new Diff(Operation.INSERT, text2)); }
private static void ComputeLineModeDiff(ReadOnlySpan <char> left, ReadOnlySpan <char> right, ImmutableArray <Diff> .Builder builder) { ComputeDiffLines(left, right, out var newLeft, out var newRight, out var linesArray); ComputeDiff(newRight, newLeft, builder); // Convert the diff back to original text. ConvertCharsToLines(builder, linesArray); // Eliminate freak matches (e.g. blank lines) CleanUpSemantic(builder); // Rediff any replacement blocks, this time character-by-character. // Add a dummy entry at the end. builder.AddIfNotEmpty(new Diff(Operation.EQUAL, string.Empty)); int pointer = 0; int deleteCount = 0; int insertCount = 0; ReadOnlySpan <char> deletedText = default; ReadOnlySpan <char> insertedText = default; while (pointer < builder.Count) { switch (builder[pointer].Operation) { case Operation.INSERT: insertCount++; if (insertedText == default) { insertedText = builder[pointer].Text.Span; } break; case Operation.DELETE: deleteCount++; if (deletedText == default) { deletedText = builder[pointer].Text.Span; } break; case Operation.EQUAL: // Upon reaching an equality, check for prior redundancies. if (deleteCount >= 1 && insertCount >= 1) { // Delete the offending records and add the merged ones. for (int i = pointer - deleteCount - insertCount; i < deleteCount + insertCount; i++) { builder.RemoveAt(i); } pointer = pointer - deleteCount - insertCount; var newBuilder = ImmutableArray.CreateBuilder <Diff>(); ComputeDiff(deletedText, insertedText, newBuilder); for (int i = 0; i < newBuilder.Count; i++) { builder.Insert(i + pointer, newBuilder[i]); } pointer = pointer + newBuilder.Count; } insertCount = 0; deleteCount = 0; deletedText = default; insertedText = default; break; default: break; } pointer++; } }