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: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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue