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