updateEditingValue method
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();
}