From 737e374a5491c9f83cdd1de278e42224afda8c5d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 16:44:37 +0800 Subject: [PATCH] refactor: text-delta --- .../flowy_editor/lib/src/document/node.dart | 4 +- .../lib/src/document/text_delta.dart | 61 +++++++++++-------- .../src/extensions/text_node_extensions.dart | 2 +- .../lib/src/infra/html_converter.dart | 6 +- .../src/operation/transaction_builder.dart | 11 ++-- .../src/render/rich_text/flowy_rich_text.dart | 2 +- .../copy_paste_handler.dart | 5 +- .../flowy_editor/test/delta_test.dart | 2 +- 8 files changed, 47 insertions(+), 46 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 0b6b941aaa..8ed5d10297 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -219,7 +219,5 @@ class TextNode extends Node { delta: delta ?? this.delta, ); - // TODO: It's unneccesry to compute everytime. - String toRawString() => - _delta.operations.whereType().map((op) => op.content).join(); + String toRawString() => _delta.toRawString(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 72378f80c6..5c81a039ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -257,8 +257,8 @@ TextOperation? _textOperationFromJson(Map json) { } // basically copy from: https://github.com/quilljs/delta -class Delta { - final List operations; +class Delta extends Iterable { + final List _operations; factory Delta.fromJson(List list) { final operations = []; @@ -273,9 +273,9 @@ class Delta { return Delta(operations); } - Delta([List? ops]) : operations = ops ?? []; + Delta([List? ops]) : _operations = ops ?? []; - Delta addAll(List textOps) { + Delta addAll(Iterable textOps) { textOps.forEach(add); return this; } @@ -285,8 +285,8 @@ class Delta { return this; } - if (operations.isNotEmpty) { - final lastOp = operations.last; + if (_operations.isNotEmpty) { + final lastOp = _operations.last; if (lastOp is TextDelete && textOp is TextDelete) { lastOp.length += textOp.length; return this; @@ -299,9 +299,9 @@ class Delta { // if there is an delete before the insert // swap the order if (lastOp is TextDelete && textOp is TextInsert) { - operations.removeLast(); - operations.add(textOp); - operations.add(lastOp); + _operations.removeLast(); + _operations.add(textOp); + _operations.add(lastOp); return this; } if (lastOp is TextRetain && textOp is TextRetain) { @@ -311,13 +311,13 @@ class Delta { } } - operations.add(textOp); + _operations.add(textOp); return this; } Delta slice(int start, [int? end]) { final result = Delta(); - final iterator = _OpIterator(operations); + final iterator = _OpIterator(_operations); int index = 0; while ((end == null || index < end) && iterator.hasNext) { @@ -351,13 +351,13 @@ class Delta { } int get length { - return operations.fold( + return _operations.fold( 0, (previousValue, element) => previousValue + element.length); } Delta compose(Delta other) { - final thisIter = _OpIterator(operations); - final otherIter = _OpIterator(other.operations); + final thisIter = _OpIterator(_operations); + final otherIter = _OpIterator(other._operations); final ops = []; final firstOther = otherIter.peek(); @@ -405,7 +405,7 @@ class Delta { // Optimization if rest of other is just retain if (!otherIter.hasNext && - delta.operations[delta.operations.length - 1] == newOp) { + delta._operations[delta._operations.length - 1] == newOp) { final rest = Delta(thisIter.rest()); return delta.concat(rest).chop(); } @@ -419,21 +419,21 @@ class Delta { } Delta concat(Delta other) { - var ops = [...operations]; - if (other.operations.isNotEmpty) { - ops.add(other.operations[0]); - ops.addAll(other.operations.sublist(1)); + var ops = [..._operations]; + if (other._operations.isNotEmpty) { + ops.add(other._operations[0]); + ops.addAll(other._operations.sublist(1)); } return Delta(ops); } Delta chop() { - if (operations.isEmpty) { + if (_operations.isEmpty) { return this; } - final lastOp = operations.last; + final lastOp = _operations.last; if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { - operations.removeLast(); + _operations.removeLast(); } return this; } @@ -443,17 +443,17 @@ class Delta { if (other is! Delta) { return false; } - return listEquals(operations, other.operations); + return listEquals(_operations, other._operations); } @override int get hashCode { - return hashList(operations); + return hashList(_operations); } Delta invert(Delta base) { final inverted = Delta(); - operations.fold(0, (int previousValue, op) { + _operations.fold(0, (int previousValue, op) { if (op is TextInsert) { inverted.delete(op.length); } else if (op is TextRetain && op.attributes == null) { @@ -462,7 +462,7 @@ class Delta { } 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) { + for (final baseOp in slice._operations) { if (op is TextDelete) { inverted.add(baseOp); } else if (op is TextRetain && op.attributes != null) { @@ -478,6 +478,13 @@ class Delta { } List toJson() { - return operations.map((e) => e.toJson()).toList(); + return _operations.map((e) => e.toJson()).toList(); } + + // TODO: It's unneccesry to compute everytime. + String toRawString() => + _operations.whereType().map((op) => op.content).join(); + + @override + Iterator get iterator => _operations.iterator; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart index 7b068f18d7..131255ab63 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart @@ -19,7 +19,7 @@ extension TextNodeExtension on TextNode { allSatisfyInSelection(StyleKey.strikethrough, selection); bool allSatisfyInSelection(String styleKey, Selection selection) { - final ops = delta.operations.whereType(); + final ops = delta.whereType(); var start = 0; for (final op in ops) { if (start >= selection.end.offset) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index eb4eb0f653..e16237c0fa 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -71,7 +71,7 @@ class HTMLToNodesConverter { delta.insert(child.text ?? ""); } } - if (delta.operations.isNotEmpty) { + if (delta.isNotEmpty) { result.add(TextNode(type: "text", delta: delta)); } return result; @@ -101,7 +101,7 @@ class HTMLToNodesConverter { } else { final delta = Delta(); delta.insert(element.text); - if (delta.operations.isNotEmpty) { + if (delta.isNotEmpty) { return [TextNode(type: "text", delta: delta)]; } } @@ -446,7 +446,7 @@ class NodesToHTMLConverter { childNodes.add(node); } - for (final op in delta.operations) { + for (final op in delta) { if (op is TextInsert) { final attributes = op.attributes; if (attributes != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 4c080f06e2..918f68118d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -94,7 +94,7 @@ class TransactionBuilder { () => Delta() ..retain(firstOffset ?? firstLength) ..delete(firstLength - (firstOffset ?? firstLength)) - ..addAll(secondNode.delta.slice(secondOffset, secondLength).operations), + ..addAll(secondNode.delta.slice(secondOffset, secondLength)), ); afterSelection = Selection.collapsed( Position( @@ -108,11 +108,8 @@ class TransactionBuilder { [Attributes? attributes]) { var newAttributes = attributes; if (index != 0 && attributes == null) { - newAttributes = node.delta - .slice(max(index - 1, 0), index) - .operations - .first - .attributes; + newAttributes = + node.delta.slice(max(index - 1, 0), index).first.attributes; } textEdit( node, @@ -140,7 +137,7 @@ class TransactionBuilder { [Attributes? attributes]) { var newAttributes = attributes; if (attributes == null) { - final ops = node.delta.slice(index, index + length).operations; + final ops = node.delta.slice(index, index + length); if (ops.isNotEmpty) { newAttributes = ops.first.attributes; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 350a5c71e6..15c40a1121 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -198,7 +198,7 @@ class _FlowyRichTextState extends State with Selectable { } TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta.operations + children: widget.textNode.delta .whereType() .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 948e5233fc..debce245c2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -175,8 +175,7 @@ _handlePastePlainText(EditorState editorState, String plainText) { final nodes = remains.map((e) { if (index++ == remains.length - 1) { return TextNode( - type: "text", - delta: Delta().insert(e).addAll(insertedLineSuffix.operations)); + type: "text", delta: Delta().insert(e).addAll(insertedLineSuffix)); } return TextNode(type: "text", delta: Delta().insert(e)); }).toList(); @@ -246,7 +245,7 @@ _deleteSelectedContent(EditorState editorState) { if (endNode is TextNode) { final remain = endNode.delta.slice(selection.end.offset); - delta.addAll(remain.operations); + delta.addAll(remain); } return delta; diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 89001e79cf..c62f600266 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -19,7 +19,7 @@ void main() { }).delete(4); final restores = delta.compose(death); - expect(restores.operations, [ + expect(restores.toList(), [ TextInsert('Gandalf', {'bold': true}), TextInsert(' the '), TextInsert('White', {'color': '#fff'}),