diffTexts function
Compares two versions of a file line by line, like git diff.
When a change could sit in more than one place, such as a line added
among identical lines, it goes where git diff puts it. Like git, that
uses the indent heuristic, which looks at the indents and blank lines
around each spot so that, for example, an added function shows as the
whole function. With indentHeuristic false, changes go where
git diff --no-indent-heuristic puts them.
Hunks get contextLines unchanged lines on each side, and hunks whose
context would touch are merged. The result keeps both full texts in
FileDiff.oldLines and FileDiff.newLines, so a viewer can expand the
unchanged regions between hunks. path names the file, and also picks
the language for highlighting. The result is always
FileDiffKind.modified with both paths set to path: an empty text is
an empty file, not a missing one.
Lines are compared with their line endings, so converting CRLF to LF is
a change, but DiffLine.text never includes the \r. Texts that differ
in more than about a thousand places, after any shared lines at the
start and end, are shown as one block that replaces the old lines with
the new ones.
Implementation
FileDiff diffTexts(
String oldText,
String newText, {
String path = '',
int contextLines = 3,
bool indentHeuristic = true,
String? id,
int revision = 0,
String? language,
}) {
RangeError.checkNotNegative(contextLines, 'contextLines');
final before = _Text(oldText);
final after = _Text(newText);
// Compare lines as ints. The last line's key includes whether it ends
// with a line break, so adding or removing a final newline is a change.
final ids = <String, int>{};
List<int> keys(_Text t) => [
for (var i = 0; i < t.raw.length; i++)
ids.putIfAbsent(
t.isLastWithoutNewline(i) ? '${t.raw[i]}\u0000' : t.raw[i],
() => ids.length,
),
];
final oldKeys = keys(before);
final newKeys = keys(after);
final edits = compactEdits(
myersDiff(oldKeys, newKeys),
oldKeys,
newKeys,
aLines: indentHeuristic ? before.raw : null,
bLines: indentHeuristic ? after.raw : null,
);
final hunks = <DiffHunk>[];
var i = 0;
while (true) {
while (i < edits.length && edits[i].kind == EditKind.equal) {
i++;
}
if (i == edits.length) break;
final start = i - contextLines < 0 ? 0 : i - contextLines;
// Take in later changes while the unchanged gap is small enough that
// their context would touch.
var end = i;
while (true) {
while (end < edits.length && edits[end].kind != EditKind.equal) {
end++;
}
var next = end;
while (next < edits.length && edits[next].kind == EditKind.equal) {
next++;
}
if (next < edits.length && next - end <= 2 * contextLines) {
end = next;
continue;
}
end = end + contextLines < edits.length
? end + contextLines
: edits.length;
break;
}
hunks.add(_hunk(edits, start, end, before, after));
i = end;
}
return FileDiff(
identity: id,
revision: revision,
language: language,
oldText: oldText,
newText: newText,
oldPath: path,
newPath: path,
kind: FileDiffKind.modified,
hunks: List.unmodifiable(hunks),
oldLines: List.unmodifiable(before.lines),
newLines: List.unmodifiable(after.lines),
);
}