mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-06-27 02:50:15 +00:00
fix: improve the document diff function to prevent partial ordering issues (#7217)
* fix: improve the document diff function to prevent partial ordering issues * fix: improve the document diff function to prevent partial ordering issues * fix: nested block padding issues * fix: improve the document diff function to prevent partial ordering issues * chore: update editor version * test: add no diff test and update text diff test * test: delete and insert text diff with different id * test: insert single text / delete single text tests * test: multiple delete and update diff * test: multiple insert and update diff * chore: revert cargo changes * chore: remove unused code * chore: optimize the code logic
This commit is contained in:
parent
eead2d20f5
commit
f35dfaf525
246
frontend/.vscode/launch.json
vendored
246
frontend/.vscode/launch.json
vendored
@ -1,125 +1,125 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -175,36 +175,36 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
|
||||
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
|
||||
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
|
||||
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
|
||||
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
|
||||
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
|
@ -11,6 +11,7 @@ import 'package:appflowy/plugins/document/application/editor_transaction_adapter
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||
@ -18,6 +19,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:appflowy/util/throttle.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
@ -25,11 +27,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show
|
||||
EditorState,
|
||||
AppFlowyEditorLogLevel,
|
||||
TransactionTime,
|
||||
Selection,
|
||||
EditorState,
|
||||
Position,
|
||||
Selection,
|
||||
TransactionTime,
|
||||
paragraphNode;
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -304,7 +306,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
..level = AppFlowyEditorLogLevel.all
|
||||
..handler = (log) {
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.info(log);
|
||||
// Log.info(log);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -363,6 +365,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
|
||||
void _throttleSyncDoc(DocEventPB docEvent) {
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.info('[DocumentBloc] throttle sync doc: ${docEvent.toProto3Json()}');
|
||||
}
|
||||
_syncThrottle.call(() {
|
||||
_onDocumentStateUpdate(docEvent);
|
||||
});
|
||||
@ -448,9 +453,17 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
if (!deepEqual) {
|
||||
Log.error('document integrity check failed');
|
||||
// Enable it to debug the document integrity check failed
|
||||
// Log.error('cloud doc: $cloudJson');
|
||||
// Log.error('local doc: $localJson');
|
||||
assert(false, 'document integrity check failed');
|
||||
Log.error('cloud doc: $cloudJson');
|
||||
Log.error('local doc: $localJson');
|
||||
|
||||
final context = AppGlobals.rootNavKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: 'document integrity check failed',
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_diff.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
@ -16,10 +17,14 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DocumentCollabAdapter {
|
||||
DocumentCollabAdapter(this.editorState, this.docId);
|
||||
DocumentCollabAdapter(
|
||||
this.editorState,
|
||||
this.docId,
|
||||
);
|
||||
|
||||
final EditorState editorState;
|
||||
final String docId;
|
||||
final DocumentDiff diff = const DocumentDiff();
|
||||
|
||||
final _service = DocumentService();
|
||||
|
||||
@ -75,13 +80,13 @@ class DocumentCollabAdapter {
|
||||
return;
|
||||
}
|
||||
|
||||
final ops = diffNodes(editorState.document.root, document.root);
|
||||
final ops = diff.diffDocument(editorState.document, document);
|
||||
if (ops.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use for debugging, DO NOT REMOVE
|
||||
// prettyPrintJson(ops.map((op) => op.toJson()).toList());
|
||||
prettyPrintJson(ops.map((op) => op.toJson()).toList());
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
for (final op in ops) {
|
||||
@ -90,17 +95,17 @@ class DocumentCollabAdapter {
|
||||
await editorState.apply(transaction, isRemote: true);
|
||||
|
||||
// Use for debugging, DO NOT REMOVE
|
||||
// assert(() {
|
||||
// final local = editorState.document.root.toJson();
|
||||
// final remote = document.root.toJson();
|
||||
// if (!const DeepCollectionEquality().equals(local, remote)) {
|
||||
// Log.error('Invalid diff status');
|
||||
// Log.error('Local: $local');
|
||||
// Log.error('Remote: $remote');
|
||||
// return false;
|
||||
// }
|
||||
// return true;
|
||||
// }());
|
||||
assert(() {
|
||||
final local = editorState.document.root.toJson();
|
||||
final remote = document.root.toJson();
|
||||
if (!const DeepCollectionEquality().equals(local, remote)) {
|
||||
Log.error('Invalid diff status');
|
||||
Log.error('Local: $local');
|
||||
Log.error('Remote: $remote');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
Future<void> forceReload() async {
|
||||
|
@ -0,0 +1,172 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// DocumentDiff compares two document states and generates operations needed
|
||||
/// to transform one state into another.
|
||||
class DocumentDiff {
|
||||
const DocumentDiff({
|
||||
this.enableDebugLog = false,
|
||||
});
|
||||
|
||||
final bool enableDebugLog;
|
||||
|
||||
// Using DeepCollectionEquality for deep comparison of collections
|
||||
static const _equality = DeepCollectionEquality();
|
||||
|
||||
/// Generates operations needed to transform oldDocument into newDocument.
|
||||
/// Returns a list of operations (Insert, Delete, Update) that can be applied sequentially.
|
||||
List<Operation> diffDocument(Document oldDocument, Document newDocument) {
|
||||
return diffNode(oldDocument.root, newDocument.root);
|
||||
}
|
||||
|
||||
/// Compares two nodes and their children recursively to generate transformation operations.
|
||||
/// Returns a list of operations that will transform oldNode into newNode.
|
||||
List<Operation> diffNode(Node oldNode, Node newNode) {
|
||||
final operations = <Operation>[];
|
||||
|
||||
// Compare and update node attributes if they're different.
|
||||
// Using DeepCollectionEquality instead of == for deep comparison of collections
|
||||
if (!_equality.equals(oldNode.attributes, newNode.attributes)) {
|
||||
operations.add(
|
||||
UpdateOperation(oldNode.path, newNode.attributes, oldNode.attributes),
|
||||
);
|
||||
}
|
||||
|
||||
final oldChildrenById = Map<String, Node>.fromEntries(
|
||||
oldNode.children.map((child) => MapEntry(child.id, child)),
|
||||
);
|
||||
final newChildrenById = Map<String, Node>.fromEntries(
|
||||
newNode.children.map((child) => MapEntry(child.id, child)),
|
||||
);
|
||||
|
||||
// Insertion or Update
|
||||
for (final newChild in newNode.children) {
|
||||
final oldChild = oldChildrenById[newChild.id];
|
||||
if (oldChild == null) {
|
||||
// If the node doesn't exist in the old document, it's a new node.
|
||||
operations.add(InsertOperation(newChild.path, [newChild]));
|
||||
} else {
|
||||
// If the node exists in the old document, recursively compare its children
|
||||
operations.addAll(diffNode(oldChild, newChild));
|
||||
}
|
||||
}
|
||||
|
||||
// Deletion
|
||||
for (final id in oldChildrenById.keys) {
|
||||
// If the node doesn't exist in the new document, it's a deletion.
|
||||
if (!newChildrenById.containsKey(id)) {
|
||||
final oldChild = oldChildrenById[id]!;
|
||||
operations.add(DeleteOperation(oldChild.path, [oldChild]));
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize operations by merging consecutive inserts and deletes
|
||||
return _optimizeOperations(operations);
|
||||
}
|
||||
|
||||
/// Optimizes the list of operations by merging consecutive operations where possible.
|
||||
/// This reduces the total number of operations that need to be applied.
|
||||
List<Operation> _optimizeOperations(List<Operation> operations) {
|
||||
// Optimize the insert operations first, then the delete operations
|
||||
final optimizedOps = mergeDeleteOperations(
|
||||
mergeInsertOperations(
|
||||
operations,
|
||||
),
|
||||
);
|
||||
return optimizedOps;
|
||||
}
|
||||
|
||||
/// Merges consecutive insert operations to reduce the number of operations.
|
||||
/// Operations are merged if they target consecutive paths in the document.
|
||||
List<Operation> mergeInsertOperations(List<Operation> operations) {
|
||||
if (enableDebugLog) {
|
||||
_logOperations('mergeInsertOperations[before]', operations);
|
||||
}
|
||||
|
||||
final copy = [...operations];
|
||||
final insertOperations = operations
|
||||
.whereType<InsertOperation>()
|
||||
.sorted(_descendingCompareTo)
|
||||
.toList();
|
||||
|
||||
_mergeConsecutiveOperations<InsertOperation>(
|
||||
insertOperations,
|
||||
(prev, current) => InsertOperation(
|
||||
prev.path,
|
||||
[...prev.nodes, ...current.nodes],
|
||||
),
|
||||
);
|
||||
|
||||
if (insertOperations.isNotEmpty) {
|
||||
copy
|
||||
..removeWhere((op) => op is InsertOperation)
|
||||
..insertAll(0, insertOperations); // Insert ops must be at the start
|
||||
}
|
||||
|
||||
if (enableDebugLog) {
|
||||
_logOperations('mergeInsertOperations[after]', copy);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Merges consecutive delete operations to reduce the number of operations.
|
||||
/// Operations are merged if they target consecutive paths in the document.
|
||||
List<Operation> mergeDeleteOperations(List<Operation> operations) {
|
||||
if (enableDebugLog) {
|
||||
_logOperations('mergeDeleteOperations[before]', operations);
|
||||
}
|
||||
|
||||
final copy = [...operations];
|
||||
final deleteOperations = operations
|
||||
.whereType<DeleteOperation>()
|
||||
.sorted(_descendingCompareTo)
|
||||
.toList();
|
||||
|
||||
_mergeConsecutiveOperations<DeleteOperation>(
|
||||
deleteOperations,
|
||||
(prev, current) => DeleteOperation(
|
||||
prev.path,
|
||||
[...prev.nodes, ...current.nodes],
|
||||
),
|
||||
);
|
||||
|
||||
if (deleteOperations.isNotEmpty) {
|
||||
copy
|
||||
..removeWhere((op) => op is DeleteOperation)
|
||||
..addAll(deleteOperations); // Delete ops must be at the end
|
||||
}
|
||||
|
||||
if (enableDebugLog) {
|
||||
_logOperations('mergeDeleteOperations[after]', copy);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Merge consecutive operations of the same type
|
||||
void _mergeConsecutiveOperations<T extends Operation>(
|
||||
List<T> operations,
|
||||
T Function(T prev, T current) merge,
|
||||
) {
|
||||
for (var i = operations.length - 1; i > 0; i--) {
|
||||
final op = operations[i];
|
||||
final previousOp = operations[i - 1];
|
||||
|
||||
if (op.path.equals(previousOp.path.next)) {
|
||||
operations
|
||||
..removeAt(i)
|
||||
..[i - 1] = merge(previousOp, op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _logOperations(String prefix, List<Operation> operations) {
|
||||
debugPrint('$prefix: ${operations.map((op) => op.toJson()).toList()}');
|
||||
}
|
||||
|
||||
int _descendingCompareTo(Operation a, Operation b) {
|
||||
return a.path > b.path ? 1 : -1;
|
||||
}
|
||||
}
|
@ -105,9 +105,16 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
|
||||
|
||||
return const EdgeInsets.symmetric(vertical: 5.0);
|
||||
},
|
||||
indentPadding: (node, textDirection) => textDirection == TextDirection.ltr
|
||||
? const EdgeInsets.only(left: 26.0)
|
||||
: const EdgeInsets.only(right: 26.0),
|
||||
indentPadding: (node, textDirection) {
|
||||
double padding = 26.0;
|
||||
// only add indent padding for the top level node to align the children
|
||||
if (UniversalPlatform.isMobile && node.path.length == 1) {
|
||||
padding += EditorStyleCustomizer.nodeHorizontalPadding;
|
||||
}
|
||||
return textDirection == TextDirection.ltr
|
||||
? EdgeInsets.only(left: padding)
|
||||
: EdgeInsets.only(right: padding);
|
||||
},
|
||||
);
|
||||
return configuration;
|
||||
}
|
||||
|
@ -287,7 +287,9 @@ class _ToggleListBlockComponentWidgetState
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: indentPadding,
|
||||
padding: UniversalPlatform.isMobile
|
||||
? const EdgeInsets.symmetric(horizontal: 26.0)
|
||||
: indentPadding,
|
||||
child: FlowyButton(
|
||||
text: FlowyText(
|
||||
buildPlaceholderText(),
|
||||
|
@ -130,31 +130,31 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||
appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9
|
||||
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38
|
||||
app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
|
||||
appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7
|
||||
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
|
||||
connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5
|
||||
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
|
||||
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6
|
||||
hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c
|
||||
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
|
||||
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2
|
||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
|
||||
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
|
||||
|
||||
|
@ -61,8 +61,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "5352bb4"
|
||||
resolved-ref: "5352bb4a2483039b97073f40c32a93f8fc5e7242"
|
||||
ref: "7b33754"
|
||||
resolved-ref: "7b33754d63841868ef4e391d1385063ef2835a61"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
|
@ -174,7 +174,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "5352bb4"
|
||||
ref: "7b33754"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
@ -0,0 +1,403 @@
|
||||
import 'package:appflowy/plugins/document/application/document_diff.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('document diff:', () {
|
||||
setUpAll(() {
|
||||
Log.shared.disableLog = true;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
Log.shared.disableLog = false;
|
||||
});
|
||||
|
||||
const diff = DocumentDiff();
|
||||
|
||||
Node createNodeWithId({required String id, required String text}) {
|
||||
return Node(
|
||||
id: id,
|
||||
type: ParagraphBlockKeys.type,
|
||||
attributes: {
|
||||
ParagraphBlockKeys.delta: (Delta()..insert(text)).toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> applyOperationAndVerifyDocument(
|
||||
Document before,
|
||||
Document after,
|
||||
List<Operation> operations,
|
||||
) async {
|
||||
final expected = after.toJson();
|
||||
final editorState = EditorState(document: before);
|
||||
final transaction = editorState.transaction;
|
||||
for (final operation in operations) {
|
||||
transaction.add(operation);
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
expect(editorState.document.toJson(), expected);
|
||||
}
|
||||
|
||||
test('no diff when the document is the same', () async {
|
||||
// create two nodes with the same id and texts
|
||||
final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');
|
||||
final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy');
|
||||
|
||||
final previous = Document.blank()..insert([0], [node1]);
|
||||
final next = Document.blank()..insert([0], [node2]);
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations, isEmpty);
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('update text diff with the same id', () async {
|
||||
final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');
|
||||
final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy 2');
|
||||
|
||||
final previous = Document.blank()..insert([0], [node1]);
|
||||
final next = Document.blank()..insert([0], [node2]);
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 1);
|
||||
expect(operations[0], isA<UpdateOperation>());
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('delete and insert text diff with different id', () async {
|
||||
final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');
|
||||
final node2 = createNodeWithId(id: '2', text: 'Hello AppFlowy 2');
|
||||
|
||||
final previous = Document.blank()..insert([0], [node1]);
|
||||
final next = Document.blank()..insert([0], [node2]);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 2);
|
||||
expect(operations[0], isA<InsertOperation>());
|
||||
expect(operations[1], isA<DeleteOperation>());
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('insert single text diff', () async {
|
||||
final node1 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node22 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
|
||||
final previous = Document.blank()..insert([0], [node1]);
|
||||
final next = Document.blank()..insert([0], [node21, node22]);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 1);
|
||||
expect(operations[0], isA<InsertOperation>());
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('delete single text diff', () async {
|
||||
final node11 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node12 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
|
||||
final previous = Document.blank()..insert([0], [node11, node12]);
|
||||
final next = Document.blank()..insert([0], [node21]);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 1);
|
||||
expect(operations[0], isA<DeleteOperation>());
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('insert multiple texts diff', () async {
|
||||
final node11 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node15 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node22 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
final node23 = createNodeWithId(
|
||||
id: '3',
|
||||
text: 'Hello AppFlowy - Third line',
|
||||
);
|
||||
final node24 = createNodeWithId(
|
||||
id: '4',
|
||||
text: 'Hello AppFlowy - Fourth line',
|
||||
);
|
||||
final node25 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
|
||||
final previous = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node11,
|
||||
node15,
|
||||
],
|
||||
);
|
||||
final next = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node21,
|
||||
node22,
|
||||
node23,
|
||||
node24,
|
||||
node25,
|
||||
],
|
||||
);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 1);
|
||||
|
||||
final op = operations[0] as InsertOperation;
|
||||
expect(op.path, [1]);
|
||||
expect(op.nodes, [node22, node23, node24]);
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('delete multiple texts diff', () async {
|
||||
final node11 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node12 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
final node13 = createNodeWithId(
|
||||
id: '3',
|
||||
text: 'Hello AppFlowy - Third line',
|
||||
);
|
||||
final node14 = createNodeWithId(
|
||||
id: '4',
|
||||
text: 'Hello AppFlowy - Fourth line',
|
||||
);
|
||||
final node15 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node25 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
|
||||
final previous = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node11,
|
||||
node12,
|
||||
node13,
|
||||
node14,
|
||||
node15,
|
||||
],
|
||||
);
|
||||
final next = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node21,
|
||||
node25,
|
||||
],
|
||||
);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 1);
|
||||
|
||||
final op = operations[0] as DeleteOperation;
|
||||
expect(op.path, [1]);
|
||||
expect(op.nodes, [node12, node13, node14]);
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('multiple delete and update diff', () async {
|
||||
final node11 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node12 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
final node13 = createNodeWithId(
|
||||
id: '3',
|
||||
text: 'Hello AppFlowy - Third line',
|
||||
);
|
||||
final node14 = createNodeWithId(
|
||||
id: '4',
|
||||
text: 'Hello AppFlowy - Fourth line',
|
||||
);
|
||||
final node15 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node22 = createNodeWithId(
|
||||
id: '2',
|
||||
text: '',
|
||||
);
|
||||
final node25 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line',
|
||||
);
|
||||
|
||||
final previous = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node11,
|
||||
node12,
|
||||
node13,
|
||||
node14,
|
||||
node15,
|
||||
],
|
||||
);
|
||||
final next = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node21,
|
||||
node22,
|
||||
node25,
|
||||
],
|
||||
);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 2);
|
||||
final op1 = operations[0] as UpdateOperation;
|
||||
expect(op1.path, [1]);
|
||||
expect(op1.attributes, node22.attributes);
|
||||
|
||||
final op2 = operations[1] as DeleteOperation;
|
||||
expect(op2.path, [2]);
|
||||
expect(op2.nodes, [node13, node14]);
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
|
||||
test('multiple insert and update diff', () async {
|
||||
final node11 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node12 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line',
|
||||
);
|
||||
final node13 = createNodeWithId(
|
||||
id: '3',
|
||||
text: 'Hello AppFlowy - Third line',
|
||||
);
|
||||
final node21 = createNodeWithId(
|
||||
id: '1',
|
||||
text: 'Hello AppFlowy - First line',
|
||||
);
|
||||
final node22 = createNodeWithId(
|
||||
id: '2',
|
||||
text: 'Hello AppFlowy - Second line - Updated',
|
||||
);
|
||||
final node23 = createNodeWithId(
|
||||
id: '3',
|
||||
text: 'Hello AppFlowy - Third line - Updated',
|
||||
);
|
||||
final node24 = createNodeWithId(
|
||||
id: '4',
|
||||
text: 'Hello AppFlowy - Fourth line - Updated',
|
||||
);
|
||||
final node25 = createNodeWithId(
|
||||
id: '5',
|
||||
text: 'Hello AppFlowy - Fifth line - Updated',
|
||||
);
|
||||
|
||||
final previous = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node11,
|
||||
node12,
|
||||
node13,
|
||||
],
|
||||
);
|
||||
final next = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
node21,
|
||||
node22,
|
||||
node23,
|
||||
node24,
|
||||
node25,
|
||||
],
|
||||
);
|
||||
|
||||
final operations = diff.diffDocument(previous, next);
|
||||
|
||||
expect(operations.length, 3);
|
||||
final op1 = operations[0] as InsertOperation;
|
||||
expect(op1.path, [3]);
|
||||
expect(op1.nodes, [node24, node25]);
|
||||
|
||||
final op2 = operations[1] as UpdateOperation;
|
||||
expect(op2.path, [1]);
|
||||
expect(op2.attributes, node22.attributes);
|
||||
|
||||
final op3 = operations[2] as UpdateOperation;
|
||||
expect(op3.path, [2]);
|
||||
expect(op3.attributes, node23.attributes);
|
||||
|
||||
await applyOperationAndVerifyDocument(previous, next, operations);
|
||||
});
|
||||
});
|
||||
}
|
8
frontend/rust-lib/Cargo.lock
generated
8
frontend/rust-lib/Cargo.lock
generated
@ -267,9 +267,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.17"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
|
||||
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
|
||||
dependencies = [
|
||||
"bzip2",
|
||||
"deflate64",
|
||||
@ -2781,9 +2781,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
|
Loading…
x
Reference in New Issue
Block a user