syncImeBufferToFragment method
void
syncImeBufferToFragment()
Syncs the platform IME buffer with the current fragment text and cursor. Call after any document mutation or cursor move.
Implementation
void syncImeBufferToFragment() {
if (_connection == null || !_connection!.attached) return;
if (_isComposing) return;
final doc = _document;
if (doc == null) return;
final currentFragId = doc.cursor.focusId.isNotEmpty ? doc.cursor.focusId : doc.cursor.anchorId;
if (!_shouldSyncBuffer) {
// Android: only reset the buffer when the cursor moves to a different
// fragment. If we're still in the same fragment, leave the buffer alone
// so the IME can keep its suggestion context.
if (currentFragId != _lastSyncedFragmentId) {
_resetPlatformBuffer();
}
_lastSyncedFragmentId = currentFragId;
return;
}
_lastSyncedFragmentId = currentFragId;
final text = _getCurrentFragmentText();
if (text == null) return;
final cursor = doc.cursor;
final isSingleFragSelection =
!cursor.isCollapsed && cursor.anchorId == cursor.focusId;
final isMultiFragSelection =
!cursor.isCollapsed && cursor.anchorId != cursor.focusId;
final offset = _getCursorOffsetInFragment();
final bool usePlaceholder = _isIOS &&
cursor.isCollapsed &&
offset == 0 &&
!text.startsWith(_emptyFragmentPlaceholder);
final syncedText = usePlaceholder ? '$_emptyFragmentPlaceholder$text' : text;
final syncedOffset = usePlaceholder ? 1 : offset.clamp(0, syncedText.length);
final TextSelection syncedSelection;
if (isSingleFragSelection && !usePlaceholder) {
syncedSelection = TextSelection(
baseOffset: cursor.anchorOffset.clamp(0, syncedText.length),
extentOffset: cursor.focusOffset.clamp(0, syncedText.length),
);
} else if (isMultiFragSelection && !usePlaceholder) {
// Multi-fragment selection: select the entire buffer so the
// platform can delete or replace it, triggering the document-
// level selection deletion/replacement.
syncedSelection = TextSelection(
baseOffset: 0,
extentOffset: syncedText.length,
);
} else {
syncedSelection = TextSelection.collapsed(offset: syncedOffset);
}
final currentSelectionKey =
'${cursor.anchorId}:${cursor.anchorOffset}:${cursor.focusId}:${cursor.focusOffset}';
final bool selectionChanged = currentSelectionKey != _prevSelectionKey;
_prevSelectionKey = currentSelectionKey;
_updatingSelf = true;
try {
// On web, if the new text is shorter than what the browser currently
// holds, the browser sends a delta with a stale composing range that
// exceeds the new text length, triggering an assertion in
// TextEditingDelta.fromJSON. Resetting to empty first (same approach
// as iOS) forces the browser to treat the next setEditingState as a
// fresh insertion with no stale composition state.
if (kIsWeb && syncedText.length < _lastSyncedText.length) {
_connection!.setEditingState(const TextEditingValue());
}
if (_isIOS && selectionChanged) {
_connection!.setEditingState(const TextEditingValue());
}
_connection!.setEditingState(TextEditingValue(
text: syncedText,
selection: syncedSelection,
composing: TextRange.empty,
));
_lastSyncedText = syncedText;
} on PlatformException catch (e) {
debugPrint('FluentTextInputHandler: syncImeBufferToFragment failed: ${e.message}');
}
_updatingSelf = false;
// On web, reposition the hidden <textarea> so the IME popup follows
// the cursor after the buffer content changed (e.g. after accepting a
// suggestion). Without this the popup stays at the old position.
if (kIsWeb) {
_updateWebImePosition();
}
}