Skip to content

Commit ad57c6b

Browse files
committed
Add fast diff
1 parent 3b4e5d8 commit ad57c6b

File tree

3 files changed

+102
-6
lines changed

3 files changed

+102
-6
lines changed

lib/src/models/quill_delta.dart

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ library quill_delta;
88
import 'dart:math' as math;
99

1010
import 'package:collection/collection.dart';
11+
import 'package:diff_match_patch/diff_match_patch.dart' as dmp;
1112
import 'package:quiver/core.dart';
1213

1314
const _attributeEquality = DeepCollectionEquality();
@@ -190,6 +191,9 @@ class Delta {
190191
factory Delta.from(Delta other) =>
191192
Delta._(List<Operation>.from(other._operations));
192193

194+
// Placeholder char for embed in diff()
195+
static final String _kNullCharacter = String.fromCharCode(0);
196+
193197
/// Transforms two attribute sets.
194198
static Map<String, dynamic>? transformAttributes(
195199
Map<String, dynamic>? a, Map<String, dynamic>? b, bool priority) {
@@ -248,6 +252,22 @@ class Delta {
248252
return inverted;
249253
}
250254

255+
/// Returns diff between two attribute sets
256+
static Map<String, dynamic>? diffAttributes(
257+
Map<String, dynamic>? a, Map<String, dynamic>? b) {
258+
a ??= const {};
259+
b ??= const {};
260+
261+
final attributes = <String, dynamic>{};
262+
(a.keys.toList()..addAll(b.keys)).forEach((key) {
263+
if (a![key] != b![key]) {
264+
attributes[key] = b.containsKey(key) ? b[key] : null;
265+
}
266+
});
267+
268+
return attributes.keys.isNotEmpty ? attributes : null;
269+
}
270+
251271
final List<Operation> _operations;
252272

253273
int _modificationCount = 0;
@@ -399,7 +419,7 @@ class Delta {
399419
if (thisIter.isNextDelete) return thisIter.next();
400420

401421
final length = math.min(thisIter.peekLength(), otherIter.peekLength());
402-
final thisOp = thisIter.next(length as int);
422+
final thisOp = thisIter.next(length);
403423
final otherOp = otherIter.next(length);
404424
assert(thisOp.length == otherOp.length);
405425

@@ -442,6 +462,80 @@ class Delta {
442462
return result..trim();
443463
}
444464

465+
/// Returns a new lazy Iterable with elements that are created by calling
466+
/// f on each element of this Iterable in iteration order.
467+
///
468+
/// Convenience method
469+
Iterable<T> map<T>(T Function(Operation) f) {
470+
return _operations.map<T>(f);
471+
}
472+
473+
/// Returns a [Delta] containing differences between 2 [Delta]s
474+
///
475+
/// Useful when one wishes to display difference between 2 documents
476+
Delta diff(Delta other) {
477+
if (_operations.equals(other._operations)) {
478+
return Delta();
479+
}
480+
final stringThis = map((op) {
481+
if (op.isInsert) {
482+
return op.data is String ? op.data : _kNullCharacter;
483+
}
484+
final prep = this == other ? 'on' : 'with';
485+
throw ArgumentError('diff() call $prep non-document');
486+
}).join();
487+
final stringOther = other.map((op) {
488+
if (op.isInsert) {
489+
return op.data is String ? op.data : _kNullCharacter;
490+
}
491+
final prep = this == other ? 'on' : 'with';
492+
throw ArgumentError('diff() call $prep non-document');
493+
}).join();
494+
495+
final retDelta = Delta();
496+
final diffResult = dmp.diff(stringThis, stringOther);
497+
final thisIter = DeltaIterator(this);
498+
final otherIter = DeltaIterator(other);
499+
500+
diffResult.forEach((component) {
501+
var length = component.text.length;
502+
while (length > 0) {
503+
var opLength = 0;
504+
switch (component.operation) {
505+
case dmp.DIFF_INSERT:
506+
opLength = math.min(otherIter.peekLength(), length);
507+
retDelta.push(otherIter.next(opLength));
508+
break;
509+
case dmp.DIFF_DELETE:
510+
opLength = math.min(length, thisIter.peekLength());
511+
thisIter.next(opLength);
512+
retDelta.delete(opLength);
513+
break;
514+
case dmp.DIFF_EQUAL:
515+
opLength = math.min(
516+
math.min(thisIter.peekLength(), otherIter.peekLength()),
517+
length,
518+
);
519+
final thisOp = thisIter.next(opLength);
520+
final otherOp = otherIter.next(opLength);
521+
if (thisOp.data == otherOp.data) {
522+
retDelta.retain(
523+
opLength,
524+
diffAttributes(thisOp.attributes, otherOp.attributes),
525+
);
526+
} else {
527+
retDelta
528+
..push(otherOp)
529+
..delete(opLength);
530+
}
531+
break;
532+
}
533+
length -= opLength;
534+
}
535+
});
536+
return retDelta..trim();
537+
}
538+
445539
/// Transforms next operation from [otherIter] against next operation in
446540
/// [thisIter].
447541
///
@@ -455,7 +549,7 @@ class Delta {
455549
}
456550

457551
final length = math.min(thisIter.peekLength(), otherIter.peekLength());
458-
final thisOp = thisIter.next(length as int);
552+
final thisOp = thisIter.next(length);
459553
final otherOp = otherIter.next(length);
460554
assert(thisOp.length == otherOp.length);
461555

@@ -558,7 +652,7 @@ class Delta {
558652
if (index < start) {
559653
op = opIterator.next(start - index);
560654
} else {
561-
op = opIterator.next(actualEnd - index as int);
655+
op = opIterator.next(actualEnd - index);
562656
delta.push(op);
563657
}
564658
index += op.length!;
@@ -643,7 +737,8 @@ class DeltaIterator {
643737
///
644738
/// If this iterator reached the end of the Delta then returns a retain
645739
/// operation with its length set to [maxLength].
646-
// TODO: Note that we used double.infinity as the default value for length here
740+
// TODO: Note that we used double.infinity as the default value
741+
// for length here
647742
// but this can now cause a type error since operation length is
648743
// expected to be an int. Changing default length to [maxLength] is
649744
// a workaround to avoid breaking changes.
@@ -686,7 +781,7 @@ class DeltaIterator {
686781
while (skipped < length && hasNext) {
687782
final opLength = peekLength();
688783
final skip = math.min(length - skipped, opLength);
689-
op = next(skip as int);
784+
op = next(skip);
690785
skipped += op.length!;
691786
}
692787
return op;

lib/src/utils/diff_delta.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ int getPositionDelta(Delta user, Delta actual) {
7676
var diff = 0;
7777
while (userItr.hasNext || actualItr.hasNext) {
7878
final length = math.min(userItr.peekLength(), actualItr.peekLength());
79-
final userOperation = userItr.next(length as int);
79+
final userOperation = userItr.next(length);
8080
final actualOperation = actualItr.next(length);
8181
if (userOperation.length != actualOperation.length) {
8282
throw 'userOp ${userOperation.length} does not match actualOp '

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies:
2525
video_player: ^2.1.10
2626
characters: ^1.1.0
2727
youtube_player_flutter: ^8.0.0
28+
diff_match_patch: ^0.4.1
2829

2930
dev_dependencies:
3031
flutter_test:

0 commit comments

Comments
 (0)