Browse Source

Merge branch 'AppFlowy-IO:feat/flowy_editor' into feat/flowy_editor

Lucas.Xu 2 years ago
parent
commit
4d6bd2b77e

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart

@@ -1,6 +1,7 @@
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
+import 'package:flowy_editor/document/attributes.dart';
 
 
 class ImageNodeBuilder extends NodeWidgetBuilder {
 class ImageNodeBuilder extends NodeWidgetBuilder {
   ImageNodeBuilder.create({
   ImageNodeBuilder.create({

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flowy_editor/flowy_editor.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
+import 'package:flowy_editor/document/attributes.dart';
 
 
 class TextNodeBuilder extends NodeWidgetBuilder {
 class TextNodeBuilder extends NodeWidgetBuilder {
   TextNodeBuilder.create({
   TextNodeBuilder.create({

+ 42 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart

@@ -0,0 +1,42 @@
+typedef Attributes = Map<String, dynamic>;
+
+int hashAttributes(Attributes attributes) {
+  return Object.hashAllUnordered(
+      attributes.entries.map((e) => Object.hash(e.key, e.value)));
+}
+
+Attributes invertAttributes(Attributes? attr, Attributes? base) {
+  attr ??= {};
+  base ??= {};
+  final Attributes baseInverted = base.keys.fold({}, (memo, key) {
+    if (base![key] != attr![key] && attr.containsKey(key)) {
+      memo[key] = base[key];
+    }
+    return memo;
+  });
+  return attr.keys.fold(baseInverted, (memo, key) {
+    if (attr![key] != base![key] && base.containsKey(key)) {
+      memo[key] = null;
+    }
+    return memo;
+  });
+}
+
+Attributes? composeAttributes(Attributes? a, Attributes? b) {
+  a ??= {};
+  b ??= {};
+  final Attributes attributes = {};
+  attributes.addAll(b);
+
+  for (final entry in a.entries) {
+    if (!b.containsKey(entry.key)) {
+      attributes[entry.key] = entry.value;
+    }
+  }
+
+  if (attributes.isEmpty) {
+    return null;
+  }
+
+  return attributes;
+}

+ 1 - 2
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -1,8 +1,7 @@
 import 'dart:collection';
 import 'dart:collection';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-
-typedef Attributes = Map<String, dynamic>;
+import './attributes.dart';
 
 
 class Node extends ChangeNotifier with LinkedListEntry<Node> {
 class Node extends ChangeNotifier with LinkedListEntry<Node> {
   Node? parent;
   Node? parent;

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart

@@ -1,5 +1,6 @@
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/path.dart';
+import './attributes.dart';
 
 
 class StateTree {
 class StateTree {
   final Node root;
   final Node root;

+ 29 - 29
frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart

@@ -3,7 +3,7 @@ import 'dart:math';
 
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import './node.dart';
+import './attributes.dart';
 
 
 // constant number: 2^53 - 1
 // constant number: 2^53 - 1
 const int _maxInt = 9007199254740991;
 const int _maxInt = 9007199254740991;
@@ -22,14 +22,6 @@ class TextOperation {
   }
   }
 }
 }
 
 
-int _hashAttributes(Attributes attributes) {
-  return Object.hashAllUnordered(
-    attributes.entries.map(
-      (e) => Object.hash(e.key, e.value),
-    ),
-  );
-}
-
 class TextInsert extends TextOperation {
 class TextInsert extends TextOperation {
   String content;
   String content;
   final Attributes? _attributes;
   final Attributes? _attributes;
@@ -60,7 +52,7 @@ class TextInsert extends TextOperation {
     final contentHash = content.hashCode;
     final contentHash = content.hashCode;
     final attrs = _attributes;
     final attrs = _attributes;
     return Object.hash(
     return Object.hash(
-        contentHash, attrs == null ? null : _hashAttributes(attrs));
+        contentHash, attrs == null ? null : hashAttributes(attrs));
   }
   }
 }
 }
 
 
@@ -104,7 +96,7 @@ class TextRetain extends TextOperation {
   @override
   @override
   int get hashCode {
   int get hashCode {
     final attrs = _attributes;
     final attrs = _attributes;
-    return Object.hash(_length, attrs == null ? null : _hashAttributes(attrs));
+    return Object.hash(_length, attrs == null ? null : hashAttributes(attrs));
   }
   }
 }
 }
 
 
@@ -344,7 +336,8 @@ class Delta {
         final length = min(thisIter.peekLength(), otherIter.peekLength());
         final length = min(thisIter.peekLength(), otherIter.peekLength());
         final thisOp = thisIter.next(length);
         final thisOp = thisIter.next(length);
         final otherOp = otherIter.next(length);
         final otherOp = otherIter.next(length);
-        final attributes = _composeMap(thisOp.attributes, otherOp.attributes);
+        final attributes =
+            composeAttributes(thisOp.attributes, otherOp.attributes);
         if (otherOp is TextRetain && otherOp.length > 0) {
         if (otherOp is TextRetain && otherOp.length > 0) {
           TextOperation? newOp;
           TextOperation? newOp;
           if (thisOp is TextRetain) {
           if (thisOp is TextRetain) {
@@ -404,23 +397,30 @@ class Delta {
   int get hashCode {
   int get hashCode {
     return hashList(operations);
     return hashList(operations);
   }
   }
-}
-
-Attributes? _composeMap(Attributes? a, Attributes? b) {
-  a ??= {};
-  b ??= {};
-  final Attributes attributes = {};
-  attributes.addAll(b);
-
-  for (final entry in a.entries) {
-    if (!b.containsKey(entry.key)) {
-      attributes[entry.key] = entry.value;
-    }
-  }
 
 
-  if (attributes.isEmpty) {
-    return null;
+  Delta invert(Delta base) {
+    final inverted = Delta();
+    operations.fold(0, (int previousValue, op) {
+      if (op is TextInsert) {
+        inverted.delete(op.length);
+      } else if (op is TextRetain && op.attributes == null) {
+        inverted.retain(op.length);
+        return previousValue + op.length;
+      } else if (op is TextDelete || op is TextRetain) {
+        final length = op.length;
+        final slice = base.slice(previousValue, previousValue + length);
+        for (final baseOp in slice.operations) {
+          if (op is TextDelete) {
+            inverted.add(baseOp);
+          } else if (op is TextRetain && op.attributes != null) {
+            inverted.retain(baseOp.length,
+                invertAttributes(op.attributes, baseOp.attributes));
+          }
+        }
+        return previousValue + length;
+      }
+      return previousValue;
+    });
+    return inverted.chop();
   }
   }
-
-  return attributes;
 }
 }

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,5 +1,6 @@
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/operation/operation.dart';
 import 'package:flowy_editor/operation/operation.dart';
+import 'package:flowy_editor/document/attributes.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
 import './document/state_tree.dart';
 import './document/state_tree.dart';

+ 17 - 0
frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart

@@ -1,5 +1,7 @@
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
+import 'package:flowy_editor/document/text_delta.dart';
+import 'package:flowy_editor/document/attributes.dart';
 
 
 abstract class Operation {
 abstract class Operation {
   Operation invert();
   Operation invert();
@@ -61,3 +63,18 @@ class DeleteOperation extends Operation {
     );
     );
   }
   }
 }
 }
+
+class TextEditOperation extends Operation {
+  final Path path;
+  final Delta delta;
+
+  TextEditOperation({
+    required this.path,
+    required this.delta,
+  });
+
+  @override
+  Operation invert() {
+    return TextEditOperation(path: path, delta: delta);
+  }
+}

+ 182 - 155
frontend/app_flowy/packages/flowy_editor/test/delta_test.dart

@@ -2,172 +2,199 @@ import 'package:flutter_test/flutter_test.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 
 
 void main() {
 void main() {
-  TestWidgetsFlutterBinding.ensureInitialized();
-  test('test delta', () {
-    final delta = Delta(<TextOperation>[
-      TextInsert('Gandalf', {
-        'bold': true,
-      }),
-      TextInsert(' the '),
-      TextInsert('Grey', {
-        'color': '#ccc',
-      })
-    ]);
+  group('compose', () {
+    test('test delta', () {
+      final delta = Delta(<TextOperation>[
+        TextInsert('Gandalf', {
+          'bold': true,
+        }),
+        TextInsert(' the '),
+        TextInsert('Grey', {
+          'color': '#ccc',
+        })
+      ]);
 
 
-    final death = Delta().retain(12).insert("White", {
-      'color': '#fff',
-    }).delete(4);
+      final death = Delta().retain(12).insert("White", {
+        'color': '#fff',
+      }).delete(4);
 
 
-    final restores = delta.compose(death);
-    expect(restores.operations, <TextOperation>[
-      TextInsert('Gandalf', {'bold': true}),
-      TextInsert(' the '),
-      TextInsert('White', {'color': '#fff'}),
-    ]);
-  });
-  test('compose()', () {
-    final a = Delta().insert('A');
-    final b = Delta().insert('B');
-    final expected = Delta().insert('B').insert('A');
-    expect(a.compose(b), expected);
-  });
-  test('insert + retain', () {
-    final a = Delta().insert('A');
-    final b = Delta().retain(1, {
-      'bold': true,
-      'color': 'red',
+      final restores = delta.compose(death);
+      expect(restores.operations, <TextOperation>[
+        TextInsert('Gandalf', {'bold': true}),
+        TextInsert(' the '),
+        TextInsert('White', {'color': '#fff'}),
+      ]);
     });
     });
-    final expected = Delta().insert('A', {
-      'bold': true,
-      'color': 'red',
+    test('compose()', () {
+      final a = Delta().insert('A');
+      final b = Delta().insert('B');
+      final expected = Delta().insert('B').insert('A');
+      expect(a.compose(b), expected);
     });
     });
-    expect(a.compose(b), expected);
-  });
-  test('insert + delete', () {
-    final a = Delta().insert('A');
-    final b = Delta().delete(1);
-    final expected = Delta();
-    expect(a.compose(b), expected);
-  });
-  test('delete + insert', () {
-    final a = Delta().delete(1);
-    final b = Delta().insert('B');
-    final expected = Delta().insert('B').delete(1);
-    expect(a.compose(b), expected);
-  });
-  test('delete + retain', () {
-    final a = Delta().delete(1);
-    final b = Delta().retain(1, {
-      'bold': true,
-      'color': 'red',
+    test('insert + retain', () {
+      final a = Delta().insert('A');
+      final b = Delta().retain(1, {
+        'bold': true,
+        'color': 'red',
+      });
+      final expected = Delta().insert('A', {
+        'bold': true,
+        'color': 'red',
+      });
+      expect(a.compose(b), expected);
     });
     });
-    final expected = Delta().delete(1).retain(1, {
-      'bold': true,
-      'color': 'red',
+    test('insert + delete', () {
+      final a = Delta().insert('A');
+      final b = Delta().delete(1);
+      final expected = Delta();
+      expect(a.compose(b), expected);
     });
     });
-    expect(a.compose(b), expected);
-  });
-  test('delete + delete', () {
-    final a = Delta().delete(1);
-    final b = Delta().delete(1);
-    final expected = Delta().delete(2);
-    expect(a.compose(b), expected);
-  });
-  test('retain + insert', () {
-    final a = Delta().retain(1, {'color': 'blue'});
-    final b = Delta().insert('B');
-    final expected = Delta().insert('B').retain(1, {
-      'color': 'blue',
+    test('delete + insert', () {
+      final a = Delta().delete(1);
+      final b = Delta().insert('B');
+      final expected = Delta().insert('B').delete(1);
+      expect(a.compose(b), expected);
     });
     });
-    expect(a.compose(b), expected);
-  });
-  test('retain + retain', () {
-    final a = Delta().retain(1, {
-      'color': 'blue',
+    test('delete + retain', () {
+      final a = Delta().delete(1);
+      final b = Delta().retain(1, {
+        'bold': true,
+        'color': 'red',
+      });
+      final expected = Delta().delete(1).retain(1, {
+        'bold': true,
+        'color': 'red',
+      });
+      expect(a.compose(b), expected);
     });
     });
-    final b = Delta().retain(1, {
-      'bold': true,
-      'color': 'red',
+    test('delete + delete', () {
+      final a = Delta().delete(1);
+      final b = Delta().delete(1);
+      final expected = Delta().delete(2);
+      expect(a.compose(b), expected);
     });
     });
-    final expected = Delta().retain(1, {
-      'bold': true,
-      'color': 'red',
+    test('retain + insert', () {
+      final a = Delta().retain(1, {'color': 'blue'});
+      final b = Delta().insert('B');
+      final expected = Delta().insert('B').retain(1, {
+        'color': 'blue',
+      });
+      expect(a.compose(b), expected);
     });
     });
-    expect(a.compose(b), expected);
-  });
-  test('retain + delete', () {
-    final a = Delta().retain(1, {
-      'color': 'blue',
+    test('retain + retain', () {
+      final a = Delta().retain(1, {
+        'color': 'blue',
+      });
+      final b = Delta().retain(1, {
+        'bold': true,
+        'color': 'red',
+      });
+      final expected = Delta().retain(1, {
+        'bold': true,
+        'color': 'red',
+      });
+      expect(a.compose(b), expected);
+    });
+    test('retain + delete', () {
+      final a = Delta().retain(1, {
+        'color': 'blue',
+      });
+      final b = Delta().delete(1);
+      final expected = Delta().delete(1);
+      expect(a.compose(b), expected);
+    });
+    test('insert in middle of text', () {
+      final a = Delta().insert('Hello');
+      final b = Delta().retain(3).insert('X');
+      final expected = Delta().insert('HelXlo');
+      expect(a.compose(b), expected);
+    });
+    test('insert and delete ordering', () {
+      final a = Delta().insert('Hello');
+      final b = Delta().insert('Hello');
+      final insertFirst = Delta().retain(3).insert('X').delete(1);
+      final deleteFirst = Delta().retain(3).delete(1).insert('X');
+      final expected = Delta().insert('HelXo');
+      expect(a.compose(insertFirst), expected);
+      expect(b.compose(deleteFirst), expected);
+    });
+    test('delete entire text', () {
+      final a = Delta().retain(4).insert('Hello');
+      final b = Delta().delete(9);
+      final expected = Delta().delete(4);
+      expect(a.compose(b), expected);
+    });
+    test('retain more than length of text', () {
+      final a = Delta().insert('Hello');
+      final b = Delta().retain(10);
+      final expected = Delta().insert('Hello');
+      expect(a.compose(b), expected);
+    });
+    test('retain start optimization', () {
+      final a = Delta()
+          .insert('A', {'bold': true})
+          .insert('B')
+          .insert('C', {'bold': true})
+          .delete(1);
+      final b = Delta().retain(3).insert('D');
+      final expected = Delta()
+          .insert('A', {'bold': true})
+          .insert('B')
+          .insert('C', {'bold': true})
+          .insert('D')
+          .delete(1);
+      expect(a.compose(b), expected);
+    });
+    test('retain end optimization', () {
+      final a = Delta()
+          .insert('A', {'bold': true})
+          .insert('B')
+          .insert('C', {'bold': true});
+      final b = Delta().delete(1);
+      final expected = Delta().insert('B').insert('C', {'bold': true});
+      expect(a.compose(b), expected);
+    });
+    test('retain end optimization join', () {
+      final a = Delta()
+          .insert('A', {'bold': true})
+          .insert('B')
+          .insert('C', {'bold': true})
+          .insert('D')
+          .insert('E', {'bold': true})
+          .insert('F');
+      final b = Delta().retain(1).delete(1);
+      final expected = Delta()
+          .insert('AC', {'bold': true})
+          .insert('D')
+          .insert('E', {'bold': true})
+          .insert('F');
+      expect(a.compose(b), expected);
     });
     });
-    final b = Delta().delete(1);
-    final expected = Delta().delete(1);
-    expect(a.compose(b), expected);
-  });
-  test('insert in middle of text', () {
-    final a = Delta().insert('Hello');
-    final b = Delta().retain(3).insert('X');
-    final expected = Delta().insert('HelXlo');
-    expect(a.compose(b), expected);
-  });
-  test('insert and delete ordering', () {
-    final a = Delta().insert('Hello');
-    final b = Delta().insert('Hello');
-    final insertFirst = Delta().retain(3).insert('X').delete(1);
-    final deleteFirst = Delta().retain(3).delete(1).insert('X');
-    final expected = Delta().insert('HelXo');
-    expect(a.compose(insertFirst), expected);
-    expect(b.compose(deleteFirst), expected);
-  });
-  test('delete entire text', () {
-    final a = Delta().retain(4).insert('Hello');
-    final b = Delta().delete(9);
-    final expected = Delta().delete(4);
-    expect(a.compose(b), expected);
-  });
-  test('retain more than length of text', () {
-    final a = Delta().insert('Hello');
-    final b = Delta().retain(10);
-    final expected = Delta().insert('Hello');
-    expect(a.compose(b), expected);
-  });
-  test('retain start optimization', () {
-    final a = Delta()
-        .insert('A', {'bold': true})
-        .insert('B')
-        .insert('C', {'bold': true})
-        .delete(1);
-    final b = Delta().retain(3).insert('D');
-    final expected = Delta()
-        .insert('A', {'bold': true})
-        .insert('B')
-        .insert('C', {'bold': true})
-        .insert('D')
-        .delete(1);
-    expect(a.compose(b), expected);
-  });
-  test('retain end optimization', () {
-    final a = Delta()
-        .insert('A', {'bold': true})
-        .insert('B')
-        .insert('C', {'bold': true});
-    final b = Delta().delete(1);
-    final expected = Delta().insert('B').insert('C', {'bold': true});
-    expect(a.compose(b), expected);
   });
   });
-  test('retain end optimization join', () {
-    final a = Delta()
-        .insert('A', {'bold': true})
-        .insert('B')
-        .insert('C', {'bold': true})
-        .insert('D')
-        .insert('E', {'bold': true})
-        .insert('F');
-    final b = Delta().retain(1).delete(1);
-    final expected = Delta()
-        .insert('AC', {'bold': true})
-        .insert('D')
-        .insert('E', {'bold': true})
-        .insert('F');
-    expect(a.compose(b), expected);
+  group('invert', () {
+    test('insert', () {
+      final delta = Delta().retain(2).insert('A');
+      final base = Delta().insert('12346');
+      final expected = Delta().retain(2).delete(1);
+      final inverted = delta.invert(base);
+      expect(expected, inverted);
+      expect(base.compose(delta).compose(inverted), base);
+    });
+    test('delete', () {
+      final delta = Delta().retain(2).delete(3);
+      final base = Delta().insert('123456');
+      final expected = Delta().retain(2).insert('345');
+      final inverted = delta.invert(base);
+      expect(expected, inverted);
+      expect(base.compose(delta).compose(inverted), base);
+    });
+    // test('retain', () {
+    //   final delta = Delta().retain(2).retain(3, {'bold': true});
+    //   final base = Delta().insert('123456');
+    //   final expected = Delta().retain(2).retain(3, {'bold': null});
+    //   final inverted = delta.invert(base);
+    //   expect(expected, inverted);
+    //   expect(base.compose(delta).compose(inverted), base);
+    // });
   });
   });
 }
 }