From ad57c6bab6bcb979b14a476d886e10f5c85a7674 Mon Sep 17 00:00:00 2001 From: Xin Yao Date: Fri, 6 Aug 2021 01:17:59 -0700 Subject: [PATCH] Add fast diff --- lib/src/models/quill_delta.dart | 105 ++++++++++++++++++++++++++++++-- lib/src/utils/diff_delta.dart | 2 +- pubspec.yaml | 1 + 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index 7b2228ba..0c699190 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -8,6 +8,7 @@ library quill_delta; import 'dart:math' as math; import 'package:collection/collection.dart'; +import 'package:diff_match_patch/diff_match_patch.dart' as dmp; import 'package:quiver/core.dart'; const _attributeEquality = DeepCollectionEquality(); @@ -190,6 +191,9 @@ class Delta { factory Delta.from(Delta other) => Delta._(List.from(other._operations)); + // Placeholder char for embed in diff() + static final String _kNullCharacter = String.fromCharCode(0); + /// Transforms two attribute sets. static Map? transformAttributes( Map? a, Map? b, bool priority) { @@ -248,6 +252,22 @@ class Delta { return inverted; } + /// Returns diff between two attribute sets + static Map? diffAttributes( + Map? a, Map? b) { + a ??= const {}; + b ??= const {}; + + final attributes = {}; + (a.keys.toList()..addAll(b.keys)).forEach((key) { + if (a![key] != b![key]) { + attributes[key] = b.containsKey(key) ? b[key] : null; + } + }); + + return attributes.keys.isNotEmpty ? attributes : null; + } + final List _operations; int _modificationCount = 0; @@ -399,7 +419,7 @@ class Delta { if (thisIter.isNextDelete) return thisIter.next(); final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length as int); + final thisOp = thisIter.next(length); final otherOp = otherIter.next(length); assert(thisOp.length == otherOp.length); @@ -442,6 +462,80 @@ class Delta { return result..trim(); } + /// Returns a new lazy Iterable with elements that are created by calling + /// f on each element of this Iterable in iteration order. + /// + /// Convenience method + Iterable map(T Function(Operation) f) { + return _operations.map(f); + } + + /// Returns a [Delta] containing differences between 2 [Delta]s + /// + /// Useful when one wishes to display difference between 2 documents + Delta diff(Delta other) { + if (_operations.equals(other._operations)) { + return Delta(); + } + final stringThis = map((op) { + if (op.isInsert) { + return op.data is String ? op.data : _kNullCharacter; + } + final prep = this == other ? 'on' : 'with'; + throw ArgumentError('diff() call $prep non-document'); + }).join(); + final stringOther = other.map((op) { + if (op.isInsert) { + return op.data is String ? op.data : _kNullCharacter; + } + final prep = this == other ? 'on' : 'with'; + throw ArgumentError('diff() call $prep non-document'); + }).join(); + + final retDelta = Delta(); + final diffResult = dmp.diff(stringThis, stringOther); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + diffResult.forEach((component) { + var length = component.text.length; + while (length > 0) { + var opLength = 0; + switch (component.operation) { + case dmp.DIFF_INSERT: + opLength = math.min(otherIter.peekLength(), length); + retDelta.push(otherIter.next(opLength)); + break; + case dmp.DIFF_DELETE: + opLength = math.min(length, thisIter.peekLength()); + thisIter.next(opLength); + retDelta.delete(opLength); + break; + case dmp.DIFF_EQUAL: + opLength = math.min( + math.min(thisIter.peekLength(), otherIter.peekLength()), + length, + ); + final thisOp = thisIter.next(opLength); + final otherOp = otherIter.next(opLength); + if (thisOp.data == otherOp.data) { + retDelta.retain( + opLength, + diffAttributes(thisOp.attributes, otherOp.attributes), + ); + } else { + retDelta + ..push(otherOp) + ..delete(opLength); + } + break; + } + length -= opLength; + } + }); + return retDelta..trim(); + } + /// Transforms next operation from [otherIter] against next operation in /// [thisIter]. /// @@ -455,7 +549,7 @@ class Delta { } final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length as int); + final thisOp = thisIter.next(length); final otherOp = otherIter.next(length); assert(thisOp.length == otherOp.length); @@ -558,7 +652,7 @@ class Delta { if (index < start) { op = opIterator.next(start - index); } else { - op = opIterator.next(actualEnd - index as int); + op = opIterator.next(actualEnd - index); delta.push(op); } index += op.length!; @@ -643,7 +737,8 @@ class DeltaIterator { /// /// If this iterator reached the end of the Delta then returns a retain /// operation with its length set to [maxLength]. - // TODO: Note that we used double.infinity as the default value for length here + // TODO: Note that we used double.infinity as the default value + // for length here // but this can now cause a type error since operation length is // expected to be an int. Changing default length to [maxLength] is // a workaround to avoid breaking changes. @@ -686,7 +781,7 @@ class DeltaIterator { while (skipped < length && hasNext) { final opLength = peekLength(); final skip = math.min(length - skipped, opLength); - op = next(skip as int); + op = next(skip); skipped += op.length!; } return op; diff --git a/lib/src/utils/diff_delta.dart b/lib/src/utils/diff_delta.dart index 0e09946c..f44a1a09 100644 --- a/lib/src/utils/diff_delta.dart +++ b/lib/src/utils/diff_delta.dart @@ -76,7 +76,7 @@ int getPositionDelta(Delta user, Delta actual) { var diff = 0; while (userItr.hasNext || actualItr.hasNext) { final length = math.min(userItr.peekLength(), actualItr.peekLength()); - final userOperation = userItr.next(length as int); + final userOperation = userItr.next(length); final actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { throw 'userOp ${userOperation.length} does not match actualOp ' diff --git a/pubspec.yaml b/pubspec.yaml index 4d371852..dcdb4c52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: video_player: ^2.1.10 characters: ^1.1.0 youtube_player_flutter: ^8.0.0 + diff_match_patch: ^0.4.1 dev_dependencies: flutter_test: