fix: lose nested children when accepting AI responses (#7760)

* chore: add gitkeep in assets/font dir

* Revert "feat: improve white label scripts on Windows (#7755)"

This reverts commit a5eb2cdd9a0171ecbc442aef09a8cf8db469a214.

* chore: use --verbose

* fix: lose nested children when accept ai response

* chore: lock analyzer version

* Reapply "feat: improve white label scripts on Windows (#7755)"

This reverts commit c73186306eaf3e9f78114fcc29e39e3a2bce528d.

* chore: lock analyzer version

* chore: update editor version
This commit is contained in:
Lucas 2025-04-16 15:59:43 +08:00 committed by GitHub
parent a5eb2cdd9a
commit f727dde74b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 84 additions and 23 deletions

View File

@ -19,7 +19,7 @@ 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;
const _aiWriterCubitDebugLog = true;
class AiWriterCubit extends Cubit<AiWriterState> {
AiWriterCubit({

View File

@ -34,7 +34,15 @@ extension AiWriterNodeExtension on EditorState {
// if the selected nodes are not entirely selected, slice the nodes
final slicedNodes = <Node>[];
final nodes = getNodesInSelection(selection);
final List<Node> flattenNodes = getNodesInSelection(selection);
final List<Node> nodes = [];
for (final node in flattenNodes) {
if (nodes.any((element) => element.isParentOf(node))) {
continue;
}
nodes.add(node);
}
for (final node in nodes) {
final delta = node.delta;
@ -76,7 +84,7 @@ extension AiWriterNodeExtension on EditorState {
// 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',
lineBreak: '\n',
);
// trim the last \n if it exists

View File

@ -398,6 +398,15 @@ class MarkdownTextRobot {
// it means the user selected the entire sentence, we just replace the node
if (startIndex == 0 && length == node.delta?.length) {
if (nodes.isNotEmpty && node.children.isNotEmpty) {
// merge the children of the selected node and the first node of the ai response
nodes[0] = nodes[0].copyWith(
children: [
...node.children.map((e) => e.deepCopy()),
...nodes[0].children,
],
);
}
transaction
..insertNodes(node.path.next, nodes)
..deleteNode(node);
@ -441,7 +450,14 @@ class MarkdownTextRobot {
).root.children;
// Get the selected nodes.
final nodes = editorState.getNodesInSelection(selection);
final flattenNodes = editorState.getNodesInSelection(selection);
final nodes = <Node>[];
for (final node in flattenNodes) {
if (nodes.any((element) => element.isParentOf(node))) {
continue;
}
nodes.add(node);
}
// 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
@ -465,9 +481,23 @@ class MarkdownTextRobot {
transaction
..deleteText(firstNode, startIndex, length)
..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);
// if the first markdown node has children, we need to insert the children
// and delete the children of the first node that are in the selection.
if (firstMarkdownNode.children.isNotEmpty) {
transaction.insertNodes(
firstNode.path.child(0),
firstMarkdownNode.children.map((e) => e.deepCopy()),
);
}
final nodesToDelete =
firstNode.children.where((e) => e.path.inSelection(selection));
transaction.deleteNodes(nodesToDelete);
}
// step 2
bool handledLastNode = false;
final lastNode = nodes.lastOrNull;
final lastDelta = lastNode?.delta;
final lastMarkdownNode = markdownNodes.lastOrNull;
@ -475,7 +505,10 @@ class MarkdownTextRobot {
if (lastNode != null &&
lastDelta != null &&
lastMarkdownNode != null &&
lastMarkdownDelta != null) {
lastMarkdownDelta != null &&
firstNode?.id != lastNode.id) {
handledLastNode = true;
final endIndex = selection.endIndex;
transaction.deleteText(lastNode, 0, endIndex);
@ -484,15 +517,30 @@ class MarkdownTextRobot {
// selected text in the first node.
if (lastMarkdownNode.id != firstMarkdownNode?.id) {
transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);
if (lastMarkdownNode.children.isNotEmpty) {
transaction
..insertNodes(
lastNode.path.child(0),
lastMarkdownNode.children.map((e) => e.deepCopy()),
)
..deleteNodes(
lastNode.children.where((e) => e.path.inSelection(selection)),
);
}
}
}
// step 3
final insertedPath = selection.start.path.nextNPath(1);
if (markdownNodes.length > 2) {
final insertLength = handledLastNode ? 2 : 1;
if (markdownNodes.length > insertLength) {
transaction.insertNodes(
insertedPath,
markdownNodes.skip(1).take(markdownNodes.length - 2).toList(),
markdownNodes
.skip(1)
.take(markdownNodes.length - insertLength)
.toList(),
);
}

View File

@ -19,6 +19,7 @@ dependencies:
freezed_annotation: ^2.1.0
file_picker: ^8.0.2
file: ^7.0.0
analyzer: 6.11.0
dev_dependencies:
build_runner: ^2.4.9

View File

@ -31,6 +31,8 @@ dependencies:
flowy_svg:
path: ../flowy_svg
analyzer: 6.11.0
dev_dependencies:
build_runner: ^2.4.9
provider: ^6.0.5

View File

@ -15,7 +15,7 @@ packages:
source: sdk
version: "0.3.3"
analyzer:
dependency: transitive
dependency: "direct main"
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
@ -98,8 +98,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "4967ed5"
resolved-ref: "4967ed57d9190948c08f868972c0babfdc470ba7"
ref: "2361899"
resolved-ref: "23618990b2f4ab88df67d50598b2b53cd6853e0a"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "5.1.0"

View File

@ -2,13 +2,13 @@ name: appflowy
description: Bring projects, wikis, and teams together with AI. AppFlowy is an
AI collaborative workspace where you achieve more without losing control of
your data. The best open source alternative to Notion.
publish_to: 'none'
publish_to: "none"
version: 0.8.9
environment:
flutter: '>=3.27.4'
sdk: '>=3.3.0 <4.0.0'
flutter: ">=3.27.4"
sdk: ">=3.3.0 <4.0.0"
dependencies:
any_date: ^1.0.4
@ -39,7 +39,7 @@ dependencies:
calendar_view:
git:
url: https://github.com/Xazin/flutter_calendar_view
ref: '6fe0c98'
ref: "6fe0c98"
collection: ^1.17.1
connectivity_plus: ^5.0.2
cross_file: ^0.3.4+1
@ -75,7 +75,7 @@ dependencies:
flutter_emoji_mart:
git:
url: https://github.com/LucasXu0/emoji_mart.git
ref: '355aa56'
ref: "355aa56"
flutter_math_fork: ^0.7.3
flutter_slidable: ^3.0.0
@ -152,6 +152,8 @@ dependencies:
talker_bloc_logger: ^4.7.1
talker: ^4.7.1
analyzer: 6.11.0
dev_dependencies:
# Introduce talker to log the bloc events, and only log the events in the development mode
@ -185,13 +187,13 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: '4967ed5'
ref: "2361899"
appflowy_editor_plugins:
git:
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
path: 'packages/appflowy_editor_plugins'
ref: '4efcff7'
path: "packages/appflowy_editor_plugins"
ref: "4efcff7"
sheet:
git:

View File

@ -66,9 +66,9 @@ if [ "$exclude_packages" = false ]; then
fi
fi
if [ "$verbose" = true ]; then
dart run build_runner build
dart run build_runner build --delete-conflicting-outputs
else
dart run build_runner build >/dev/null 2>&1
dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1
fi
echo "🧊 Done generating freezed files ($d)."
fi
@ -108,9 +108,9 @@ fi
# Start the build_runner in the background
if [ "$verbose" = true ]; then
dart run build_runner build -d &
dart run build_runner build --delete-conflicting-outputs &
else
dart run build_runner build -d >/dev/null 2>&1 &
dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 &
fi
# Get the PID of the background process

View File

@ -64,7 +64,7 @@ cd ..
cd freezed
# Allow execution permissions on CI
chmod +x ./generate_freezed.sh
./generate_freezed.sh "${args[@]}" --show-loading
./generate_freezed.sh "${args[@]}" --show-loading --verbose
# Return to the original directory
cd "$original_dir"