TextEditingDelta.fromJSON constructor

TextEditingDelta.fromJSON(
  1. Map<String, dynamic> encoded
)

Creates an instance of this class from a JSON object by inferring the type of delta based on values sent from the engine.

Implementation

factory TextEditingDelta.fromJSON(Map<String, dynamic> encoded) {
  // An insertion delta is one where replacement destination is collapsed.
  //
  // A deletion delta is one where the replacement source is empty.
  //
  // An insertion/deletion can still occur when the replacement destination is not
  // collapsed, or the replacement source is not empty.
  //
  // On native platforms when composing text, the entire composing region is
  // replaced on input, rather than reporting character by character
  // insertion/deletion. In these cases we can detect if there was an
  // insertion/deletion by checking if the text inside the original composing
  // region was modified by the replacement. If the text is the same then we have
  // an insertion/deletion. If the text is different then we can say we have
  // a replacement.
  //
  // For example say we are currently composing the word: 'world'.
  // Our current state is 'worl|' with the cursor at the end of 'l'. If we
  // input the character 'd', the platform will tell us 'worl' was replaced
  // with 'world' at range (0,4). Here we can check if the text found in the
  // composing region (0,4) has been modified. We see that it hasn't because
  // 'worl' == 'worl', so this means that the text in
  // 'world'{replacementDestinationEnd, replacementDestinationStart + replacementSourceEnd}
  // can be considered an insertion. In this case we inserted 'd'.
  //
  // Similarly for a deletion, say we are currently composing the word: 'worl'.
  // Our current state is 'world|' with the cursor at the end of 'd'. If we
  // press backspace to delete the character 'd', the platform will tell us 'world'
  // was replaced with 'worl' at range (0,5). Here we can check if the text found
  // in the new composing region, is the same as the replacement text. We can do this
  // by using oldText{replacementDestinationStart, replacementDestinationStart + replacementSourceEnd}
  // which in this case is 'worl'. We then compare 'worl' with 'worl' and
  // verify that they are the same. This means that the text in
  // 'world'{replacementDestinationEnd, replacementDestinationStart + replacementSourceEnd} was deleted.
  // In this case the character 'd' was deleted.
  //
  // A replacement delta occurs when the original composing region has been
  // modified.
  //
  // A non text update delta occurs when the selection and/or composing region
  // has been changed by the platform, and there have been no changes to the
  // text value.
  final String oldText = encoded['oldText'] as String;
  final int replacementDestinationStart = encoded['deltaStart'] as int;
  final int replacementDestinationEnd = encoded['deltaEnd'] as int;
  final String replacementSource = encoded['deltaText'] as String;
  const int replacementSourceStart = 0;
  final int replacementSourceEnd = replacementSource.length;

  // This delta is explicitly a non text update.
  final bool isNonTextUpdate =
      replacementDestinationStart == -1 &&
      replacementDestinationStart == replacementDestinationEnd;
  final TextRange newComposing = TextRange(
    start: encoded['composingBase'] as int? ?? -1,
    end: encoded['composingExtent'] as int? ?? -1,
  );
  final TextSelection newSelection = TextSelection(
    baseOffset: encoded['selectionBase'] as int? ?? -1,
    extentOffset: encoded['selectionExtent'] as int? ?? -1,
    affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream,
    isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,
  );

  if (isNonTextUpdate) {
    assert(
      _debugTextRangeIsValid(newSelection, oldText),
      'The selection range: $newSelection is not within the bounds of text: $oldText of length: ${oldText.length}',
    );
    assert(
      _debugTextRangeIsValid(newComposing, oldText),
      'The composing range: $newComposing is not within the bounds of text: $oldText of length: ${oldText.length}',
    );

    return TextEditingDeltaNonTextUpdate(
      oldText: oldText,
      selection: newSelection,
      composing: newComposing,
    );
  }

  assert(
    _debugTextRangeIsValid(
      TextRange(start: replacementDestinationStart, end: replacementDestinationEnd),
      oldText,
    ),
    'The delta range: ${TextRange(start: replacementSourceStart, end: replacementSourceEnd)} is not within the bounds of text: $oldText of length: ${oldText.length}',
  );

  final String newText = _replace(
    oldText,
    replacementSource,
    TextRange(start: replacementDestinationStart, end: replacementDestinationEnd),
  );

  assert(
    _debugTextRangeIsValid(newSelection, newText),
    'The selection range: $newSelection is not within the bounds of text: $newText of length: ${newText.length}',
  );
  assert(
    _debugTextRangeIsValid(newComposing, newText),
    'The composing range: $newComposing is not within the bounds of text: $newText of length: ${newText.length}',
  );

  final bool isEqual = oldText == newText;

  final bool isDeletionGreaterThanOne =
      (replacementDestinationEnd - replacementDestinationStart) -
          (replacementSourceEnd - replacementSourceStart) >
      1;
  final bool isDeletingByReplacingWithEmpty =
      replacementSource.isEmpty &&
      replacementSourceStart == 0 &&
      replacementSourceStart == replacementSourceEnd;

  final bool isReplacedByShorter =
      isDeletionGreaterThanOne &&
      (replacementSourceEnd - replacementSourceStart <
          replacementDestinationEnd - replacementDestinationStart);
  final bool isReplacedByLonger =
      replacementSourceEnd - replacementSourceStart >
      replacementDestinationEnd - replacementDestinationStart;
  final bool isReplacedBySame =
      replacementSourceEnd - replacementSourceStart ==
      replacementDestinationEnd - replacementDestinationStart;

  final bool isInsertingInsideComposingRegion =
      replacementDestinationStart + replacementSourceEnd > replacementDestinationEnd;
  final bool isDeletingInsideComposingRegion =
      !isReplacedByShorter &&
      !isDeletingByReplacingWithEmpty &&
      replacementDestinationStart + replacementSourceEnd < replacementDestinationEnd;

  String newComposingText;
  String originalComposingText;

  if (isDeletingByReplacingWithEmpty || isDeletingInsideComposingRegion || isReplacedByShorter) {
    newComposingText = replacementSource.substring(replacementSourceStart, replacementSourceEnd);
    originalComposingText = oldText.substring(
      replacementDestinationStart,
      replacementDestinationStart + replacementSourceEnd,
    );
  } else {
    newComposingText = replacementSource.substring(
      replacementSourceStart,
      replacementSourceStart + (replacementDestinationEnd - replacementDestinationStart),
    );
    originalComposingText = oldText.substring(
      replacementDestinationStart,
      replacementDestinationEnd,
    );
  }

  final bool isOriginalComposingRegionTextChanged = !(originalComposingText == newComposingText);
  final bool isReplaced =
      isOriginalComposingRegionTextChanged ||
      (isReplacedByLonger || isReplacedByShorter || isReplacedBySame);

  if (isEqual) {
    return TextEditingDeltaNonTextUpdate(
      oldText: oldText,
      selection: newSelection,
      composing: newComposing,
    );
  } else if ((isDeletingByReplacingWithEmpty || isDeletingInsideComposingRegion) &&
      !isOriginalComposingRegionTextChanged) {
    // Deletion.
    int actualStart = replacementDestinationStart;

    if (!isDeletionGreaterThanOne) {
      actualStart = replacementDestinationEnd - 1;
    }

    return TextEditingDeltaDeletion(
      oldText: oldText,
      deletedRange: TextRange(start: actualStart, end: replacementDestinationEnd),
      selection: newSelection,
      composing: newComposing,
    );
  } else if ((replacementDestinationStart == replacementDestinationEnd ||
          isInsertingInsideComposingRegion) &&
      !isOriginalComposingRegionTextChanged) {
    // Insertion.
    return TextEditingDeltaInsertion(
      oldText: oldText,
      textInserted: replacementSource.substring(
        replacementDestinationEnd - replacementDestinationStart,
        (replacementDestinationEnd - replacementDestinationStart) +
            (replacementSource.length -
                (replacementDestinationEnd - replacementDestinationStart)),
      ),
      insertionOffset: replacementDestinationEnd,
      selection: newSelection,
      composing: newComposing,
    );
  } else if (isReplaced) {
    // Replacement.
    return TextEditingDeltaReplacement(
      oldText: oldText,
      replacementText: replacementSource,
      replacedRange: TextRange(
        start: replacementDestinationStart,
        end: replacementDestinationEnd,
      ),
      selection: newSelection,
      composing: newComposing,
    );
  }
  assert(false);
  return TextEditingDeltaNonTextUpdate(
    oldText: oldText,
    selection: newSelection,
    composing: newComposing,
  );
}