chore: integrate delete line command into cut command (#907)

This commit is contained in:
Lucas 2024-09-30 13:26:39 +08:00 • committed by GitHub
commit c5de9ed84d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 84 additions and 125 deletions

View file

@ -1,43 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
/// Delete the current line
///
/// - support
/// - desktop
/// - web
///
final CommandShortcutEvent deleteLineCommand = CommandShortcutEvent(
key: 'delete line',
getDescription: () => 'Delete the current line',
command: 'ctrl+x',
macOSCommand: 'cmd+x',
handler: _deleteLineCommandHandler,
);
// when the selection is collapsed, press cmd+x(or ctrl+x) should delete the current line
CommandShortcutEventHandler _deleteLineCommandHandler = (editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return KeyEventResult.ignored;
}
final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return KeyEventResult.ignored;
}
final nextNode = node.next;
Selection? afterSelection;
if (nextNode != null && nextNode.delta != null) {
afterSelection = Selection.collapsed(
Position(path: node.path, offset: nextNode.delta?.length ?? 0),
);
}
final transaction = editorState.transaction
..deleteNode(node)
..afterSelection = afterSelection;
editorState.apply(transaction);
return KeyEventResult.handled;
};

View file

@ -6,7 +6,6 @@ export 'base_component/block_component_action_wrapper.dart';
export 'base_component/block_component_configuration.dart'; export 'base_component/block_component_configuration.dart';
// base // base
export 'base_component/convert_to_paragraph_command.dart'; export 'base_component/convert_to_paragraph_command.dart';
export 'base_component/delete_line_command.dart';
export 'base_component/indent_command.dart'; export 'base_component/indent_command.dart';
export 'base_component/insert_newline_in_type_command.dart'; export 'base_component/insert_newline_in_type_command.dart';
export 'base_component/markdown_format_helper.dart'; export 'base_component/markdown_format_helper.dart';

View file

@ -148,9 +148,6 @@ final List<CommandShortcutEvent> standardCommandShortcutEvents = [
// //
selectAllCommand, selectAllCommand,
// delete line
deleteLineCommand,
// copy paste and cut // copy paste and cut
copyCommand, copyCommand,
...pasteCommands, ...pasteCommands,

View file

@ -13,6 +13,9 @@ class AppFlowyClipboardData {
class AppFlowyClipboard { class AppFlowyClipboard {
static AppFlowyClipboardData? _mockData; static AppFlowyClipboardData? _mockData;
@visibleForTesting
static String? lastText;
static Future<void> setData({ static Future<void> setData({
String? text, String? text,
String? html, String? html,
@ -21,6 +24,8 @@ class AppFlowyClipboard {
return; return;
} }
lastText = text;
return Clipboard.setData( return Clipboard.setData(
ClipboardData( ClipboardData(
text: text, text: text,

View file

@ -136,22 +136,38 @@ Selection _computeSelectionAfterPasteMultipleNodes(
void handleCopy(EditorState editorState) async { void handleCopy(EditorState editorState) async {
final selection = editorState.selection?.normalized; final selection = editorState.selection?.normalized;
if (selection == null || selection.isCollapsed) { if (selection == null) {
return; return;
} }
final text = editorState.getTextInSelection(selection).join('\n'); String text;
final nodes = editorState.getSelectedNodes(selection: selection); String html;
if (nodes.isEmpty) {
return; if (selection.isCollapsed) {
} final node = editorState.getNodeAtPath(selection.end.path);
final html = documentToHTML( if (node == null) {
Document( return;
root: Node( }
type: 'page', text = node.delta?.toPlainText() ?? '';
children: nodes, html = documentToHTML(
Document(
root: pageNode(children: [node.copyWith()]),
), ),
), );
); } else {
text = editorState.getTextInSelection(selection).join('\n');
final nodes = editorState.getSelectedNodes(selection: selection);
if (nodes.isEmpty) {
return;
}
html = documentToHTML(
Document(
root: pageNode(
children: nodes.map((node) => node.copyWith()),
),
),
);
}
return AppFlowyClipboard.setData( return AppFlowyClipboard.setData(
text: text, text: text,
html: html.isEmpty ? null : html, html: html.isEmpty ? null : html,
@ -332,13 +348,30 @@ void handleCut(EditorState editorState) {
deleteSelectedContent(editorState); deleteSelectedContent(editorState);
} }
void deleteSelectedContent(EditorState editorState) async { Future<void> deleteSelectedContent(EditorState editorState) async {
final selection = editorState.selection?.normalized; final selection = editorState.selection?.normalized;
if (selection == null || selection.isCollapsed) { if (selection == null) {
return; return;
} }
final tb = editorState.transaction; final transaction = editorState.transaction;
await editorState.deleteSelection(selection); if (selection.isCollapsed) {
tb.afterSelection = Selection.collapsed(selection.start); // if the selection is collapsed, delete the current node
editorState.apply(tb); final node = editorState.getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
transaction.deleteNode(node);
final nextNode = node.next;
if (nextNode != null && nextNode.delta != null) {
transaction.afterSelection = Selection.collapsed(
Position(path: node.path, offset: nextNode.delta?.length ?? 0),
);
}
} else {
// if the selection is not collapsed, delete the selection
await editorState.deleteSelection(selection);
transaction.afterSelection = Selection.collapsed(selection.start);
}
await editorState.apply(transaction);
} }

View file

@ -1,19 +1,46 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../../../infra/testable_editor.dart'; import '../../../infra/testable_editor.dart';
import '../../../util/util.dart';
void main() async { void main() async {
setUpAll(() { setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
}); });
group('cut_command_test.dart', () { group('cut_command_test.dart', () {
const text = 'Welcome to AppFlowy Editor 🔥!';
testWidgets('update selection and execute cut command', (tester) async { testWidgets('update selection and execute cut command', (tester) async {
await _testCutHandle(tester, Document.fromJson(paragraphdata)); await _testCutHandle(tester, Document.fromJson(paragraphdata));
}); });
test('cut current line if the selection is collapsed', () async {
final document = Document.blank().addParagraphs(
2,
builder: (index) => Delta()..insert('${index + 1}. $text'),
);
final editorState = EditorState(document: document);
final selection = Selection.collapsed(
Position(path: [0], offset: text.length),
);
editorState.selection = selection;
final result = cutCommand.execute(editorState);
expect(result, KeyEventResult.handled);
// check the clipboard
final lastText = AppFlowyClipboard.lastText!;
expect(lastText, '1. $text');
final after = editorState.getNodeAtPath([0])!;
expect(after.delta!.toPlainText(), '2. $text');
});
}); });
} }

View file

@ -1,59 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../util/util.dart';
void main() async {
group('deleteLineCommand', () {
group('delete current line if the selection is collapsed', () {
const text = 'Welcome to AppFlowy Editor 🔥!';
test('delete in collapsed selection when the index > 0', () async {
final document = Document.blank().addParagraphs(
2,
builder: (index) => Delta()..insert('${index + 1}. $text'),
);
final editorState = EditorState(document: document);
final selection = Selection.collapsed(
Position(path: [0], offset: text.length),
);
editorState.selection = selection;
final result = deleteLineCommand.execute(editorState);
expect(result, KeyEventResult.handled);
final after = editorState.getNodeAtPath([0])!;
expect(after.delta!.toPlainText(), '2. $text');
});
test('do nothing if the selection is not collapsed', () async {
final document = Document.blank().addParagraphs(
2,
builder: (index) => Delta()..insert('${index + 1}. $text'),
);
final editorState = EditorState(document: document);
final selection = Selection(
start: Position(path: [0], offset: 0),
end: Position(path: [0], offset: text.length),
);
editorState.selection = selection;
final result = deleteLineCommand.execute(editorState);
expect(result, KeyEventResult.ignored);
expect(
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
'1. $text',
);
expect(
editorState.getNodeAtPath([1])!.delta!.toPlainText(),
'2. $text',
);
expect(editorState.selection, selection);
});
});
});
}