mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 01:54:37 +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. |   // Use IntelliSense to learn about possible attributes. | ||||||
|     // Hover to view descriptions of existing attributes. |   // Hover to view descriptions of existing attributes. | ||||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |   // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||||
|     "version": "0.2.0", |   "version": "0.2.0", | ||||||
|     "configurations": [ |   "configurations": [ | ||||||
|         { |       { | ||||||
|             // This task only builds the Dart code of AppFlowy. |           // This task only builds the Dart code of AppFlowy. | ||||||
|             // It supports both the desktop and mobile version. |           // It supports both the desktop and mobile version. | ||||||
|             "name": "AF: Build Dart Only", |           "name": "AF: Build Dart Only", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "debug", |               "RUST_LOG": "debug", | ||||||
|             }, |           }, | ||||||
|             // uncomment the following line to testing performance. |           // uncomment the following line to testing performance. | ||||||
|             // "flutterMode": "profile", |           // "flutterMode": "profile", | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             // This task builds the Rust and Dart code of AppFlowy. |           // This task builds the Rust and Dart code of AppFlowy. | ||||||
|             "name": "AF-desktop: Build All", |           "name": "AF-desktop: Build All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Build Appflowy Core", |           "preLaunchTask": "AF: Build Appflowy Core", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace", |               "RUST_LOG": "trace", | ||||||
|                 "RUST_BACKTRACE": "1" |               "RUST_BACKTRACE": "1" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             // This task builds will: |           // This task builds will: | ||||||
|             // - call the clean task, |           // - call the clean task, | ||||||
|             // - rebuild all the generated Files (including freeze and language files) |           // - rebuild all the generated Files (including freeze and language files) | ||||||
|             // - rebuild the the Rust and Dart code of AppFlowy. |           // - rebuild the the Rust and Dart code of AppFlowy. | ||||||
|             "name": "AF-desktop: Clean + Rebuild All", |           "name": "AF-desktop: Clean + Rebuild All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Clean + Rebuild All", |           "preLaunchTask": "AF: Clean + Rebuild All", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-iOS: Build All", |           "name": "AF-iOS: Build All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Build Appflowy Core For iOS", |           "preLaunchTask": "AF: Build Appflowy Core For iOS", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-iOS: Clean + Rebuild All", |           "name": "AF-iOS: Clean + Rebuild All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Clean + Rebuild All (iOS)", |           "preLaunchTask": "AF: Clean + Rebuild All (iOS)", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-iOS-Simulator: Build All", |           "name": "AF-iOS-Simulator: Build All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Build Appflowy Core For iOS Simulator", |           "preLaunchTask": "AF: Build Appflowy Core For iOS Simulator", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-iOS-Simulator: Clean + Rebuild All", |           "name": "AF-iOS-Simulator: Clean + Rebuild All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)", |           "preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-Android: Build All", |           "name": "AF-Android: Build All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Build Appflowy Core For Android", |           "preLaunchTask": "AF: Build Appflowy Core For Android", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-Android: Clean + Rebuild All", |           "name": "AF-Android: Clean + Rebuild All", | ||||||
|             "request": "launch", |           "request": "launch", | ||||||
|             "program": "./lib/main.dart", |           "program": "./lib/main.dart", | ||||||
|             "type": "dart", |           "type": "dart", | ||||||
|             "preLaunchTask": "AF: Clean + Rebuild All (Android)", |           "preLaunchTask": "AF: Clean + Rebuild All (Android)", | ||||||
|             "env": { |           "env": { | ||||||
|                 "RUST_LOG": "trace" |               "RUST_LOG": "trace" | ||||||
|             }, |           }, | ||||||
|             "cwd": "${workspaceRoot}/appflowy_flutter" |           "cwd": "${workspaceRoot}/appflowy_flutter" | ||||||
|         }, |       }, | ||||||
|         { |       { | ||||||
|             "name": "AF-desktop: Debug Rust", |           "name": "AF-desktop: Debug Rust", | ||||||
|             "type": "lldb", |           "type": "lldb", | ||||||
|             "request": "attach", |           "request": "attach", | ||||||
|             "pid": "${command:pickMyProcess}" |           "pid": "${command:pickMyProcess}" | ||||||
|             // To launch the application directly, use the following configuration: |           // To launch the application directly, use the following configuration: | ||||||
|             // "request": "launch", |           // "request": "launch", | ||||||
|             // "program": "[YOUR_APPLICATION_PATH]", |           // "program": "[YOUR_APPLICATION_PATH]", | ||||||
|         }, |       }, | ||||||
|     ] |   ] | ||||||
| } | } | ||||||
|  | |||||||
| @ -175,36 +175,36 @@ EXTERNAL SOURCES: | |||||||
|     :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" |     :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" | ||||||
| 
 | 
 | ||||||
| SPEC CHECKSUMS: | SPEC CHECKSUMS: | ||||||
|   app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 |   app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 | ||||||
|   appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 |   appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a | ||||||
|   connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d |   connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf | ||||||
|   device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d |   device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 | ||||||
|   DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac |   DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac | ||||||
|   DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 |   DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 | ||||||
|   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 |   file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 | ||||||
|   flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc |   flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 | ||||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 |   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||||
|   fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c |   fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 | ||||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 |   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||||
|   integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 |   integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a | ||||||
|   irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 |   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||||
|   keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 |   keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 | ||||||
|   open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 |   open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 | ||||||
|   package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 |   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 | ||||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 |   permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d | ||||||
|   ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 |   ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 | ||||||
|   SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 |   SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 | ||||||
|   Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 |   Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 | ||||||
|   sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 |   sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 | ||||||
|   share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f |   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a | ||||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 |   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||||
|   sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec |   sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 | ||||||
|   super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 |   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||||
|   SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 |   SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 | ||||||
|   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 |   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 | ||||||
|   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe |   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||||
|   webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 |   webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b | ||||||
| 
 | 
 | ||||||
| PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca | 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/plugins/trash/application/trash_service.dart'; | ||||||
| import 'package:appflowy/shared/feature_flags.dart'; | import 'package:appflowy/shared/feature_flags.dart'; | ||||||
| import 'package:appflowy/startup/startup.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/startup/tasks/device_info_task.dart'; | ||||||
| import 'package:appflowy/user/application/auth/auth_service.dart'; | import 'package:appflowy/user/application/auth/auth_service.dart'; | ||||||
| import 'package:appflowy/util/color_generator/color_generator.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/debounce.dart'; | ||||||
| import 'package:appflowy/util/throttle.dart'; | import 'package:appflowy/util/throttle.dart'; | ||||||
| import 'package:appflowy/workspace/application/view/view_listener.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/log.dart'; | ||||||
| import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; | import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; | ||||||
| import 'package:appflowy_backend/protobuf/flowy-document/protobuf.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_backend/protobuf/flowy-user/protobuf.dart'; | ||||||
| import 'package:appflowy_editor/appflowy_editor.dart' | import 'package:appflowy_editor/appflowy_editor.dart' | ||||||
|     show |     show | ||||||
|         EditorState, |  | ||||||
|         AppFlowyEditorLogLevel, |         AppFlowyEditorLogLevel, | ||||||
|         TransactionTime, |         EditorState, | ||||||
|         Selection, |  | ||||||
|         Position, |         Position, | ||||||
|  |         Selection, | ||||||
|  |         TransactionTime, | ||||||
|         paragraphNode; |         paragraphNode; | ||||||
| import 'package:appflowy_result/appflowy_result.dart'; | import 'package:appflowy_result/appflowy_result.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| @ -304,7 +306,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { | |||||||
|         ..level = AppFlowyEditorLogLevel.all |         ..level = AppFlowyEditorLogLevel.all | ||||||
|         ..handler = (log) { |         ..handler = (log) { | ||||||
|           if (enableDocumentInternalLog) { |           if (enableDocumentInternalLog) { | ||||||
|             Log.info(log); |             // Log.info(log); | ||||||
|           } |           } | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| @ -363,6 +365,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   void _throttleSyncDoc(DocEventPB docEvent) { |   void _throttleSyncDoc(DocEventPB docEvent) { | ||||||
|  |     if (enableDocumentInternalLog) { | ||||||
|  |       Log.info('[DocumentBloc] throttle sync doc: ${docEvent.toProto3Json()}'); | ||||||
|  |     } | ||||||
|     _syncThrottle.call(() { |     _syncThrottle.call(() { | ||||||
|       _onDocumentStateUpdate(docEvent); |       _onDocumentStateUpdate(docEvent); | ||||||
|     }); |     }); | ||||||
| @ -448,9 +453,17 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { | |||||||
|     if (!deepEqual) { |     if (!deepEqual) { | ||||||
|       Log.error('document integrity check failed'); |       Log.error('document integrity check failed'); | ||||||
|       // Enable it to debug the document integrity check failed |       // Enable it to debug the document integrity check failed | ||||||
|       // Log.error('cloud doc: $cloudJson'); |       Log.error('cloud doc: $cloudJson'); | ||||||
|       // Log.error('local doc: $localJson'); |       Log.error('local doc: $localJson'); | ||||||
|       assert(false, 'document integrity check failed'); | 
 | ||||||
|  |       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_awareness_metadata.dart'; | ||||||
| import 'package:appflowy/plugins/document/application/document_data_pb_extension.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/plugins/document/application/prelude.dart'; | ||||||
| import 'package:appflowy/shared/list_extension.dart'; | import 'package:appflowy/shared/list_extension.dart'; | ||||||
| import 'package:appflowy/startup/tasks/device_info_task.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'; | import 'package:flutter/material.dart'; | ||||||
| 
 | 
 | ||||||
| class DocumentCollabAdapter { | class DocumentCollabAdapter { | ||||||
|   DocumentCollabAdapter(this.editorState, this.docId); |   DocumentCollabAdapter( | ||||||
|  |     this.editorState, | ||||||
|  |     this.docId, | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   final EditorState editorState; |   final EditorState editorState; | ||||||
|   final String docId; |   final String docId; | ||||||
|  |   final DocumentDiff diff = const DocumentDiff(); | ||||||
| 
 | 
 | ||||||
|   final _service = DocumentService(); |   final _service = DocumentService(); | ||||||
| 
 | 
 | ||||||
| @ -75,13 +80,13 @@ class DocumentCollabAdapter { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     final ops = diffNodes(editorState.document.root, document.root); |     final ops = diff.diffDocument(editorState.document, document); | ||||||
|     if (ops.isEmpty) { |     if (ops.isEmpty) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Use for debugging, DO NOT REMOVE |     // Use for debugging, DO NOT REMOVE | ||||||
|     // prettyPrintJson(ops.map((op) => op.toJson()).toList()); |     prettyPrintJson(ops.map((op) => op.toJson()).toList()); | ||||||
| 
 | 
 | ||||||
|     final transaction = editorState.transaction; |     final transaction = editorState.transaction; | ||||||
|     for (final op in ops) { |     for (final op in ops) { | ||||||
| @ -90,17 +95,17 @@ class DocumentCollabAdapter { | |||||||
|     await editorState.apply(transaction, isRemote: true); |     await editorState.apply(transaction, isRemote: true); | ||||||
| 
 | 
 | ||||||
|     // Use for debugging, DO NOT REMOVE |     // Use for debugging, DO NOT REMOVE | ||||||
|     // assert(() { |     assert(() { | ||||||
|     //   final local = editorState.document.root.toJson(); |       final local = editorState.document.root.toJson(); | ||||||
|     //   final remote = document.root.toJson(); |       final remote = document.root.toJson(); | ||||||
|     //   if (!const DeepCollectionEquality().equals(local, remote)) { |       if (!const DeepCollectionEquality().equals(local, remote)) { | ||||||
|     //     Log.error('Invalid diff status'); |         Log.error('Invalid diff status'); | ||||||
|     //     Log.error('Local: $local'); |         Log.error('Local: $local'); | ||||||
|     //     Log.error('Remote: $remote'); |         Log.error('Remote: $remote'); | ||||||
|     //     return false; |         return false; | ||||||
|     //   } |       } | ||||||
|     //   return true; |       return true; | ||||||
|     // }()); |     }()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> forceReload() async { |   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); |       return const EdgeInsets.symmetric(vertical: 5.0); | ||||||
|     }, |     }, | ||||||
|     indentPadding: (node, textDirection) => textDirection == TextDirection.ltr |     indentPadding: (node, textDirection) { | ||||||
|         ? const EdgeInsets.only(left: 26.0) |       double padding = 26.0; | ||||||
|         : const EdgeInsets.only(right: 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; |   return configuration; | ||||||
| } | } | ||||||
|  | |||||||
| @ -287,7 +287,9 @@ class _ToggleListBlockComponentWidgetState | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return Padding( |     return Padding( | ||||||
|       padding: indentPadding, |       padding: UniversalPlatform.isMobile | ||||||
|  |           ? const EdgeInsets.symmetric(horizontal: 26.0) | ||||||
|  |           : indentPadding, | ||||||
|       child: FlowyButton( |       child: FlowyButton( | ||||||
|         text: FlowyText( |         text: FlowyText( | ||||||
|           buildPlaceholderText(), |           buildPlaceholderText(), | ||||||
|  | |||||||
| @ -130,31 +130,31 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos |     :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos | ||||||
| 
 | 
 | ||||||
| SPEC CHECKSUMS: | SPEC CHECKSUMS: | ||||||
|   app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a |   app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 | ||||||
|   appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9 |   appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 | ||||||
|   bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 |   bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 | ||||||
|   connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 |   connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 | ||||||
|   desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 |   desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 | ||||||
|   device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 |   device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 | ||||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d |   file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 | ||||||
|   flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 |   flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e | ||||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 |   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||||
|   HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6 |   HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6 | ||||||
|   hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c |   hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 | ||||||
|   irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 |   irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba | ||||||
|   local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff |   local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e | ||||||
|   package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b |   package_info_plus: f0052d280d17aa382b932f399edf32507174e870 | ||||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|   ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 |   ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 | ||||||
|   screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 |   screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936 | ||||||
|   Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 |   Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 | ||||||
|   sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 |   sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 | ||||||
|   share_plus: 1fa619de8392a4398bfaf176d441853922614e89 |   share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc | ||||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 |   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||||
|   sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec |   sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 | ||||||
|   super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 |   super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 | ||||||
|   url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 |   url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 | ||||||
|   window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 |   window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c | ||||||
| 
 | 
 | ||||||
| PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 | PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -61,8 +61,8 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       path: "." |       path: "." | ||||||
|       ref: "5352bb4" |       ref: "7b33754" | ||||||
|       resolved-ref: "5352bb4a2483039b97073f40c32a93f8fc5e7242" |       resolved-ref: "7b33754d63841868ef4e391d1385063ef2835a61" | ||||||
|       url: "https://github.com/AppFlowy-IO/appflowy-editor.git" |       url: "https://github.com/AppFlowy-IO/appflowy-editor.git" | ||||||
|     source: git |     source: git | ||||||
|     version: "4.0.0" |     version: "4.0.0" | ||||||
|  | |||||||
| @ -174,7 +174,7 @@ dependency_overrides: | |||||||
|   appflowy_editor: |   appflowy_editor: | ||||||
|     git: |     git: | ||||||
|       url: https://github.com/AppFlowy-IO/appflowy-editor.git |       url: https://github.com/AppFlowy-IO/appflowy-editor.git | ||||||
|       ref: "5352bb4" |       ref: "7b33754" | ||||||
| 
 | 
 | ||||||
|   appflowy_editor_plugins: |   appflowy_editor_plugins: | ||||||
|     git: |     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]] | [[package]] | ||||||
| name = "async-compression" | name = "async-compression" | ||||||
| version = "0.4.17" | version = "0.4.18" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" | checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bzip2", |  "bzip2", | ||||||
|  "deflate64", |  "deflate64", | ||||||
| @ -2781,9 +2781,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "futures-lite" | name = "futures-lite" | ||||||
| version = "2.3.0" | version = "2.6.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "fastrand", |  "fastrand", | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Lucas
						Lucas