Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to paste plain text #7045

Merged
merged 2 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
asjqkkkk marked this conversation as resolved.
Show resolved Hide resolved
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,52 @@ void main() {
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
});
});

testWidgets('paste markdowns', (tester) async {
await tester.pasteContent(
plainText: '''
# I'm h1
## I'm h2
### I'm h3
#### I'm h4
##### I'm h5
###### I'm h6''',
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (var i = 0; i < children.length; i++) {
final text = children[i].delta!.toPlainText();
expect(
text,
'I\'m h${i + 1}',
);
}
asjqkkkk marked this conversation as resolved.
Show resolved Hide resolved
},
);
});

testWidgets('paste markdowns as plain', (tester) async {
const markdown = '''
# I'm h1
## I'm h2
### I'm h3
#### I'm h4
##### I'm h5
###### I'm h6''';
asjqkkkk marked this conversation as resolved.
Show resolved Hide resolved
await tester.pasteContent(
plainText: markdown,
pasteAsPlain: true,
(editorState) {
final children = editorState.document.root.children;
expect(children.length, 6);
for (var i = 0; i < children.length; i++) {
final text = children[i].delta!.toPlainText();
final expectText = '${'#' * (i + 1)} I\'m h${i + 1}';
expect(text, expectText);
}
asjqkkkk marked this conversation as resolved.
Show resolved Hide resolved
},
);
});
}

extension on WidgetTester {
Expand All @@ -476,6 +524,7 @@ extension on WidgetTester {
String? plainText,
String? html,
String? inAppJson,
bool pasteAsPlain = false,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
Expand All @@ -502,6 +551,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,22 @@ 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);
return Log.info('Pasted plain text');
}

return Log.info('unable to parse the clipboard content');
asjqkkkk marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@ -1841,7 +1841,8 @@
"contextMenu": {
"copy": "Copy",
"cut": "Cut",
"paste": "Paste"
"paste": "Paste",
"pasteAsPlainText": "Paste as plain text"
},
"action": "Actions",
"database": {
Expand Down
Loading