Forráskód Böngészése

fix: paste multiple lines in codeblock (#3151)

* fix: paste multiple lines in codeblock

* fix: works with non collapsed selection

* chore: localize code block

* test: multiline paste in codeblock

* chore: remove unused import

* fix: only hanlde code block paste command if all the selected nodes are code block

---------

Co-authored-by: Lucas.Xu <[email protected]>
Mayur Mahajan 1 éve
szülő
commit
b88aed887a

+ 60 - 0
frontend/appflowy_flutter/integration_test/document/document_codeblock_paste_test.dart

@@ -0,0 +1,60 @@
+import 'dart:io';
+
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import '../util/util.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('paste in codeblock', () {
+    testWidgets('paste multiple lines in codeblock', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      // create a new document
+      await tester.createNewPageWithName();
+
+      // mock the clipboard
+      const lines = 3;
+      final text = List.generate(lines, (index) => 'line $index').join('\n');
+      AppFlowyClipboard.mockSetData(
+        AppFlowyClipboardData(
+          text: text,
+        ),
+      );
+
+      await insertCodeBlockInDocument(tester);
+
+      // paste the text
+      await tester.simulateKeyEvent(
+        LogicalKeyboardKey.keyV,
+        isControlPressed: Platform.isLinux || Platform.isWindows,
+        isMetaPressed: Platform.isMacOS,
+      );
+      await tester.pumpAndSettle();
+
+      final editorState = tester.editor.getCurrentEditorState();
+      expect(editorState.document.root.children.length, 1);
+      expect(
+        editorState.getNodeAtPath([0])!.delta!.toPlainText(),
+        text,
+      );
+    });
+  });
+}
+
+/// Inserts an codeBlock in the document
+Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
+  // open the actions menu and insert the codeBlock
+  await tester.editor.showSlashMenu();
+  await tester.editor.tapSlashMenuItemWithName(
+    LocaleKeys.document_selectionMenu_codeBlock.tr(),
+  );
+  await tester.pumpAndSettle();
+}

+ 2 - 0
frontend/appflowy_flutter/integration_test/document/document_test_runner.dart

@@ -11,6 +11,7 @@ import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
 import 'edit_document_test.dart' as document_edit_test;
 import 'document_with_outline_block_test.dart' as document_with_outline_block;
 import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
+import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
 
 void startTesting() {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -25,4 +26,5 @@ void startTesting() {
   document_with_outline_block.main();
   document_with_toggle_list_test.main();
   document_copy_and_paste_test.main();
+  document_codeblock_paste_test.main();
 }

+ 3 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_component.dart

@@ -1,7 +1,9 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';
 import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
 import 'package:highlight/highlight.dart' as highlight;
@@ -40,7 +42,7 @@ Node codeBlockNode({
 
 // defining the callout block menu item for selection
 SelectionMenuItem codeBlockItem = SelectionMenuItem.node(
-  name: 'Code Block',
+  name: LocaleKeys.document_selectionMenu_codeBlock.tr(),
   iconData: Icons.abc,
   keywords: ['code', 'codeblock'],
   nodeBuilder: (editorState, _) => codeBlockNode(),

+ 60 - 1
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart

@@ -9,9 +9,10 @@ final List<CharacterShortcutEvent> codeBlockCharacterEvents = [
 
 final List<CommandShortcutEvent> codeBlockCommands = [
   insertNewParagraphNextToCodeBlockCommand,
+  pasteInCodeblock,
+  selectAllInCodeBlockCommand,
   tabToInsertSpacesInCodeBlockCommand,
   tabToDeleteSpacesInCodeBlockCommand,
-  selectAllInCodeBlockCommand,
 ];
 
 /// press the enter key in code block to insert a new line in it.
@@ -97,6 +98,18 @@ final CommandShortcutEvent selectAllInCodeBlockCommand = CommandShortcutEvent(
   handler: _selectAllInCodeBlockCommandHandler,
 );
 
+/// ctrl + v to paste text in code block.
+///
+/// - support
+///   - desktop
+///   - web
+final CommandShortcutEvent pasteInCodeblock = CommandShortcutEvent(
+  key: 'paste in codeblock',
+  command: 'ctrl+v',
+  macOSCommand: 'cmd+v',
+  handler: _pasteInCodeBlock,
+);
+
 CharacterShortcutEventHandler _enterInCodeBlockCommandHandler =
     (editorState) async {
   final selection = editorState.selection;
@@ -267,3 +280,49 @@ CommandShortcutEventHandler _selectAllInCodeBlockCommandHandler =
 
   return KeyEventResult.handled;
 };
+
+CommandShortcutEventHandler _pasteInCodeBlock = (editorState) {
+  var selection = editorState.selection;
+
+  if (selection == null) {
+    return KeyEventResult.ignored;
+  }
+
+  if (editorState.getNodesInSelection(selection).length != 1) {
+    return KeyEventResult.ignored;
+  }
+
+  final node = editorState.getNodeAtPath(selection.end.path);
+  if (node == null || node.type != CodeBlockKeys.type) {
+    return KeyEventResult.ignored;
+  }
+
+  // delete the selection first.
+  if (!selection.isCollapsed) {
+    editorState.deleteSelection(selection);
+  }
+
+  // fetch selection again.
+  selection = editorState.selection;
+  if (selection == null) {
+    return KeyEventResult.skipRemainingHandlers;
+  }
+  assert(selection.isCollapsed);
+
+  () async {
+    final data = await AppFlowyClipboard.getData();
+    final text = data.text;
+    if (text != null && text.isNotEmpty) {
+      final transaction = editorState.transaction
+        ..insertText(
+          node,
+          selection!.end.offset,
+          text,
+        );
+
+      await editorState.apply(transaction);
+    }
+  }();
+
+  return KeyEventResult.handled;
+};

+ 2 - 1
frontend/resources/translations/en.json

@@ -477,7 +477,8 @@
       }
     },
     "selectionMenu": {
-      "outline": "Outline"
+      "outline": "Outline",
+      "codeBlock": "Code Block"
     },
     "plugins": {
       "referencedBoard": "Referenced Board",