updateEditingValue method

  1. @override
void updateEditingValue(
  1. TextEditingValue value
)
override

Requests that this client update its editing state to the given value.

The new value is treated as user input and thus may subject to input formatting.

Implementation

@override
void updateEditingValue(TextEditingValue value) {
  if (defaultTargetPlatform == TargetPlatform.macOS) {
    _currentPlatformValue = value;
  }
  final doc = _document;
  if (doc == null) return;
  if (state.updatingSelf) return;
  final cursor = doc.cursor;
  final fragId = cursor.focusId.isNotEmpty ? cursor.focusId : cursor.anchorId;
  final currentText = _getCurrentFragmentText() ?? '';

  // -------------------------------------------------------------------------
  // 1. Preedit Active / Updating
  // -------------------------------------------------------------------------
  if (value.composing.isValid) {
    final wasComposing = state.isComposing;
    if (!doc.cursor.isCollapsed && !wasComposing) {
      executeHandleBackspace(doc);
    }
    final hasPlaceholder = value.text.startsWith(_emptyFragmentPlaceholder);
    final rawStart = (hasPlaceholder && value.composing.start > 0)
        ? (value.composing.start - 1).clamp(0, value.text.length)
        : value.composing.start.clamp(0, value.text.length);
    final rawEnd = (hasPlaceholder && value.composing.end > 0)
        ? (value.composing.end - 1).clamp(0, value.text.length)
        : value.composing.end.clamp(0, value.text.length);
    final actualStart = value.composing.start.clamp(0, value.text.length);
    final actualEnd = value.composing.end.clamp(0, value.text.length);
    final rawPreedit = value.text.substring(actualStart, actualEnd);

    state.preeditFragmentId = fragId;
    if (!wasComposing) {
      state.isComposing = true;
      final initialOffset = cursor.isCollapsed
          ? cursor.focusOffset
          : (cursor.anchorOffset < cursor.focusOffset
                ? cursor.anchorOffset
                : cursor.focusOffset);
      state.preeditLocalOffset = initialOffset;
      state.lastSyncedText = _getCurrentFragmentText() ?? '';
      final parentId = doc.findParentCached(fragId);
      state.preeditContainerId = parentId ?? '';
      state.justCommittedComposition = false;
    }
    state.isComposing = true;

    if (rawPreedit.isNotEmpty) {
      final preeditText = _extractPreeditText(value);
      if (preeditText.isNotEmpty) {
        state.preeditText = preeditText;
        state.composingRange = value.composing;
        state.preeditCaretOffset = value.selection.isValid
            ? (value.selection.extentOffset - rawStart).clamp(
                0,
                preeditText.length,
              )
            : preeditText.length;

        cursor.imeComposing = true;
        cursor.imeComposingStart = state.preeditLocalOffset;

        final targetNode = doc.nodeById(fragId);
        if (targetNode is Fragment && targetNode.text.isNotEmpty) {
          if (rawStart >= 0 && rawEnd <= value.text.length) {
            final prefixText = value.text.substring(0, rawStart);
            final suffixText = value.text.substring(rawEnd);
            if (targetNode.text == prefixText + preeditText + suffixText) {
              targetNode.text = prefixText + suffixText;
              doc.cursor.moveTo(fragId, rawStart);
            }
          }
        }
      }
    }

    doc.selectionManager.clear();
    _invalidatePreeditRender();
    return;
  }

  // -------------------------------------------------------------------------
  // 2. Post-Commit Platform Buffer Sync Guard
  // -------------------------------------------------------------------------
  if (state.justCommittedComposition) {
    if (!value.composing.isValid) {
      state.justCommittedComposition = false;
      if (state.lastCommittedText.isEmpty ||
          value.text.contains(state.lastCommittedText)) {
        final parentId = doc.findParentCached(fragId);
        final paragraphNode = parentId != null
            ? doc.nodeById(parentId)
            : null;
        if (paragraphNode is Paragraph) {
          final pText = paragraphNode.text;
          if (pText == value.text ||
              (value.text.isNotEmpty && pText.contains(value.text)) ||
              (pText.isNotEmpty && value.text.contains(pText))) {
            state.lastSyncedText = value.text;
            return;
          }
        }
        state.lastSyncedText = value.text;
        syncImeBufferToFragment();
        return;
      }
    }
  }

  // -------------------------------------------------------------------------
  // 3. Composition Transition / Commit
  // -------------------------------------------------------------------------
  if (state.isComposing) {
    if (value.text.isNotEmpty) {
      final (prefix, suffix) = _getParagraphPrefixAndSuffix();
      final prefixPos = prefix.isNotEmpty ? value.text.indexOf(prefix) : 0;
      final suffixPos = suffix.isNotEmpty
          ? _findSuffixPos(value.text, suffix)
          : value.text.length;
      if (prefixPos != -1 &&
          suffixPos != -1 &&
          suffixPos >= prefixPos + prefix.length) {
        final extracted = value.text.substring(
          prefixPos + prefix.length,
          suffixPos,
        );
        final current = _getCurrentFragmentText() ?? '';
        if (extracted.isNotEmpty &&
            extracted != current &&
            !current.contains(extracted)) {
          final cleaned =
              (state.preeditText.isNotEmpty &&
                  extracted.trim() == state.preeditText.trim())
              ? state.preeditText
              : extracted.trim();
          if (cleaned.isNotEmpty) {
            state.preeditText = _sanitizeUtf16(cleaned);
          }
        }
      }
    }
    final textToCommit = state.preeditText;
    if (textToCommit.isNotEmpty) {
      _commitPreedit(textToCommit);
    }
    _resetComposition();
    _invalidatePreeditRender();
    if (!value.composing.isValid) {
      state.lastSyncedText = value.text;
      syncImeBufferToFragment();
      return;
    }
  }

  // -------------------------------------------------------------------------
  // 4. Non-Composing Text Edits (Typing, Selection Replace, Deletion)
  // -------------------------------------------------------------------------
  final oldText = currentText;
  final newText = value.text;

  if (!cursor.isCollapsed) {
    if (newText != oldText) {
      final inserted = _computeInsertedText(oldText, newText);
      doc.saveState(description: 'Replace selection', forceNewAction: false);
      if (inserted.isNotEmpty) {
        _insertFinalizedText(inserted);
      } else {
        _replaceFragmentText(
          newText,
          cursorOffset: value.selection.extentOffset,
        );
      }
      syncImeBufferToFragment();
      return;
    }
  }

  if (newText.length > oldText.length) {
    var prefixLen = _commonPrefixLength(oldText, newText);
    if (prefixLen == oldText.length &&
        oldText.isNotEmpty &&
        !oldText.endsWith(' ')) {
      final lastSpace = oldText.lastIndexOf(' ');
      final lastWordStart = lastSpace == -1 ? 0 : lastSpace + 1;
      final lastWord = oldText.substring(lastWordStart);
      if (lastWord.isNotEmpty &&
          lastWordStart < newText.length &&
          newText.substring(lastWordStart).startsWith(lastWord)) {
        final newTail = newText.substring(lastWordStart);
        if (newTail.length > lastWord.length &&
            (newTail.endsWith(' ') || newTail.contains(' '))) {
          prefixLen = lastWordStart;
        }
      }
    }
    final suffixLen = _commonSuffixLength(
      oldText.substring(prefixLen),
      newText.substring(prefixLen),
    );
    final oldReplaced = oldText.substring(
      prefixLen,
      oldText.length - suffixLen,
    );
    final newInserted = newText.substring(
      prefixLen,
      newText.length - suffixLen,
    );

    if (oldReplaced.isNotEmpty) {
      doc.saveState(description: 'Candidate replace', forceNewAction: false);
      final containerId = doc.findLogicalContainerId(fragId) ?? fragId;
      doc.cursor.anchorId = fragId;
      doc.cursor.anchorOffset = prefixLen;
      doc.cursor.focusId = fragId;
      doc.cursor.focusOffset = prefixLen + oldReplaced.length;
      doc.selectionManager.startSelection(containerId, fragId, prefixLen);
      doc.selectionManager.updateFocus(
        containerId,
        fragId,
        prefixLen + oldReplaced.length,
      );
      _insertFinalizedText(newInserted);
      syncImeBufferToFragment();
      return;
    } else {
      final inserted = _computeInsertedText(oldText, newText);
      if (inserted.isNotEmpty) {
        doc.saveState(description: 'Type', forceNewAction: false);
        doc.cursor.moveTo(fragId, prefixLen);
        _insertFinalizedText(inserted);
      }
    }
  } else if (newText.length < oldText.length) {
    doc.saveState(description: 'Delete', forceNewAction: false);
    if (oldText.length > newText.length) {
      executeHandleBackspace(doc);
    } else {
      _replaceFragmentText(
        newText,
        cursorOffset: value.selection.extentOffset,
      );
    }
  }

  syncImeBufferToFragment();
}