From f17b6cb91038a11feda9522512a247c0350b39f4 Mon Sep 17 00:00:00 2001 From: Mathias Mogensencd collaction_website Date: Sat, 25 Mar 2023 14:54:23 +0100 Subject: [PATCH] feat: delete sentence shortcut Closes: #4 --- .../arrow_keys_handler.dart | 40 ++++++++++++ .../built_in_shortcut_events.dart | 9 ++- .../backspace_handler_test.dart | 27 +++++++- .../shortcut_event/shortcut_event_test.dart | 63 +++++++++++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index 2d1953c68..d34b0dfff 100644 --- a/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -191,6 +191,7 @@ ShortcutEventHandler cursorBeginSelect = (editorState, event) { if (position != null) { end = position; } + editorState.service.selectionService.updateSelection( selection.copyWith(start: start, end: end), ); @@ -349,6 +350,45 @@ ShortcutEventHandler cursorLeftWordDelete = (editorState, event) { return KeyEventResult.handled; }; +ShortcutEventHandler cursorLeftSentenceDelete = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + + if (nodes.length == 1 && nodes.first is TextNode) { + final textNode = nodes.first as TextNode; + if (textNode.toPlainText().isEmpty) { + return KeyEventResult.ignored; + } + } + + final deleteTransaction = editorState.transaction; + deleteTransaction.deleteNodes( + editorState.service.selectionService.getNodesInSelection(selection), + ); + editorState.apply(deleteTransaction, withUpdateCursor: false); + + final cursorPosition = + selection.start.copyWith(offset: 0).goLeft(editorState); + if (cursorPosition != null) { + final next = cursorPosition.path.next; + final transaction = editorState.transaction + ..insertNode( + next, + TextNode.empty(), + ) + ..afterSelection = Selection.collapsed( + Position(path: next, offset: 0), + ); + + editorState.apply(transaction); + } + + return KeyEventResult.handled; +}; + enum _SelectionRange { character, word, diff --git a/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/lib/src/service/shortcut_event/built_in_shortcut_events.dart index d6338b6fe..b6794b1a0 100644 --- a/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -63,11 +63,18 @@ List builtInShortcutEvents = [ ), ShortcutEvent( key: 'Cursor word delete', - command: 'meta+backspace', + command: 'alt+backspace', windowsCommand: 'ctrl+backspace', linuxCommand: 'ctrl+backspace', handler: cursorLeftWordDelete, ), + ShortcutEvent( + key: 'Cursor sentence delete', + command: 'meta+backspace', + windowsCommand: 'alt+backspace', + linuxCommand: 'alt+backspace', + handler: cursorLeftSentenceDelete, + ), ShortcutEvent( key: 'Cursor left select', command: 'shift+arrow left', diff --git a/test/service/internal_key_event_handlers/backspace_handler_test.dart b/test/service/internal_key_event_handlers/backspace_handler_test.dart index e786377de..a9cadeb8e 100644 --- a/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -59,6 +59,7 @@ void main() async { (tester) async { await _deleteTextByBackspace(tester, true); }); + testWidgets( 'Presses backspace key in non-empty document and selection is forward', (tester) async { @@ -85,6 +86,7 @@ void main() async { (tester) async { await _deleteTextByDelete(tester, true); }); + testWidgets( 'Presses delete key in non-empty document and selection is forward', (tester) async { @@ -135,14 +137,17 @@ void main() async { (tester) async { await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.checkbox); }); + testWidgets('Presses backspace key in styled text (bulletedList)', (tester) async { await _deleteStyledTextByBackspace( tester, BuiltInAttributeKey.bulletedList); }); + testWidgets('Presses backspace key in styled text (heading)', (tester) async { await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.heading); }); + testWidgets('Presses backspace key in styled text (quote)', (tester) async { await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.quote); }); @@ -161,13 +166,16 @@ void main() async { testWidgets('Presses delete key in styled text (checkbox)', (tester) async { await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.checkbox); }); + testWidgets('Presses delete key in styled text (bulletedList)', (tester) async { await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.bulletedList); }); + testWidgets('Presses delete key in styled text (heading)', (tester) async { await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.heading); }); + testWidgets('Presses delete key in styled text (quote)', (tester) async { await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.quote); }); @@ -251,7 +259,7 @@ void main() async { await editor.insertText(textNode, '#', 0); await editor.pressLogicKey(LogicalKeyboardKey.space); expect( - (editor.nodeAtPath([0]) as TextNode).attributes.heading, + textNode.attributes.heading, BuiltInAttributeKey.h1, ); @@ -264,7 +272,7 @@ void main() async { await editor.insertText(textNode, '#', 0); await editor.pressLogicKey(LogicalKeyboardKey.space); expect( - (editor.nodeAtPath([0]) as TextNode).attributes.heading, + textNode.attributes.heading, BuiltInAttributeKey.h1, ); }); @@ -297,17 +305,23 @@ void main() async { Selection.single(path: [0, 0, 0], startOffset: 0), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([0, 0, 0])?.subtype, null); + await editor.updateSelection( Selection.single(path: [0, 0, 0], startOffset: 0), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([0, 1]) != null, true); + await editor.updateSelection( Selection.single(path: [0, 1], startOffset: 0), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.nodeAtPath([1]) != null, true); + await editor.updateSelection( Selection.single(path: [1], startOffset: 0), ); @@ -315,6 +329,7 @@ void main() async { // * Welcome to Appflowy 😁 // * Welcome to Appflowy 😁Welcome to Appflowy 😁 await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( editor.documentSelection, Selection.single(path: [0, 0], startOffset: text.length), @@ -357,18 +372,22 @@ void main() async { Selection.single(path: [0, 1], startOffset: 0), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList, true, ); + expect( editor.nodeAtPath([0, 1, 0])!.subtype, BuiltInAttributeKey.bulletedList, ); + expect( editor.nodeAtPath([0, 1, 1])!.subtype, BuiltInAttributeKey.bulletedList, ); + expect(find.byType(FlowyRichText), findsNWidgets(5)); // Before @@ -383,18 +402,22 @@ void main() async { // * Welcome to Appflowy 😁 // * Welcome to Appflowy 😁 await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( editor.nodeAtPath([0, 0])!.subtype == BuiltInAttributeKey.bulletedList, true, ); + expect( (editor.nodeAtPath([0, 0]) as TextNode).toPlainText() == text * 2, true, ); + expect( editor.nodeAtPath([0, 1])!.subtype == BuiltInAttributeKey.bulletedList, true, ); + expect( editor.nodeAtPath([0, 2])!.subtype == BuiltInAttributeKey.bulletedList, true, diff --git a/test/service/shortcut_event/shortcut_event_test.dart b/test/service/shortcut_event/shortcut_event_test.dart index 034d91a93..648778f60 100644 --- a/test/service/shortcut_event/shortcut_event_test.dart +++ b/test/service/shortcut_event/shortcut_event_test.dart @@ -21,6 +21,7 @@ void main() async { return KeyEventResult.handled; }, ); + shortcutEvent.updateCommand(command: 'cmd+shift+alt+ctrl+b'); expect(shortcutEvent.keybindings.length, 1); expect(shortcutEvent.keybindings.first.isMetaPressed, true); @@ -35,10 +36,13 @@ void main() async { final editor = tester.editor ..insertTextNode(text) ..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); + if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( LogicalKeyboardKey.arrowLeft, @@ -50,10 +54,12 @@ void main() async { isMetaPressed: true, ); } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0), ); + await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); @@ -67,6 +73,7 @@ void main() async { ); } } + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( LogicalKeyboardKey.arrowLeft, @@ -78,6 +85,7 @@ void main() async { isMetaPressed: true, ); } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0), @@ -91,10 +99,13 @@ void main() async { final editor = tester.editor ..insertTextNode(text) ..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( Selection.single(path: [1], startOffset: 0), ); + if (Platform.isWindows || Platform.isLinux) { await editor.pressLogicKey( LogicalKeyboardKey.arrowRight, @@ -106,10 +117,12 @@ void main() async { isMetaPressed: true, ); } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: text.length), ); + await editor.updateSelection( Selection.single(path: [1], startOffset: 0), ); @@ -123,10 +136,12 @@ void main() async { ); } } + await editor.pressLogicKey( LogicalKeyboardKey.arrowRight, isAltPressed: true, ); + expect( editor.documentSelection, Selection.single(path: [1], startOffset: text.length), @@ -139,10 +154,13 @@ void main() async { final editor = tester.editor ..insertTextNode(text) ..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { await editor.pressLogicKey( LogicalKeyboardKey.home, @@ -153,6 +171,7 @@ void main() async { editor.documentSelection, Selection.single(path: [1], startOffset: 0), ); + await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); @@ -166,11 +185,13 @@ void main() async { ); } } + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( LogicalKeyboardKey.home, ); } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: 0), @@ -182,10 +203,13 @@ void main() async { final editor = tester.editor ..insertTextNode(text) ..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( Selection.single(path: [1], startOffset: text.length), ); + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { await editor.pressLogicKey( LogicalKeyboardKey.end, @@ -196,6 +220,7 @@ void main() async { editor.documentSelection, Selection.single(path: [1], startOffset: text.length), ); + await editor.updateSelection( Selection.single(path: [1], startOffset: 0), ); @@ -209,15 +234,53 @@ void main() async { ); } } + if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await editor.pressLogicKey( LogicalKeyboardKey.end, ); } + expect( editor.documentSelection, Selection.single(path: [1], startOffset: text.length), ); }); + + testWidgets('delete sentence to beginning', (tester) async { + const text = "Hello World!"; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + + await editor.startTesting(); + + await editor.updateSelection( + Selection.collapsed(Position(path: [1])), + ); + + expect(editor.documentLength, 2); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(); + + expect( + editor.documentSelection, + Selection.collapsed(Position(path: [1], offset: 0)), + ); + + expect(editor.documentLength, 1); + }); }); }