Skip to content

Commit

Permalink
feat: add option to paste plain text (#7045)
Browse files Browse the repository at this point in the history
* feat: add option to paste plain text

* refactor: optimize the code

Co-authored-by: Mathias Mogensen <[email protected]>
Co-authored-by: Lucas <[email protected]>

---------

Co-authored-by: Mathias Mogensen <[email protected]>
Co-authored-by: Lucas <[email protected]>
  • Loading branch information
3 people authored Dec 26, 2024
1 parent 200b367 commit 3959cdb
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import 'dart:io';

import 'package:flutter/services.dart';

import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
Expand All @@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.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 'package:universal_platform/universal_platform.dart';
Expand All @@ -27,10 +26,13 @@ void main() {
await tester.pasteContent(
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
(editorState) {
expect(editorState.document.root.children.length, 3);
expect(editorState.document.root.children.length, 1);
final text =
editorState.document.root.children.first.delta!.toPlainText();
final textLines = text.split('\n');
for (var i = 0; i < lines; i++) {
expect(
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
textLines[i],
'line $i',
);
}
Expand Down Expand Up @@ -467,6 +469,44 @@ void main() {
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});

const testMarkdownText = '''
# I'm h1
## I'm h2
### I'm h3
#### I'm h4
##### I'm h5
###### I'm h6''';

testWidgets('paste markdowns', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
expect(text, 'I\'m h$i');
}
},
);
});

testWidgets('paste markdowns as plain', (tester) async {
await tester.pasteContent(
plainText: testMarkdownText,
pasteAsPlain: true,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (int i = 1; i <= children.length; i++) {
final text = children[i - 1].delta!.toPlainText();
final expectText = '${'#' * i} I\'m h$i';
expect(text, expectText);
}
},
);
});
}

extension on WidgetTester {
Expand All @@ -476,6 +516,7 @@ extension on WidgetTester {
String? plainText,
String? html,
String? inAppJson,
bool pasteAsPlain = false,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
Expand All @@ -502,6 +543,7 @@ extension on WidgetTester {
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isShiftPressed: pasteAsPlain,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ void main() {

// set clipboard data
final data = [
"123456\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"1234567\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"12345678\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
"123456\n\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
"1234567\n\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
"12345678\n\n",
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
].join();
await getIt<ClipboardService>().setData(
ClipboardServiceData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ final List<List<ContextMenuItem>> customContextMenuItems = [
getName: LocaleKeys.document_plugins_contextMenu_paste.tr,
onPressed: (editorState) => customPasteCommand.execute(editorState),
),
ContextMenuItem(
getName: LocaleKeys.document_plugins_contextMenu_pasteAsPlainText.tr,
onPressed: (editorState) =>
customPastePlainTextCommand.execute(editorState),
),
ContextMenuItem(
getName: LocaleKeys.document_plugins_contextMenu_cut.tr,
onPressed: (editorState) => customCutCommand.execute(editorState),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ final CommandShortcutEvent customPasteCommand = CommandShortcutEvent(
handler: _pasteCommandHandler,
);

final CommandShortcutEvent customPastePlainTextCommand = CommandShortcutEvent(
key: 'paste the plain content',
getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent,
command: 'ctrl+shift+v',
macOSCommand: 'cmd+shift+v',
handler: _pastePlainCommandHandler,
);

CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null) {
Expand All @@ -45,6 +53,22 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
return KeyEventResult.handled;
};

CommandShortcutEventHandler _pastePlainCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null) {
return KeyEventResult.ignored;
}

doPlainPaste(editorState).then((_) {
final context = editorState.document.root.context;
if (context != null && context.mounted) {
context.read<ClipboardState>().didPaste();
}
});

return KeyEventResult.handled;
};

Future<void> doPaste(EditorState editorState) async {
final selection = editorState.selection;
if (selection == null) {
Expand Down Expand Up @@ -119,7 +143,7 @@ Future<void> doPaste(EditorState editorState) async {
}

if (plainText != null && plainText.isNotEmpty) {
await editorState.pastePlainText(plainText);
await editorState.pasteText(plainText);
return Log.info('Pasted plain text');
}

Expand Down Expand Up @@ -190,3 +214,24 @@ Future<bool> _pasteAsLinkPreview(

return true;
}

Future<void> doPlainPaste(EditorState editorState) async {
final selection = editorState.selection;
if (selection == null) {
return;
}

EditorNotification.paste().post();

// dispatch the paste event
final data = await getIt<ClipboardService>().getData();
final plainText = data.plainText;
if (plainText != null && plainText.isNotEmpty) {
await editorState.pastePlainText(plainText);
Log.info('Pasted plain text');
return;
}

Log.info('unable to parse the clipboard content');
return;
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

extension PasteFromPlainText on EditorState {
Future<void> pastePlainText(String plainText) async {
if (await pasteHtmlIfAvailable(plainText)) {
return;
}

await deleteSelectionIfNeeded();

final nodes = plainText
.split('\n')
.map(
(e) => e
..replaceAll(r'\r', '')
..trimRight(),
)
.map((e) {
// parse the url content
final Attributes attributes = {};
if (hrefRegex.hasMatch(e)) {
attributes[AppFlowyRichTextKeys.href] = e;
}
return Delta()..insert(e, attributes: attributes);
})
.map((e) => Delta()..insert(e))
.map((e) => paragraphNode(delta: e))
.toList();
if (nodes.isEmpty) {
Expand All @@ -36,6 +25,24 @@ extension PasteFromPlainText on EditorState {
}
}

Future<void> pasteText(String plainText) async {
if (await pasteHtmlIfAvailable(plainText)) {
return;
}

await deleteSelectionIfNeeded();

final nodes = customMarkdownToDocument(plainText).root.children;
if (nodes.isEmpty) {
return;
}
if (nodes.length == 1) {
await pasteSingleLineNode(nodes.first);
} else {
await pasteMultiLineNodes(nodes.toList());
}
}

Future<bool> pasteHtmlIfAvailable(String plainText) async {
final selection = this.selection;
if (selection == null ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ List<CommandShortcutEvent> commandShortcutEvents = [

customCopyCommand,
customPasteCommand,
customPastePlainTextCommand,
customCutCommand,
customUndoCommand,
customRedoCommand,
Expand All @@ -43,6 +44,7 @@ List<CommandShortcutEvent> commandShortcutEvents = [
copyCommand,
cutCommand,
pasteCommand,
pasteTextWithoutFormattingCommand,
toggleTodoListCommand,
undoCommand,
redoCommand,
Expand Down
3 changes: 2 additions & 1 deletion frontend/resources/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1847,7 +1847,8 @@
"contextMenu": {
"copy": "Copy",
"cut": "Cut",
"paste": "Paste"
"paste": "Paste",
"pasteAsPlainText": "Paste as plain text"
},
"action": "Actions",
"database": {
Expand Down

0 comments on commit 3959cdb

Please sign in to comment.