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:
parent
3f2db9b416
commit
dd59eb5210
4 changed files with 125 additions and 14 deletions
|
|
@ -4,6 +4,10 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
abstract class NodeExternalValues {
|
||||
const NodeExternalValues();
|
||||
}
|
||||
|
||||
/// [Node] represents a node in the document tree.
|
||||
///
|
||||
/// It contains three parts:
|
||||
|
|
@ -85,6 +89,8 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
/// The path of the node.
|
||||
Path get path => _computePath();
|
||||
|
||||
NodeExternalValues? externalValues;
|
||||
|
||||
// Render Part
|
||||
final key = GlobalKey();
|
||||
final layerLink = LayerLink();
|
||||
|
|
@ -235,6 +241,7 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||
);
|
||||
}
|
||||
}
|
||||
node.externalValues = externalValues;
|
||||
return node;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.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 '../../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) {
|
||||
TextOperation? nextOp;
|
||||
if (index < start) {
|
||||
nextOp = iterator._next(start - index);
|
||||
nextOp = iterator.next(start - index);
|
||||
} else {
|
||||
nextOp = iterator._next(end == null ? null : end - index);
|
||||
nextOp = iterator.next(end == null ? null : end - index);
|
||||
result.add(nextOp);
|
||||
}
|
||||
|
||||
|
|
@ -255,27 +255,27 @@ class Delta extends Iterable<TextOperation> {
|
|||
while (
|
||||
thisIter.peek() is TextInsert && thisIter.peekLength() <= firstLeft) {
|
||||
firstLeft -= thisIter.peekLength();
|
||||
final next = thisIter._next();
|
||||
final next = thisIter.next();
|
||||
operations.add(next);
|
||||
}
|
||||
if (firstOther.length - firstLeft > 0) {
|
||||
otherIter._next(firstOther.length - firstLeft);
|
||||
otherIter.next(firstOther.length - firstLeft);
|
||||
}
|
||||
}
|
||||
|
||||
final delta = Delta(operations: operations);
|
||||
while (thisIter.hasNext || otherIter.hasNext) {
|
||||
if (otherIter.peek() is TextInsert) {
|
||||
final next = otherIter._next();
|
||||
final next = otherIter.next();
|
||||
delta.add(next);
|
||||
} else if (thisIter.peek() is TextDelete) {
|
||||
final next = thisIter._next();
|
||||
final next = thisIter.next();
|
||||
delta.add(next);
|
||||
} else {
|
||||
// otherIs
|
||||
final length = min(thisIter.peekLength(), otherIter.peekLength());
|
||||
final thisOp = thisIter._next(length);
|
||||
final otherOp = otherIter._next(length);
|
||||
final thisOp = thisIter.next(length);
|
||||
final otherOp = otherIter.next(length);
|
||||
final attributes = composeAttributes(
|
||||
thisOp.attributes,
|
||||
otherOp.attributes,
|
||||
|
|
@ -310,6 +310,79 @@ class Delta extends Iterable<TextOperation> {
|
|||
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.
|
||||
Delta operator +(Delta other) {
|
||||
var operations = [..._operations];
|
||||
|
|
@ -488,7 +561,7 @@ class _OpIterator {
|
|||
return _maxInt;
|
||||
}
|
||||
|
||||
TextOperation _next([int? length]) {
|
||||
TextOperation next([int? length]) {
|
||||
length ??= _maxInt;
|
||||
|
||||
if (_index >= _operations.length) {
|
||||
|
|
@ -528,7 +601,7 @@ class _OpIterator {
|
|||
} else {
|
||||
final offset = _offset;
|
||||
final index = _index;
|
||||
final next = _next();
|
||||
final next = this.next();
|
||||
final rest = _operations.sublist(_index);
|
||||
_offset = offset;
|
||||
_index = index;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ dependencies:
|
|||
visibility_detector: ^0.4.0+2
|
||||
file_picker: ^5.3.1
|
||||
path: ^1.8.3
|
||||
diff_match_patch: ^0.4.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('text_delta.dart', () {
|
||||
|
|
@ -490,5 +490,35 @@ void main() {
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue