Skip to content

Commit 285101c

Browse files
Cherry Pick: [SuperEditor] - Fix: Use copies of nodes when inserting new nodes so that undo has pristine copies. (Resolves #2164) (#2233)
1 parent 2d36218 commit 285101c

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

super_editor/lib/src/default_editor/multi_node_editing.dart

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,16 @@ class InsertNodeBeforeNodeCommand extends EditCommand {
325325
void execute(EditContext context, CommandExecutor executor) {
326326
final document = context.document;
327327
final existingNode = document.getNodeById(existingNodeId)!;
328-
document.insertNodeBefore(existingNode: existingNode, newNode: newNode);
328+
329+
// Make a copy of the node so that this command can be re-run without retaining
330+
// future mutations of this node.
331+
final newNodeCopy = newNode.copy();
332+
333+
document.insertNodeBefore(existingNode: existingNode, newNode: newNodeCopy);
329334

330335
executor.logChanges([
331336
DocumentEdit(
332-
NodeInsertedEvent(newNode.id, document.getNodeIndexById(newNode.id)),
337+
NodeInsertedEvent(newNodeCopy.id, document.getNodeIndexById(newNodeCopy.id)),
333338
)
334339
]);
335340
}
@@ -358,11 +363,16 @@ class InsertNodeAfterNodeCommand extends EditCommand {
358363
void execute(EditContext context, CommandExecutor executor) {
359364
final document = context.document;
360365
final existingNode = document.getNodeById(existingNodeId)!;
361-
document.insertNodeAfter(existingNode: existingNode, newNode: newNode);
366+
367+
// Make a copy of the node so that this command can be re-run without retaining
368+
// future mutations of this node.
369+
final newNodeCopy = newNode.copy();
370+
371+
document.insertNodeAfter(existingNode: existingNode, newNode: newNodeCopy);
362372

363373
executor.logChanges([
364374
DocumentEdit(
365-
NodeInsertedEvent(newNode.id, document.getNodeIndexById(newNode.id)),
375+
NodeInsertedEvent(newNodeCopy.id, document.getNodeIndexById(newNodeCopy.id)),
366376
)
367377
]);
368378
}

super_editor/test/super_editor/super_editor_undo_redo_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import 'dart:ui';
2+
13
import 'package:clock/clock.dart';
4+
import 'package:flutter/services.dart';
25
import 'package:flutter_test/flutter_test.dart';
36
import 'package:flutter_test_robots/flutter_test_robots.dart';
47
import 'package:flutter_test_runners/flutter_test_runners.dart';
@@ -158,6 +161,58 @@ void main() {
158161
await tester.pressCmdShiftZ(tester);
159162
_expectDocumentWithCaret("Hello", "1", 5);
160163
});
164+
165+
testWidgetsOnMac("undo when typing after an image", (tester) async {
166+
// A reported bug found that when inserting a paragraph after an image, typing some
167+
// text, and then undo'ing the text, the paragraph's text duplicates during the
168+
// undo operation: https://github.com/superlistapp/super_editor/issues/2164
169+
// TODO: The root cause of this problem was mutability of DocumentNode's. Delete this test after completing: https://github.com/superlistapp/super_editor/issues/2166
170+
final testContext = await tester
171+
.createDocument() //
172+
.withCustomContent(MutableDocument(
173+
nodes: [
174+
ImageNode(id: "1", imageUrl: "https://fakeimage.com/myimage.png"),
175+
],
176+
))
177+
.withComponentBuilders([
178+
const FakeImageComponentBuilder(size: Size(1000, 400)),
179+
...defaultComponentBuilders,
180+
])
181+
.enableHistory(true)
182+
.autoFocus(true)
183+
.pump();
184+
185+
await tester.tapAtDocumentPosition(
186+
const DocumentPosition(nodeId: "1", nodePosition: UpstreamDownstreamNodePosition.downstream()),
187+
);
188+
189+
// Press enter to insert a new paragraph.
190+
await tester.pressEnter();
191+
192+
// Ensure we inserted a paragraph.
193+
expect(testContext.document.nodeCount, 2);
194+
expect(testContext.document.getNodeAt(0), isA<ImageNode>());
195+
expect(testContext.document.getNodeAt(1), isA<TextNode>());
196+
197+
// Type some text.
198+
await tester.pressKey(LogicalKeyboardKey.keyA);
199+
200+
// Wait long enough to avoid combining actions into a single transaction.
201+
await tester.pump(const Duration(seconds: 2));
202+
203+
// Type more text.
204+
await tester.pressKey(LogicalKeyboardKey.keyB);
205+
206+
// Ensure we inserted the text.
207+
expect((testContext.document.getNodeAt(1) as TextNode).text.text, "ab");
208+
209+
// Undo the text insertion.
210+
// TODO: remove `tester` reference after updating flutter_test_robots
211+
await tester.pressCmdZ(tester);
212+
213+
// Ensure that the paragraph removed the last entered character.
214+
expect((testContext.document.getNodeAt(1) as TextNode).text.text, "a");
215+
});
161216
});
162217

163218
group("content conversions >", () {

0 commit comments

Comments
 (0)