From 565617d1f0bdd5eaae8a2b08271e0ed4e05c20f9 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 6 Sep 2022 14:48:04 +0800 Subject: [PATCH 01/16] feat: recognize number --- ...er_without_shift_in_text_node_handler.dart | 59 ++++++++++++++----- .../whitespace_handler.dart | 37 ++++++++++++ 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 8f12dbb9e6..bef42cc521 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -1,14 +1,9 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/document/position.dart'; -import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/extensions/path_extensions.dart'; -import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; /// Handle some cases where enter is pressed and shift is not pressed. /// @@ -104,19 +99,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // Otherwise, // split the node into two nodes with style - final needCopyAttributes = StyleKey.globalStyleKeys - .where((key) => key != StyleKey.heading) - .contains(textNode.subtype); - Attributes attributes = {}; - if (needCopyAttributes) { - attributes = Attributes.from(textNode.attributes); - if (attributes.check) { - attributes[StyleKey.checkbox] = false; - } - } + Attributes attributes = _attributesFromPreviousLine(textNode); + final afterSelection = Selection.collapsed( Position(path: textNode.path.next, offset: 0), ); + TransactionBuilder(editorState) ..insertNode( textNode.path.next, @@ -132,5 +120,44 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = ) ..afterSelection = afterSelection ..commit(); + + // If the new type of a text node is number list, + // the numbers of the following nodes should be incremental. + if (textNode.subtype == StyleKey.numberList) { + _makeFollowingNodeIncremental(editorState, textNode); + } + return KeyEventResult.handled; }; + +Attributes _attributesFromPreviousLine(TextNode textNode) { + final prevAttributes = textNode.attributes; + final subType = textNode.subtype; + if (subType == null || subType == StyleKey.heading) { + return {}; + } + + final copy = Attributes.from(prevAttributes); + if (subType == StyleKey.numberList) { + return _nextNumberAttributesFromPreviousLine(copy, textNode); + } + + if (subType == StyleKey.checkbox) { + copy[StyleKey.checkbox] = false; + return copy; + } + + return copy; +} + +Attributes _nextNumberAttributesFromPreviousLine( + Attributes copy, TextNode textNode) { + final prevNum = textNode.attributes[StyleKey.number] as int?; + copy[StyleKey.number] = prevNum == null ? 1 : prevNum + 1; + return copy; +} + +void _makeFollowingNodeIncremental(EditorState editorState, TextNode textNode) { + debugPrint("following nodes"); + TransactionBuilder(editorState).commit(); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index c6046ba6dd..b667f0c100 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -20,6 +20,8 @@ const _bulletedListSymbols = ['*', '-']; const _checkboxListSymbols = ['[x]', '-[x]']; const _unCheckboxListSymbols = ['[]', '-[]']; +final _numberRegex = RegExp(r'^(\d+)\.'); + ShortcutEventHandler whiteSpaceHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.space) { return KeyEventResult.ignored; @@ -42,6 +44,16 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) { final textNode = textNodes.first; final text = textNode.toRawString(); + + final numberMatch = _numberRegex.firstMatch(text); + if (numberMatch != null) { + final matchText = numberMatch.group(0); + final numText = numberMatch.group(1); + if (matchText != null && numText != null) { + return _toNumberList(editorState, textNode, matchText, numText); + } + } + if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) { return _toCheckboxList(editorState, textNode); } else if (_bulletedListSymbols.any(text.startsWith)) { @@ -53,6 +65,31 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) { return KeyEventResult.ignored; }; +KeyEventResult _toNumberList(EditorState editorState, TextNode textNode, + String matchText, String numText) { + if (textNode.subtype == StyleKey.bulletedList) { + return KeyEventResult.ignored; + } + + final numValue = int.tryParse(numText); + if (numValue == null) { + return KeyEventResult.ignored; + } + + TransactionBuilder(editorState) + ..deleteText(textNode, 0, matchText.length) + ..updateNode(textNode, + {StyleKey.subtype: StyleKey.numberList, StyleKey.number: numValue}) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + return KeyEventResult.handled; +} + KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) { if (textNode.subtype == StyleKey.bulletedList) { return KeyEventResult.ignored; From 69f04d09580e627a7298201dd2ab624ac7547fca Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 6 Sep 2022 16:45:25 +0800 Subject: [PATCH 02/16] feat: make following lines incremental --- ...er_without_shift_in_text_node_handler.dart | 11 ++---- .../number_list_helper.dart | 37 +++++++++++++++++++ .../whitespace_handler.dart | 18 ++++++--- 3 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index bef42cc521..15a1222942 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:appflowy_editor/src/extensions/path_extensions.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import './number_list_helper.dart'; /// Handle some cases where enter is pressed and shift is not pressed. /// @@ -101,8 +102,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // split the node into two nodes with style Attributes attributes = _attributesFromPreviousLine(textNode); + final nextPath = textNode.path.next; final afterSelection = Selection.collapsed( - Position(path: textNode.path.next, offset: 0), + Position(path: nextPath, offset: 0), ); TransactionBuilder(editorState) @@ -124,7 +126,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // If the new type of a text node is number list, // the numbers of the following nodes should be incremental. if (textNode.subtype == StyleKey.numberList) { - _makeFollowingNodeIncremental(editorState, textNode); + makeFollowingNodesIncremental(editorState, nextPath, afterSelection); } return KeyEventResult.handled; @@ -156,8 +158,3 @@ Attributes _nextNumberAttributesFromPreviousLine( copy[StyleKey.number] = prevNum == null ? 1 : prevNum + 1; return copy; } - -void _makeFollowingNodeIncremental(EditorState editorState, TextNode textNode) { - debugPrint("following nodes"); - TransactionBuilder(editorState).commit(); -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart new file mode 100644 index 0000000000..7c5d846a36 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart @@ -0,0 +1,37 @@ +import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/document/attributes.dart'; + +void makeFollowingNodesIncremental( + EditorState editorState, List insertPath, Selection afterSelection) { + final insertNode = editorState.document.nodeAtPath(insertPath); + if (insertNode == null) { + return; + } + final int beginNum = insertNode.attributes[StyleKey.number] as int; + + int numPtr = beginNum + 1; + var ptr = insertNode.next; + + final builder = TransactionBuilder(editorState); + + while (ptr != null) { + if (ptr.subtype != StyleKey.numberList) { + break; + } + final currentNum = ptr.attributes[StyleKey.number] as int; + if (currentNum != numPtr) { + Attributes updateAttributes = {}; + updateAttributes[StyleKey.number] = numPtr; + builder.updateNode(ptr, updateAttributes); + } + + ptr = ptr.next; + numPtr++; + } + + builder.afterSelection = afterSelection; + builder.commit(); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index b667f0c100..f9d1f4a5fa 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -8,6 +8,7 @@ import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import './number_list_helper.dart'; @visibleForTesting List get checkboxListSymbols => _checkboxListSymbols; @@ -76,17 +77,22 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode, return KeyEventResult.ignored; } + final afterSelection = Selection.collapsed(Position( + path: textNode.path, + offset: 0, + )); + + final insertPath = textNode.path; + TransactionBuilder(editorState) ..deleteText(textNode, 0, matchText.length) ..updateNode(textNode, {StyleKey.subtype: StyleKey.numberList, StyleKey.number: numValue}) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: 0, - ), - ) + ..afterSelection = afterSelection ..commit(); + + makeFollowingNodesIncremental(editorState, insertPath, afterSelection); + return KeyEventResult.handled; } From 2810097b956cebcfc9778e0ba60e18396cd52344 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 7 Sep 2022 14:21:16 +0800 Subject: [PATCH 03/16] feat: backspace --- .../backspace_handler.dart | 11 +++++++++++ .../enter_without_shift_in_text_node_handler.dart | 4 +++- .../number_list_helper.dart | 5 +++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index ee3f80f197..a8337543e7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -29,6 +30,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { nodes.where((node) => node is! TextNode).toList(growable: false); final transactionBuilder = TransactionBuilder(editorState); + List? cancelNumberListPath; if (nonTextNodes.isNotEmpty) { transactionBuilder.deleteNodes(nonTextNodes); @@ -40,6 +42,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (index < 0 && selection.isCollapsed) { // 1. style if (textNode.subtype != null) { + if (textNode.subtype == StyleKey.numberList) { + cancelNumberListPath = textNode.path; + } transactionBuilder ..updateNode(textNode, { StyleKey.subtype: null, @@ -100,6 +105,12 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { transactionBuilder.commit(); } + if (cancelNumberListPath != null) { + makeFollowingNodesIncremental( + editorState, cancelNumberListPath, Selection.collapsed(selection.start), + beginNum: 0); + } + return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 15a1222942..0a8ee552cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -69,7 +69,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // If selection is collapsed and position.start.offset == 0, // insert a empty text node before. - if (selection.isCollapsed && selection.start.offset == 0) { + if (selection.isCollapsed && + selection.start.offset == 0 && + textNode.subtype != StyleKey.numberList) { if (textNode.toRawString().isEmpty && textNode.subtype != null) { final afterSelection = Selection.collapsed( Position(path: textNode.path, offset: 0), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart index 7c5d846a36..4e726fc86e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart @@ -5,12 +5,13 @@ import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/document/attributes.dart'; void makeFollowingNodesIncremental( - EditorState editorState, List insertPath, Selection afterSelection) { + EditorState editorState, List insertPath, Selection afterSelection, + {int? beginNum}) { final insertNode = editorState.document.nodeAtPath(insertPath); if (insertNode == null) { return; } - final int beginNum = insertNode.attributes[StyleKey.number] as int; + beginNum ??= insertNode.attributes[StyleKey.number] as int; int numPtr = beginNum + 1; var ptr = insertNode.next; From 875c109b1406ad8453dda40cee1054113a1c502a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 7 Sep 2022 14:58:33 +0800 Subject: [PATCH 04/16] feat: check previous node's number --- .../whitespace_handler.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index f9d1f4a5fa..fb78fce1b3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -77,6 +77,22 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode, return KeyEventResult.ignored; } + // The user types number + . + space, he wants to turn + // this line into number list, but we should check if previous line + // is number list. + // + // Check whether the number input by the user is the successor of the previous + // line. If it's not, ignore it. + final prevNode = textNode.previous; + if (prevNode != null && + prevNode is TextNode && + prevNode.attributes[StyleKey.subtype] == StyleKey.numberList) { + final prevNumber = prevNode.attributes[StyleKey.number] as int; + if (numValue != prevNumber + 1) { + return KeyEventResult.ignored; + } + } + final afterSelection = Selection.collapsed(Position( path: textNode.path, offset: 0, From 1736fb794d2bff3af04fa9de971cae1d3d70f5b2 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 8 Sep 2022 15:49:58 +0800 Subject: [PATCH 05/16] fix: unit test on number list --- ...enter_without_shift_in_text_node_handler_test.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index 5bfe1ada67..7307f9cf2f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -176,8 +176,15 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { await editor.pressLogicKey( LogicalKeyboardKey.enter, ); - expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, null); + if (style == StyleKey.numberList) { + expect( + editor.documentSelection, Selection.single(path: [5], startOffset: 0)); + expect(editor.nodeAtPath([4])?.subtype, StyleKey.numberList); + } else { + expect( + editor.documentSelection, Selection.single(path: [4], startOffset: 0)); + expect(editor.nodeAtPath([4])?.subtype, null); + } } Future _testMultipleSelection( From e223eecf325284f353b1ac8697e9722f2187ddc8 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 8 Sep 2022 16:34:06 +0800 Subject: [PATCH 06/16] fix: selection after pasting nodes --- .../copy_paste_handler.dart | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 15a7081653..a1d88c26f7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -4,6 +4,33 @@ import 'package:appflowy_editor/src/document/node_iterator.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; +int _textLengthOfNode(Node node) { + if (node is TextNode) { + return node.delta.length; + } + + return 0; +} + +Selection _computeSelectionAfterPasteMultipleNodes( + EditorState editorState, List nodes) { + final currentSelection = editorState.cursorSelection!; + final currentCursor = currentSelection.start; + final currentNode = editorState.document.nodeAtPath(currentCursor.path)!; + final currentPath = [...currentCursor.path]; + if (currentNode is TextNode) { + currentPath[currentPath.length - 1] += nodes.length; + int lenOfLastNode = _textLengthOfNode(nodes.last); + return Selection.collapsed( + Position(path: currentPath, offset: lenOfLastNode)); + } else { + currentPath[currentPath.length - 1] += nodes.length; + int lenOfLastNode = _textLengthOfNode(nodes.last); + return Selection.collapsed( + Position(path: currentPath, offset: lenOfLastNode)); + } +} + _handleCopy(EditorState editorState) async { final selection = editorState.cursorSelection?.normalize; if (selection == null || selection.isCollapsed) { @@ -80,6 +107,9 @@ _pasteHTML(EditorState editorState, String html) { _pasteMultipleLinesInText( EditorState editorState, List path, int offset, List nodes) { + final afterSelection = + _computeSelectionAfterPasteMultipleNodes(editorState, nodes); + final tb = TransactionBuilder(editorState); final firstNode = nodes[0]; @@ -112,6 +142,7 @@ _pasteMultipleLinesInText( tailNodes.add(TextNode(type: "text", delta: remain)); } + tb.setAfterSelection(afterSelection); tb.insertNodes(path, tailNodes); tb.commit(); return; @@ -219,16 +250,21 @@ _handlePastePlainText(EditorState editorState, String plainText) { final insertedLineSuffix = node.delta.slice(beginOffset); path[path.length - 1]++; - var index = 0; final tb = TransactionBuilder(editorState); - final nodes = remains.map((e) { - if (index++ == remains.length - 1) { - return TextNode( - type: "text", - delta: _lineContentToDelta(e)..addAll(insertedLineSuffix)); - } - return TextNode(type: "text", delta: _lineContentToDelta(e)); - }).toList(); + final List nodes = remains + .map((e) => TextNode(type: "text", delta: _lineContentToDelta(e))) + .toList(); + + final afterSelection = + _computeSelectionAfterPasteMultipleNodes(editorState, nodes); + + // append remain text to the last line + if (nodes.isNotEmpty) { + final last = nodes.last; + nodes[nodes.length - 1] = + TextNode(type: "text", delta: last.delta..addAll(insertedLineSuffix)); + } + // insert first line tb.textEdit( node, @@ -238,11 +274,8 @@ _handlePastePlainText(EditorState editorState, String plainText) { ..delete(node.delta.length - beginOffset)); // insert remains tb.insertNodes(path, nodes); + tb.setAfterSelection(afterSelection); tb.commit(); - - // fixme: don't set the cursor manually - editorState.updateCursorSelection(Selection.collapsed( - Position(path: nodes.last.path, offset: lines.last.length))); } } From 880669c0e501e9cb90621bcfb2a770b60d385400 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 8 Sep 2022 16:39:53 +0800 Subject: [PATCH 07/16] feat: handle paste multi lines --- .../copy_paste_handler.dart | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index a1d88c26f7..c2e1e8e16d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -16,19 +16,11 @@ Selection _computeSelectionAfterPasteMultipleNodes( EditorState editorState, List nodes) { final currentSelection = editorState.cursorSelection!; final currentCursor = currentSelection.start; - final currentNode = editorState.document.nodeAtPath(currentCursor.path)!; final currentPath = [...currentCursor.path]; - if (currentNode is TextNode) { - currentPath[currentPath.length - 1] += nodes.length; - int lenOfLastNode = _textLengthOfNode(nodes.last); - return Selection.collapsed( - Position(path: currentPath, offset: lenOfLastNode)); - } else { - currentPath[currentPath.length - 1] += nodes.length; - int lenOfLastNode = _textLengthOfNode(nodes.last); - return Selection.collapsed( - Position(path: currentPath, offset: lenOfLastNode)); - } + currentPath[currentPath.length - 1] += nodes.length; + int lenOfLastNode = _textLengthOfNode(nodes.last); + return Selection.collapsed( + Position(path: currentPath, offset: lenOfLastNode)); } _handleCopy(EditorState editorState) async { @@ -105,11 +97,8 @@ _pasteHTML(EditorState editorState, String html) { _pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes); } -_pasteMultipleLinesInText( +void _pasteMultipleLinesInText( EditorState editorState, List path, int offset, List nodes) { - final afterSelection = - _computeSelectionAfterPasteMultipleNodes(editorState, nodes); - final tb = TransactionBuilder(editorState); final firstNode = nodes[0]; @@ -131,6 +120,10 @@ _pasteMultipleLinesInText( final tailNodes = nodes.sublist(1); path[path.length - 1]++; + + final afterSelection = + _computeSelectionAfterPasteMultipleNodes(editorState, tailNodes); + if (tailNodes.isNotEmpty) { if (tailNodes.last.type == "text") { final tailTextNode = tailNodes.last as TextNode; @@ -148,7 +141,11 @@ _pasteMultipleLinesInText( return; } + final afterSelection = + _computeSelectionAfterPasteMultipleNodes(editorState, nodes); + path[path.length - 1]++; + tb.setAfterSelection(afterSelection); tb.insertNodes(path, nodes); tb.commit(); } From c9b6e021670238801af41a59bc84d1713df8ae7b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 8 Sep 2022 17:58:17 +0800 Subject: [PATCH 08/16] feat: add return value for copy paste handler --- .../copy_paste_handler.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index c2e1e8e16d..5b3151ced5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -23,7 +23,7 @@ Selection _computeSelectionAfterPasteMultipleNodes( Position(path: currentPath, offset: lenOfLastNode)); } -_handleCopy(EditorState editorState) async { +void _handleCopy(EditorState editorState) async { final selection = editorState.cursorSelection?.normalize; if (selection == null || selection.isCollapsed) { return; @@ -59,7 +59,7 @@ _handleCopy(EditorState editorState) async { RichClipboard.setData(RichClipboardData(html: copyString)); } -_pasteHTML(EditorState editorState, String html) { +void _pasteHTML(EditorState editorState, String html) { final selection = editorState.cursorSelection?.normalize; if (selection == null) { return; @@ -150,7 +150,7 @@ void _pasteMultipleLinesInText( tb.commit(); } -_handlePaste(EditorState editorState) async { +void _handlePaste(EditorState editorState) async { final data = await RichClipboard.getData(); if (editorState.cursorSelection?.isCollapsed ?? false) { @@ -165,7 +165,7 @@ _handlePaste(EditorState editorState) async { }); } -_pastRichClipboard(EditorState editorState, RichClipboardData data) { +void _pastRichClipboard(EditorState editorState, RichClipboardData data) { if (data.html != null) { _pasteHTML(editorState, data.html!); return; @@ -176,7 +176,8 @@ _pastRichClipboard(EditorState editorState, RichClipboardData data) { } } -_pasteSingleLine(EditorState editorState, Selection selection, String line) { +void _pasteSingleLine( + EditorState editorState, Selection selection, String line) { final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode; final beginOffset = selection.end.offset; TransactionBuilder(editorState) @@ -216,7 +217,7 @@ Delta _lineContentToDelta(String lineContent) { return delta; } -_handlePastePlainText(EditorState editorState, String plainText) { +void _handlePastePlainText(EditorState editorState, String plainText) { final selection = editorState.cursorSelection?.normalize; if (selection == null) { return; @@ -278,12 +279,12 @@ _handlePastePlainText(EditorState editorState, String plainText) { /// 1. copy the selected content /// 2. delete selected content -_handleCut(EditorState editorState) { +void _handleCut(EditorState editorState) { _handleCopy(editorState); _deleteSelectedContent(editorState); } -_deleteSelectedContent(EditorState editorState) { +void _deleteSelectedContent(EditorState editorState) { final selection = editorState.cursorSelection?.normalize; if (selection == null || selection.isCollapsed) { return; From 002eac63f7febbf10311de1e4861441d1f3306e8 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 9 Sep 2022 14:24:57 +0800 Subject: [PATCH 09/16] feat: handle paste number list --- .../copy_paste_handler.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 5b3151ced5..df54753e86 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,6 +1,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/document/node_iterator.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; @@ -105,6 +107,11 @@ void _pasteMultipleLinesInText( final nodeAtPath = editorState.document.nodeAtPath(path)!; if (nodeAtPath.type == "text" && firstNode.type == "text") { + int? startNumber; + if (nodeAtPath.subtype == StyleKey.numberList) { + startNumber = nodeAtPath.attributes[StyleKey.number] as int; + } + // split and merge final textNodeAtPath = nodeAtPath as TextNode; final firstTextNode = firstNode as TextNode; @@ -119,6 +126,7 @@ void _pasteMultipleLinesInText( firstTextNode.delta); final tailNodes = nodes.sublist(1); + final originalPath = [...path]; path[path.length - 1]++; final afterSelection = @@ -138,6 +146,11 @@ void _pasteMultipleLinesInText( tb.setAfterSelection(afterSelection); tb.insertNodes(path, tailNodes); tb.commit(); + + if (startNumber != null) { + makeFollowingNodesIncremental(editorState, originalPath, afterSelection, + beginNum: startNumber); + } return; } From 0310b92723801a0d9269a874d97444c923ab9eea Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 9 Sep 2022 14:34:14 +0800 Subject: [PATCH 10/16] fix: dynamic width of the number --- .../lib/src/render/rich_text/number_list_text.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index 36d3d93ed9..36778c46b2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -47,8 +47,6 @@ class _NumberListTextNodeWidgetState extends State final iconKey = GlobalKey(); final _richTextKey = GlobalKey(debugLabel: 'number_list_text'); - final _iconWidth = 20.0; - final _iconRightPadding = 5.0; @override SelectableMixin get forward => @@ -61,12 +59,13 @@ class _NumberListTextNodeWidgetState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FlowySvg( + Padding( key: iconKey, - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - number: widget.textNode.attributes.number, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + child: Text( + '${widget.textNode.attributes.number.toString()}.', + style: const TextStyle(fontSize: 16), + ), ), Flexible( child: FlowyRichText( From 27757372607d86649f93864173a1028ba67b21cd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 9 Sep 2022 15:13:21 +0800 Subject: [PATCH 11/16] feat: refresh number list when delete multiple lines --- .../backspace_handler.dart | 25 ++++++++++++++++--- ...er_without_shift_in_text_node_handler.dart | 7 ++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index a8337543e7..1e252d9114 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -93,9 +93,19 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } } } else { - if (textNodes.isNotEmpty) { - _deleteTextNodes(transactionBuilder, textNodes, selection); + if (textNodes.isEmpty) { + return KeyEventResult.handled; } + final startPosition = selection.start; + final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; + _deleteTextNodes(transactionBuilder, textNodes, selection); + transactionBuilder.commit(); + + if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) { + makeFollowingNodesIncremental( + editorState, startPosition.path, transactionBuilder.afterSelection!); + } + return KeyEventResult.handled; } if (transactionBuilder.operations.isNotEmpty) { @@ -156,11 +166,18 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { ); } } + transactionBuilder.commit(); } else { + final startPosition = selection.start; + final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; _deleteTextNodes(transactionBuilder, textNodes, selection); - } + transactionBuilder.commit(); - transactionBuilder.commit(); + if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) { + makeFollowingNodesIncremental( + editorState, startPosition.path, transactionBuilder.afterSelection!); + } + } return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 0a8ee552cb..e6cb91d318 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -37,6 +37,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // Multiple selection if (!selection.isSingle) { + final startNode = editorState.document.nodeAtPath(selection.start.path)!; final length = textNodes.length; final List subTextNodes = length >= 3 ? textNodes.sublist(1, textNodes.length - 1) : []; @@ -57,6 +58,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = ) ..afterSelection = afterSelection ..commit(); + + if (startNode is TextNode && startNode.subtype == StyleKey.numberList) { + makeFollowingNodesIncremental( + editorState, selection.start.path, afterSelection); + } + return KeyEventResult.handled; } From 4402f4dec9d489d26b0c326305b76b17e810ce69 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 9 Sep 2022 15:43:56 +0800 Subject: [PATCH 12/16] feat: use const num --- .../lib/src/render/rich_text/number_list_text.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index 36778c46b2..2085b3a48a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -40,6 +40,7 @@ class NumberListTextNodeWidget extends StatefulWidget { } // customize +const double _numberHorizontalPadding = 8; class _NumberListTextNodeWidgetState extends State with SelectableMixin, DefaultSelectable { @@ -61,7 +62,8 @@ class _NumberListTextNodeWidgetState extends State children: [ Padding( key: iconKey, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + padding: const EdgeInsets.symmetric( + horizontal: _numberHorizontalPadding, vertical: 0), child: Text( '${widget.textNode.attributes.number.toString()}.', style: const TextStyle(fontSize: 16), From 06bd6064ac37244b956d3223d44f68847c276ee3 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 13 Sep 2022 11:26:52 +0800 Subject: [PATCH 13/16] fix: cancel number list style after enter in empty line --- ...er_without_shift_in_text_node_handler.dart | 45 ++++++++++++++----- ...thout_shift_in_text_node_handler_test.dart | 11 +---- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index e6cb91d318..4ed21f21f8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -76,9 +76,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = // If selection is collapsed and position.start.offset == 0, // insert a empty text node before. - if (selection.isCollapsed && - selection.start.offset == 0 && - textNode.subtype != StyleKey.numberList) { + if (selection.isCollapsed && selection.start.offset == 0) { if (textNode.toRawString().isEmpty && textNode.subtype != null) { final afterSelection = Selection.collapsed( Position(path: textNode.path, offset: 0), @@ -92,17 +90,44 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = )) ..afterSelection = afterSelection ..commit(); + + final nextNode = textNode.next; + if (nextNode is TextNode && nextNode.subtype == StyleKey.numberList) { + makeFollowingNodesIncremental( + editorState, textNode.path, afterSelection, + beginNum: 0); + } } else { + final subtype = textNode.subtype; final afterSelection = Selection.collapsed( Position(path: textNode.path.next, offset: 0), ); - TransactionBuilder(editorState) - ..insertNode( - textNode.path, - TextNode.empty(), - ) - ..afterSelection = afterSelection - ..commit(); + + if (subtype == StyleKey.numberList) { + final prevNumber = textNode.attributes[StyleKey.number] as int; + final newNode = TextNode.empty(); + newNode.attributes[StyleKey.subtype] = StyleKey.numberList; + newNode.attributes[StyleKey.number] = prevNumber; + final insertPath = textNode.path; + TransactionBuilder(editorState) + ..insertNode( + insertPath, + newNode, + ) + ..afterSelection = afterSelection + ..commit(); + + makeFollowingNodesIncremental(editorState, insertPath, afterSelection, + beginNum: prevNumber); + } else { + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + TextNode.empty(), + ) + ..afterSelection = afterSelection + ..commit(); + } } return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index 7307f9cf2f..5bfe1ada67 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -176,15 +176,8 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { await editor.pressLogicKey( LogicalKeyboardKey.enter, ); - if (style == StyleKey.numberList) { - expect( - editor.documentSelection, Selection.single(path: [5], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, StyleKey.numberList); - } else { - expect( - editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, null); - } + expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); + expect(editor.nodeAtPath([4])?.subtype, null); } Future _testMultipleSelection( From 9b3701dd88106053b8ddedaa01583a9eb715ddcf Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 13 Sep 2022 14:28:18 +0800 Subject: [PATCH 14/16] feat: back delete to previous line --- .../backspace_handler.dart | 82 ++++++++++++++----- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 1e252d9114..5b391aea41 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -26,7 +26,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); selection = selection.isBackward ? selection : selection.reversed; final textNodes = nodes.whereType().toList(); - final nonTextNodes = + final List nonTextNodes = nodes.where((node) => node is! TextNode).toList(growable: false); final transactionBuilder = TransactionBuilder(editorState); @@ -59,23 +59,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } else { // 2. non-style // find previous text node. - var previous = textNode.previous; - while (previous != null) { - if (previous is TextNode) { - transactionBuilder - ..mergeText(previous, textNode) - ..deleteNode(textNode) - ..afterSelection = Selection.collapsed( - Position( - path: previous.path, - offset: previous.toRawString().length, - ), - ); - break; - } else { - previous = previous.previous; - } - } + return _backDeleteToPreviousTextNode( + editorState, + textNode, + transactionBuilder, + nonTextNodes, + selection, + ); } } else { if (selection.isCollapsed) { @@ -103,7 +93,10 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) { makeFollowingNodesIncremental( - editorState, startPosition.path, transactionBuilder.afterSelection!); + editorState, + startPosition.path, + transactionBuilder.afterSelection!, + ); } return KeyEventResult.handled; } @@ -117,8 +110,55 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (cancelNumberListPath != null) { makeFollowingNodesIncremental( - editorState, cancelNumberListPath, Selection.collapsed(selection.start), - beginNum: 0); + editorState, + cancelNumberListPath, + Selection.collapsed(selection.start), + beginNum: 0, + ); + } + + return KeyEventResult.handled; +} + +KeyEventResult _backDeleteToPreviousTextNode( + EditorState editorState, + TextNode textNode, + TransactionBuilder transactionBuilder, + List nonTextNodes, + Selection selection) { + var previous = textNode.previous; + bool prevIsNumberList = false; + while (previous != null) { + if (previous is TextNode) { + if (previous.subtype == StyleKey.numberList) { + prevIsNumberList = true; + } + + transactionBuilder + ..mergeText(previous, textNode) + ..deleteNode(textNode) + ..afterSelection = Selection.collapsed( + Position( + path: previous.path, + offset: previous.toRawString().length, + ), + ); + break; + } else { + previous = previous.previous; + } + } + + if (transactionBuilder.operations.isNotEmpty) { + if (nonTextNodes.isNotEmpty) { + transactionBuilder.afterSelection = Selection.collapsed(selection.start); + } + transactionBuilder.commit(); + } + + if (prevIsNumberList) { + makeFollowingNodesIncremental( + editorState, previous!.path, transactionBuilder.afterSelection!); } return KeyEventResult.handled; From cfdd8919913f05ef6fd22c1d4f9ca1932382604d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 13 Sep 2022 17:33:26 +0800 Subject: [PATCH 15/16] fix: redo/undo error --- .../src/operation/transaction_builder.dart | 9 ++- .../appflowy_editor/lib/src/undo_manager.dart | 24 +++--- .../test/legacy/undo_manager_test.dart | 76 +++++++++++++++++++ 3 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 1390b23918..41be1c8c48 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -10,6 +10,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/transaction.dart'; +import 'package:logging/logging.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save a snapshot of the cursor selection state automatically. @@ -193,7 +194,7 @@ class TransactionBuilder { /// /// Also, this method will transform the path of the operations /// to avoid conflicts. - add(Operation op) { + add(Operation op, {bool transform = true}) { final Operation? last = operations.isEmpty ? null : operations.last; if (last != null) { if (op is TextEditOperation && @@ -208,8 +209,10 @@ class TransactionBuilder { return; } } - for (var i = 0; i < operations.length; i++) { - op = transformOperation(operations[i], op); + if (transform) { + for (var i = 0; i < operations.length; i++) { + op = transformOperation(operations[i], op); + } } if (op is TextEditOperation && op.delta.isEmpty) { return; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart index cfa3f75688..737076e930 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/undo_manager.dart @@ -43,7 +43,7 @@ class HistoryItem extends LinkedListEntry { for (var i = operations.length - 1; i >= 0; i--) { final operation = operations[i]; final inverted = operation.invert(); - builder.add(inverted); + builder.add(inverted, transform: false); } builder.afterSelection = beforeSelection; builder.beforeSelection = afterSelection; @@ -123,11 +123,12 @@ class UndoManager { } final transaction = historyItem.toTransaction(s); s.apply( - transaction, - const ApplyOptions( - recordUndo: false, - recordRedo: true, - )); + transaction, + const ApplyOptions( + recordUndo: false, + recordRedo: true, + ), + ); } redo() { @@ -142,10 +143,11 @@ class UndoManager { } final transaction = historyItem.toTransaction(s); s.apply( - transaction, - const ApplyOptions( - recordUndo: true, - recordRedo: false, - )); + transaction, + const ApplyOptions( + recordUndo: true, + recordRedo: false, + ), + ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart new file mode 100644 index 0000000000..f77e404273 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/legacy/undo_manager_test.dart @@ -0,0 +1,76 @@ +import 'dart:collection'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/undo_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + Node _createEmptyEditorRoot() { + return Node( + type: 'editor', + children: LinkedList(), + attributes: {}, + ); + } + + test("HistoryItem #1", () { + final document = StateTree(root: _createEmptyEditorRoot()); + final editorState = EditorState(document: document); + + final historyItem = HistoryItem(); + historyItem.add(DeleteOperation( + [0], [TextNode(type: 'text', delta: Delta()..insert('0'))])); + historyItem.add(DeleteOperation( + [0], [TextNode(type: 'text', delta: Delta()..insert('1'))])); + historyItem.add(DeleteOperation( + [0], [TextNode(type: 'text', delta: Delta()..insert('2'))])); + + final transaction = historyItem.toTransaction(editorState); + assert(isInsertAndPathEqual(transaction.operations[0], [0], '2')); + assert(isInsertAndPathEqual(transaction.operations[1], [0], '1')); + assert(isInsertAndPathEqual(transaction.operations[2], [0], '0')); + }); + + test("HistoryItem #2", () { + final document = StateTree(root: _createEmptyEditorRoot()); + final editorState = EditorState(document: document); + + final historyItem = HistoryItem(); + historyItem.add(DeleteOperation( + [0], [TextNode(type: 'text', delta: Delta()..insert('0'))])); + historyItem + .add(UpdateOperation([0], {"subType": "number"}, {"subType": null})); + historyItem.add(DeleteOperation([0], [TextNode.empty(), TextNode.empty()])); + historyItem.add(DeleteOperation([0], [TextNode.empty()])); + + final transaction = historyItem.toTransaction(editorState); + assert(isInsertAndPathEqual(transaction.operations[0], [0])); + assert(isInsertAndPathEqual(transaction.operations[1], [0])); + assert(transaction.operations[2] is UpdateOperation); + assert(isInsertAndPathEqual(transaction.operations[3], [0], '0')); + }); +} + +bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) { + if (operation is! InsertOperation) { + return false; + } + + if (!pathEquals(operation.path, path)) { + return false; + } + + final firstNode = operation.nodes[0]; + if (firstNode is! TextNode) { + return false; + } + + if (content == null) { + return true; + } + + return firstNode.delta.toRawString() == content; +} From ee0f3d322726d7504a1b40a401be79a9367e6a70 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 13 Sep 2022 19:36:22 +0800 Subject: [PATCH 16/16] feat: handle number list on delete event --- .../src/operation/transaction_builder.dart | 1 - .../backspace_handler.dart | 65 ++++++++++++------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 41be1c8c48..c990a3921f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -10,7 +10,6 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/transaction.dart'; -import 'package:logging/logging.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save a snapshot of the cursor selection state automatically. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 5b391aea41..82d7f7f3b3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -181,30 +181,29 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { final transactionBuilder = TransactionBuilder(editorState); if (textNodes.length == 1) { final textNode = textNodes.first; + // The cursor is at the end of the line, + // merge next line into this line. if (selection.start.offset >= textNode.delta.length) { - final nextNode = textNode.next; - if (nextNode == null) { - return KeyEventResult.ignored; - } - if (nextNode is TextNode) { - transactionBuilder.mergeText(textNode, nextNode); - } - transactionBuilder.deleteNode(nextNode); + return _mergeNextLineIntoThisLine( + editorState, + textNode, + transactionBuilder, + selection, + ); + } + final index = textNode.delta.nextRunePosition(selection.start.offset); + if (selection.isCollapsed) { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + index - selection.start.offset, + ); } else { - final index = textNode.delta.nextRunePosition(selection.start.offset); - if (selection.isCollapsed) { - transactionBuilder.deleteText( - textNode, - selection.start.offset, - index - selection.start.offset, - ); - } else { - transactionBuilder.deleteText( - textNode, - selection.start.offset, - selection.end.offset - selection.start.offset, - ); - } + transactionBuilder.deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ); } transactionBuilder.commit(); } else { @@ -222,6 +221,28 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { return KeyEventResult.handled; } +KeyEventResult _mergeNextLineIntoThisLine( + EditorState editorState, + TextNode textNode, + TransactionBuilder transactionBuilder, + Selection selection) { + final nextNode = textNode.next; + if (nextNode == null) { + return KeyEventResult.ignored; + } + if (nextNode is TextNode) { + transactionBuilder.mergeText(textNode, nextNode); + } + transactionBuilder.deleteNode(nextNode); + transactionBuilder.commit(); + + if (textNode.subtype == StyleKey.numberList) { + makeFollowingNodesIncremental(editorState, textNode.path, selection); + } + + return KeyEventResult.handled; +} + void _deleteTextNodes(TransactionBuilder transactionBuilder, List textNodes, Selection selection) { final first = textNodes.first;