diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 66ed5e3779..1af9c7ee7e 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/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:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:flowy_editor/document/attributes.dart'; class TextNodeBuilder extends NodeWidgetBuilder { TextNodeBuilder.create({ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart new file mode 100644 index 0000000000..ea7106ab0a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart @@ -0,0 +1,23 @@ +typedef Attributes = Map; + +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; + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 9eccdb7897..84493de0f9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,8 +1,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; import 'package:flutter/material.dart'; - -typedef Attributes = Map; +import './attributes.dart'; class Node extends ChangeNotifier with LinkedListEntry { Node? parent; 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 af343f54a0..6fe58d2886 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 @@ -1,5 +1,6 @@ import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; +import './attributes.dart'; class StateTree { final Node root; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart index a52a96426a..2d3f8301f5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/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/material.dart'; -import './node.dart'; +import './attributes.dart'; // constant number: 2^53 - 1 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 { String content; final Attributes? _attributes; @@ -60,7 +52,7 @@ class TextInsert extends TextOperation { final contentHash = content.hashCode; final attrs = _attributes; return Object.hash( - contentHash, attrs == null ? null : _hashAttributes(attrs)); + contentHash, attrs == null ? null : hashAttributes(attrs)); } } @@ -104,7 +96,7 @@ class TextRetain extends TextOperation { @override int get hashCode { final attrs = _attributes; - return Object.hash(_length, attrs == null ? null : _hashAttributes(attrs)); + return Object.hash(_length, attrs == null ? null : hashAttributes(attrs)); } } @@ -404,6 +396,32 @@ class Delta { int get hashCode { return hashList(operations); } + + 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(); + } } Attributes? _composeMap(Attributes? a, Attributes? b) { 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 4ca7b9ca83..5fb9a523a8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/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/node.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/document/attributes.dart'; abstract class Operation { 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); + } +}