lineSpanEdit function

LineSpanEdit? lineSpanEdit(
  1. TerminalRenderLine oldLine,
  2. TerminalRenderLine newLine
)

Computes the minimal single-span edit that turns oldLine (currently on the terminal) into newLine, both taken from parsed render frames of the same row. Returns null when the difference has no visible effect (e.g. only zero-width sequences changed, with no impact on later cells).

The edit never touches display columns outside the changed span: a shared prefix is skipped, and a byte-identical tail is kept when the two lines have the same total display width and the ANSI state in effect where the tail starts is identical under both lines (otherwise the tail's rendered appearance could differ even though its bytes match, so it is rewritten).

Implementation

LineSpanEdit? lineSpanEdit(
  TerminalRenderLine oldLine,
  TerminalRenderLine newLine,
) {
  final oldTokens = _tokenize(oldLine.raw);
  final newTokens = _tokenize(newLine.raw);
  final oldWidth = _totalWidth(oldTokens);
  final newWidth = _totalWidth(newTokens);

  // A different inherited state at the line start restyles every cell — the
  // whole row must be rewritten.
  if (oldLine.statePrefix != newLine.statePrefix) {
    return LineSpanEdit(
      column: 0,
      text:
          '${Ansi.reset}${UvAnsi.resetHyperlink()}'
          '${newLine.statePrefix}${newLine.raw}'
          '${_tailErase(oldWidth, newWidth)}',
    );
  }

  // Longest common token prefix.
  final minLen = oldTokens.length < newTokens.length
      ? oldTokens.length
      : newTokens.length;
  var prefix = 0;
  while (prefix < minLen && oldTokens[prefix].text == newTokens[prefix].text) {
    prefix++;
  }

  var prefixColumns = 0;
  for (var i = 0; i < prefix; i++) {
    prefixColumns += newTokens[i].width;
  }

  // Longest common token suffix — only meaningful when both lines are the
  // same total display width (otherwise the shared bytes sit at different
  // columns), and only safe when the ANSI state at the suffix start is the
  // same under both lines (matching bytes can still render differently when
  // an earlier sequence changed the pen).
  var suffix = 0;
  if (oldWidth == newWidth) {
    final maxSuffix = (oldTokens.length - prefix) < (newTokens.length - prefix)
        ? oldTokens.length - prefix
        : newTokens.length - prefix;
    while (suffix < maxSuffix &&
        oldTokens[oldTokens.length - 1 - suffix].text ==
            newTokens[newTokens.length - 1 - suffix].text) {
      suffix++;
    }
    if (suffix > 0) {
      final oldState = _statePrefixAt(
        oldLine.statePrefix,
        oldTokens,
        oldTokens.length - suffix,
      );
      final newState = _statePrefixAt(
        newLine.statePrefix,
        newTokens,
        newTokens.length - suffix,
      );
      if (oldState != newState) suffix = 0;
    }
  }

  final middle = StringBuffer();
  for (var i = prefix; i < newTokens.length - suffix; i++) {
    middle.write(newTokens[i].text);
  }
  final tail = suffix == 0 ? _tailErase(oldWidth, newWidth) : '';

  // Nothing visible changed (zero-width-only difference).
  if (middle.isEmpty && tail.isEmpty) return null;

  return LineSpanEdit(
    column: prefixColumns,
    text:
        '${Ansi.reset}${UvAnsi.resetHyperlink()}'
        '${_statePrefixAt(newLine.statePrefix, newTokens, prefix)}'
        '$middle$tail',
  );
}