using System.Text; namespace ExtractMethod.Tooling; /// /// Line-based unified diff (v1, hand-rolled, no dependencies): an LCS over /// lines is backtracked into equal/delete/insert runs, and the runs are /// printed as unified hunks with 3 lines of context (hunks closer than /// 2×context lines apart merge). The shape is the familiar /// ---/+++/@@ one — enough for a human to review the generated /// refactoring; no patch-file semantics are promised. /// public static class UnifiedDiff { private const int Context = 3; public static string Diff(string original, string modified, string originalLabel, string modifiedLabel) { var a = SplitLines(original); var b = SplitLines(modified); var sb = new StringBuilder($"--- {originalLabel}\n+++ {modifiedLabel}\n"); foreach (var hunk in BuildHunks(a, b)) { sb.Append(hunk); } return sb.ToString(); } /// /// Split on newlines, dropping a trailing carriage return (CRLF tolerance). /// A trailing newline does NOT start a new line: "a\n" is ONE line, so the /// phantom empty element a naive Split('\n') leaves at the end is dropped /// (it would inflate every hunk header by one). The empty string is zero /// lines. /// private static string[] SplitLines(string text) { if (text.Length == 0) { return []; } var lines = text.Split('\n').Select(line => line.TrimEnd('\r')).ToArray(); return lines[^1].Length == 0 ? lines[..^1] : lines; } private enum Op { Equal, Del, Ins } /// /// LCS + backtracking + hunk assembly. lcs[i, j] is the length of /// the longest common suffix of a[i..] and b[j..]. /// private static IReadOnlyList BuildHunks(string[] a, string[] b) { var n = a.Length; var m = b.Length; var lcs = new int[n + 1, m + 1]; for (var row = n - 1; row >= 0; row--) { for (var col = m - 1; col >= 0; col--) { lcs[row, col] = a[row] == b[col] ? lcs[row + 1, col + 1] + 1 : Math.Max(lcs[row + 1, col], lcs[row, col + 1]); } } // Backtrack: equal lines advance both; on a mismatch prefer a deletion // (ties go to delete, which keeps the printed block old-then-new). var ops = new List<(Op Tag, int A, int B)>(); var i = 0; var j = 0; while (i < n && j < m) { if (a[i] == b[j]) { ops.Add((Op.Equal, i, j)); i++; j++; } else if (lcs[i + 1, j] >= lcs[i, j + 1]) { ops.Add((Op.Del, i, j)); i++; } else { ops.Add((Op.Ins, i, j)); j++; } } while (i < n) { ops.Add((Op.Del, i, j)); i++; } while (j < m) { ops.Add((Op.Ins, i, j)); j++; } // A "region" is a maximal run of non-equal ops: [AStart, AEnd) × // [BStart, BEnd) in the two line arrays. var regions = new List<(int AStart, int AEnd, int BStart, int BEnd)>(); var index = 0; while (index < ops.Count) { if (ops[index].Tag == Op.Equal) { index++; continue; } var first = ops[index]; var regionAStart = first.Tag == Op.Del ? first.A : (index > 0 ? ops[index - 1].A + 1 : 0); var regionBStart = first.B; while (index < ops.Count && ops[index].Tag != Op.Equal) { index++; } var last = ops[index - 1]; regions.Add((regionAStart, last.Tag == Op.Del ? last.A + 1 : last.A, regionBStart, last.Tag == Op.Ins ? last.B + 1 : last.B)); } // Extend every region by Context lines (clamped), then merge regions // whose extended ranges overlap. var hunks = new List<(int AStart, int AEnd, int BStart, int BEnd)>(); foreach (var (aStart, aEnd, bStart, bEnd) in regions) { var (haStart, haEnd) = (Math.Max(0, aStart - Context), Math.Min(n, aEnd + Context)); var (hbStart, hbEnd) = (Math.Max(0, bStart - Context), Math.Min(m, bEnd + Context)); if (hunks.Count > 0) { var prev = hunks[^1]; if (haStart <= prev.AEnd && hbStart <= prev.BEnd) { hunks[^1] = (prev.AStart, Math.Max(prev.AEnd, haEnd), prev.BStart, Math.Max(prev.BEnd, hbEnd)); continue; } } hunks.Add((haStart, haEnd, hbStart, hbEnd)); } return hunks.Select(hunk => RenderHunk(ops, a, b, hunk.AStart, hunk.AEnd, hunk.BStart, hunk.BEnd)).ToList(); } private static string RenderHunk( IReadOnlyList<(Op Tag, int A, int B)> ops, string[] a, string[] b, int aStart, int aEnd, int bStart, int bEnd) { var sb = new StringBuilder(); sb.AppendLine($"@@ -{aStart + 1},{aEnd - aStart} +{bStart + 1},{bEnd - bStart} @@"); foreach (var (tag, aIndex, bIndex) in ops) { switch (tag) { case Op.Equal when aStart <= aIndex && aIndex < aEnd: sb.Append(' ').AppendLine(a[aIndex]); break; case Op.Del when aStart <= aIndex && aIndex < aEnd: sb.Append('-').AppendLine(a[aIndex]); break; case Op.Ins when bStart <= bIndex && bIndex < bEnd: sb.Append('+').AppendLine(b[bIndex]); break; } } return sb.ToString(); } }