diff --git a/super_editor/lib/src/core/document_composer.dart b/super_editor/lib/src/core/document_composer.dart index c2451e5b5b..e930f02a80 100644 --- a/super_editor/lib/src/core/document_composer.dart +++ b/super_editor/lib/src/core/document_composer.dart @@ -255,8 +255,8 @@ class PushCaretRequest extends ChangeSelectionRequest { PushCaretRequest( DocumentPosition newPosition, this.direction, - ) : super(DocumentSelection.collapsed(position: newPosition), SelectionChangeType.pushCaret, - SelectionReason.userInteraction); + SelectionChangeType changeType, + ) : super(DocumentSelection.collapsed(position: newPosition), changeType, SelectionReason.userInteraction); final TextAffinity direction; @@ -483,12 +483,29 @@ enum SelectionChangeType { /// dragging with the mouse. placeExtent, - /// Place the caret based on a desire to move the previous caret position upstream or downstream. - pushCaret, + /// Move the caret one unit downstream. + pushCaretDownstream, - /// Expand/contract a selection by pushing the extent upstream or downstream, such as by pressing - /// SHIFT + LEFT ARROW. - pushExtent, + /// Move the caret one unit upstream. + pushCaretUpstream, + + /// Move the caret one unit down. + pushCaretDown, + + /// Move the caret one unit up. + pushCaretUp, + + /// Expand/contract a selection downstream, such as by pressing SHIFT + RIGHT ARROW. + pushExtentDownstream, + + /// Expand/contract a selection upstream, such as by pressing SHIFT + LEFT ARROW. + pushExtentUpstream, + + /// Expand/contract a selection down, such as by pressing SHIFT + DOWN ARROW. + pushExtentDown, + + /// Expand/contract a selection up, such as by pressing SHIFT + UP ARROW. + pushExtentUp, /// Expand a caret to an expanded selection, or move the base or extent of an already expanded selection. expandSelection, 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 102be8c8c9..ab9a524fbb 100644 --- a/super_editor/lib/src/default_editor/common_editor_operations.dart +++ b/super_editor/lib/src/default_editor/common_editor_operations.dart @@ -346,7 +346,7 @@ class CommonEditorOperations { composer.selection!.expandTo( newExtent, ), - SelectionChangeType.pushExtent, + SelectionChangeType.pushExtentUpstream, SelectionReason.userInteraction, ), ]); @@ -357,7 +357,7 @@ class CommonEditorOperations { DocumentSelection.collapsed( position: newExtent, ), - SelectionChangeType.pushCaret, + SelectionChangeType.pushCaretUpstream, SelectionReason.userInteraction, ), ]); @@ -454,7 +454,7 @@ class CommonEditorOperations { composer.selection!.expandTo( newExtent, ), - SelectionChangeType.pushExtent, + SelectionChangeType.pushExtentDownstream, SelectionReason.userInteraction, ), ]); @@ -465,7 +465,7 @@ class CommonEditorOperations { DocumentSelection.collapsed( position: newExtent, ), - SelectionChangeType.pushCaret, + SelectionChangeType.pushCaretDownstream, SelectionReason.userInteraction, ), ]); @@ -513,6 +513,8 @@ class CommonEditorOperations { String newExtentNodeId = nodeId; NodePosition? newExtentNodePosition = extentComponent.movePositionUp(currentExtent.nodePosition); + SelectionChangeType selectionChangeType = + expand ? SelectionChangeType.pushExtentUp : SelectionChangeType.pushCaretUp; if (newExtentNodePosition == null) { // Move to next node @@ -530,6 +532,7 @@ class CommonEditorOperations { // We're at the top of the document. Move the cursor to the // beginning of the current node. newExtentNodePosition = extentComponent.getBeginningPosition(); + selectionChangeType = expand ? SelectionChangeType.pushExtentUpstream : SelectionChangeType.pushCaretUpstream; } } @@ -538,7 +541,11 @@ class CommonEditorOperations { nodePosition: newExtentNodePosition, ); - _updateSelectionExtent(position: newExtent, expandSelection: expand); + _updateSelectionExtent( + position: newExtent, + expandSelection: expand, + selectionChangeType: selectionChangeType, + ); return true; } @@ -582,6 +589,8 @@ class CommonEditorOperations { String newExtentNodeId = nodeId; NodePosition? newExtentNodePosition = extentComponent.movePositionDown(currentExtent.nodePosition); + SelectionChangeType selectionChangeType = + expand ? SelectionChangeType.pushExtentDown : SelectionChangeType.pushCaretDown; if (newExtentNodePosition == null) { // Move to next node @@ -599,6 +608,8 @@ class CommonEditorOperations { // We're at the bottom of the document. Move the cursor to the // end of the current node. newExtentNodePosition = extentComponent.getEndPosition(); + selectionChangeType = + expand ? SelectionChangeType.pushExtentDownstream : SelectionChangeType.pushCaretDownstream; } } @@ -607,7 +618,11 @@ class CommonEditorOperations { nodePosition: newExtentNodePosition, ); - _updateSelectionExtent(position: newExtent, expandSelection: expand); + _updateSelectionExtent( + position: newExtent, + expandSelection: expand, + selectionChangeType: selectionChangeType, + ); return true; } @@ -658,7 +673,11 @@ class CommonEditorOperations { nodeId: newNodeId, nodePosition: newPosition, ); - _updateSelectionExtent(position: newExtent, expandSelection: expand); + _updateSelectionExtent( + position: newExtent, + expandSelection: expand, + selectionChangeType: SelectionChangeType.placeCaret, + ); return true; } @@ -794,13 +813,14 @@ class CommonEditorOperations { void _updateSelectionExtent({ required DocumentPosition position, required bool expandSelection, + required SelectionChangeType selectionChangeType, }) { if (expandSelection) { // Selection should be expanded. editor.execute([ ChangeSelectionRequest( composer.selection!.expandTo(position), - SelectionChangeType.expandSelection, + selectionChangeType, SelectionReason.userInteraction, ), ]); @@ -809,7 +829,7 @@ class CommonEditorOperations { editor.execute([ ChangeSelectionRequest( DocumentSelection.collapsed(position: position), - SelectionChangeType.collapseSelection, + selectionChangeType, SelectionReason.userInteraction, ), ]); @@ -967,7 +987,7 @@ class CommonEditorOperations { nodePosition: nodeAfter.beginningPosition, ), ), - SelectionChangeType.pushCaret, + SelectionChangeType.pushCaretDownstream, SelectionReason.userInteraction, ), ]); diff --git a/super_editor/lib/src/default_editor/composer/composer_reactions.dart b/super_editor/lib/src/default_editor/composer/composer_reactions.dart index 8cdf5d0cc6..0a4de7e2cb 100644 --- a/super_editor/lib/src/default_editor/composer/composer_reactions.dart +++ b/super_editor/lib/src/default_editor/composer/composer_reactions.dart @@ -70,7 +70,10 @@ class UpdateComposerTextStylesReaction extends EditReaction { switch (lastSelectionChange.changeType) { case SelectionChangeType.placeCaret: - case SelectionChangeType.pushCaret: + case SelectionChangeType.pushCaretDownstream: + case SelectionChangeType.pushCaretUpstream: + case SelectionChangeType.pushCaretUp: + case SelectionChangeType.pushCaretDown: case SelectionChangeType.collapseSelection: case SelectionChangeType.deleteContent: _updateComposerStylesAtCaret(editContext); diff --git a/super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart b/super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart index f2b1f23b91..71f97570e7 100644 --- a/super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart +++ b/super_editor/lib/src/default_editor/document_ime/document_delta_editing.dart @@ -262,7 +262,7 @@ class TextDeltasDocumentEditor { editor.execute([ ChangeSelectionRequest( docSelection, - docSelection.isCollapsed ? SelectionChangeType.placeCaret : SelectionChangeType.expandSelection, + _identifySelectionChangeType(selection.value, docSelection), SelectionReason.userInteraction, ), ChangeComposingRegionRequest(docComposingRegion), @@ -273,6 +273,29 @@ class TextDeltasDocumentEditor { _previousImeValue = delta.apply(_previousImeValue); } + SelectionChangeType _identifySelectionChangeType(DocumentSelection? oldSelection, DocumentSelection newSelection) { + if (!newSelection.isCollapsed) { + // The selection is expanded. + return SelectionChangeType.expandSelection; + } + + if (oldSelection != null && !oldSelection.isCollapsed) { + // The selection was expanded before, but now it's collapsed. + return SelectionChangeType.collapseSelection; + } + + if (oldSelection != null) { + // The selection was collapsed before and still is. Check the direction of the caret movement. + final affinity = document.getAffinityBetween(base: oldSelection.extent, extent: newSelection.extent); + return affinity == TextAffinity.downstream + ? SelectionChangeType.pushCaretDownstream + : SelectionChangeType.pushCaretUpstream; + } + + // The selection is collapsed and there was no previous selection. + return SelectionChangeType.placeCaret; + } + void insert(DocumentSelection insertionSelection, String textInserted) { editorImeLog.fine('Inserting "$textInserted" at position "$insertionSelection"'); editorImeLog diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart index e18d13806f..b7e0ffc7d6 100644 --- a/super_editor/lib/src/default_editor/super_editor.dart +++ b/super_editor/lib/src/default_editor/super_editor.dart @@ -849,7 +849,7 @@ class SuperEditorState extends State { getDocumentLayout: () => _docLayoutKey.currentState as DocumentLayout, selection: _composer.selectionNotifier, setSelection: (newSelection) => editContext.editor.execute([ - ChangeSelectionRequest(newSelection, SelectionChangeType.pushCaret, SelectionReason.userInteraction), + ChangeSelectionRequest(newSelection, SelectionChangeType.placeCaret, SelectionReason.userInteraction), ]), scrollChangeSignal: _scrollChangeSignal, dragHandleAutoScroller: _dragHandleAutoScroller, diff --git a/super_editor/lib/src/default_editor/tasks.dart b/super_editor/lib/src/default_editor/tasks.dart index f89a7e3761..a542528278 100644 --- a/super_editor/lib/src/default_editor/tasks.dart +++ b/super_editor/lib/src/default_editor/tasks.dart @@ -942,7 +942,7 @@ class SplitExistingTaskCommand extends EditCommand { SelectionChangeEvent( oldSelection: oldSelection, newSelection: newSelection, - changeType: SelectionChangeType.pushCaret, + changeType: SelectionChangeType.pushCaretDownstream, reason: SelectionReason.userInteraction, ), ComposingRegionChangeEvent( diff --git a/super_editor/lib/src/default_editor/text_tokenizing/stable_tags.dart b/super_editor/lib/src/default_editor/text_tokenizing/stable_tags.dart index fc7e326e61..d8dc74a249 100644 --- a/super_editor/lib/src/default_editor/text_tokenizing/stable_tags.dart +++ b/super_editor/lib/src/default_editor/text_tokenizing/stable_tags.dart @@ -1214,12 +1214,18 @@ class AdjustSelectionAroundTagReaction extends EditReaction { // Move the caret to the nearest edge of the tag. _moveCaretToNearestTagEdge(requestDispatcher, selectionChangeEvent, textNode.id, tagAroundCaret); break; - case SelectionChangeType.pushCaret: + case SelectionChangeType.pushCaretDownstream: + case SelectionChangeType.pushCaretUpstream: + case SelectionChangeType.pushCaretUp: + case SelectionChangeType.pushCaretDown: // Move the caret to the side of the tag in the direction of push motion. _pushCaretToOppositeTagEdge(editContext, requestDispatcher, selectionChangeEvent, textNode.id, tagAroundCaret); break; case SelectionChangeType.placeExtent: - case SelectionChangeType.pushExtent: + case SelectionChangeType.pushExtentDownstream: + case SelectionChangeType.pushExtentUpstream: + case SelectionChangeType.pushExtentUp: + case SelectionChangeType.pushExtentDown: case SelectionChangeType.expandSelection: throw AssertionError( "A collapsed selection reported a SelectionChangeType for an expanded selection: ${selectionChangeEvent.changeType}\n${selectionChangeEvent.newSelection}"); @@ -1267,7 +1273,10 @@ class AdjustSelectionAroundTagReaction extends EditReaction { // Move the caret to the nearest edge of the tag. _moveCaretToNearestTagEdge(requestDispatcher, selectionChangeEvent, extentNode.id, tagAroundCaret); break; - case SelectionChangeType.pushExtent: + case SelectionChangeType.pushExtentDownstream: + case SelectionChangeType.pushExtentUpstream: + case SelectionChangeType.pushExtentUp: + case SelectionChangeType.pushExtentDown: if (tagAroundCaret == null) { return; } @@ -1299,7 +1308,10 @@ class AdjustSelectionAroundTagReaction extends EditReaction { ); break; case SelectionChangeType.placeCaret: - case SelectionChangeType.pushCaret: + case SelectionChangeType.pushCaretDownstream: + case SelectionChangeType.pushCaretUpstream: + case SelectionChangeType.pushCaretUp: + case SelectionChangeType.pushCaretDown: case SelectionChangeType.collapseSelection: throw AssertionError( "An expanded selection reported a SelectionChangeType for a collapsed selection: ${selectionChangeEvent.changeType}\n${selectionChangeEvent.newSelection}"); @@ -1353,6 +1365,8 @@ class AdjustSelectionAroundTagReaction extends EditReaction { TagAroundPosition tagAroundCaret, ) { DocumentSelection? newSelection; + late SelectionChangeType newSelectionChangeType; + editorStableTagsLog.info("oldCaret is null. Pushing caret to end of tag."); // The caret was placed directly in the token without a previous selection. This might // be a user tap, or programmatic placement. Move the caret to the nearest edge of the @@ -1360,6 +1374,7 @@ class AdjustSelectionAroundTagReaction extends EditReaction { if ((tagAroundCaret.searchOffset - tagAroundCaret.indexedTag.startOffset).abs() < (tagAroundCaret.searchOffset - tagAroundCaret.indexedTag.endOffset).abs()) { // Move the caret to the start of the tag. + newSelectionChangeType = SelectionChangeType.pushCaretUpstream; newSelection = DocumentSelection.collapsed( position: DocumentPosition( nodeId: textNodeId, @@ -1368,6 +1383,7 @@ class AdjustSelectionAroundTagReaction extends EditReaction { ); } else { // Move the caret to the end of the tag. + newSelectionChangeType = SelectionChangeType.pushCaretDownstream; newSelection = DocumentSelection.collapsed( position: DocumentPosition( nodeId: textNodeId, @@ -1379,7 +1395,7 @@ class AdjustSelectionAroundTagReaction extends EditReaction { requestDispatcher.execute([ ChangeSelectionRequest( newSelection, - newSelection.isCollapsed ? SelectionChangeType.pushCaret : SelectionChangeType.expandSelection, + newSelection.isCollapsed ? newSelectionChangeType : SelectionChangeType.expandSelection, SelectionReason.contentChange, ), ]); @@ -1401,15 +1417,19 @@ class AdjustSelectionAroundTagReaction extends EditReaction { extent: selectionChangeEvent.newSelection!.extent, ); + late SelectionChangeType selectionChangeType; late int textOffset; switch (pushDirection) { case TextAffinity.upstream: // Move to starting edge. textOffset = tagAroundCaret.indexedTag.startOffset; + selectionChangeType = expand ? SelectionChangeType.pushExtentUpstream : SelectionChangeType.pushCaretUpstream; break; case TextAffinity.downstream: // Move to ending edge. textOffset = tagAroundCaret.indexedTag.endOffset; + selectionChangeType = + expand ? SelectionChangeType.pushExtentDownstream : SelectionChangeType.pushCaretDownstream; break; } @@ -1435,7 +1455,7 @@ class AdjustSelectionAroundTagReaction extends EditReaction { requestDispatcher.execute([ ChangeSelectionRequest( newSelection, - SelectionChangeType.pushCaret, + selectionChangeType, SelectionReason.contentChange, ), ]); diff --git a/super_editor/test/super_editor/supereditor_keyboard_test.dart b/super_editor/test/super_editor/supereditor_keyboard_test.dart index 4e4f4ecbe1..36e8660e5c 100644 --- a/super_editor/test/super_editor/supereditor_keyboard_test.dart +++ b/super_editor/test/super_editor/supereditor_keyboard_test.dart @@ -19,128 +19,264 @@ void main() { tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 2, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 2, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 1)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnDesktop("left by one character and expands when SHIFT + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 2, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 2, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 2, to: 1)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUpstream)); }); testAllInputsOnDesktop("right by one character when RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 2, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 2, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 3)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDownstream)); }); testAllInputsOnDesktop("right by one character and expands when SHIFT + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 2, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 2, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 2, to: 3)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDownstream)); }); testAllInputsOnApple("to beginning of word when ALT + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressAltLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 8)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnApple("to beginning of word and expands when SHIFT + ALT + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftAltLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 8)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUpstream)); + }); + + testAllInputsOnDesktop("to beginning of word and collapses when LEFT_ARROW is pressed", ( + tester, { + required TextInputSource inputSource, + }) async { + final collector = _EditorSelectionChangeEventsCollector(); + // Pumps the editor with the word "second" selected. + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 45, + expandedSelection: true, + inputSource: inputSource, + selectionChangeCollector: collector, + ); + + await tester.pressLeftArrow(); + + expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 41)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.collapseSelection)); }); testAllInputsOnApple("to end of word when ALT + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressAltRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDownstream)); }); testAllInputsOnApple("to end of word and expands when SHIFT + ALT + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftAltRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDownstream)); + }); + + testAllInputsOnDesktop("to end of word and collapses when RIGHT_ARROW is pressed", ( + tester, { + required TextInputSource inputSource, + }) async { + final collector = _EditorSelectionChangeEventsCollector(); + // Pumps the editor with the word "second" selected. + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 45, + expandedSelection: true, + inputSource: inputSource, + selectionChangeCollector: collector, + ); + + await tester.pressRightArrow(); + + expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 47)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.collapseSelection)); }); testAllInputsOnApple("to beginning of line when CMD + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressCmdLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 0)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnApple("to beginning of line and expands when SHIFT + CMD + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftCmdLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 0)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUpstream)); }); testAllInputsOnApple("to end of line when CMD + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressCmdRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 26, TextAffinity.upstream)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDownstream)); }); testAllInputsOnApple("to end of line and expands when SHIFT + CMD + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftCmdRightArrow(); @@ -148,144 +284,290 @@ void main() { SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 26, toAffinity: TextAffinity.upstream), ); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDownstream)); }); testAllInputsOnWindowsAndLinux("to beginning of word when CTL + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressCtlLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 8)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnWindowsAndLinux("to beginning of word and expands when SHIFT + CTL + LEFT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftCtlLeftArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 8)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUpstream)); }); testAllInputsOnWindowsAndLinux("to end of word when CTL + Right_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressCtlRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDownstream)); }); testAllInputsOnWindowsAndLinux("to end of word and expands when SHIFT + CTL + RIGHT_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftCtlRightArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 10, to: 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDownstream)); }); testAllInputsOnDesktop("up one line when UP_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 41, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 41, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressUpArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUp)); }); testAllInputsOnDesktop("up one line and expands when SHIFT + UP_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 41, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 41, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftUpArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 41, to: 12)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUp)); + }); + + testAllInputsOnDesktop("up one line and collapses when UP_ARROW is pressed", ( + tester, { + required TextInputSource inputSource, + }) async { + final collector = _EditorSelectionChangeEventsCollector(); + // Pumps the editor with the word "second" selected. + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 44, + expandedSelection: true, + inputSource: inputSource, + selectionChangeCollector: collector, + ); + + await tester.pressUpArrow(); + + expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 18)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUp)); }); testAllInputsOnDesktop("down one line when DOWN_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 12, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 12, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressDownArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 41)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDown)); }); testAllInputsOnDesktop("down one line and expands when SHIFT + DOWN_ARROW is pressed", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 12, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 12, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftDownArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 12, to: 41)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDown)); + }); + + testAllInputsOnDesktop("down one line and collapses when DOWN_ARROW is pressed", ( + tester, { + required TextInputSource inputSource, + }) async { + final collector = _EditorSelectionChangeEventsCollector(); + // Pumps the editor with the word "first" selected. + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 15, + expandedSelection: true, + inputSource: inputSource, + selectionChangeCollector: collector, + ); + + await tester.pressDownArrow(); + + expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 46)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDown)); }); testAllInputsOnDesktop("to beginning of line when UP_ARROW is pressed at top of document", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 12, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 12, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressUpArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 0)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnDesktop("to beginning of line and expands when SHIFT + UP_ARROW is pressed at top of document", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 12, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 12, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftUpArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 12, to: 0)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentUpstream)); }); testAllInputsOnDesktop("to end of line when DOWN_ARROW is pressed at end of document", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 41, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 41, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressDownArrow(); expect(SuperEditorInspector.findDocumentSelection(), _caretInParagraph(nodeId, 58)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretDownstream)); }); testAllInputsOnDesktop("end of line and expands when SHIFT + DOWN_ARROW is pressed at end of document", ( tester, { required TextInputSource inputSource, }) async { - final nodeId = await _pumpDoubleLineWithCaret(tester, offset: 41, inputSource: inputSource); + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpDoubleLineWithCaret( + tester, + offset: 41, + inputSource: inputSource, + selectionChangeCollector: collector, + ); await tester.pressShiftDownArrow(); expect(SuperEditorInspector.findDocumentSelection(), _selectionInParagraph(nodeId, from: 41, to: 58)); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushExtentDownstream)); }); }); }); - testWidgetsOnMacWeb("on web moves caret to beginning of line when CMD + LEFT_ARROW is pressed", (tester) async { - final nodeId = await _pumpSingleLineWithCaret(tester, offset: 10, inputSource: TextInputSource.ime); + testWidgetsOnMacWeb("moves caret to beginning of line when CMD + LEFT_ARROW is pressed", (tester) async { + final collector = _EditorSelectionChangeEventsCollector(); + final nodeId = await _pumpSingleLineWithCaret( + tester, + offset: 10, + inputSource: TextInputSource.ime, + selectionChangeCollector: collector, + ); // Simulate the user pressing CMD + LEFT ARROW, which generates a delta moving // the selection to the beginning of the line. @@ -314,6 +596,8 @@ void main() { end: DocumentPosition(nodeId: nodeId, nodePosition: const TextNodePosition(offset: 0)), ), ); + expect(collector.events.length, equals(1)); + expect(collector.events.first.changeType, equals(SelectionChangeType.pushCaretUpstream)); }); testAllInputsOnAllPlatforms('does nothing without primary focus', ( @@ -872,6 +1156,7 @@ Future _pumpSingleLineWithCaret( WidgetTester tester, { required int offset, required TextInputSource inputSource, + _EditorSelectionChangeEventsCollector? selectionChangeCollector, }) async { final testContext = await tester // .createDocument() @@ -883,11 +1168,20 @@ Future _pumpSingleLineWithCaret( await tester.placeCaretInParagraph(nodeId, offset); + if (selectionChangeCollector != null) { + testContext.editor.addListener(selectionChangeCollector); + } + return nodeId; } -Future _pumpDoubleLineWithCaret(WidgetTester tester, - {required int offset, required TextInputSource inputSource}) async { +Future _pumpDoubleLineWithCaret( + WidgetTester tester, { + required int offset, + bool expandedSelection = false, + required TextInputSource inputSource, + _EditorSelectionChangeEventsCollector? selectionChangeCollector, +}) async { final testContext = await tester // .createDocument() // Text indices: @@ -896,10 +1190,17 @@ Future _pumpDoubleLineWithCaret(WidgetTester tester, // - second line: [30, 58] .fromMarkdown("This is the first paragraph.\nThis is the second paragraph.") .pump(); - final nodeId = testContext.findEditContext().document.first.id; - await tester.placeCaretInParagraph(nodeId, offset); + if (expandedSelection) { + await tester.doubleTapInParagraph(nodeId, offset); + } else { + await tester.placeCaretInParagraph(nodeId, offset); + } + + if (selectionChangeCollector != null) { + testContext.editor.addListener(selectionChangeCollector); + } return nodeId; } @@ -972,3 +1273,13 @@ class _CloseKeyboardOnDisposeState extends State<_CloseKeyboardOnDispose> { return widget.child; } } + +/// An [EditListener] that collects all [SelectionChangeEvent]s reported. +class _EditorSelectionChangeEventsCollector implements EditListener { + final List events = []; + + @override + void onEdit(List changeList) { + events.addAll(changeList.whereType()); + } +}