فهرست منبع

feat: operation transforming

Vincent Chan 2 سال پیش
والد
کامیت
c72fead19c

+ 74 - 11
frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart

@@ -1,21 +1,27 @@
-import 'package:flowy_editor/document/path.dart';
-import 'package:flowy_editor/document/node.dart';
-import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/document/attributes.dart';
+import 'package:flowy_editor/flowy_editor.dart';
 
 abstract class Operation {
+  final Path path;
+  Operation({required this.path});
+  Operation copyWithPath(Path path);
   Operation invert();
 }
 
 class InsertOperation extends Operation {
-  final Path path;
   final Node value;
 
   InsertOperation({
-    required this.path,
+    required super.path,
     required this.value,
   });
 
+  InsertOperation copyWith({Path? path, Node? value}) =>
+      InsertOperation(path: path ?? this.path, value: value ?? this.value);
+
+  @override
+  Operation copyWithPath(Path path) => copyWith(path: path);
+
   @override
   Operation invert() {
     return DeleteOperation(
@@ -26,16 +32,25 @@ class InsertOperation extends Operation {
 }
 
 class UpdateOperation extends Operation {
-  final Path path;
   final Attributes attributes;
   final Attributes oldAttributes;
 
   UpdateOperation({
-    required this.path,
+    required super.path,
     required this.attributes,
     required this.oldAttributes,
   });
 
+  UpdateOperation copyWith(
+          {Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
+      UpdateOperation(
+          path: path ?? this.path,
+          attributes: attributes ?? this.attributes,
+          oldAttributes: oldAttributes ?? this.oldAttributes);
+
+  @override
+  Operation copyWithPath(Path path) => copyWith(path: path);
+
   @override
   Operation invert() {
     return UpdateOperation(
@@ -47,14 +62,19 @@ class UpdateOperation extends Operation {
 }
 
 class DeleteOperation extends Operation {
-  final Path path;
   final Node removedValue;
 
   DeleteOperation({
-    required this.path,
+    required super.path,
     required this.removedValue,
   });
 
+  DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
+      path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
+
+  @override
+  Operation copyWithPath(Path path) => copyWith(path: path);
+
   @override
   Operation invert() {
     return InsertOperation(
@@ -65,18 +85,61 @@ class DeleteOperation extends Operation {
 }
 
 class TextEditOperation extends Operation {
-  final Path path;
   final Delta delta;
   final Delta inverted;
 
   TextEditOperation({
-    required this.path,
+    required super.path,
     required this.delta,
     required this.inverted,
   });
 
+  TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
+      TextEditOperation(
+          path: path ?? this.path,
+          delta: delta ?? this.delta,
+          inverted: inverted ?? this.inverted);
+
+  @override
+  Operation copyWithPath(Path path) => copyWith(path: path);
+
   @override
   Operation invert() {
     return TextEditOperation(path: path, delta: inverted, inverted: delta);
   }
 }
+
+Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
+  if (preInsertPath.length > b.length) {
+    return b;
+  }
+  if (preInsertPath.isEmpty || b.isEmpty) {
+    return b;
+  }
+  // check the prefix
+  for (var i = 0; i < preInsertPath.length - 1; i++) {
+    if (preInsertPath[i] != b[i]) {
+      return b;
+    }
+  }
+  final prefix = preInsertPath.sublist(0, preInsertPath.length - 1);
+  final suffix = b.sublist(preInsertPath.length);
+  final preInsertLast = preInsertPath.last;
+  final bAtIndex = b[preInsertPath.length - 1];
+  if (preInsertLast <= bAtIndex) {
+    prefix.add(bAtIndex + delta);
+  }
+  prefix.addAll(suffix);
+  return prefix;
+}
+
+Operation transformOperation(Operation a, Operation b) {
+  if (a is InsertOperation) {
+    final newPath = transformPath(a.path, b.path);
+    return b.copyWithPath(newPath);
+  } else if (b is DeleteOperation) {
+    final newPath = transformPath(a.path, b.path, -1);
+    return b.copyWithPath(newPath);
+  }
+  return b;
+}

+ 49 - 0
frontend/app_flowy/packages/flowy_editor/test/operation_test.dart

@@ -0,0 +1,49 @@
+import 'dart:collection';
+
+import 'package:flowy_editor/document/node.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flowy_editor/operation/operation.dart';
+
+void main() {
+  group('transform path', () {
+    test('transform path changed', () {
+      expect(transformPath([0, 1], [0, 1]), [0, 2]);
+      expect(transformPath([0, 1], [0, 2]), [0, 3]);
+      expect(transformPath([0, 1], [0, 2, 7, 8, 9]), [0, 3, 7, 8, 9]);
+      expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
+    });
+    test("transform path not changed", () {
+      expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
+      expect(transformPath([0, 1, 2], [0, 1]), [0, 1]);
+    });
+    test("transform path delta", () {
+      expect(transformPath([0, 1], [0, 1], 5), [0, 6]);
+    });
+  });
+  group('transform operation', () {
+    test('insert + insert', () {
+      final t = transformOperation(
+          InsertOperation(path: [
+            0,
+            1
+          ], value: Node(type: "node", attributes: {}, children: LinkedList())),
+          InsertOperation(
+              path: [0, 1],
+              value:
+                  Node(type: "node", attributes: {}, children: LinkedList())));
+      expect(t.path, [0, 2]);
+    });
+    test('delete + delete', () {
+      final t = transformOperation(
+          DeleteOperation(
+              path: [0, 1],
+              removedValue:
+                  Node(type: "node", attributes: {}, children: LinkedList())),
+          DeleteOperation(
+              path: [0, 2],
+              removedValue:
+                  Node(type: "node", attributes: {}, children: LinkedList())));
+      expect(t.path, [0, 1]);
+    });
+  });
+}