Prechádzať zdrojové kódy

refactor: rename state_tree to document and move document to core/state

Lucas.Xu 2 rokov pred
rodič
commit
5e7507c8e7

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -98,7 +98,7 @@ class _MyHomePageState extends State<MyHomePage> {
         if (snapshot.hasData &&
             snapshot.connectionState == ConnectionState.done) {
           _editorState ??= EditorState(
-            document: StateTree.fromJson(
+            document: Document.fromJson(
               Map<String, Object>.from(
                 json.decode(snapshot.data!),
               ),

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

@@ -7,7 +7,7 @@ export 'src/core/document/node.dart';
 export 'src/core/document/path.dart';
 export 'src/core/location/position.dart';
 export 'src/core/location/selection.dart';
-export 'src/document/state_tree.dart';
+export 'src/core/state/document.dart';
 export 'src/core/document/text_delta.dart';
 export 'src/core/document/attributes.dart';
 export 'src/document/built_in_attribute_keys.dart';

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/node_iterator.dart

@@ -1,15 +1,15 @@
 import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/document/state_tree.dart';
+import 'package:appflowy_editor/src/core/state/document.dart';
 
 /// [NodeIterator] is used to traverse the nodes in visual order.
 class NodeIterator implements Iterator<Node> {
   NodeIterator({
-    required this.stateTree,
+    required this.document,
     required this.startNode,
     this.endNode,
   });
 
-  final StateTree stateTree;
+  final Document document;
   final Node startNode;
   final Node? endNode;
 

+ 18 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/path.dart

@@ -69,4 +69,22 @@ extension PathExtensions on Path {
       ..removeLast()
       ..add(last + 1);
   }
+
+  Path get previous {
+    Path previousPath = Path.from(this, growable: true);
+    if (isEmpty) {
+      return previousPath;
+    }
+    final last = previousPath.last;
+    return previousPath
+      ..removeLast()
+      ..add(max(0, last - 1));
+  }
+
+  Path get parent {
+    if (isEmpty) {
+      return this;
+    }
+    return Path.from(this, growable: true)..removeLast();
+  }
 }

+ 118 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/core/state/document.dart

@@ -0,0 +1,118 @@
+import 'dart:collection';
+
+import 'package:appflowy_editor/src/core/document/node.dart';
+import 'package:appflowy_editor/src/core/document/path.dart';
+import 'package:appflowy_editor/src/core/document/text_delta.dart';
+import '../document/attributes.dart';
+
+/// [Document] reprensents a AppFlowy Editor document structure.
+///
+/// It stores the root of the document.
+///
+/// DO NOT directly mutate the properties of a [Document] object.
+class Document {
+  Document({
+    required this.root,
+  });
+
+  factory Document.fromJson(Map<String, dynamic> json) {
+    assert(json['document'] is Map);
+
+    final document = Map<String, Object>.from(json['document'] as Map);
+    final root = Node.fromJson(document);
+    return Document(root: root);
+  }
+
+  /// Creates a empty document with a single text node.
+  factory Document.empty() {
+    final root = Node(
+      type: 'editor',
+      children: LinkedList<Node>()..add(TextNode.empty()),
+    );
+    return Document(
+      root: root,
+    );
+  }
+
+  final Node root;
+
+  /// Returns the node at the given [path].
+  Node? nodeAtPath(Path path) {
+    return root.childAtPath(path);
+  }
+
+  /// Inserts a [Node]s at the given [Path].
+  bool insert(Path path, List<Node> nodes) {
+    if (path.isEmpty || nodes.isEmpty) {
+      return false;
+    }
+
+    final target = nodeAtPath(path);
+    if (target != null) {
+      for (final node in nodes) {
+        target.insertBefore(node);
+      }
+      return true;
+    }
+
+    final parent = nodeAtPath(path.parent);
+    if (parent != null) {
+      for (final node in nodes) {
+        parent.insert(node, index: path.last);
+      }
+      return true;
+    }
+
+    return false;
+  }
+
+  /// Deletes the [Node]s at the given [Path].
+  bool delete(Path path, [int length = 1]) {
+    if (path.isEmpty || length <= 0) {
+      return false;
+    }
+    var target = nodeAtPath(path);
+    if (target == null) {
+      return false;
+    }
+    while (target != null && length > 0) {
+      final next = target.next;
+      target.unlink();
+      target = next;
+      length--;
+    }
+    return true;
+  }
+
+  /// Updates the [Node] at the given [Path]
+  bool update(Path path, Attributes attributes) {
+    if (path.isEmpty) {
+      return false;
+    }
+    final target = nodeAtPath(path);
+    if (target == null) {
+      return false;
+    }
+    target.updateAttributes(attributes);
+    return true;
+  }
+
+  /// Updates the [TextNode] at the given [Path]
+  bool updateText(Path path, Delta delta) {
+    if (path.isEmpty) {
+      return false;
+    }
+    final target = nodeAtPath(path);
+    if (target == null || target is! TextNode) {
+      return false;
+    }
+    target.delta = target.delta.compose(delta);
+    return true;
+  }
+
+  Map<String, Object> toJson() {
+    return {
+      'document': root.toJson(),
+    };
+  }
+}

+ 0 - 116
frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart

@@ -1,116 +0,0 @@
-import 'dart:math';
-
-import 'package:appflowy_editor/src/core/document/node.dart';
-import 'package:appflowy_editor/src/core/document/path.dart';
-import 'package:appflowy_editor/src/core/document/text_delta.dart';
-import '../core/document/attributes.dart';
-
-class StateTree {
-  final Node root;
-
-  StateTree({
-    required this.root,
-  });
-
-  factory StateTree.empty() {
-    return StateTree(
-      root: Node.fromJson({
-        'type': 'editor',
-        'children': [
-          {
-            'type': 'text',
-          }
-        ]
-      }),
-    );
-  }
-
-  factory StateTree.fromJson(Attributes json) {
-    assert(json['document'] is Map);
-
-    final document = Map<String, Object>.from(json['document'] as Map);
-    final root = Node.fromJson(document);
-    return StateTree(root: root);
-  }
-
-  Map<String, Object> toJson() {
-    return {
-      'document': root.toJson(),
-    };
-  }
-
-  Node? nodeAtPath(Path path) {
-    return root.childAtPath(path);
-  }
-
-  bool insert(Path path, List<Node> nodes) {
-    if (path.isEmpty) {
-      return false;
-    }
-    Node? insertedNode = root.childAtPath(
-      path.sublist(0, path.length - 1) + [max(0, path.last - 1)],
-    );
-    if (insertedNode == null) {
-      final insertedNode = root.childAtPath(
-        path.sublist(0, path.length - 1),
-      );
-      if (insertedNode != null) {
-        for (final node in nodes) {
-          insertedNode.insert(node);
-        }
-        return true;
-      }
-      return false;
-    }
-    if (path.last <= 0) {
-      for (var i = 0; i < nodes.length; i++) {
-        final node = nodes[i];
-        insertedNode.insertBefore(node);
-      }
-    } else {
-      for (var i = 0; i < nodes.length; i++) {
-        final node = nodes[i];
-        insertedNode!.insertAfter(node);
-        insertedNode = node;
-      }
-    }
-    return true;
-  }
-
-  bool textEdit(Path path, Delta delta) {
-    if (path.isEmpty) {
-      return false;
-    }
-    final node = root.childAtPath(path);
-    if (node == null || node is! TextNode) {
-      return false;
-    }
-    node.delta = node.delta.compose(delta);
-    return false;
-  }
-
-  delete(Path path, [int length = 1]) {
-    if (path.isEmpty) {
-      return null;
-    }
-    var deletedNode = root.childAtPath(path);
-    while (deletedNode != null && length > 0) {
-      final next = deletedNode.next;
-      deletedNode.unlink();
-      length--;
-      deletedNode = next;
-    }
-  }
-
-  bool update(Path path, Attributes attributes) {
-    if (path.isEmpty) {
-      return false;
-    }
-    final updatedNode = root.childAtPath(path);
-    if (updatedNode == null) {
-      return false;
-    }
-    updatedNode.updateAttributes(attributes);
-    return true;
-  }
-}

+ 4 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/service/service.dart';
 import 'package:flutter/material.dart';
 
 import 'package:appflowy_editor/src/core/location/selection.dart';
-import 'package:appflowy_editor/src/document/state_tree.dart';
+import 'package:appflowy_editor/src/core/state/document.dart';
 import 'package:appflowy_editor/src/operation/operation.dart';
 import 'package:appflowy_editor/src/operation/transaction.dart';
 import 'package:appflowy_editor/src/undo_manager.dart';
@@ -46,7 +46,7 @@ enum CursorUpdateReason {
 ///
 /// Mutating the document with document's API is not recommended.
 class EditorState {
-  final StateTree document;
+  final Document document;
 
   // Service reference.
   final service = FlowyService();
@@ -105,7 +105,7 @@ class EditorState {
   }
 
   factory EditorState.empty() {
-    return EditorState(document: StateTree.empty());
+    return EditorState(document: Document.empty());
   }
 
   /// Apply the transaction to the state.
@@ -167,7 +167,7 @@ class EditorState {
     } else if (op is DeleteOperation) {
       document.delete(op.path, op.nodes.length);
     } else if (op is TextEditOperation) {
-      document.textEdit(op.path, op.delta);
+      document.updateText(op.path, op.delta);
     }
     _observer.add(op);
   }

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -50,7 +50,7 @@ void _handleCopy(EditorState editorState) async {
   final endNode = editorState.document.nodeAtPath(selection.end.path)!;
 
   final nodes = NodeIterator(
-    stateTree: editorState.document,
+    document: editorState.document,
     startNode: beginNode,
     endNode: endNode,
   ).toList();
@@ -321,7 +321,7 @@ void _deleteSelectedContent(EditorState editorState) {
     return;
   }
   final traverser = NodeIterator(
-    stateTree: editorState.document,
+    document: editorState.document,
     startNode: beginNode,
     endNode: endNode,
   );

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -180,7 +180,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
     final endNode = editorState.document.nodeAtPath(end);
     if (startNode != null && endNode != null) {
       final nodes = NodeIterator(
-        stateTree: editorState.document,
+        document: editorState.document,
         startNode: startNode,
         endNode: endNode,
       ).toList();

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/test/core/document/node_iterator_test.dart

@@ -14,7 +14,7 @@ void main() async {
         root.insert(node);
       }
       final nodes = NodeIterator(
-        stateTree: StateTree(root: root),
+        document: Document(root: root),
         startNode: root.childAtPath([0])!,
         endNode: root.childAtPath([10, 10]),
       );

+ 77 - 0
frontend/app_flowy/packages/appflowy_editor/test/core/state/document_test.dart

@@ -0,0 +1,77 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  group('documemnt.dart', () {
+    test('insert', () {
+      final document = Document.empty();
+
+      expect(document.insert([-1], []), false);
+      expect(document.insert([100], []), false);
+
+      final node0 = Node(type: '0');
+      final node1 = Node(type: '1');
+      expect(document.insert([0], [node0, node1]), true);
+      expect(document.nodeAtPath([0])?.type, '0');
+      expect(document.nodeAtPath([1])?.type, '1');
+    });
+
+    test('delete', () {
+      final document = Document(root: Node(type: 'root'));
+
+      expect(document.delete([-1], 1), false);
+      expect(document.delete([100], 1), false);
+
+      for (var i = 0; i < 10; i++) {
+        final node = Node(type: '$i');
+        document.insert([i], [node]);
+      }
+
+      document.delete([0], 10);
+      expect(document.root.children.isEmpty, true);
+    });
+
+    test('update', () {
+      final node = Node(type: 'example', attributes: {'a': 'a'});
+      final document = Document(root: Node(type: 'root'));
+      document.insert([0], [node]);
+
+      final attributes = {
+        'a': 'b',
+        'b': 'c',
+      };
+
+      expect(document.update([0], attributes), true);
+      expect(document.nodeAtPath([0])?.attributes, attributes);
+
+      expect(document.update([-1], attributes), false);
+    });
+
+    test('updateText', () {
+      final delta = Delta()..insert('Editor');
+      final textNode = TextNode(delta: delta);
+      final document = Document(root: Node(type: 'root'));
+      document.insert([0], [textNode]);
+      document.updateText([0], Delta()..insert('AppFlowy'));
+      expect((document.nodeAtPath([0]) as TextNode).toPlainText(),
+          'AppFlowyEditor');
+    });
+
+    test('serialize', () {
+      final json = {
+        'document': {
+          'type': 'editor',
+          'children': [
+            {
+              'type': 'text',
+              'delta': [],
+            }
+          ],
+          'attributes': {'a': 'a'}
+        }
+      };
+      final document = Document.fromJson(json);
+      expect(document.toJson(), json);
+    });
+  });
+}

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart

@@ -19,7 +19,7 @@ class EditorWidgetTester {
   EditorState get editorState => _editorState;
   Node get root => _editorState.document.root;
 
-  StateTree get document => _editorState.document;
+  Document get document => _editorState.document;
   int get documentLength => _editorState.document.root.children.length;
   Selection? get documentSelection =>
       _editorState.service.selectionService.currentSelection.value;
@@ -155,7 +155,7 @@ class EditorWidgetTester {
 
   EditorState _createEmptyDocument() {
     return EditorState(
-      document: StateTree(
+      document: Document(
         root: _createEmptyEditorRoot(),
       ),
     )..disableSealTimer = true;

+ 16 - 16
frontend/app_flowy/packages/appflowy_editor/test/legacy/flowy_editor_test.dart

@@ -9,16 +9,16 @@ void main() {
   test('create state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
-    // expect(stateTree.root.type, 'root');
-    // expect(stateTree.root.toJson(), data['document']);
+    // final document = StateTree.fromJson(data);
+    // expect(document.root.type, 'root');
+    // expect(document.root.toJson(), data['document']);
   });
 
   test('search node by Path in state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
-    // final checkBoxNode = stateTree.root.childAtPath([1, 0]);
+    // final document = StateTree.fromJson(data);
+    // final checkBoxNode = document.root.childAtPath([1, 0]);
     // expect(checkBoxNode != null, true);
     // final textType = checkBoxNode!.attributes['text-type'];
     // expect(textType != null, true);
@@ -27,8 +27,8 @@ void main() {
   test('search node by Self in state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
-    // final checkBoxNode = stateTree.root.childAtPath([1, 0]);
+    // final document = StateTree.fromJson(data);
+    // final checkBoxNode = document.root.childAtPath([1, 0]);
     // expect(checkBoxNode != null, true);
     // final textType = checkBoxNode!.attributes['text-type'];
     // expect(textType != null, true);
@@ -39,21 +39,21 @@ void main() {
   test('insert node in state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
+    // final document = StateTree.fromJson(data);
     // final insertNode = Node.fromJson({
     //   'type': 'text',
     // });
-    // bool result = stateTree.insert([1, 1], [insertNode]);
+    // bool result = document.insert([1, 1], [insertNode]);
     // expect(result, true);
-    // expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true);
+    // expect(identical(insertNode, document.nodeAtPath([1, 1])), true);
   });
 
   test('delete node in state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
-    // stateTree.delete([1, 1], 1);
-    // final node = stateTree.nodeAtPath([1, 1]);
+    // final document = StateTree.fromJson(data);
+    // document.delete([1, 1], 1);
+    // final node = document.nodeAtPath([1, 1]);
     // expect(node != null, true);
     // expect(node!.attributes['tag'], '**');
   });
@@ -61,10 +61,10 @@ void main() {
   test('update node in state tree', () async {
     // final String response = await rootBundle.loadString('assets/document.json');
     // final data = Map<String, Object>.from(json.decode(response));
-    // final stateTree = StateTree.fromJson(data);
-    // final test = stateTree.update([1, 1], {'text-type': 'heading1'});
+    // final document = StateTree.fromJson(data);
+    // final test = document.update([1, 1], {'text-type': 'heading1'});
     // expect(test, true);
-    // final updatedNode = stateTree.nodeAtPath([1, 1]);
+    // final updatedNode = document.nodeAtPath([1, 1]);
     // expect(updatedNode != null, true);
     // expect(updatedNode!.attributes['text-type'], 'heading1');
   });

+ 4 - 4
frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart

@@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
 import 'package:appflowy_editor/src/operation/operation.dart';
 import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/document/state_tree.dart';
+import 'package:appflowy_editor/src/core/state/document.dart';
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -56,7 +56,7 @@ void main() {
             item2,
             item3,
           ]));
-    final state = EditorState(document: StateTree(root: root));
+    final state = EditorState(document: Document(root: root));
 
     expect(item1.path, [0]);
     expect(item2.path, [1]);
@@ -74,7 +74,7 @@ void main() {
   group("toJson", () {
     test("insert", () {
       final root = Node(type: "root", attributes: {}, children: LinkedList());
-      final state = EditorState(document: StateTree(root: root));
+      final state = EditorState(document: Document(root: root));
 
       final item1 = Node(type: "node", attributes: {}, children: LinkedList());
       final tb = TransactionBuilder(state);
@@ -100,7 +100,7 @@ void main() {
             ..addAll([
               item1,
             ]));
-      final state = EditorState(document: StateTree(root: root));
+      final state = EditorState(document: Document(root: root));
       final tb = TransactionBuilder(state);
       tb.deleteNode(item1);
       final transaction = tb.finish();

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart

@@ -17,7 +17,7 @@ void main() async {
   }
 
   test("HistoryItem #1", () {
-    final document = StateTree(root: _createEmptyEditorRoot());
+    final document = Document(root: _createEmptyEditorRoot());
     final editorState = EditorState(document: document);
 
     final historyItem = HistoryItem();
@@ -35,7 +35,7 @@ void main() async {
   });
 
   test("HistoryItem #2", () {
-    final document = StateTree(root: _createEmptyEditorRoot());
+    final document = Document(root: _createEmptyEditorRoot());
     final editorState = EditorState(document: document);
 
     final historyItem = HistoryItem();