From c64aec61322883cb8d895999d10556090b57be87 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 21 Jan 2026 00:38:33 -0600 Subject: [PATCH] Enhance WhitespaceUtil with LCS-based diff algorithm for improved change detection - Implemented a Longest Common Subsequence (LCS) algorithm to accurately compute line differences, enhancing the detection of whitespace-only changes. - Refactored the CountLineDifferences function to utilize LCS for both exact and normalized comparisons, improving performance and clarity. - Added normalization option to handle whitespace discrepancies effectively, ensuring more precise diff results. --- plugin/src/WhitespaceUtil.lua | 97 +++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/plugin/src/WhitespaceUtil.lua b/plugin/src/WhitespaceUtil.lua index 51baf3fe..63ac7374 100644 --- a/plugin/src/WhitespaceUtil.lua +++ b/plugin/src/WhitespaceUtil.lua @@ -1,5 +1,7 @@ --[[ Utility for detecting whitespace-only changes in source code. + Uses LCS (Longest Common Subsequence) based diff algorithm for accurate + change detection that properly handles insertions and deletions. ]] local WhitespaceUtil = {} @@ -16,7 +18,52 @@ local function NormalizeLine(line: string): string return string.gsub(string.gsub(line, "\r+$", ""), "[ \t]+$", "") end --- Counts lines that differ between two strings (capped at 100) +-- Computes the length of the Longest Common Subsequence between two line arrays +-- If normalize is true, lines are compared after whitespace normalization +local function ComputeLcsLength(linesA: { string }, linesB: { string }, normalize: boolean): number + local m, n = #linesA, #linesB + if m == 0 or n == 0 then + return 0 + end + + -- Precompute comparison lines (normalized if requested) + local compareA, compareB = linesA, linesB + if normalize then + compareA = table.create(m) + compareB = table.create(n) + for i = 1, m do + compareA[i] = NormalizeLine(linesA[i]) + end + for j = 1, n do + compareB[j] = NormalizeLine(linesB[j]) + end + end + + -- Space-optimized DP for LCS + -- prev[j+1] = LCS length for linesA[1..i-1] and linesB[1..j] + -- curr[j+1] = LCS length for linesA[1..i] and linesB[1..j] + local prev = table.create(n + 1, 0) + local curr = table.create(n + 1, 0) + + for i = 1, m do + for j = 1, n do + if compareA[i] == compareB[j] then + curr[j + 1] = prev[j] + 1 + else + curr[j + 1] = math.max(prev[j + 1], curr[j]) + end + end + -- Swap rows and reset curr + prev, curr = curr, prev + for j = 1, n + 1 do + curr[j] = 0 + end + end + + return prev[n + 1] +end + +-- Counts lines that differ between two strings using LCS-based diff -- Returns: totalDiff, whitespaceDiff (lines that only differ in whitespace) -- Returns 10000 for totalDiff if exceeds 100 (frontend displays as "100+") -- whitespaceDiff is 10000 only if ALL checked differences were whitespace-only @@ -27,29 +74,37 @@ function WhitespaceUtil.CountLineDifferences(current: string, incoming: string): local currentLines = SplitLines(current) local incomingLines = SplitLines(incoming) - local maxLines = math.max(#currentLines, #incomingLines) + local m, n = #currentLines, #incomingLines - local totalDiff, whitespaceDiff = 0, 0 - local hasContentChange = false - for i = 1, maxLines do - local currentLine = currentLines[i] or "" - local incomingLine = incomingLines[i] or "" - - if currentLine ~= incomingLine then - totalDiff += 1 - if NormalizeLine(currentLine) == NormalizeLine(incomingLine) then - whitespaceDiff += 1 - else - hasContentChange = true - end - if totalDiff > 100 then - -- Return 10000 for both only if ALL differences were whitespace-only - -- Otherwise return different values so isWhitespaceOnly is false - return 10000, if hasContentChange then 0 else 10000 - end - end + -- Quick check: if size difference alone exceeds limit + local minPossibleDiff = math.abs(m - n) + if minPossibleDiff > 100 then + return 10000, 0 end + -- Compute LCS with exact matching + -- totalDiff = deletions + insertions = (m - lcs) + (n - lcs) + local exactLcs = ComputeLcsLength(currentLines, incomingLines, false) + local totalDiff = (m - exactLcs) + (n - exactLcs) + + if totalDiff > 100 then + -- Compute normalized LCS to determine if changes are whitespace-only + local normalizedLcs = ComputeLcsLength(currentLines, incomingLines, true) + local normalizedDiff = (m - normalizedLcs) + (n - normalizedLcs) + return 10000, if normalizedDiff == 0 then 10000 else 0 + end + + if totalDiff == 0 then + return 0, 0 + end + + -- Compute LCS with normalized matching to find whitespace-only changes + local normalizedLcs = ComputeLcsLength(currentLines, incomingLines, true) + local normalizedDiff = (m - normalizedLcs) + (n - normalizedLcs) + + -- whitespaceDiff = changes that disappear after normalization + local whitespaceDiff = totalDiff - normalizedDiff + return totalDiff, whitespaceDiff end