diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 22f4b88c24..cf49f48ac8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -22,17 +22,21 @@ class StateTree { return root.childAtPath(path); } - bool insert(Path path, Node node) { + bool insert(Path path, List nodes) { if (path.isEmpty) { return false; } - final insertedNode = root.childAtPath( + Node? insertedNode = root.childAtPath( path.sublist(0, path.length - 1) + [path.last - 1], ); if (insertedNode == null) { return false; } - insertedNode.insertAfter(node); + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode!.insertAfter(node); + insertedNode = node; + } return true; } @@ -48,13 +52,17 @@ class StateTree { return false; } - Node? delete(Path path) { + delete(Path path, [int length = 1]) { if (path.isEmpty) { return null; } - final deletedNode = root.childAtPath(path); - deletedNode?.unlink(); - return deletedNode; + var deletedNode = root.childAtPath(path); + while (deletedNode != null && length > 0) { + final next = deletedNode.next; + deletedNode.unlink(); + length--; + deletedNode = next; + } } Attributes? update(Path path, Attributes attributes) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 1019ad3510..277b742604 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -94,11 +94,11 @@ class EditorState { _applyOperation(Operation op) { if (op is InsertOperation) { - document.insert(op.path, op.value); + document.insert(op.path, op.nodes); } else if (op is UpdateOperation) { document.update(op.path, op.attributes); } else if (op is DeleteOperation) { - document.delete(op.path); + document.delete(op.path, op.nodes.length); } else if (op is TextEditOperation) { document.textEdit(op.path, op.delta); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index a505df4b1d..e07c196768 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -24,18 +24,19 @@ abstract class Operation { } class InsertOperation extends Operation { - final Node value; + final List nodes; factory InsertOperation.fromJson(Map map) { final path = map["path"] as List; - final value = Node.fromJson(map["value"]); + final value = + (map["nodes"] as List).map((n) => Node.fromJson(n)).toList(); 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(path ?? this.path, value ?? this.value); + InsertOperation copyWith({Path? path, List? nodes}) => + InsertOperation(path ?? this.path, nodes ?? this.nodes); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -44,7 +45,7 @@ class InsertOperation extends Operation { Operation invert() { return DeleteOperation( path, - value, + nodes, ); } @@ -53,7 +54,7 @@ class InsertOperation extends Operation { return { "type": "insert-operation", "path": path.toList(), - "value": value.toJson(), + "nodes": nodes.map((n) => n.toJson()), }; } } @@ -104,28 +105,29 @@ class UpdateOperation extends Operation { } class DeleteOperation extends Operation { - final Node removedValue; + final List nodes; factory DeleteOperation.fromJson(Map map) { final path = map["path"] as List; - final removedValue = Node.fromJson(map["removedValue"]); - return DeleteOperation(path, removedValue); + final List nodes = + (map["nodes"] as List).map((e) => Node.fromJson(e)).toList(); + return DeleteOperation(path, nodes); } DeleteOperation( Path path, - this.removedValue, + this.nodes, ) : super(path); - DeleteOperation copyWith({Path? path, Node? removedValue}) => - DeleteOperation(path ?? this.path, removedValue ?? this.removedValue); + DeleteOperation copyWith({Path? path, List? nodes}) => + DeleteOperation(path ?? this.path, nodes ?? this.nodes); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return InsertOperation(path, removedValue); + return InsertOperation(path, nodes); } @override @@ -133,7 +135,7 @@ class DeleteOperation extends Operation { return { "type": "delete-operation", "path": path.toList(), - "removedValue": removedValue.toJson(), + "nodes": nodes.map((n) => n.toJson()), }; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 3b88ed1627..ac7fd4353f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -29,8 +29,12 @@ class TransactionBuilder { } insertNode(Path path, Node node) { + insertNodes(path, [node]); + } + + insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; - add(InsertOperation(path, node)); + add(InsertOperation(path, nodes)); } updateNode(Node node, Attributes attributes) { @@ -43,14 +47,28 @@ class TransactionBuilder { } deleteNode(Node node) { - beforeSelection = state.cursorSelection; - add(DeleteOperation(node.path, node)); + deleteNodesAtPath(node.path); } deleteNodes(List nodes) { nodes.forEach(deleteNode); } + deleteNodesAtPath(Path path, [int length = 1]) { + if (path.isEmpty) { + return; + } + final nodes = []; + 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) { beforeSelection = state.cursorSelection; final path = node.path; diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 16ccadb079..49d0fd00f5 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; @@ -50,7 +49,7 @@ void main() { final insertNode = Node.fromJson({ 'type': 'text', }); - bool result = stateTree.insert([1, 1], insertNode); + bool result = stateTree.insert([1, 1], [insertNode]); expect(result, 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 data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final deletedNode = stateTree.delete([1, 1]); - expect(deletedNode != null, true); - expect(deletedNode!.attributes['text-type'], 'checkbox'); + stateTree.delete([1, 1], 1); final node = stateTree.nodeAtPath([1, 1]); expect(node != null, true); expect(node!.attributes['tag'], '**'); diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index c6811b86e2..7507cb65bf 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -28,17 +28,17 @@ void main() { test('insert + insert', () { final t = transformOperation( InsertOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList())), + [Node(type: "node", attributes: {}, children: LinkedList())]), InsertOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList()))); + [Node(type: "node", attributes: {}, children: LinkedList())])); expect(t.path, [0, 2]); }); test('delete + delete', () { final t = transformOperation( DeleteOperation([0, 1], - Node(type: "node", attributes: {}, children: LinkedList())), + [Node(type: "node", attributes: {}, children: LinkedList())]), DeleteOperation([0, 2], - Node(type: "node", attributes: {}, children: LinkedList()))); + [Node(type: "node", attributes: {}, children: LinkedList())])); expect(t.path, [0, 1]); }); }); @@ -85,7 +85,7 @@ void main() { { "type": "insert-operation", "path": [0], - "value": item1.toJson(), + "nodes": [item1.toJson()], } ], }); @@ -108,7 +108,7 @@ void main() { { "type": "delete-operation", "path": [0], - "removedValue": item1.toJson(), + "nodes": [item1.toJson()], } ], });