mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 18:15:09 +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
	 Lucas
						Lucas