Skip to content

Commit b375f3b

Browse files
committed
Reworked PasteEditorCommand to handle insertion of text with afterwards correctly placed selection when a) text spanning a single text node is pasted inside a node or at the end of a node, b) text spanning several text nodes is pasted inside a node or at the end of a node. Added test for behavior with multiple nodes. Renamed documentPositionAfterPast > selectionAfterPaste
1 parent 029e8ce commit b375f3b

2 files changed

Lines changed: 125 additions & 38 deletions

File tree

super_editor/lib/src/default_editor/common_editor_operations.dart

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,19 +2465,22 @@ class PasteEditorCommand extends EditCommand {
24652465

24662466
final textNode = document.getNode(_pastePosition) as TextNode;
24672467
final pasteTextOffset = (_pastePosition.nodePosition as TextPosition).offset;
2468-
2469-
if (parsedContent.length > 1 && pasteTextOffset < textNode.endPosition.offset) {
2470-
// There is more than 1 node of content being pasted. Therefore,
2471-
// new nodes will need to be added, which means that the currently
2472-
// selected text node will be split at the current text offset.
2473-
// Configure a new node to be added at the end of the pasted content
2474-
// which contains the trailing text from the currently selected
2475-
// node.
2468+
String? newNodeId;
2469+
bool doInsertInsideParagraph = pasteTextOffset < textNode.endPosition.offset;
2470+
2471+
if (parsedContent.length > 1 && doInsertInsideParagraph) {
2472+
// There is more than 1 node of content being pasted, and inside a
2473+
// paragraph, not at the end. Therefore, new nodes will need to be
2474+
// added, which means that the currently selected text node will be
2475+
// split at the current text offset and a new node has to be added
2476+
// at the end of the pasted content which contains the trailing text
2477+
// from the currently selected text node.
2478+
newNodeId = Editor.createNodeId();
24762479
executor.executeCommand(
24772480
SplitParagraphCommand(
24782481
nodeId: currentNodeWithSelection.id,
24792482
splitPosition: TextPosition(offset: pasteTextOffset),
2480-
newNodeId: Editor.createNodeId(),
2483+
newNodeId: newNodeId,
24812484
replicateExistingMetadata: true,
24822485
),
24832486
);
@@ -2494,10 +2497,37 @@ class PasteEditorCommand extends EditCommand {
24942497
}
24952498

24962499
// The first line of pasted text was added to the selected paragraph.
2497-
// Now, add all remaining pasted nodes to the document..
2500+
// Now, add all remaining pasted nodes to the document.
24982501
DocumentNode previousNode = document.getNodeById(_pastePosition.nodeId)!;
24992502
// ^ re-query the node where the first paragraph was pasted because nodes are immutable.
25002503
for (final pastedNode in parsedContent.sublist(1)) {
2504+
if (pastedNode == parsedContent.last && pastedNode is TextNode) {
2505+
// This is the last node being pasted, and it's a TextNode.
2506+
// If we had to split the existing node to insert several nodes,
2507+
// we need to merge this last pasted node with the new node that
2508+
// resulted from splitting the text node:
2509+
if (doInsertInsideParagraph) {
2510+
// this is guaranteed to be a TextNode because it was created from SplitParagraphCommand.
2511+
executor.executeCommand(
2512+
InsertAttributedTextCommand(
2513+
documentPosition: DocumentPosition(
2514+
nodeId: newNodeId ?? previousNode.id, nodePosition: const TextNodePosition(offset: 0)),
2515+
textToInsert: (parsedContent.last as TextNode).text,
2516+
),
2517+
);
2518+
2519+
executor.logChanges([
2520+
DocumentEdit(
2521+
NodeChangeEvent(newNodeId ?? previousNode.id),
2522+
)
2523+
]);
2524+
previousNode = pastedNode;
2525+
break;
2526+
}
2527+
}
2528+
// This is either not the last node, or it's not a TextNode, or it's the
2529+
// last node but we didn't split the node that we pasted in; in all cases,
2530+
// just insert it as a new node.
25012531
document.insertNodeAfter(
25022532
existingNodeId: previousNode.id,
25032533
newNode: pastedNode,
@@ -2511,31 +2541,32 @@ class PasteEditorCommand extends EditCommand {
25112541
]);
25122542
}
25132543

2514-
late DocumentPosition documentPositionAfterPaste;
2515-
if (parsedContent.length > 1) {
2516-
// Place the caret at the end of the pasted content.
2544+
late DocumentPosition selectionAfterPaste;
2545+
if (doInsertInsideParagraph && parsedContent.last is TextNode) {
2546+
// Content was pasted in the middle of a text node, and it ends in text, so
2547+
// the caret is placed accordingly at the end of the pasted content at
2548+
// the split position.
2549+
selectionAfterPaste = DocumentPosition(
2550+
nodeId: newNodeId ?? previousNode.id,
2551+
nodePosition: TextNodePosition(
2552+
offset: (newNodeId == null ? pasteTextOffset : 0) + (_parsedContent!.last as TextNode).text.length,
2553+
),
2554+
);
2555+
} else {
2556+
// Either insertion took place at the end of a paragraph, or the last node is
2557+
// non-text and therefore is inserted as a node of its own. In both cases,
2558+
// place the caret at the end of the pasted content.
25172559
final pastedNode = document.getNodeById(previousNode.id)!;
25182560
// ^ re-query the node where we pasted content because nodes are immutable.
2519-
documentPositionAfterPaste = DocumentPosition(
2561+
selectionAfterPaste = DocumentPosition(
25202562
nodeId: pastedNode.id,
25212563
nodePosition: pastedNode.endPosition,
25222564
);
2523-
} else {
2524-
// The user only pasted content without any newlines in it. Place the
2525-
// caret in the existing node at the end of the pasted text. This is
2526-
// guaranteed to be a TextNode.
2527-
documentPositionAfterPaste = DocumentPosition(
2528-
nodeId: _pastePosition.nodeId,
2529-
nodePosition: TextNodePosition(
2530-
offset:
2531-
pasteTextOffset + (_parsedContent!.first as TextNode).text.length,
2532-
),
2533-
);
25342565
}
25352566
executor.executeCommand(
25362567
ChangeSelectionCommand(
25372568
DocumentSelection.collapsed(
2538-
position: documentPositionAfterPaste,
2569+
position: selectionAfterPaste,
25392570
),
25402571
SelectionChangeType.insertContent,
25412572
SelectionReason.userInteraction,

super_editor/test/super_editor/supereditor_copy_and_paste_test.dart

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ void main() {
3838
});
3939

4040
testAllInputsOnDesktop(
41-
'pastes some text in the middle of a paragraph, correctly placing the caret at the end of the pasted text',
41+
'pastes some text without newlines in the middle of a paragraph, placing the caret at the end of the pasted text',
4242
(
4343
tester, {
4444
required TextInputSource inputSource,
@@ -51,15 +51,15 @@ void main() {
5151

5252
// Place the caret at the empty paragraph.
5353
await tester.placeCaretInParagraph('1', 0);
54-
54+
5555
// Type some text.
5656
switch (inputSource) {
5757
case TextInputSource.keyboard:
58-
await tester.typeKeyboardText('This is a paragraph');
58+
await tester.typeKeyboardText('This is a paragraph.');
5959
case TextInputSource.ime:
60-
await tester.typeImeText('This is a paragraph');
60+
await tester.typeImeText('This is a paragraph.');
6161
}
62-
62+
6363
// Place the cursor somewhere in the middle of the text.
6464
await tester.placeCaretInParagraph('1', 8);
6565

@@ -77,15 +77,71 @@ void main() {
7777
// end of the pasted text.
7878
final doc = testContext.document;
7979
expect(doc.nodeCount, 1);
80-
expect((doc.getNodeAt(0)! as ParagraphNode).text.toPlainText(), 'This is some content in a paragraph');
81-
expect(testContext.composer.selection, selectionEquivalentTo(const DocumentSelection.collapsed(
82-
position: DocumentPosition(
83-
nodeId: '1',
84-
nodePosition: TextNodePosition(offset: 24),
85-
),
86-
)));
80+
expect((doc.getNodeAt(0)! as ParagraphNode).text.toPlainText(), 'This is some content in a paragraph.');
81+
expect(
82+
testContext.composer.selection,
83+
selectionEquivalentTo(const DocumentSelection.collapsed(
84+
position: DocumentPosition(
85+
nodeId: '1',
86+
nodePosition: TextNodePosition(offset: 24),
87+
),
88+
)));
8789
});
8890

91+
testAllInputsOnDesktop(
92+
'pastes some text with newlines in the middle of a paragraph, correctly splitting it, inserting nodes and placing the caret at the end of the pasted text',
93+
(
94+
tester, {
95+
required TextInputSource inputSource,
96+
}) async {
97+
final testContext = await tester //
98+
.createDocument()
99+
.withSingleEmptyParagraph()
100+
.withInputSource(inputSource)
101+
.pump();
102+
103+
// Place the caret at the empty paragraph.
104+
await tester.placeCaretInParagraph('1', 0);
105+
106+
// Type some text.
107+
switch (inputSource) {
108+
case TextInputSource.keyboard:
109+
await tester.typeKeyboardText('This is a paragraph.');
110+
case TextInputSource.ime:
111+
await tester.typeImeText('This is a paragraph.');
112+
}
113+
114+
// Place the cursor somewhere in the middle of the text.
115+
await tester.placeCaretInParagraph('1', 8);
116+
117+
// Simulate pasting multiple lines.
118+
tester
119+
..simulateClipboard()
120+
..setSimulatedClipboardContent('''some content in a paragraph.
121+
There is a second paragraph here.
122+
And a third one is also ''');
123+
if (defaultTargetPlatform == TargetPlatform.macOS) {
124+
await tester.pressCmdV();
125+
} else {
126+
await tester.pressCtlV();
127+
}
128+
129+
// Ensure the text is correctly pasted and the caret is placed at the
130+
// end of the pasted text.
131+
final doc = testContext.document;
132+
expect(doc.nodeCount, 3);
133+
expect((doc.getNodeAt(0)! as ParagraphNode).text.toPlainText(), 'This is some content in a paragraph.');
134+
expect((doc.getNodeAt(1)! as ParagraphNode).text.toPlainText(), 'There is a second paragraph here.');
135+
expect((doc.getNodeAt(2)! as ParagraphNode).text.toPlainText(), 'And a third one is also a paragraph.');
136+
expect(
137+
testContext.composer.selection,
138+
selectionEquivalentTo(DocumentSelection.collapsed(
139+
position: DocumentPosition(
140+
nodeId: testContext.document.last.id,
141+
nodePosition: const TextNodePosition(offset: 24),
142+
),
143+
)));
144+
});
89145

90146
testWidgetsOnApple('pastes within a list item', (tester) async {
91147
await tester //

0 commit comments

Comments
 (0)