Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry Pick: [SuperEditor] Add ClearDocumentRequest (Resolves #2360) #2464

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!'));
});
});
}
Loading