From 56cc0a4f6478cb023afc558992392017d4332c3b Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Tue, 2 Apr 2024 19:11:12 +0330 Subject: [PATCH 1/2] Send empty composing range to engine if invalid --- .../lib/src/widgets/editor_input_client_mixin.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart index ffff85aa..642400e2 100644 --- a/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart +++ b/packages/fleather/lib/src/widgets/editor_input_client_mixin.dart @@ -95,7 +95,7 @@ mixin RawEditorStateTextInputClientMixin on EditorState // with the last known remote value. // It is important to prevent excessive remote updates as it can cause // race conditions. - final actualValue = value.copyWith( + var actualValue = value.copyWith( composing: _lastKnownRemoteTextEditingValue?.composing, ); @@ -107,6 +107,17 @@ mixin RawEditorStateTextInputClientMixin on EditorState performSpellCheck(value.text); } + // This is to prevent sending an editing state with invalid composing range + // to engine. + // TODO: Maybe it's better to also include composing range in controller? + // Actions like [_DeleteTextAction] are using controller's editing value + // to update remote value. + // See https://github.com/fleather-editor/fleather/issues/259#issuecomment-2032404450 + if (actualValue.composing != TextRange.empty && + !actualValue.isComposingRangeValid) { + actualValue = actualValue.copyWith(composing: TextRange.empty); + } + _lastKnownRemoteTextEditingValue = actualValue; _textInputConnection!.setEditingState(actualValue); } From cc9c86a7340554b61cd4114823f6b2e702287dd5 Mon Sep 17 00:00:00 2001 From: Amir Panahandeh Date: Wed, 3 Apr 2024 11:08:54 +0330 Subject: [PATCH 2/2] Add tests --- .../editor_input_client_mixin_test.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/fleather/test/widgets/editor_input_client_mixin_test.dart b/packages/fleather/test/widgets/editor_input_client_mixin_test.dart index 7b59562a..f3e1234f 100644 --- a/packages/fleather/test/widgets/editor_input_client_mixin_test.dart +++ b/packages/fleather/test/widgets/editor_input_client_mixin_test.dart @@ -7,6 +7,55 @@ import 'package:flutter_test/flutter_test.dart'; import '../testing.dart'; void main() { + group('send text editing state to TextInputConnection', () { + final composingRanges = []; + + void bind(WidgetTester tester) { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.textInput, (MethodCall methodCall) async { + if (methodCall.method == 'TextInput.setEditingState') { + final Map args = + methodCall.arguments as Map; + composingRanges.add(TextRange( + start: args['composingBase'], end: args['composingExtent'])); + } + return null; + }); + } + + setUp(() => composingRanges.clear()); + + testWidgets( + 'sends empty composing range if composing range becomes invalid', + (tester) async { + bind(tester); + final document = ParchmentDocument.fromJson([ + {'insert': 'some text\n'} + ]); + final editor = EditorSandBox(tester: tester, document: document); + await editor.pump(); + await editor.tap(); + tester.binding.scheduleWarmUpFrame(); + final editorState = + tester.state(find.byType(RawEditor)) as RawEditorState; + editorState.updateEditingValueWithDeltas([ + TextEditingDeltaNonTextUpdate( + oldText: editorState.textEditingValue.text, + selection: const TextSelection.collapsed(offset: 9), + composing: const TextRange(start: 5, end: 9), + ) + ]); + await tester.pumpAndSettle(); + editor.controller.replaceText(4, 5, '', + selection: const TextSelection.collapsed(offset: 4)); + await tester.pumpAndSettle(throttleDuration); + expect( + composingRanges.fold( + true, (v, e) => v && (e == TextRange.empty || e.isValid)), + isTrue); + }); + }); + group('sets style to TextInputConnection', () { final log = [];