diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index d6b52fc94e..e38ac4e671 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -23,13 +23,14 @@ jobs: uses: docker/setup-buildx-action@v3 # cache the docker layers - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- + # don't cache anything temporarly, because it always triggers "no space left on device" error + # - name: Cache Docker layers + # uses: actions/cache@v3 + # with: + # path: /tmp/.buildx-cache + # key: ${{ runner.os }}-buildx-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-buildx- - name: Build the app uses: docker/build-push-action@v5 @@ -37,10 +38,10 @@ jobs: context: . file: ./frontend/scripts/docker-buildfiles/Dockerfile push: false - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + # cache-from: type=local,src=/tmp/.buildx-cache + # cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache + # - name: Move cache + # run: | + # rm -rf /tmp/.buildx-cache + # mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart new file mode 100644 index 0000000000..5cbb133f9d --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart @@ -0,0 +1,61 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('customer:', () { + testWidgets('backtick issue - inline code', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + const pageName = 'backtick issue'; + await tester.createNewPageWithNameUnderParent(name: pageName); + + // focus on the editor + await tester.tap(find.byType(AppFlowyEditor)); + // input backtick + const text = '`Hello` AppFlowy'; + + for (var i = 0; i < text.length; i++) { + await tester.ime.insertCharacter(text[i]); + } + + final node = tester.editor.getNodeAtPath([0]); + expect( + node.delta?.toJson(), + equals([ + { + "insert": "Hello", + "attributes": {"code": true}, + }, + {"insert": " AppFlowy"}, + ]), + ); + }); + + testWidgets('backtick issue - inline code', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + const pageName = 'backtick issue'; + await tester.createNewPageWithNameUnderParent(name: pageName); + + // focus on the editor + await tester.tap(find.byType(AppFlowyEditor)); + // input backtick + const text = '```'; + + for (var i = 0; i < text.length; i++) { + await tester.ime.insertCharacter(text[i]); + } + + final node = tester.editor.getNodeAtPath([0]); + expect(node.type, equals(CodeBlockKeys.type)); + }); + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 026f08442f..48d1000880 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -270,9 +270,9 @@ class DocumentBloc extends Bloc { editorState.logConfiguration ..level = AppFlowyEditorLogLevel.all ..handler = (log) { - // if (enableDocumentInternalLog) { - Log.info(log); - // } + if (enableDocumentInternalLog) { + Log.info(log); + } }; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 3f7c2845e8..70564bbcde 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -213,6 +213,8 @@ class _AppFlowyEditorPageState extends State { late final ViewInfoBloc viewInfoBloc = context.read(); + final editorKeyboardInterceptor = EditorKeyboardInterceptor(); + Future showSlashMenu(editorState) async => customSlashCommand( slashMenuItems, shouldInsertSlash: false, @@ -278,6 +280,10 @@ class _AppFlowyEditorPageState extends State { if (widget.initialSelection != null) { widget.editorState.updateSelectionWithReason(widget.initialSelection); } + + widget.editorState.service.keyboardService?.registerInterceptor( + editorKeyboardInterceptor, + ); }); } @@ -302,6 +308,9 @@ class _AppFlowyEditorPageState extends State { @override void dispose() { + widget.editorState.service.keyboardService?.unregisterInterceptor( + editorKeyboardInterceptor, + ); focusManager?.loseFocusNotifier.removeListener(_loseFocus); if (widget.useViewInfoBloc && !viewInfoBloc.isClosed) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart new file mode 100644 index 0000000000..3b170bf76f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart @@ -0,0 +1,62 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:flutter/services.dart'; + +class EditorKeyboardInterceptor extends AppFlowyKeyboardServiceInterceptor { + @override + Future interceptNonTextUpdate( + TextEditingDeltaNonTextUpdate nonTextUpdate, + EditorState editorState, + List characterShortcutEvents, + ) async { + return _checkIfBacktickPressed( + editorState, + nonTextUpdate, + ); + } + + /// Check if the backtick pressed event should be handled + Future _checkIfBacktickPressed( + EditorState editorState, + TextEditingDeltaNonTextUpdate nonTextUpdate, + ) async { + // if the composing range is not empty, it means the user is typing a text, + // so we don't need to handle the backtick pressed event + if (!nonTextUpdate.composing.isCollapsed || + !nonTextUpdate.selection.isCollapsed) { + return false; + } + + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + AppFlowyEditorLog.input.debug('selection is null or not collapsed'); + return false; + } + + final node = editorState.getNodesInSelection(selection).firstOrNull; + if (node == null) { + AppFlowyEditorLog.input.debug('node is null'); + return false; + } + + // get last character of the node + final plainText = node.delta?.toPlainText(); + // three backticks to code block + if (plainText != '```') { + return false; + } + + final transaction = editorState.transaction; + transaction.insertNode( + selection.end.path, + codeBlockNode(), + ); + transaction.deleteNode(node); + transaction.afterSelection = Selection.collapsed( + Position(path: selection.start.path), + ); + await editorState.apply(transaction); + + return true; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 3e69374476..1f3104b5f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -30,6 +30,7 @@ export 'image/mobile_image_toolbar_item.dart'; export 'image/multi_image_block_component/multi_image_menu.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; +export 'keyboard_interceptor/keyboard_interceptor.dart'; export 'link_preview/custom_link_preview.dart'; export 'link_preview/link_preview_cache.dart'; export 'link_preview/link_preview_menu.dart'; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 895140f52c..ab4f254d6b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: "737681d" - resolved-ref: "737681dd97b501dd88c7c016c5c359f9ffff7d31" + ref: "6da7b4e" + resolved-ref: "6da7b4e5dc76dd6b9d4361c744359b04ddb5b983" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "3.3.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 1983d915ee..95dfbdfa78 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -177,7 +177,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "737681d" + ref: "6da7b4e" appflowy_editor_plugins: git: