Summary
A Traditional Chinese user on macOS reports that picking multi-character candidates in FleatherEditor produces duplicated text. Single-character picks work. Representative output from their note (intended 但神不會過去式):
Same shape as the closed #150.
What I investigated
I couldn't reproduce with a programmatic test. A minimal widget test that drives RawEditorState.updateEditingValueWithDeltas with the canonical delta sequence — N TextEditingDeltaInsertion events for composing keystrokes, then a single TextEditingDeltaReplacement for the candidate pick — passes. Test source is included below.
I also tried a "stale-IME-state" hypothesis (IME emits deltas against its own pre-pick view of the text). That crashes parchment with index == 0 || (index > 0 && index < length), so whatever macOS actually emits must stay index-consistent with the post-pick document.
Ask
Does anyone with a Traditional Chinese IME (Pinyin or Zhuyin) on macOS see this? A debugPrint of the received delta list inside RawEditorStateTextInputClientMixin.updateEditingValueWithDeltas during a real multi-character candidate pick would let us tell whether this is a fleather bug or something on the Flutter macOS engine side that needs to go upstream to flutter/flutter.
Environment
- OS: macOS (user-reported)
- Flutter: 3.41.7 (stable, 2026-04-15)
- Fleather: 1.26.0
- Parchment: 1.25.1
Canonical-delta test (passes — included as a baseline)
// Baseline test for Fleather's IME delta handling, written as part of
// investigating a user-reported bug where multi-character CJK candidate
// picks produce duplicated text on macOS (fleather 1.26.0, Flutter 3.41.7).
//
// This test confirms that fleather handles the CANONICAL delta sequence
// correctly: a run of TextEditingDeltaInsertion events (one per composing
// keystroke) followed by a TextEditingDeltaReplacement (candidate pick)
// produces exactly the candidate text with no duplication.
//
// Since this passes, the user-reported duplication must originate from a
// different delta stream than the canonical one — most likely from macOS's
// platform-to-Dart translation for CJK input methods, not from fleather's
// delta handler itself. Upstream maintainers with a Chinese IME installed
// should capture the actual delta stream for comparison.
//
// Run with: flutter test test/fleather_ime_test.dart
import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
/// Feeds an IME composition sequence: N insertions (one per phonetic key) and
/// a final replacement when the user picks the candidate.
Future<void> _composeAndPick({
required WidgetTester tester,
required RawEditorState editorState,
required String existingText,
required String composing,
required String candidate,
}) async {
final baseOffset = existingText.length;
String running = existingText;
for (var i = 0; i < composing.length; i++) {
final ch = composing[i];
final oldText = running;
running = '$running$ch';
editorState.updateEditingValueWithDeltas([
TextEditingDeltaInsertion(
oldText: oldText,
textInserted: ch,
insertionOffset: baseOffset + i,
selection: TextSelection.collapsed(offset: baseOffset + i + 1),
composing: TextRange(start: baseOffset, end: baseOffset + i + 1),
),
]);
await tester.pump();
}
editorState.updateEditingValueWithDeltas([
TextEditingDeltaReplacement(
oldText: running,
replacedRange: TextRange(
start: baseOffset, end: baseOffset + composing.length),
replacementText: candidate,
selection: TextSelection.collapsed(offset: baseOffset + candidate.length),
composing: TextRange.empty,
),
]);
await tester.pump();
}
void main() {
testWidgets(
'canonical IME delta sequence: multi-character candidate pick replaces composing run',
(tester) async {
final controller = FleatherController();
final focusNode = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: FleatherEditor(
controller: controller,
focusNode: focusNode,
autofocus: true,
),
),
));
focusNode.requestFocus();
await tester.pumpAndSettle();
final editorState = tester.state<RawEditorState>(find.byType(RawEditor));
await _composeAndPick(
tester: tester,
editorState: editorState,
existingText: '',
composing: 'guoqu',
candidate: '過去',
);
expect(
controller.document.toPlainText().replaceAll('\n', ''),
'過去',
reason: 'single candidate pick should yield only the candidate',
);
await _composeAndPick(
tester: tester,
editorState: editorState,
existingText: '過去',
composing: 'danshenbuhui',
candidate: '但神不會',
);
expect(
controller.document.toPlainText().replaceAll('\n', ''),
'過去但神不會',
reason:
'two successive candidate picks must append cleanly (no accumulation)',
);
// Let fleather's history-throttle timer drain before teardown.
await tester.pump(const Duration(milliseconds: 600));
},
);
}
Summary
A Traditional Chinese user on macOS reports that picking multi-character candidates in
FleatherEditorproduces duplicated text. Single-character picks work. Representative output from their note (intended但神不會過去式):Same shape as the closed #150.
What I investigated
I couldn't reproduce with a programmatic test. A minimal widget test that drives
RawEditorState.updateEditingValueWithDeltaswith the canonical delta sequence — NTextEditingDeltaInsertionevents for composing keystrokes, then a singleTextEditingDeltaReplacementfor the candidate pick — passes. Test source is included below.I also tried a "stale-IME-state" hypothesis (IME emits deltas against its own pre-pick view of the text). That crashes parchment with
index == 0 || (index > 0 && index < length), so whatever macOS actually emits must stay index-consistent with the post-pick document.Ask
Does anyone with a Traditional Chinese IME (Pinyin or Zhuyin) on macOS see this? A
debugPrintof the received delta list insideRawEditorStateTextInputClientMixin.updateEditingValueWithDeltasduring a real multi-character candidate pick would let us tell whether this is a fleather bug or something on the Flutter macOS engine side that needs to go upstream to flutter/flutter.Environment
Canonical-delta test (passes — included as a baseline)