refactor: batch insert and delete nodes

This commit is contained in:
Vincent Chan 2022-08-01 17:36:31 +08:00
parent 9b764731e7
commit 2f58c54b81
6 changed files with 63 additions and 38 deletions

View File

@ -22,17 +22,21 @@ class StateTree {
return root.childAtPath(path); return root.childAtPath(path);
} }
bool insert(Path path, Node node) { bool insert(Path path, List<Node> nodes) {
if (path.isEmpty) { if (path.isEmpty) {
return false; return false;
} }
final insertedNode = root.childAtPath( Node? insertedNode = root.childAtPath(
path.sublist(0, path.length - 1) + [path.last - 1], path.sublist(0, path.length - 1) + [path.last - 1],
); );
if (insertedNode == null) { if (insertedNode == null) {
return false; return false;
} }
insertedNode.insertAfter(node); for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
insertedNode!.insertAfter(node);
insertedNode = node;
}
return true; return true;
} }
@ -48,13 +52,17 @@ class StateTree {
return false; return false;
} }
Node? delete(Path path) { delete(Path path, [int length = 1]) {
if (path.isEmpty) { if (path.isEmpty) {
return null; return null;
} }
final deletedNode = root.childAtPath(path); var deletedNode = root.childAtPath(path);
deletedNode?.unlink(); while (deletedNode != null && length > 0) {
return deletedNode; final next = deletedNode.next;
deletedNode.unlink();
length--;
deletedNode = next;
}
} }
Attributes? update(Path path, Attributes attributes) { Attributes? update(Path path, Attributes attributes) {

View File

@ -94,11 +94,11 @@ class EditorState {
_applyOperation(Operation op) { _applyOperation(Operation op) {
if (op is InsertOperation) { if (op is InsertOperation) {
document.insert(op.path, op.value); document.insert(op.path, op.nodes);
} else if (op is UpdateOperation) { } else if (op is UpdateOperation) {
document.update(op.path, op.attributes); document.update(op.path, op.attributes);
} else if (op is DeleteOperation) { } else if (op is DeleteOperation) {
document.delete(op.path); document.delete(op.path, op.nodes.length);
} else if (op is TextEditOperation) { } else if (op is TextEditOperation) {
document.textEdit(op.path, op.delta); document.textEdit(op.path, op.delta);
} }

View File

@ -24,18 +24,19 @@ abstract class Operation {
} }
class InsertOperation extends Operation { class InsertOperation extends Operation {
final Node value; final List<Node> nodes;
factory InsertOperation.fromJson(Map<String, dynamic> map) { factory InsertOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>; final path = map["path"] as List<int>;
final value = Node.fromJson(map["value"]); final value =
(map["nodes"] as List<dynamic>).map((n) => Node.fromJson(n)).toList();
return InsertOperation(path, value); return InsertOperation(path, value);
} }
InsertOperation(Path path, this.value) : super(path); InsertOperation(Path path, this.nodes) : super(path);
InsertOperation copyWith({Path? path, Node? value}) => InsertOperation copyWith({Path? path, List<Node>? nodes}) =>
InsertOperation(path ?? this.path, value ?? this.value); InsertOperation(path ?? this.path, nodes ?? this.nodes);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@ -44,7 +45,7 @@ class InsertOperation extends Operation {
Operation invert() { Operation invert() {
return DeleteOperation( return DeleteOperation(
path, path,
value, nodes,
); );
} }
@ -53,7 +54,7 @@ class InsertOperation extends Operation {
return { return {
"type": "insert-operation", "type": "insert-operation",
"path": path.toList(), "path": path.toList(),
"value": value.toJson(), "nodes": nodes.map((n) => n.toJson()),
}; };
} }
} }
@ -104,28 +105,29 @@ class UpdateOperation extends Operation {
} }
class DeleteOperation extends Operation { class DeleteOperation extends Operation {
final Node removedValue; final List<Node> nodes;
factory DeleteOperation.fromJson(Map<String, dynamic> map) { factory DeleteOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>; final path = map["path"] as List<int>;
final removedValue = Node.fromJson(map["removedValue"]); final List<Node> nodes =
return DeleteOperation(path, removedValue); (map["nodes"] as List<dynamic>).map((e) => Node.fromJson(e)).toList();
return DeleteOperation(path, nodes);
} }
DeleteOperation( DeleteOperation(
Path path, Path path,
this.removedValue, this.nodes,
) : super(path); ) : super(path);
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation copyWith({Path? path, List<Node>? nodes}) =>
DeleteOperation(path ?? this.path, removedValue ?? this.removedValue); DeleteOperation(path ?? this.path, nodes ?? this.nodes);
@override @override
Operation copyWithPath(Path path) => copyWith(path: path); Operation copyWithPath(Path path) => copyWith(path: path);
@override @override
Operation invert() { Operation invert() {
return InsertOperation(path, removedValue); return InsertOperation(path, nodes);
} }
@override @override
@ -133,7 +135,7 @@ class DeleteOperation extends Operation {
return { return {
"type": "delete-operation", "type": "delete-operation",
"path": path.toList(), "path": path.toList(),
"removedValue": removedValue.toJson(), "nodes": nodes.map((n) => n.toJson()),
}; };
} }
} }

View File

@ -29,8 +29,12 @@ class TransactionBuilder {
} }
insertNode(Path path, Node node) { insertNode(Path path, Node node) {
insertNodes(path, [node]);
}
insertNodes(Path path, List<Node> nodes) {
beforeSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
add(InsertOperation(path, node)); add(InsertOperation(path, nodes));
} }
updateNode(Node node, Attributes attributes) { updateNode(Node node, Attributes attributes) {
@ -43,14 +47,28 @@ class TransactionBuilder {
} }
deleteNode(Node node) { deleteNode(Node node) {
beforeSelection = state.cursorSelection; deleteNodesAtPath(node.path);
add(DeleteOperation(node.path, node));
} }
deleteNodes(List<Node> nodes) { deleteNodes(List<Node> nodes) {
nodes.forEach(deleteNode); nodes.forEach(deleteNode);
} }
deleteNodesAtPath(Path path, [int length = 1]) {
if (path.isEmpty) {
return;
}
final nodes = <Node>[];
final prefix = path.sublist(0, path.length - 1);
final last = path.last;
for (var i = 0; i < length; i++) {
final node = state.document.nodeAtPath(prefix + [last + i])!;
nodes.add(node);
}
add(DeleteOperation(path, nodes));
}
textEdit(TextNode node, Delta Function() f) { textEdit(TextNode node, Delta Function() f) {
beforeSelection = state.cursorSelection; beforeSelection = state.cursorSelection;
final path = node.path; final path = node.path;

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/state_tree.dart'; import 'package:flowy_editor/document/state_tree.dart';
@ -50,7 +49,7 @@ void main() {
final insertNode = Node.fromJson({ final insertNode = Node.fromJson({
'type': 'text', 'type': 'text',
}); });
bool result = stateTree.insert([1, 1], insertNode); bool result = stateTree.insert([1, 1], [insertNode]);
expect(result, true); expect(result, true);
expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true);
}); });
@ -59,9 +58,7 @@ void main() {
final String response = await rootBundle.loadString('assets/document.json'); final String response = await rootBundle.loadString('assets/document.json');
final data = Map<String, Object>.from(json.decode(response)); final data = Map<String, Object>.from(json.decode(response));
final stateTree = StateTree.fromJson(data); final stateTree = StateTree.fromJson(data);
final deletedNode = stateTree.delete([1, 1]); stateTree.delete([1, 1], 1);
expect(deletedNode != null, true);
expect(deletedNode!.attributes['text-type'], 'checkbox');
final node = stateTree.nodeAtPath([1, 1]); final node = stateTree.nodeAtPath([1, 1]);
expect(node != null, true); expect(node != null, true);
expect(node!.attributes['tag'], '**'); expect(node!.attributes['tag'], '**');

View File

@ -28,17 +28,17 @@ void main() {
test('insert + insert', () { test('insert + insert', () {
final t = transformOperation( final t = transformOperation(
InsertOperation([0, 1], InsertOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList())), [Node(type: "node", attributes: {}, children: LinkedList())]),
InsertOperation([0, 1], InsertOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList()))); [Node(type: "node", attributes: {}, children: LinkedList())]));
expect(t.path, [0, 2]); expect(t.path, [0, 2]);
}); });
test('delete + delete', () { test('delete + delete', () {
final t = transformOperation( final t = transformOperation(
DeleteOperation([0, 1], DeleteOperation([0, 1],
Node(type: "node", attributes: {}, children: LinkedList())), [Node(type: "node", attributes: {}, children: LinkedList())]),
DeleteOperation([0, 2], DeleteOperation([0, 2],
Node(type: "node", attributes: {}, children: LinkedList()))); [Node(type: "node", attributes: {}, children: LinkedList())]));
expect(t.path, [0, 1]); expect(t.path, [0, 1]);
}); });
}); });
@ -85,7 +85,7 @@ void main() {
{ {
"type": "insert-operation", "type": "insert-operation",
"path": [0], "path": [0],
"value": item1.toJson(), "nodes": [item1.toJson()],
} }
], ],
}); });
@ -108,7 +108,7 @@ void main() {
{ {
"type": "delete-operation", "type": "delete-operation",
"path": [0], "path": [0],
"removedValue": item1.toJson(), "nodes": [item1.toJson()],
} }
], ],
}); });