From 3f813c80e6910a5220b25df92766c729aea044d1 Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Fri, 29 Sep 2023 16:58:19 -0300 Subject: [PATCH] [SuperEditor][iOS] Fix exception when moving floating cursor between paragraphs (Resolves #1449) (#1471) --- .../document_ime_communication.dart | 21 +++++++++ .../supereditor_floating_cursor_test.dart | 47 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart b/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart index afe6a7052a..28da77ff57 100644 --- a/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart +++ b/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart @@ -69,6 +69,11 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput /// For the list of selectors, see [MacOsSelectors]. final void Function(String selectorName) onPerformSelector; + /// Whether the floating cursor is being displayed. + /// + /// This value is updated on [updateFloatingCursor]. + bool _isFloatingCursorVisible = false; + void _onContentChange() { if (!attached) { return; @@ -187,6 +192,18 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput return; } + if (_isFloatingCursorVisible && textEditingDeltas.every((e) => e is TextEditingDeltaNonTextUpdate)) { + // On iOS, dragging the floating cursor generates non-text deltas to update the selection. + // + // When dragging the floating cursor between paragraphs, we receive a non-text delta for the previously + // selected paragraph when our selection already changed to another paragraph. If the previously selected + // paragraph is bigger than the newly selected paragraph, a mapping error occurs, because we try + // to select an offset bigger than the paragraph's length. + // + // As we already change the selection when the floating cursor moves, we ignore these deltas. + return; + } + editorImeLog.fine("Received edit deltas from platform: ${textEditingDeltas.length} deltas"); for (final delta in textEditingDeltas) { editorImeLog.fine("$delta"); @@ -277,10 +294,14 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput void updateFloatingCursor(RawFloatingCursorPoint point) { switch (point.state) { case FloatingCursorDragState.Start: + _isFloatingCursorVisible = true; + _floatingCursorController?.offset = point.offset; + break; case FloatingCursorDragState.Update: _floatingCursorController?.offset = point.offset; break; case FloatingCursorDragState.End: + _isFloatingCursorVisible = false; _floatingCursorController?.offset = null; break; } diff --git a/super_editor/test/super_editor/supereditor_floating_cursor_test.dart b/super_editor/test/super_editor/supereditor_floating_cursor_test.dart index ce68ea9397..8d2cce6d49 100644 --- a/super_editor/test/super_editor/supereditor_floating_cursor_test.dart +++ b/super_editor/test/super_editor/supereditor_floating_cursor_test.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_robots/flutter_test_robots.dart'; import 'package:flutter_test_runners/flutter_test_runners.dart'; import 'package:super_editor/src/core/document.dart'; import 'package:super_editor/src/core/document_selection.dart'; import 'package:super_editor/src/default_editor/text.dart'; import 'package:super_editor/src/infrastructure/blinking_caret.dart'; +import 'package:super_editor/src/test/ime.dart'; import 'package:super_editor/src/test/super_editor_test/supereditor_inspector.dart'; import 'package:super_editor/src/test/super_editor_test/supereditor_robot.dart'; @@ -175,6 +178,50 @@ void main() { ), ); }); + + testWidgetsOnIos('moves selection between paragraphs', (tester) async { + final testContext = await tester // + .createDocument() + .fromMarkdown(''' +This is the first paragraph + +Second paragraph''') // + .pump(); + + // Place the caret at the end of the first paragraph. + await tester.placeCaretInParagraph(testContext.document.nodes.first.id, 27); + + // Show the floating cursor. + await tester.startFloatingCursorGesture(); + await tester.pump(); + + // Move the floating cursor down to the next paragraph. + await tester.updateFloatingCursorGesture(const Offset(0, 30)); + await tester.pump(); + + // Simulate iOS IME generating deltas as a result of moving the floating cursor. + // At this point, the selection already changed to the second paragraph, which is + // smaller than the selection offset reported in the delta. + await tester.ime.sendDeltas([ + const TextEditingDeltaNonTextUpdate( + oldText: 'This is the first paragraph', + selection: TextSelection.collapsed(offset: 27), + composing: TextRange.empty, + ) + ], getter: imeClientGetter); + await tester.pump(); + + // Ensure the selection changed to the end of the second paragraph. + expect( + SuperEditorInspector.findDocumentSelection(), + DocumentSelection.collapsed( + position: DocumentPosition( + nodeId: testContext.document.nodes.last.id, + nodePosition: const TextNodePosition(offset: 16, affinity: TextAffinity.upstream), + ), + ), + ); + }); }); }); }