feat: implement delta diff and provide external values (#444)

* feat: support diff funciton in delta

* feat: support diff funciton in delta
This commit is contained in:
Lucas.Xu 2023-09-11 11:29:25 +08:00 • committed by GitHub
commit dd59eb5210
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 14 deletions

View file

@ -4,6 +4,10 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
abstract class NodeExternalValues {
const NodeExternalValues();
}
/// [Node] represents a node in the document tree. /// [Node] represents a node in the document tree.
/// ///
/// It contains three parts: /// It contains three parts:
@ -85,6 +89,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
/// The path of the node. /// The path of the node.
Path get path => _computePath(); Path get path => _computePath();
NodeExternalValues? externalValues;
// Render Part // Render Part
final key = GlobalKey(); final key = GlobalKey();
final layerLink = LayerLink(); final layerLink = LayerLink();
@ -235,6 +241,7 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
); );
} }
} }
node.externalValues = externalValues;
return node; return node;
} }

View file

@ -1,9 +1,9 @@
import 'dart:collection';
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/src/core/document/attributes.dart';
import 'package:collection/collection.dart';
import 'package:diff_match_patch/diff_match_patch.dart' as diff_match_patch;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../editor/block_component/rich_text/appflowy_rich_text_keys.dart'; import '../../editor/block_component/rich_text/appflowy_rich_text_keys.dart';
@ -204,9 +204,9 @@ class Delta extends Iterable<TextOperation> {
while ((end == null || index < end) && iterator.hasNext) { while ((end == null || index < end) && iterator.hasNext) {
TextOperation? nextOp; TextOperation? nextOp;
if (index < start) { if (index < start) {
nextOp = iterator._next(start - index); nextOp = iterator.next(start - index);
} else { } else {
nextOp = iterator._next(end == null ? null : end - index); nextOp = iterator.next(end == null ? null : end - index);
result.add(nextOp); result.add(nextOp);
} }
@ -255,27 +255,27 @@ class Delta extends Iterable<TextOperation> {
while ( while (
thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) { thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) {
firstLeft -= thisIter.peekLength(); firstLeft -= thisIter.peekLength();
final next = thisIter._next(); final next = thisIter.next();
operations.add(next); operations.add(next);
} }
if (firstOther.length - firstLeft > 0) { if (firstOther.length - firstLeft > 0) {
otherIter._next(firstOther.length - firstLeft); otherIter.next(firstOther.length - firstLeft);
} }
} }
final delta = Delta(operations: operations); final delta = Delta(operations: operations);
while (thisIter.hasNext || otherIter.hasNext) { while (thisIter.hasNext || otherIter.hasNext) {
if (otherIter.peek() is TextInsert) { if (otherIter.peek() is TextInsert) {
final next = otherIter._next(); final next = otherIter.next();
delta.add(next); delta.add(next);
} else if (thisIter.peek() is TextDelete) { } else if (thisIter.peek() is TextDelete) {
final next = thisIter._next(); final next = thisIter.next();
delta.add(next); delta.add(next);
} else { } else {
// otherIs // otherIs
final length = min(thisIter.peekLength(), otherIter.peekLength()); final length = min(thisIter.peekLength(), otherIter.peekLength());
final thisOp = thisIter._next(length); final thisOp = thisIter.next(length);
final otherOp = otherIter._next(length); final otherOp = otherIter.next(length);
final attributes = composeAttributes( final attributes = composeAttributes(
thisOp.attributes, thisOp.attributes,
otherOp.attributes, otherOp.attributes,
@ -310,6 +310,79 @@ class Delta extends Iterable<TextOperation> {
return delta..chop(); return delta..chop();
} }
Delta diff(Delta other) {
if (_operations.equals(other._operations)) {
return Delta();
}
final strings = [this, other].map((e) {
final text = e.map((op) {
if (op is TextInsert) {
return op.text;
}
throw Exception('diff() called $e');
}).join();
return text;
}).toList();
final retDelta = Delta();
final diffResult = diff_match_patch.diff(strings[0], strings[1]);
final thisIter = _OpIterator(this);
final otherIter = _OpIterator(other);
for (var component in diffResult) {
var length = component.text.length;
while (length > 0) {
var opLength = 0;
switch (component.operation) {
case diff_match_patch.DIFF_INSERT:
opLength = min(otherIter.peekLength(), length);
retDelta.add(otherIter.next(opLength));
break;
case diff_match_patch.DIFF_DELETE:
opLength = min(length, thisIter.peekLength());
thisIter.next(opLength);
retDelta.delete(opLength);
break;
case diff_match_patch.DIFF_EQUAL:
opLength = min(
min(thisIter.peekLength(), otherIter.peekLength()),
length,
);
final thisOp = thisIter.next(opLength);
final otherOp = otherIter.next(opLength);
if (thisOp.attributes == otherOp.attributes) {
retDelta.retain(
opLength,
attributes: invertAttributes(
thisOp.attributes,
otherOp.attributes,
),
);
} else {
retDelta
..add(otherOp)
..delete(opLength);
}
break;
}
length -= opLength;
}
}
return retDelta..trim();
}
void trim() {
if (isNotEmpty) {
final last = _operations.last;
if (last is TextRetain &&
(last.attributes == null || last.attributes!.isEmpty)) {
_operations.removeLast();
}
}
}
/// This method joins two Delta together. /// This method joins two Delta together.
Delta operator +(Delta other) { Delta operator +(Delta other) {
var operations = [..._operations]; var operations = [..._operations];
@ -488,7 +561,7 @@ class _OpIterator {
return _maxInt; return _maxInt;
} }
TextOperation _next([int? length]) { TextOperation next([int? length]) {
length ??= _maxInt; length ??= _maxInt;
if (_index >= _operations.length) { if (_index >= _operations.length) {
@ -528,7 +601,7 @@ class _OpIterator {
} else { } else {
final offset = _offset; final offset = _offset;
final index = _index; final index = _index;
final next = _next(); final next = this.next();
final rest = _operations.sublist(_index); final rest = _operations.sublist(_index);
_offset = offset; _offset = offset;
_index = index; _index = index;

View file

@ -36,6 +36,7 @@ dependencies:
visibility_detector: ^0.4.0+2 visibility_detector: ^0.4.0+2
file_picker: ^5.3.1 file_picker: ^5.3.1
path: ^1.8.3 path: ^1.8.3
diff_match_patch: ^0.4.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,6 +1,6 @@
import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/src/core/document/attributes.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart'; import 'package:appflowy_editor/src/core/document/text_delta.dart';
import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
group('text_delta.dart', () { group('text_delta.dart', () {
@ -490,5 +490,35 @@ void main() {
expect(attrs?["b"], null); expect(attrs?["b"], null);
}); });
}); });
group('diff', () {
test('diff1', () {
final a = Delta()..insert('Hello');
final b = Delta()..insert('Hello!');
final diff = a.diff(b);
expect(a.compose(diff), b);
});
test('diff2', () {
final delta = Delta()
..insert('Gandalf', attributes: {'bold': true})
..insert(' the ')
..insert('Grey', attributes: {'color': '#ccc'});
final death = Delta()
..retain(12)
..insert('White', attributes: {'color': '#ccc'})
..delete(4);
final restored = delta.compose(death);
expect(
restored,
Delta()
..insert('Gandalf', attributes: {'bold': true})
..insert(' the ')
..insert('White', attributes: {'color': '#ccc'}),
);
final diff = delta.diff(restored);
expect(delta.compose(diff), restored);
});
});
}); });
} }