Skip to content

Commit

Permalink
[SuperEditor] Add ClearDocumentRequest (Resolves #2360) (#2460)
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre authored Dec 20, 2024
1 parent 16b4b21 commit a5857c5
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 0 deletions.
6 changes: 6 additions & 0 deletions super_editor/lib/src/core/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,12 @@ class MutableDocument with Iterable<DocumentNode> implements Document, Editable
return isRemoved;
}

/// Deletes all nodes from the [Document].
void clear() {
_nodes.clear();
_refreshNodeIdCaches();
}

/// Moves a [DocumentNode] matching the given [nodeId] from its current index
/// in the [Document] to the given [targetIndex].
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ final defaultRequestHandlers = List.unmodifiable(<EditRequestHandler>[
(request) => request is DeleteNodeRequest //
? DeleteNodeCommand(nodeId: request.nodeId)
: null,
(request) => request is ClearDocumentRequest //
? ClearDocumentCommand()
: null,
(request) => request is DeleteUpstreamCharacterRequest //
? const DeleteUpstreamCharacterCommand()
: null,
Expand Down
54 changes: 54 additions & 0 deletions super_editor/lib/src/default_editor/multi_node_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1312,3 +1312,57 @@ class DeleteNodeCommand extends EditCommand {
]);
}
}

/// An [EditRequest] to clear the document's content.
///
/// This request:
///
/// - Removes all nodes from the document.
/// - Adds a new empty paragraph.
/// - Places the caret at the beginning of the new paragraph.
/// - Clears the composing region.
class ClearDocumentRequest implements EditRequest {
const ClearDocumentRequest();
}

class ClearDocumentCommand extends EditCommand {
@override
void execute(EditContext context, CommandExecutor executor) {
final document = context.document;

for (final node in document) {
executor.logChanges([
DocumentEdit(
NodeRemovedEvent(node.id, node),
)
]);
}

document.clear();

final newNodeId = Editor.createNodeId();
executor
..executeCommand(
InsertNodeAtIndexCommand(
nodeIndex: 0,
newNode: ParagraphNode(
id: newNodeId,
text: AttributedText(),
),
),
)
..executeCommand(
ChangeSelectionCommand(
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: newNodeId,
nodePosition: const TextNodePosition(offset: 0),
),
),
SelectionChangeType.insertContent,
SelectionReason.userInteraction,
),
)
..executeCommand(ChangeComposingRegionCommand(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_runners/flutter_test_runners.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor/super_editor_test.dart';

import 'supereditor_test_tools.dart';

void main() {
group('SuperEditor > content deletion >', () {
testWidgetsOnAllPlatforms('clears document', (tester) async {
final testContext = await tester //
.createDocument()
.withLongDoc()
.pump();

// Place the caret at an arbitraty node. We don't place the caret at the
// beginning of the document to make sure the selection will move
// to the beginning of the document after the deletion.
await tester.placeCaretInParagraph('2', 0);

// Hold the state sent to the platform.
String? text;
int? selectionBase;
int? selectionExtent;
String? selectionAffinity;
int? composingBase;
int? composingExtent;

// Intercept the setEditingState message sent to the platform.
tester
.interceptChannel(SystemChannels.textInput.name) //
.interceptMethod(
'TextInput.setEditingState',
(methodCall) {
if (methodCall.method == 'TextInput.setEditingState') {
text = methodCall.arguments['text'];
selectionBase = methodCall.arguments['selectionBase'];
selectionExtent = methodCall.arguments['selectionExtent'];
selectionAffinity = methodCall.arguments['selectionAffinity'];
composingBase = methodCall.arguments["composingBase"];
composingExtent = methodCall.arguments["composingExtent"];
}
return null;
},
);

// Delete all content.
testContext.editor.execute([const ClearDocumentRequest()]);
await tester.pump();

// Ensure the document was cleared and a new empty paragraph was added.
final document = testContext.document;
expect(document.length, equals(1));
expect(document.first, isA<ParagraphNode>());
expect((document.first as ParagraphNode).text.text, equals(''));

// Ensure the selection was moved to the end of the document.
expect(
SuperEditorInspector.findDocumentSelection(),
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: document.first.id,
nodePosition: const TextNodePosition(offset: 0),
),
),
);

// Ensure the composing region was cleared.
expect(testContext.composer.composingRegion.value, isNull);

// Ensure the state was correctly sent to the platform.
expect(text, equals('. '));
expect(selectionBase, equals(2));
expect(selectionExtent, equals(2));
expect(selectionAffinity, equals('TextAffinity.downstream'));
expect(composingBase, equals(-1));
expect(composingExtent, equals(-1));

// Ensure the user can still type text.
await tester.typeImeText('Hello world!');
expect((document.first as ParagraphNode).text.text, equals('Hello world!'));
});
});
}

0 comments on commit a5857c5

Please sign in to comment.