Repository: superlistapp/super_editor

Issue:
[SuperEditor] - Editor broken after inline markdown
**To Reproduce**
1. Run demo app
2. Clear content in editor
3. Press Return to add a new paragraph
4. Type "**a**"
5. Press arrow-up to move selection

**Actual behavior**
Exception is thrown and it's not possible to interact with the editor:
```
flutter: (50.355) superlist.errorReportingService > SEVERE: Exception: No such document position in the IME content: [DocumentPosition] - node: "e1e309b1-d2a0-486e-aa7f-534b23b12b97", position: (TextPosition(offset: 1, affinity: TextAffinity.downstream))
#0      DocumentImeSerializer._documentToImePosition (package:super_editor/src/default_editor/document_ime/document_serialization.dart:351:7)
#1      DocumentImeSerializer.documentToImeRange (package:super_editor/src/default_editor/document_ime/document_serialization.dart:335:30)
#2      DocumentImeSerializer.toTextEditingValue (package:super_editor/src/default_editor/document_ime/document_serialization.dart:382:32)
#3      DocumentImeInputClient._sendDocumentToIme (package:super_editor/src/default_editor/document_ime/document_ime_communication.dart:273:58)
#4      DocumentImeInputClient._onContentChange (package:super_editor/src/default_editor/document_ime/document_ime_communication.dart:87:5)
#5      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:439:24)
#6      ValueNotifier.value= (package:flutter/src/foundation/change_notifier.dart:564:5)
#7      PausableValueNotifier.resumeNotifications (package:super_editor/src/infrastructure/pausable_value_notifier.dart:49:11)
#8      MutableDocumentComposer.onTransactionEnd (package:super_editor/src/core/document_composer.dart:155:24)
#9      Editor._onTransactionEnd (package:super_editor/src/core/editor.dart:331:16)
#10     Editor.endTransaction (package:super_editor/src/core/editor.dart:231:5)
#11     Editor.execute (package:super_editor/src/core/editor.dart:283:7)
#12     CommonEditorOperations._updateSelectionExtent (package:super_editor/src/default_editor/common_editor_operations.dart:809:14)
#13     CommonEditorOperations.moveCaretUp (package:super_editor/src/default_editor/common_editor_operations.dart:541:5)
#14     moveUp (package:super_editor/src/default_editor/document_ime/supereditor_ime_interactor.dart:555:21)
#15     SuperEditorImeInteractorState._onPerformSelector (package:super_editor/src/default_editor/document_ime/supereditor_ime_interactor.dart:494:12)
#16     DocumentImeInputClient.performSelector (package:super_editor/src/default_editor/document_ime/document_ime_communication.dart:305:22)
#17     DeltaTextInputClientDecorator.performSelector (package:super_editor/src/default_editor/document_ime/ime_decoration.dart:104:14)
#18     DeltaTextInputClientDecorator.performSelector (package:super_editor/src/default_editor/document_ime/ime_decoration.dart:104:14)
#19     ListBase.forEach (dart:collection/list.dart:51:13)
#20     TextInput._handleTextInputInvocation (package:flutter/src/services/text_input.dart:2009:19)
#21     TextInput._loudlyHandleTextInputInvocation (package:flutter/src/services/text_input.dart:1866:20)
#22     MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:611:55)
#23     MethodChannel.setMethodCallHandler.<anonymous closure> (package:flutter/src/services/platform_channel.dart:601:55)
#24     _DefaultBinaryMessenger.setMessageHandler.<anonymous closure> (package:flutter/src/services/binding.dart:650:35)
#25     _invoke2.<anonymous closure> (dart:ui/hooks.dart:351:15)
#26     _rootRun (dart:async/zone.dart:1525:13)
#27     _CustomZone.run (dart:async/zone.dart:1422:19)
#28     _CustomZone.runGuarded (dart:async/zone.dart:1321:7)
#29     _invoke2 (dart:ui/hooks.dart:350:10)
#30     _ChannelCallbackRecord.invoke (dart:ui/channel_buffers.dart:45:5)
#31     _Channel.push (dart:ui/channel_buffers.dart:136:31)
#32     ChannelBuffers.push (dart:ui/channel_buffers.dart:344:17)
#33     PlatformDispatcher._dispatchPlatformMessage (dart:ui/platform_dispatcher.dart:786:22)
#34     _dispatchPlatformMessage (dart:ui/hooks.dart:262:31)
```

**Expected behavior**
Move selection and continue to interact with editor.

**Screenshots**

https://github.com/user-attachments/assets/7ac33968-7326-4510-b961-dfd832667162


Discussion: Cc @matthew-carroll @angelosilvestre 

Fix patch:
diff --git a/super_editor/lib/src/default_editor/common_editor_operations.dart b/super_editor/lib/src/default_editor/common_editor_operations.dart
index 102be8c8c..b64692730 100644
--- a/super_editor/lib/src/default_editor/common_editor_operations.dart
+++ b/super_editor/lib/src/default_editor/common_editor_operations.dart
@@ -5,7 +5,6 @@ import 'package:attributed_text/attributed_text.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
-import 'package:linkify/linkify.dart';
 import 'package:super_editor/src/core/document.dart';
 import 'package:super_editor/src/core/document_composer.dart';
 import 'package:super_editor/src/core/document_layout.dart';
@@ -20,11 +19,11 @@ import 'package:super_editor/src/default_editor/tasks.dart';
 import 'package:super_editor/src/default_editor/text.dart';
 import 'package:super_editor/src/infrastructure/_logging.dart';
 
-import 'attributions.dart';
-import 'horizontal_rule.dart';
-import 'image.dart';
-import 'multi_node_editing.dart';
-import 'text_tools.dart';
+import 'package:super_editor/src/default_editor/attributions.dart';
+import 'package:super_editor/src/default_editor/horizontal_rule.dart';
+import 'package:super_editor/src/default_editor/image.dart';
+import 'package:super_editor/src/default_editor/multi_node_editing.dart';
+import 'package:super_editor/src/default_editor/text_tools.dart';
 
 /// Performs common, high-level editing and composition tasks
 /// with a simplified API.
@@ -273,6 +272,8 @@ class CommonEditorOperations {
   /// in [movementModifier]. To move to the beginning of a line, pass
   /// [MovementModifier.line] in [movementModifier].
   ///
+  /// Clears the composing region.
+  ///
   /// Returns [true] if the extent moved, or the selection changed, e.g., the
   /// selection collapsed but the extent stayed in the same place. Returns
   /// [false] if the extent did not move and the selection did not change.
@@ -291,6 +292,11 @@ class CommonEditorOperations {
           SelectionChangeType.collapseSelection,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
 
       return true;
@@ -349,6 +355,11 @@ class CommonEditorOperations {
           SelectionChangeType.pushExtent,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
     } else {
       // Push the caret upstream.
@@ -360,6 +371,11 @@ class CommonEditorOperations {
           SelectionChangeType.pushCaret,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
     }
 
@@ -379,6 +395,8 @@ class CommonEditorOperations {
   /// in [movementModifier]. To move to the end of a line, pass
   /// [MovementModifier.line] in [movementModifier].
   ///
+  /// Clears any composing region.
+  ///
   /// Returns [true] if the extent moved, or the selection changed, e.g., the
   /// selection collapsed but the extent stayed in the same place. Returns
   /// [false] if the extent did not move and the selection did not change.
@@ -397,6 +415,11 @@ class CommonEditorOperations {
           SelectionChangeType.collapseSelection,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
 
       return true;
@@ -457,6 +480,11 @@ class CommonEditorOperations {
           SelectionChangeType.pushExtent,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
     } else {
       // Push the caret downstream.
@@ -468,6 +496,11 @@ class CommonEditorOperations {
           SelectionChangeType.pushCaret,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
     }
 
@@ -490,6 +523,8 @@ class CommonEditorOperations {
   /// Expands/contracts the selection if [expand] is [true], otherwise
   /// collapses the selection or keeps it collapsed.
   ///
+  /// Clears any composing region.
+  ///
   /// Returns [true] if the extent moved, or the selection changed, e.g., the
   /// selection collapsed but the extent stayed in the same place. Returns
   /// [false] if the extent did not move and the selection did not change.
@@ -559,6 +594,8 @@ class CommonEditorOperations {
   /// Expands/contracts the selection if [expand] is [true], otherwise
   /// collapses the selection or keeps it collapsed.
   ///
+  /// Clears any composing region.
+  ///
   /// Returns [true] if the extent moved, or the selection changed, e.g., the
   /// selection collapsed but the extent stayed in the same place. Returns
   /// [false] if the extent did not move and the selection did not change.
@@ -803,6 +840,11 @@ class CommonEditorOperations {
           SelectionChangeType.expandSelection,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest()
       ]);
     } else {
       // Selection should be replaced by new collapsed position.
@@ -812,6 +854,11 @@ class CommonEditorOperations {
           SelectionChangeType.collapseSelection,
           SelectionReason.userInteraction,
         ),
+        // Since we are moving the caret, we clear the composing region (if any)
+        // to avoid lefting a composing region far from the caret. Without this,
+        // we can end up with a selection in one node and a composing region
+        // in another node.
+        const ClearComposingRegionRequest(),
       ]);
     }
   }


Test patch:
diff --git a/super_editor/test/super_editor/supereditor_input_ime_test.dart b/super_editor/test/super_editor/supereditor_input_ime_test.dart
index 7e7fac4c3..10123bafc 100644
--- a/super_editor/test/super_editor/supereditor_input_ime_test.dart
+++ b/super_editor/test/super_editor/supereditor_input_ime_test.dart
@@ -1466,208 +1466,454 @@ Paragraph two
       expect(viewId, isNotNull);
     });
 
-    testWidgetsOnAllPlatforms('clears composing region after losing focus', (tester) async {
-      final focusNode = FocusNode();
+    group('clears composing region', () {
+      testWidgetsOnAllPlatforms('after losing focus', (tester) async {
+        final focusNode = FocusNode();
 
-      final testContext = await tester
-          .createDocument() //
-          .withTwoEmptyParagraphs()
-          .withInputSource(TextInputSource.ime)
-          .withFocusNode(focusNode)
-          .pump();
+        final testContext = await tester
+            .createDocument() //
+            .withTwoEmptyParagraphs()
+            .withInputSource(TextInputSource.ime)
+            .withFocusNode(focusNode)
+            .pump();
 
-      // Place the caret at the beginning of the document.
-      await tester.placeCaretInParagraph('1', 0);
+        // Place the caret at the beginning of the document.
+        await tester.placeCaretInParagraph('1', 0);
 
-      // Type something to have some text to tap on.
-      await tester.typeImeText('Composing: ');
+        // Type something to have some text to tap on.
+        await tester.typeImeText('Composing: ');
 
-      // Ensure we don't have a composing region.
-      expect(testContext.composer.composingRegion.value, isNull);
+        // Ensure we don't have a composing region.
+        expect(testContext.composer.composingRegion.value, isNull);
 
-      // Simulate an insertion containing a composing region.
-      await tester.ime.sendDeltas(
-        [
-          const TextEditingDeltaInsertion(
-            oldText: '. Composing: ',
-            textInserted: "あs",
-            insertionOffset: 13,
-            selection: TextSelection.collapsed(offset: 15),
-            composing: TextRange(start: 13, end: 15),
-          ),
-        ],
-        getter: imeClientGetter,
-      );
+        // Simulate an insertion containing a composing region.
+        await tester.ime.sendDeltas(
+          [
+            const TextEditingDeltaInsertion(
+              oldText: '. Composing: ',
+              textInserted: "あs",
+              insertionOffset: 13,
+              selection: TextSelection.collapsed(offset: 15),
+              composing: TextRange(start: 13, end: 15),
+            ),
+          ],
+          getter: imeClientGetter,
+        );
 
-      // Ensure the editor applied a composing region.
-      expect(
-        testContext.composer.composingRegion.value,
-        isNotNull,
-      );
+        // Ensure the editor applied a composing region.
+        expect(
+          testContext.composer.composingRegion.value,
+          isNotNull,
+        );
 
-      // Remove focus from the editor.
-      focusNode.unfocus();
-      await tester.pump();
+        // Remove focus from the editor.
+        focusNode.unfocus();
+        await tester.pump();
 
-      // Ensure the composing region was cleared.
-      expect(testContext.composer.composingRegion.value, isNull);
-    });
+        // Ensure the composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+      });
 
-    testWidgetsOnAllPlatforms('clears composing region after selection changes', (tester) async {
-      final testContext = await tester
-          .createDocument() //
-          .withTwoEmptyParagraphs()
-          .withInputSource(TextInputSource.ime)
-          .pump();
+      testWidgetsOnAllPlatforms('after selection changes', (tester) async {
+        final testContext = await tester
+            .createDocument() //
+            .withTwoEmptyParagraphs()
+            .withInputSource(TextInputSource.ime)
+            .pump();
 
-      // Place the caret at the beginning of the document.
-      await tester.placeCaretInParagraph('1', 0);
+        // Place the caret at the beginning of the document.
+        await tester.placeCaretInParagraph('1', 0);
 
-      // Type something to have some text to tap on.
-      await tester.typeImeText('Composing: ');
+        // Type something to have some text to tap on.
+        await tester.typeImeText('Composing: ');
 
-      // Ensure we don't have a composing region.
-      expect(testContext.composer.composingRegion.value, isNull);
+        // Ensure we don't have a composing region.
+        expect(testContext.composer.composingRegion.value, isNull);
 
-      // Simulate an insertion containing a composing region.
-      await tester.ime.sendDeltas(
-        [
-          const TextEditingDeltaInsertion(
-            oldText: '. Composing: ',
-            textInserted: "あs",
-            insertionOffset: 13,
-            selection: TextSelection.collapsed(offset: 15),
-            composing: TextRange(start: 13, end: 15),
+        // Simulate an insertion containing a composing region.
+        await tester.ime.sendDeltas(
+          [
+            const TextEditingDeltaInsertion(
+              oldText: '. Composing: ',
+              textInserted: "あs",
+              insertionOffset: 13,
+              selection: TextSelection.collapsed(offset: 15),
+              composing: TextRange(start: 13, end: 15),
+            ),
+          ],
+          getter: imeClientGetter,
+        );
+
+        // Ensure the editor applied a composing region.
+        expect(
+          testContext.composer.composingRegion.value,
+          isNotNull,
+        );
+
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region when changing the selection.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
+
+            return null;
+          },
+        );
+
+        // Place the caret at the second paragraph.
+        await tester.placeCaretInParagraph('2', 0);
+
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+      });
+
+      testWidgetsOnAllPlatforms('when moving the caret up', (tester) async {
+        // FIXME: When we intercept the text input messages, the test text input
+        // does not reset some internal state, which causes macOS selectors
+        // to not be reported. Remove this after this is fixed.
+        SystemChannels.textInput.invokeMethod("TextInput.hide");
+
+        final testContext = await tester
+            .createDocument() //
+            .fromMarkdown('A\n\nB')
+            .withInputSource(TextInputSource.ime)
+            .pump();
+
+        // Place the caret at the end of the second paragraph.
+        await tester.placeCaretInParagraph(testContext.document.last.id, 1);
+
+        // Place the composing region at the same position as the caret.
+        final lastCharacterPosition = DocumentPosition(
+          nodeId: testContext.document.last.id,
+          nodePosition: const TextNodePosition(offset: 1),
+        );
+        testContext.editor.execute([
+          ChangeComposingRegionRequest(
+            DocumentRange(
+              start: lastCharacterPosition,
+              end: lastCharacterPosition,
+            ),
           ),
-        ],
-        getter: imeClientGetter,
-      );
+        ]);
+        await tester.pump();
 
-      // Ensure the editor applied a composing region.
-      expect(
-        testContext.composer.composingRegion.value,
-        isNotNull,
-      );
+        // Ensure we have a composing region.
+        expect(testContext.composer.composingRegion.value, isNotNull);
 
-      // Intercept the setEditingState message sent to the platform to check if we
-      // cleared the IME composing region when changing the selection.
-      int? composingBase;
-      int? composingExtent;
-      tester
-          .interceptChannel(SystemChannels.textInput.name) //
-          .interceptMethod(
-        'TextInput.setEditingState',
-        (methodCall) {
-          final params = methodCall.arguments as Map;
-          composingBase = params['composingBase'];
-          composingExtent = params['composingExtent'];
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region after moving the caret up.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
 
-          return null;
-        },
-      );
+            return null;
+          },
+        );
 
-      // Place the caret at the second paragraph.
-      await tester.placeCaretInParagraph('2', 0);
+        // Press up arrow to move the caret to the first paragraph.
+        await tester.pressUpArrow();
 
-      // Ensure the composing region was cleared in the IME.
-      expect(composingBase, -1);
-      expect(composingExtent, -1);
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
 
-      // Ensure SuperEditor composing region was cleared.
-      expect(testContext.composer.composingRegion.value, isNull);
-    });
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+      });
 
-    testWidgetsOnMac('clears composing region after merging paragraphs', (tester) async {
-      final testContext = await tester
-          .createDocument() //
-          .withTwoEmptyParagraphs()
-          .withInputSource(TextInputSource.ime)
-          .pump();
+      testWidgetsOnAllPlatforms('when moving the caret upstream', (tester) async {
+        // FIXME: When we intercept the text input messages, the test text input
+        // does not reset some internal state, which causes macOS selectors
+        // to not be reported. Remove this after this is fixed.
+        SystemChannels.textInput.invokeMethod("TextInput.hide");
 
-      // Place the caret at the beginning of the second paragraph.
-      await tester.placeCaretInParagraph('2', 0);
+        final testContext = await tester
+            .createDocument() //
+            .fromMarkdown('A\n\nB')
+            .withInputSource(TextInputSource.ime)
+            .pump();
 
-      // Ensure we don't have a composing region.
-      expect(testContext.composer.composingRegion.value, isNull);
+        // Place the caret at the beginning of the second paragraph.
+        await tester.placeCaretInParagraph(testContext.document.last.id, 0);
 
-      // Simulate an insertion containing a composing region.
-      await tester.ime.sendDeltas(
-        [
-          const TextEditingDeltaNonTextUpdate(
-            oldText: '. ',
-            selection: TextSelection.collapsed(offset: 2),
-            composing: TextRange(start: 2, end: 2),
+        // Place the composing region at the same position as the caret.
+        final lastCharacterPosition = DocumentPosition(
+          nodeId: testContext.document.last.id,
+          nodePosition: const TextNodePosition(offset: 0),
+        );
+        testContext.editor.execute([
+          ChangeComposingRegionRequest(
+            DocumentRange(
+              start: lastCharacterPosition,
+              end: lastCharacterPosition,
+            ),
           ),
-          const TextEditingDeltaInsertion(
-            oldText: '. ',
-            textInserted: 'あ',
-            insertionOffset: 2,
-            selection: TextSelection.collapsed(offset: 3),
-            composing: TextRange(start: 2, end: 3),
+        ]);
+        await tester.pump();
+
+        // Ensure we have a composing region.
+        expect(testContext.composer.composingRegion.value, isNotNull);
+
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region when merging paragraphs.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
+
+            return null;
+          },
+        );
+
+        // Press up arrow to move the caret to the first paragraph.
+        await tester.pressLeftArrow();
+
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+      });
+
+      testWidgetsOnAllPlatforms('when moving the caret down', (tester) async {
+        // FIXME: When we intercept the text input messages, the test text input
+        // does not reset some internal state, which causes macOS selectors
+        // to not be reported. Remove this after this is fixed.
+        SystemChannels.textInput.invokeMethod("TextInput.hide");
+
+        final testContext = await tester
+            .createDocument() //
+            .fromMarkdown('A\n\nC')
+            .withInputSource(TextInputSource.ime)
+            .pump();
+
+        // Place the caret at the end of first paragraph.
+        await tester.placeCaretInParagraph(testContext.document.first.id, 1);
+
+        // Place the composing region at the same position as the caret.
+        final lastCharacterPosition = DocumentPosition(
+          nodeId: testContext.document.first.id,
+          nodePosition: const TextNodePosition(offset: 1),
+        );
+        testContext.editor.execute([
+          ChangeComposingRegionRequest(
+            DocumentRange(
+              start: lastCharacterPosition,
+              end: lastCharacterPosition,
+            ),
           ),
-        ],
-        getter: imeClientGetter,
-      );
+        ]);
+        await tester.pump();
 
-      // Ensure the editor applied the composing region.
-      expect(
-        testContext.composer.composingRegion.value,
-        isNotNull,
-      );
+        // Ensure we have a composing region.
+        expect(testContext.composer.composingRegion.value, isNotNull);
 
-      // Intercept the setEditingState message sent to the platform to check if we
-      // cleared the IME composing region when merging paragraphs.
-      int? composingBase;
-      int? composingExtent;
-      tester
-          .interceptChannel(SystemChannels.textInput.name) //
-          .interceptMethod(
-        'TextInput.setEditingState',
-        (methodCall) {
-          final params = methodCall.arguments as Map;
-          composingBase = params['composingBase'];
-          composingExtent = params['composingExtent'];
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region when merging paragraphs.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
 
-          return null;
-        },
-      );
+            return null;
+          },
+        );
 
-      // Simulate the user pressing BACKSPACE to delete the first character.
-      // Even though the selection sits after a whitespace in the IME, mac still reports
-      // a composing region starting after the space.
-      await tester.ime.sendDeltas(
-        [
-          const TextEditingDeltaDeletion(
-            oldText: '. あ',
-            deletedRange: TextRange(start: 2, end: 3),
-            selection: TextSelection.collapsed(offset: 2),
-            composing: TextRange(start: 2, end: 2),
+        // Press down arrow to move the caret to the second paragraph.
+        await tester.pressDownArrow();
+
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+      });
+
+      testWidgetsOnAllPlatforms('when moving the caret downstream', (tester) async {
+        // FIXME: When we intercept the text input messages, the test text input
+        // does not reset some internal state, which causes macOS selectors
+        // to not be reported. Remove this after this is fixed.
+        SystemChannels.textInput.invokeMethod("TextInput.hide");
+
+        final testContext = await tester
+            .createDocument() //
+            .fromMarkdown('A\n\nB')
+            .withInputSource(TextInputSource.ime)
+            .pump();
+
+        // Place the caret at the end of the first paragraph.
+        await tester.placeCaretInParagraph(testContext.document.first.id, 1);
+
+        // Place the composing region at the same position as the caret.
+        final lastCharacterPosition = DocumentPosition(
+          nodeId: testContext.document.first.id,
+          nodePosition: const TextNodePosition(offset: 1),
+        );
+        testContext.editor.execute([
+          ChangeComposingRegionRequest(
+            DocumentRange(
+              start: lastCharacterPosition,
+              end: lastCharacterPosition,
+            ),
           ),
-        ],
-        getter: imeClientGetter,
-      );
+        ]);
+        await tester.pump();
 
-      // Ensure we still have a composing region in the editor.
-      expect(
-        testContext.composer.composingRegion.value,
-        isNotNull,
-      );
+        // Ensure we have a composing region.
+        expect(testContext.composer.composingRegion.value, isNotNull);
+
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region when merging paragraphs.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
+
+            return null;
+          },
+        );
+
+        // Press right arrow to move the caret to the second paragraph.
+        await tester.pressRightArrow();
+
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+      });
 
-      // Simulate the user pressing BACKSPACE to merge the paragraphs.
-      // Now that we are deleting a whitespace, mac reports a deleteBackward: selector
-      // instead of a deletion delta.
-      await _receiveSelector('deleteBackward:');
-      await tester.pump();
+      testWidgetsOnMac('after merging paragraphs', (tester) async {
+        final testContext = await tester
+            .createDocument() //
+            .withTwoEmptyParagraphs()
+            .withInputSource(TextInputSource.ime)
+            .pump();
 
-      // Ensure the composing region was cleared in the IME.
-      expect(composingBase, -1);
-      expect(composingExtent, -1);
+        // Place the caret at the beginning of the second paragraph.
+        await tester.placeCaretInParagraph('2', 0);
 
-      // Ensure SuperEditor composing region was cleared.
-      expect(testContext.composer.composingRegion.value, isNull);
+        // Ensure we don't have a composing region.
+        expect(testContext.composer.composingRegion.value, isNull);
 
-      // Ensure the paragraphs were merged.
-      expect(testContext.document.nodeCount, equals(1));
+        // Simulate an insertion containing a composing region.
+        await tester.ime.sendDeltas(
+          [
+            const TextEditingDeltaNonTextUpdate(
+              oldText: '. ',
+              selection: TextSelection.collapsed(offset: 2),
+              composing: TextRange(start: 2, end: 2),
+            ),
+            const TextEditingDeltaInsertion(
+              oldText: '. ',
+              textInserted: 'あ',
+              insertionOffset: 2,
+              selection: TextSelection.collapsed(offset: 3),
+              composing: TextRange(start: 2, end: 3),
+            ),
+          ],
+          getter: imeClientGetter,
+        );
+
+        // Ensure the editor applied the composing region.
+        expect(
+          testContext.composer.composingRegion.value,
+          isNotNull,
+        );
+
+        // Intercept the setEditingState message sent to the platform to check if we
+        // cleared the IME composing region when merging paragraphs.
+        int? composingBase;
+        int? composingExtent;
+        tester
+            .interceptChannel(SystemChannels.textInput.name) //
+            .interceptMethod(
+          'TextInput.setEditingState',
+          (methodCall) {
+            final params = methodCall.arguments as Map;
+            composingBase = params['composingBase'];
+            composingExtent = params['composingExtent'];
+
+            return null;
+          },
+        );
+
+        // Simulate the user pressing BACKSPACE to delete the first character.
+        // Even though the selection sits after a whitespace in the IME, mac still reports
+        // a composing region starting after the space.
+        await tester.ime.sendDeltas(
+          [
+            const TextEditingDeltaDeletion(
+              oldText: '. あ',
+              deletedRange: TextRange(start: 2, end: 3),
+              selection: TextSelection.collapsed(offset: 2),
+              composing: TextRange(start: 2, end: 2),
+            ),
+          ],
+          getter: imeClientGetter,
+        );
+
+        // Ensure we still have a composing region in the editor.
+        expect(
+          testContext.composer.composingRegion.value,
+          isNotNull,
+        );
+
+        // Simulate the user pressing BACKSPACE to merge the paragraphs.
+        // Now that we are deleting a whitespace, mac reports a deleteBackward: selector
+        // instead of a deletion delta.
+        await _receiveSelector('deleteBackward:');
+        await tester.pump();
+
+        // Ensure the composing region was cleared in the IME.
+        expect(composingBase, -1);
+        expect(composingExtent, -1);
+
+        // Ensure SuperEditor composing region was cleared.
+        expect(testContext.composer.composingRegion.value, isNull);
+
+        // Ensure the paragraphs were merged.
+        expect(testContext.document.nodeCount, equals(1));
+      });
     });
   });
 }