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