diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index afef57cceb..ccddff27be 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -80,11 +80,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { - final selection = editorState.service.selectionService.currentSelection.value; + var selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { return KeyEventResult.ignored; } - final nodes = editorState.service.selectionService.currentSelectedNodes; + var nodes = editorState.service.selectionService.currentSelectedNodes; + nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); if (textNodes.length != nodes.length) { diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index b6aebeaa41..ddbe4d5b2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -95,6 +95,15 @@ class EditorWidgetTester { } } +extension TestString on String { + String safeSubString([int start = 0, int? end]) { + end ??= length - 1; + end = end.clamp(start, length - 1); + final sRunes = runes; + return String.fromCharCodes(sRunes, start, end); + } +} + extension TestEditorExtension on WidgetTester { EditorWidgetTester get editor => EditorWidgetTester(tester: this)..initialize(); diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index aa98781d7d..48c4ab3e67 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -58,6 +58,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.backspace) { return PhysicalKeyboardKey.backspace; } + if (this == LogicalKeyboardKey.delete) { + return PhysicalKeyboardKey.delete; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart index 4a42ac2c47..15af27e7a4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart @@ -36,6 +36,87 @@ void main() async { }); }); + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses backspace key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByBackspace(tester, true); + }); + testWidgets( + 'Presses backspace key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByBackspace(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses delete key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByDelete(tester, true); + }); + testWidgets( + 'Presses delete key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByDelete(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁Welcome Appflowy 😁 + testWidgets( + 'Presses delete key in non-empty document and selection is at the end of the text', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [0], startOffset: text.length), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 1); + expect(editor.documentSelection, + Selection.single(path: [0], startOffset: text.length)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), text * 2); + }); + // Before // // Welcome to Appflowy 😁 @@ -47,12 +128,49 @@ void main() async { // Welcome to Appflowy 😁 // [Style] Welcome to Appflowy 😁Welcome to Appflowy 😁 // - testWidgets('Presses backspace key in styled text', (tester) async { - await _deleteStyledText(tester, StyleKey.checkbox); + testWidgets('Presses backspace key in styled text (checkbox)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.checkbox); + }); + testWidgets('Presses backspace key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.bulletedList); + }); + testWidgets('Presses backspace key in styled text (heading)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.heading); + }); + testWidgets('Presses backspace key in styled text (quote)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.quote); + }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + testWidgets('Presses delete key in styled text (checkbox)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.checkbox); + }); + testWidgets('Presses delete key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.bulletedList); + }); + testWidgets('Presses delete key in styled text (heading)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.heading); + }); + testWidgets('Presses delete key in styled text (quote)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.quote); }); } -Future _deleteStyledText(WidgetTester tester, String style) async { +Future _deleteStyledTextByBackspace( + WidgetTester tester, String style) async { const text = 'Welcome to Appflowy 😁'; Attributes attributes = { StyleKey.subtype: style, @@ -61,6 +179,8 @@ Future _deleteStyledText(WidgetTester tester, String style) async { attributes[StyleKey.checkbox] = true; } else if (style == StyleKey.numberList) { attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; } final editor = tester.editor ..insertTextNode(text) @@ -95,3 +215,137 @@ Future _deleteStyledText(WidgetTester tester, String style) async { expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); expect(editor.nodeAtPath([1])?.subtype, null); } + +Future _deleteStyledTextByDelete( + WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = true; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + for (var i = 1; i < text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect( + editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + text.safeSubString(i)); + } + + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); +} + +Future _deleteTextByBackspace( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 10), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +} + +Future _deleteTextByDelete( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 9), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +}