textDiff function

TextDiff textDiff(
  1. String oldText,
  2. String newText,
  3. int cursorPosition
)

Computes the edit that turned oldText into newText, using cursorPosition — the caret position in newText after the edit — to resolve ambiguity (in a run of identical characters the change is anchored so it ends at the caret, matching what the user actually typed).

Safe on every input: no unchecked index operations, out-of-range cursors are clamped, and the reported boundaries never split a UTF-16 surrogate pair or a grapheme cluster (emoji ZWJ sequences, flags, combining marks) of either string. The result always satisfies diff.applyTo(oldText) == newText.

Implementation

TextDiff textDiff(String oldText, String newText, int cursorPosition) {
  final cursor = math.min(math.max(cursorPosition, 0), newText.length);
  if (oldText == newText) {
    return TextDiff(cursor, '', '');
  }
  final delta = newText.length - oldText.length;

  // Common suffix, in code units. The scan stops at the caret (translated to
  // old-text coordinates) so an edit inside a run of identical characters is
  // attributed to the caret position.
  var oldEnd = oldText.length;
  final suffixLimit = math.max(0, cursor - delta);
  while (oldEnd > suffixLimit &&
      oldEnd + delta > 0 &&
      oldText.codeUnitAt(oldEnd - 1) ==
          newText.codeUnitAt(oldEnd + delta - 1)) {
    oldEnd -= 1;
  }

  // Common prefix, never crossing into the suffix of either string.
  final maxStart = math.min(oldEnd, oldEnd + delta);
  var start = 0;
  while (start < maxStart &&
      oldText.codeUnitAt(start) == newText.codeUnitAt(start)) {
    start += 1;
  }
  var newEnd = oldEnd + delta;

  // Widen the changed region until every boundary falls on a grapheme
  // cluster boundary of both strings, so the diff never splits a surrogate
  // pair or a ZWJ/flag/combining sequence. Widening stays correct: it only
  // moves equal code units from the common prefix/suffix into the change.
  while (start > 0 &&
      !(_isGraphemeBoundary(oldText, start) &&
          _isGraphemeBoundary(newText, start))) {
    start -= 1;
  }
  while (!(_isGraphemeBoundary(oldText, oldEnd) &&
      _isGraphemeBoundary(newText, newEnd))) {
    oldEnd += 1;
    newEnd += 1;
  }

  return TextDiff(
    start,
    oldText.substring(start, oldEnd),
    newText.substring(start, newEnd),
  );
}