diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 0deb3d44d2..0b0b834936 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -9,6 +9,13 @@ import 'package:flowy_editor/src/operation/transaction_builder.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; +@visibleForTesting +List get checkboxListSymbols => _checkboxListSymbols; +@visibleForTesting +List get unCheckboxListSymbols => _unCheckboxListSymbols; +@visibleForTesting +List get bulletedListSymbols => _bulletedListSymbols; + const _bulletedListSymbols = ['*', '-']; const _checkboxListSymbols = ['[x]', '-[x]']; const _unCheckboxListSymbols = ['[]', '-[]']; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart index fc2e4e3f31..158aab4615 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart @@ -1,7 +1,4 @@ -import 'package:flowy_editor/src/service/keyboard_service.dart'; -import 'package:flowy_editor/src/service/render_plugin_service.dart'; -import 'package:flowy_editor/src/service/scroll_service.dart'; -import 'package:flowy_editor/src/service/selection_service.dart'; +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/src/service/toolbar_service.dart'; import 'package:flutter/material.dart'; @@ -26,6 +23,13 @@ class FlowyService { // input service final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); + FlowyInputService? get inputService { + if (inputServiceKey.currentState != null && + inputServiceKey.currentState is FlowyInputService) { + return inputServiceKey.currentState! as FlowyInputService; + } + return null; + } // render plugin service late FlowyRenderPlugin renderPluginService; 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 533cace586..61ece83c5a 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 @@ -70,6 +70,31 @@ class EditorWidgetTester { _editorState.service.selectionService.updateSelection(selection); } await tester.pumpAndSettle(); + + expect(_editorState.service.selectionService.currentSelection.value, + selection); + } + + Future insertText(TextNode textNode, String text, int offset, + {Selection? selection}) async { + await apply([ + TextEditingDeltaInsertion( + oldText: textNode.toRawString(), + textInserted: text, + insertionOffset: offset, + selection: selection != null + ? TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset) + : TextSelection.collapsed(offset: offset), + composing: TextRange.empty, + ) + ]); + } + + Future apply(List deltas) async { + _editorState.service.inputService?.apply(deltas); + await tester.pumpAndSettle(); } Future pressLogicKey( 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 e4eb99b60e..04b5a11789 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 @@ -79,6 +79,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.enter) { return PhysicalKeyboardKey.enter; } + if (this == LogicalKeyboardKey.space) { + return PhysicalKeyboardKey.space; + } if (this == LogicalKeyboardKey.backspace) { return PhysicalKeyboardKey.backspace; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart new file mode 100644 index 0000000000..fb4c187f0f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart @@ -0,0 +1,178 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('white_space_handler.dart', () { + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // [h1]Welcome to Appflowy 😁 + // [h2]Welcome to Appflowy 😁 + // [h3]Welcome to Appflowy 😁 + // [h4]Welcome to Appflowy 😁 + // [h5]Welcome to Appflowy 😁 + // [h6]Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key after #*', (tester) async { + const maxSignCount = 6; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 1; i <= maxSignCount; i++) { + editor.insertTextNode('${'#' * i}$text'); + } + await editor.startTesting(); + + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [i - 1], startOffset: i), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + final textNode = (editor.nodeAtPath([i - 1]) as TextNode); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h1 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + } + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // [h1]##Welcome to Appflowy 😁 + // [h2]##Welcome to Appflowy 😁 + // [h3]##Welcome to Appflowy 😁 + // [h4]##Welcome to Appflowy 😁 + // [h5]##Welcome to Appflowy 😁 + // [h6]##Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key inside #*', (tester) async { + const maxSignCount = 6; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 1; i <= maxSignCount; i++) { + editor.insertTextNode('${'###' * i}$text'); + } + await editor.startTesting(); + + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [i - 1], startOffset: i), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + final textNode = (editor.nodeAtPath([i - 1]) as TextNode); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h1 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + expect(textNode.toRawString().startsWith('##'), true); + } + }); + + // Before + // + // Welcome to Appflowy 😁 + // + // After + // [h1 ~ h6]##Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key in heading styled text', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + + await editor.startTesting(); + + const maxSignCount = 6; + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final textNode = (editor.nodeAtPath([0]) as TextNode); + + await editor.insertText(textNode, '#' * i, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h2 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + } + }); + + testWidgets('Presses whitespace key after (un)checkbox symbols', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in unCheckboxListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.checkbox); + expect(textNode.attributes.check, false); + } + }); + + testWidgets('Presses whitespace key after checkbox symbols', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in checkboxListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.checkbox); + expect(textNode.attributes.check, true); + } + }); + + testWidgets('Presses whitespace key after bulleted list', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in bulletedListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.bulletedList); + } + }); + }); +}