mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 06:37:14 -04:00
fix: replace the selected text with ai response in the same line (#7708)
* fix: replace the selected text with ai response in the same line * fix: replace the selected text with ai response in the multiple lines * fix: integrate the replace function in the ai writer cubit * fix: unit test and integration test
This commit is contained in:
parent
5c19c08cb3
commit
c1f6a0efea
8 changed files with 773 additions and 16 deletions
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
@ -15,6 +16,11 @@ import 'ai_writer_block_operations.dart';
|
|||
import 'ai_writer_entities.dart';
|
||||
import 'ai_writer_node_extension.dart';
|
||||
|
||||
/// Enable the debug log for the AiWriterCubit.
|
||||
///
|
||||
/// This is useful for debugging the AI writer cubit.
|
||||
const _aiWriterCubitDebugLog = false;
|
||||
|
||||
class AiWriterCubit extends Cubit<AiWriterState> {
|
||||
AiWriterCubit({
|
||||
required this.documentId,
|
||||
|
@ -95,6 +101,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
final command = node.aiWriterCommand;
|
||||
final (run, prompt) = await _addSelectionTextToRecords(command);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'command: $command, run: $run, prompt: $prompt',
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
await exit();
|
||||
return;
|
||||
|
@ -211,20 +221,26 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Accept
|
||||
//
|
||||
// If the user clicks accept, we need to replace the selection with the AI's response
|
||||
if (action case SuggestionAction.accept) {
|
||||
await _textRobot.persist();
|
||||
await formatSelection(
|
||||
editorState,
|
||||
selection,
|
||||
ApplySuggestionFormatType.clear,
|
||||
// trim the markdown text to avoid extra new lines
|
||||
final trimmedMarkdownText = _textRobot.markdownText.trim();
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'trigger accept action, markdown text: $trimmedMarkdownText',
|
||||
);
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
withUpdateSelection: false,
|
||||
|
||||
await _textRobot.deleteAINodes();
|
||||
|
||||
await _textRobot.replace(
|
||||
selection: selection,
|
||||
markdownText: trimmedMarkdownText,
|
||||
);
|
||||
|
||||
await exit(withDiscard: false, withUnformat: false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -276,17 +292,24 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
AiWriterCommand command,
|
||||
) async {
|
||||
final node = aiWriterNode;
|
||||
|
||||
// check the node is registered
|
||||
if (node == null) {
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
// check the selection is valid
|
||||
final selection = node.aiWriterSelection?.normalized;
|
||||
if (selection == null) {
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
// if the command is continue writing, we don't need to get the selection text
|
||||
if (command == AiWriterCommand.continueWriting) {
|
||||
return (true, '');
|
||||
}
|
||||
|
||||
// if the selection is collapsed, we don't need to get the selection text
|
||||
if (selection.isCollapsed) {
|
||||
return (true, '');
|
||||
}
|
||||
|
@ -297,6 +320,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.user(content: selectionText, format: null),
|
||||
);
|
||||
|
||||
return (true, '');
|
||||
} else {
|
||||
return (true, selectionText);
|
||||
|
@ -540,6 +564,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
attributes: ApplySuggestionFormatType.replace.attributes,
|
||||
);
|
||||
onAppendToDocument?.call();
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'received message: $text',
|
||||
);
|
||||
},
|
||||
processAssistMessage: (text) async {
|
||||
if (state case final GeneratingAiWriterState generatingState) {
|
||||
|
@ -551,6 +579,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'received assist message: $text',
|
||||
);
|
||||
},
|
||||
onEnd: () async {
|
||||
if (state case final GeneratingAiWriterState generatingState) {
|
||||
|
@ -567,6 +599,10 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
records.add(
|
||||
AiWriterRecord.ai(content: _textRobot.markdownText),
|
||||
);
|
||||
|
||||
_aiWriterCubitLog(
|
||||
'returned response: ${_textRobot.markdownText}',
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) async {
|
||||
|
@ -658,6 +694,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _aiWriterCubitLog(String message) {
|
||||
if (_aiWriterCubitDebugLog) {
|
||||
Log.debug('[AiWriterCubit] $message');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mixin RegisteredAiWriter {
|
||||
|
|
|
@ -57,11 +57,30 @@ extension AiWriterNodeExtension on EditorState {
|
|||
slicedNodes.add(copiedNode);
|
||||
}
|
||||
|
||||
for (final (i, node) in slicedNodes.indexed) {
|
||||
final childNodesShouldBeDeleted = <Node>[];
|
||||
for (final child in node.children) {
|
||||
if (!child.path.inSelection(selection)) {
|
||||
childNodesShouldBeDeleted.add(child);
|
||||
}
|
||||
}
|
||||
for (final child in childNodesShouldBeDeleted) {
|
||||
slicedNodes[i] = node.copyWith(
|
||||
children: node.children.where((e) => e.id != child.id).toList(),
|
||||
type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// use \n\n as line break to improve the ai response
|
||||
// using \n will cause the ai response treat the text as a single line
|
||||
final markdown = await customDocumentToMarkdown(
|
||||
Document.blank()..insert([0], slicedNodes),
|
||||
lineBreak: '\n\n',
|
||||
);
|
||||
|
||||
return markdown;
|
||||
// trim the last \n if it exists
|
||||
return markdown.trimRight();
|
||||
}
|
||||
|
||||
List<String> getPlainTextInSelection(Selection? selection) {
|
||||
|
|
|
@ -110,10 +110,13 @@ class MarkdownTextRobot {
|
|||
}
|
||||
|
||||
/// Persist the text into the document
|
||||
Future<void> persist({String? markdownText}) async {
|
||||
Future<void> persist({
|
||||
String? markdownText,
|
||||
}) async {
|
||||
if (markdownText != null) {
|
||||
_markdownText = markdownText;
|
||||
}
|
||||
|
||||
await _lock.synchronized(() async {
|
||||
await _refresh(inMemoryUpdate: false);
|
||||
});
|
||||
|
@ -124,6 +127,34 @@ class MarkdownTextRobot {
|
|||
}
|
||||
}
|
||||
|
||||
/// Replace the selected content with the AI's response
|
||||
Future<void> replace({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
if (selection.isSingle) {
|
||||
await _replaceInSameLine(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
} else {
|
||||
await _replaceInMultiLines(
|
||||
selection: selection,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the temporary inserted AI nodes
|
||||
Future<void> deleteAINodes() async {
|
||||
final nodes = getInsertedNodes();
|
||||
final transaction = editorState.transaction..deleteNodes(nodes);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
|
||||
/// Discard the inserted content
|
||||
Future<void> discard() async {
|
||||
final start = _insertPosition;
|
||||
|
@ -282,6 +313,161 @@ class MarkdownTextRobot {
|
|||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
/// If the selected content is in the same line,
|
||||
/// keep the selected node and replace the delta.
|
||||
Future<void> _replaceInSameLine({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
selection = selection.normalized;
|
||||
|
||||
// If the selection is not a single node, do nothing.
|
||||
if (!selection.isSingle) {
|
||||
assert(false, 'Expected single node selection');
|
||||
Log.error('Expected single node selection');
|
||||
return;
|
||||
}
|
||||
|
||||
final startIndex = selection.startIndex;
|
||||
final endIndex = selection.endIndex;
|
||||
final length = endIndex - startIndex;
|
||||
|
||||
// Get the selected node.
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
assert(false, 'Expected non-null node and delta');
|
||||
Log.error('Expected non-null node and delta');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the markdown text to delta.
|
||||
// Question: Why we need to convert the markdown to document first?
|
||||
// Answer: Because the markdown text may contain the list item,
|
||||
// if we convert the markdown to delta directly, the list item will be
|
||||
// treated as a normal text node, and the delta will be incorrect.
|
||||
// For example, the markdown text is:
|
||||
// ```
|
||||
// 1. item1
|
||||
// ```
|
||||
// if we convert the markdown to delta directly, the delta will be:
|
||||
// ```
|
||||
// [
|
||||
// {
|
||||
// "insert": "1. item1"
|
||||
// }
|
||||
// ]
|
||||
// ```
|
||||
// if we convert the markdown to document first, the document will be:
|
||||
// ```
|
||||
// [
|
||||
// {
|
||||
// "type": "numbered_list",
|
||||
// "children": [
|
||||
// {
|
||||
// "insert": "item1"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
final document = customMarkdownToDocument(markdownText);
|
||||
final decoder = DeltaMarkdownDecoder();
|
||||
final markdownDelta =
|
||||
document.nodeAtPath([0])?.delta ?? decoder.convert(markdownText);
|
||||
|
||||
// Replace the delta of the selected node.
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..deleteText(node, startIndex, length)
|
||||
..insertTextDelta(node, startIndex, markdownDelta);
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
|
||||
/// If the selected content is in multiple lines
|
||||
Future<void> _replaceInMultiLines({
|
||||
required Selection selection,
|
||||
required String markdownText,
|
||||
}) async {
|
||||
selection = selection.normalized;
|
||||
|
||||
// If the selection is a single node, do nothing.
|
||||
if (selection.isSingle) {
|
||||
assert(false, 'Expected multi-line selection');
|
||||
Log.error('Expected multi-line selection');
|
||||
return;
|
||||
}
|
||||
|
||||
final markdownNodes = customMarkdownToDocument(
|
||||
markdownText,
|
||||
tableWidth: 250.0,
|
||||
).root.children;
|
||||
|
||||
// Get the selected nodes.
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
|
||||
// Note: Don't change its order, otherwise the delta will be incorrect.
|
||||
// step 1. merge the first selected node and the first node from the ai response
|
||||
// step 2. merge the last selected node and the last node from the ai response
|
||||
// step 3. insert the middle nodes from the ai response
|
||||
// step 4. delete the middle nodes
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// step 1
|
||||
final firstNode = nodes.firstOrNull;
|
||||
final delta = firstNode?.delta;
|
||||
final firstMarkdownNode = markdownNodes.firstOrNull;
|
||||
final firstMarkdownDelta = firstMarkdownNode?.delta;
|
||||
if (firstNode != null &&
|
||||
delta != null &&
|
||||
firstMarkdownNode != null &&
|
||||
firstMarkdownDelta != null) {
|
||||
final startIndex = selection.startIndex;
|
||||
final length = delta.length - startIndex;
|
||||
|
||||
transaction
|
||||
..deleteText(firstNode, startIndex, length)
|
||||
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
|
||||
}
|
||||
|
||||
// step 2
|
||||
final lastNode = nodes.lastOrNull;
|
||||
final lastDelta = lastNode?.delta;
|
||||
final lastMarkdownNode = markdownNodes.lastOrNull;
|
||||
final lastMarkdownDelta = lastMarkdownNode?.delta;
|
||||
if (lastNode != null &&
|
||||
lastDelta != null &&
|
||||
lastMarkdownNode != null &&
|
||||
lastMarkdownDelta != null) {
|
||||
final endIndex = selection.endIndex;
|
||||
|
||||
transaction.deleteText(lastNode, 0, endIndex);
|
||||
|
||||
// if the last node is same as the first node, it means we have replaced the
|
||||
// selected text in the first node.
|
||||
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
|
||||
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// step 3
|
||||
final insertedPath = selection.start.path.nextNPath(1);
|
||||
if (markdownNodes.length > 2) {
|
||||
transaction.insertNodes(
|
||||
insertedPath,
|
||||
markdownNodes.skip(1).take(markdownNodes.length - 2).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// step 4
|
||||
final length = nodes.length - 2;
|
||||
if (length > 0) {
|
||||
final middleNodes = nodes.skip(1).take(length).toList();
|
||||
transaction.deleteNodes(middleNodes);
|
||||
}
|
||||
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
class AINodeExternalValues extends NodeExternalValues {
|
||||
|
|
|
@ -27,6 +27,7 @@ Future<String> customDocumentToMarkdown(
|
|||
Document document, {
|
||||
String path = '',
|
||||
AsyncValueSetter<Archive>? onArchive,
|
||||
String lineBreak = '',
|
||||
}) async {
|
||||
final List<Future<ArchiveFile>> fileFutures = [];
|
||||
|
||||
|
@ -41,6 +42,7 @@ Future<String> customDocumentToMarkdown(
|
|||
try {
|
||||
markdown = documentToMarkdown(
|
||||
document,
|
||||
lineBreak: lineBreak,
|
||||
customParsers: [
|
||||
const MathEquationNodeParser(),
|
||||
const CalloutNodeParser(),
|
||||
|
|
|
@ -98,8 +98,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "552f95f"
|
||||
resolved-ref: "552f95fd15627e10a138c6db2a6d0a8089bc9a25"
|
||||
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||
resolved-ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "5.1.0"
|
||||
|
|
|
@ -184,7 +184,7 @@ dependency_overrides:
|
|||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "552f95f"
|
||||
ref: "361b99c38370abeeb19656f89e8c31cb3666623b"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
|
|
@ -375,7 +375,7 @@ void main() {
|
|||
await blocResponseFuture();
|
||||
bloc.runResponseAction(SuggestionAction.accept);
|
||||
await blocResponseFuture();
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
expect(
|
||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||
'Hello World',
|
||||
|
|
|
@ -294,6 +294,514 @@ void main() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('markdown text robot - replace in same line:', () {
|
||||
final text1 =
|
||||
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
|
||||
final text2 =
|
||||
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
|
||||
final text3 =
|
||||
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
|
||||
|
||||
Document buildTestDocument() {
|
||||
return Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(delta: Delta()..insert(text1 + text2 + text3)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 1. create a document with a paragraph node
|
||||
// 2. use the text robot to replace the selected content in the same line
|
||||
// 3. check the document
|
||||
test('the selection is in the middle of the text', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: text1.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 5);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'World Wide Web');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(d3.text, ' transformed the internet, making it accessible to ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'non-technical users');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(
|
||||
d5.text,
|
||||
' and opening the floodgates for global mass adoption.$text3',
|
||||
);
|
||||
expect(d5.attributes, null);
|
||||
});
|
||||
|
||||
test('replace markdown text with selection from start to middle', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 5);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(d1.text, 'The ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'invention');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(d3.text, ' of the ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'World Wide Web');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(
|
||||
d5.text,
|
||||
' by Tim Berners-Lee transformed how we access information.$text2$text3',
|
||||
);
|
||||
expect(d5.attributes, null);
|
||||
});
|
||||
|
||||
test('replace markdown text with selection from middle to end', () async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: text1.length + text2.length + text3.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterDelta = editorState.document.root.children[0].delta!.toList();
|
||||
expect(afterDelta.length, 7);
|
||||
|
||||
final d1 = afterDelta[0] as TextInsert;
|
||||
expect(
|
||||
d1.text,
|
||||
text1 + text2,
|
||||
);
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = afterDelta[1] as TextInsert;
|
||||
expect(d2.text, 'Email');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = afterDelta[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
' became widespread, and instant messaging services like ',
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = afterDelta[3] as TextInsert;
|
||||
expect(d4.text, 'ICQ');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = afterDelta[4] as TextInsert;
|
||||
expect(d5.text, ' and ');
|
||||
expect(d5.attributes, null);
|
||||
|
||||
final d6 = afterDelta[5] as TextInsert;
|
||||
expect(
|
||||
d6.text,
|
||||
'AOL Instant Messenger',
|
||||
);
|
||||
expect(d6.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d7 = afterDelta[6] as TextInsert;
|
||||
expect(
|
||||
d7.text,
|
||||
' gained tremendous popularity, allowing for seamless real-time text communication across the globe.',
|
||||
);
|
||||
expect(d7.attributes, null);
|
||||
});
|
||||
});
|
||||
|
||||
group('markdown text robot - replace in multiple lines:', () {
|
||||
final text1 =
|
||||
'''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';
|
||||
final text2 =
|
||||
'''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';
|
||||
final text3 =
|
||||
'''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';
|
||||
|
||||
Document buildTestDocument() {
|
||||
return Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
paragraphNode(delta: Delta()..insert(text1)),
|
||||
paragraphNode(delta: Delta()..insert(text2)),
|
||||
paragraphNode(delta: Delta()..insert(text3)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 1. create a document with 3 paragraph nodes
|
||||
// 2. use the text robot to replace the selected content in the multiple lines
|
||||
// 3. check the document
|
||||
test(
|
||||
'the selection starts with the first paragraph and ends with the middle of second paragraph',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [1],
|
||||
offset: text2.length -
|
||||
', opening the floodgates for mass adoption. '.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
'''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point.
|
||||
|
||||
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 3);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, 'The ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, 'introduction');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, ' of the World Wide Web in the ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, 'early 1990s');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, ' marked a significant turning point.');
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 3);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, "Tim Berners-Lee's ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta2[1] as TextInsert;
|
||||
expect(d2.text, "revolutionary invention");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta2[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
" made the internet accessible to non-technical users, opening the floodgates for mass adoption. ",
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// third paragraph
|
||||
final delta3 = afterNodes[2].delta!.toList();
|
||||
expect(delta3.length, 1);
|
||||
|
||||
final d1 = delta3[0] as TextInsert;
|
||||
expect(d1.text, text3);
|
||||
expect(d1.attributes, null);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 'The introduction of the World Wide Web'.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset:
|
||||
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
|
||||
.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
''' in the **early 1990s** marked a *significant turning point* in technological history.
|
||||
|
||||
Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*.
|
||||
|
||||
Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity
|
||||
''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 3);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, 'The introduction of the World Wide Web in the ');
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, 'early 1990s');
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, ' marked a ');
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, 'significant turning point');
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, ' in technological history.');
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 5);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, "Tim Berners-Lee's ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta2[1] as TextInsert;
|
||||
expect(d2.text, "revolutionary invention");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta2[2] as TextInsert;
|
||||
expect(
|
||||
d3.text,
|
||||
" made the internet accessible to non-technical users, opening the floodgates for ",
|
||||
);
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta2[3] as TextInsert;
|
||||
expect(d4.text, "unprecedented mass adoption");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta2[4] as TextInsert;
|
||||
expect(d5.text, ".");
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// third paragraph
|
||||
// third paragraph
|
||||
final delta3 = afterNodes[2].delta!.toList();
|
||||
expect(delta3.length, 7);
|
||||
|
||||
final d1 = delta3[0] as TextInsert;
|
||||
expect(d1.text, "Email became ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta3[1] as TextInsert;
|
||||
expect(d2.text, "widely prevalent");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta3[2] as TextInsert;
|
||||
expect(d3.text, ", and instant messaging services like ");
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta3[3] as TextInsert;
|
||||
expect(d4.text, "ICQ");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta3[4] as TextInsert;
|
||||
expect(d5.text, " and ");
|
||||
expect(d5.attributes, null);
|
||||
|
||||
final d6 = delta3[5] as TextInsert;
|
||||
expect(d6.text, "AOL Instant Messenger");
|
||||
expect(d6.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d7 = delta3[6] as TextInsert;
|
||||
expect(
|
||||
d7.text,
|
||||
" gained tremendous popularity, allowing for real-time text communication.",
|
||||
);
|
||||
expect(d7.attributes, null);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'the length of the returned response less than the length of the selected text',
|
||||
() async {
|
||||
final document = buildTestDocument();
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
editorState.selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 'The introduction of the World Wide Web'.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset:
|
||||
'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'
|
||||
.length,
|
||||
),
|
||||
);
|
||||
|
||||
final markdownText =
|
||||
''' in the **early 1990s** marked a *significant turning point* in technological history.''';
|
||||
final markdownTextRobot = MarkdownTextRobot(
|
||||
editorState: editorState,
|
||||
);
|
||||
await markdownTextRobot.replace(
|
||||
selection: editorState.selection!,
|
||||
markdownText: markdownText,
|
||||
);
|
||||
|
||||
final afterNodes = editorState.document.root.children;
|
||||
expect(afterNodes.length, 2);
|
||||
|
||||
{
|
||||
// first paragraph
|
||||
final delta1 = afterNodes[0].delta!.toList();
|
||||
expect(delta1.length, 5);
|
||||
|
||||
final d1 = delta1[0] as TextInsert;
|
||||
expect(d1.text, "The introduction of the World Wide Web in the ");
|
||||
expect(d1.attributes, null);
|
||||
|
||||
final d2 = delta1[1] as TextInsert;
|
||||
expect(d2.text, "early 1990s");
|
||||
expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});
|
||||
|
||||
final d3 = delta1[2] as TextInsert;
|
||||
expect(d3.text, " marked a ");
|
||||
expect(d3.attributes, null);
|
||||
|
||||
final d4 = delta1[3] as TextInsert;
|
||||
expect(d4.text, "significant turning point");
|
||||
expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});
|
||||
|
||||
final d5 = delta1[4] as TextInsert;
|
||||
expect(d5.text, " in technological history.");
|
||||
expect(d5.attributes, null);
|
||||
}
|
||||
|
||||
{
|
||||
// second paragraph
|
||||
final delta2 = afterNodes[1].delta!.toList();
|
||||
expect(delta2.length, 1);
|
||||
|
||||
final d1 = delta2[0] as TextInsert;
|
||||
expect(d1.text, ", allowing for real-time text communication.");
|
||||
expect(d1.attributes, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const _sample1 = '''# The Curious Cat
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue