diffTexts function

FileDiff diffTexts(
  1. String oldText,
  2. String newText, {
  3. String path = '',
  4. int contextLines = 3,
  5. bool indentHeuristic = true,
  6. String? id,
  7. int revision = 0,
  8. String? language,
})

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),
  );
}