Merge pull request #615 from vincentdchan/feat/invert-delta

Feat: invert delta
This commit is contained in:
Nathan.fooo 2022-07-15 16:52:24 +08:00 committed by GitHub
commit 0ef92a0897
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 276 additions and 187 deletions

View File

@ -1,6 +1,7 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flowy_editor/document/attributes.dart';
class ImageNodeBuilder extends NodeWidgetBuilder {
ImageNodeBuilder.create({

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flowy_editor/document/attributes.dart';
class TextNodeBuilder extends NodeWidgetBuilder {
TextNodeBuilder.create({

View File

@ -0,0 +1,42 @@
typedef Attributes = Map<String, dynamic>;
int hashAttributes(Attributes attributes) {
return Object.hashAllUnordered(
attributes.entries.map((e) => Object.hash(e.key, e.value)));
}
Attributes invertAttributes(Attributes? attr, Attributes? base) {
attr ??= {};
base ??= {};
final Attributes baseInverted = base.keys.fold({}, (memo, key) {
if (base![key] != attr![key] && attr.containsKey(key)) {
memo[key] = base[key];
}
return memo;
});
return attr.keys.fold(baseInverted, (memo, key) {
if (attr![key] != base![key] && base.containsKey(key)) {
memo[key] = null;
}
return memo;
});
}
Attributes? composeAttributes(Attributes? a, Attributes? b) {
a ??= {};
b ??= {};
final Attributes attributes = {};
attributes.addAll(b);
for (final entry in a.entries) {
if (!b.containsKey(entry.key)) {
attributes[entry.key] = entry.value;
}
}
if (attributes.isEmpty) {
return null;
}
return attributes;
}

View File

@ -1,8 +1,7 @@
import 'dart:collection';
import 'package:flowy_editor/document/path.dart';
import 'package:flutter/material.dart';
typedef Attributes = Map<String, dynamic>;
import './attributes.dart';
class Node extends ChangeNotifier with LinkedListEntry<Node> {
Node? parent;

View File

@ -1,5 +1,6 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/path.dart';
import './attributes.dart';
class StateTree {
final Node root;

View File

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import './node.dart';
import './attributes.dart';
// constant number: 2^53 - 1
const int _maxInt = 9007199254740991;
@ -22,14 +22,6 @@ class TextOperation {
}
}
int _hashAttributes(Attributes attributes) {
return Object.hashAllUnordered(
attributes.entries.map(
(e) => Object.hash(e.key, e.value),
),
);
}
class TextInsert extends TextOperation {
String content;
final Attributes? _attributes;
@ -60,7 +52,7 @@ class TextInsert extends TextOperation {
final contentHash = content.hashCode;
final attrs = _attributes;
return Object.hash(
contentHash, attrs == null ? null : _hashAttributes(attrs));
contentHash, attrs == null ? null : hashAttributes(attrs));
}
}
@ -104,7 +96,7 @@ class TextRetain extends TextOperation {
@override
int get hashCode {
final attrs = _attributes;
return Object.hash(_length, attrs == null ? null : _hashAttributes(attrs));
return Object.hash(_length, attrs == null ? null : hashAttributes(attrs));
}
}
@ -344,7 +336,8 @@ class Delta {
final length = min(thisIter.peekLength(), otherIter.peekLength());
final thisOp = thisIter.next(length);
final otherOp = otherIter.next(length);
final attributes = _composeMap(thisOp.attributes, otherOp.attributes);
final attributes =
composeAttributes(thisOp.attributes, otherOp.attributes);
if (otherOp is TextRetain && otherOp.length > 0) {
TextOperation? newOp;
if (thisOp is TextRetain) {
@ -404,23 +397,30 @@ class Delta {
int get hashCode {
return hashList(operations);
}
}
Attributes? _composeMap(Attributes? a, Attributes? b) {
a ??= {};
b ??= {};
final Attributes attributes = {};
attributes.addAll(b);
for (final entry in a.entries) {
if (!b.containsKey(entry.key)) {
attributes[entry.key] = entry.value;
}
Delta invert(Delta base) {
final inverted = Delta();
operations.fold(0, (int previousValue, op) {
if (op is TextInsert) {
inverted.delete(op.length);
} else if (op is TextRetain && op.attributes == null) {
inverted.retain(op.length);
return previousValue + op.length;
} else if (op is TextDelete || op is TextRetain) {
final length = op.length;
final slice = base.slice(previousValue, previousValue + length);
for (final baseOp in slice.operations) {
if (op is TextDelete) {
inverted.add(baseOp);
} else if (op is TextRetain && op.attributes != null) {
inverted.retain(baseOp.length,
invertAttributes(op.attributes, baseOp.attributes));
}
}
return previousValue + length;
}
return previousValue;
});
return inverted.chop();
}
if (attributes.isEmpty) {
return null;
}
return attributes;
}

View File

@ -1,5 +1,6 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/operation/operation.dart';
import 'package:flowy_editor/document/attributes.dart';
import 'package:flutter/material.dart';
import './document/state_tree.dart';

View File

@ -1,5 +1,7 @@
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/document/attributes.dart';
abstract class Operation {
Operation invert();
@ -61,3 +63,18 @@ class DeleteOperation extends Operation {
);
}
}
class TextEditOperation extends Operation {
final Path path;
final Delta delta;
TextEditOperation({
required this.path,
required this.delta,
});
@override
Operation invert() {
return TextEditOperation(path: path, delta: delta);
}
}

View File

@ -2,172 +2,199 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flowy_editor/document/text_delta.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('test delta', () {
final delta = Delta(<TextOperation>[
TextInsert('Gandalf', {
group('compose', () {
test('test delta', () {
final delta = Delta(<TextOperation>[
TextInsert('Gandalf', {
'bold': true,
}),
TextInsert(' the '),
TextInsert('Grey', {
'color': '#ccc',
})
]);
final death = Delta().retain(12).insert("White", {
'color': '#fff',
}).delete(4);
final restores = delta.compose(death);
expect(restores.operations, <TextOperation>[
TextInsert('Gandalf', {'bold': true}),
TextInsert(' the '),
TextInsert('White', {'color': '#fff'}),
]);
});
test('compose()', () {
final a = Delta().insert('A');
final b = Delta().insert('B');
final expected = Delta().insert('B').insert('A');
expect(a.compose(b), expected);
});
test('insert + retain', () {
final a = Delta().insert('A');
final b = Delta().retain(1, {
'bold': true,
}),
TextInsert(' the '),
TextInsert('Grey', {
'color': '#ccc',
})
]);
final death = Delta().retain(12).insert("White", {
'color': '#fff',
}).delete(4);
final restores = delta.compose(death);
expect(restores.operations, <TextOperation>[
TextInsert('Gandalf', {'bold': true}),
TextInsert(' the '),
TextInsert('White', {'color': '#fff'}),
]);
});
test('compose()', () {
final a = Delta().insert('A');
final b = Delta().insert('B');
final expected = Delta().insert('B').insert('A');
expect(a.compose(b), expected);
});
test('insert + retain', () {
final a = Delta().insert('A');
final b = Delta().retain(1, {
'bold': true,
'color': 'red',
'color': 'red',
});
final expected = Delta().insert('A', {
'bold': true,
'color': 'red',
});
expect(a.compose(b), expected);
});
final expected = Delta().insert('A', {
'bold': true,
'color': 'red',
test('insert + delete', () {
final a = Delta().insert('A');
final b = Delta().delete(1);
final expected = Delta();
expect(a.compose(b), expected);
});
expect(a.compose(b), expected);
});
test('insert + delete', () {
final a = Delta().insert('A');
final b = Delta().delete(1);
final expected = Delta();
expect(a.compose(b), expected);
});
test('delete + insert', () {
final a = Delta().delete(1);
final b = Delta().insert('B');
final expected = Delta().insert('B').delete(1);
expect(a.compose(b), expected);
});
test('delete + retain', () {
final a = Delta().delete(1);
final b = Delta().retain(1, {
'bold': true,
'color': 'red',
test('delete + insert', () {
final a = Delta().delete(1);
final b = Delta().insert('B');
final expected = Delta().insert('B').delete(1);
expect(a.compose(b), expected);
});
final expected = Delta().delete(1).retain(1, {
'bold': true,
'color': 'red',
test('delete + retain', () {
final a = Delta().delete(1);
final b = Delta().retain(1, {
'bold': true,
'color': 'red',
});
final expected = Delta().delete(1).retain(1, {
'bold': true,
'color': 'red',
});
expect(a.compose(b), expected);
});
expect(a.compose(b), expected);
});
test('delete + delete', () {
final a = Delta().delete(1);
final b = Delta().delete(1);
final expected = Delta().delete(2);
expect(a.compose(b), expected);
});
test('retain + insert', () {
final a = Delta().retain(1, {'color': 'blue'});
final b = Delta().insert('B');
final expected = Delta().insert('B').retain(1, {
'color': 'blue',
test('delete + delete', () {
final a = Delta().delete(1);
final b = Delta().delete(1);
final expected = Delta().delete(2);
expect(a.compose(b), expected);
});
expect(a.compose(b), expected);
});
test('retain + retain', () {
final a = Delta().retain(1, {
'color': 'blue',
test('retain + insert', () {
final a = Delta().retain(1, {'color': 'blue'});
final b = Delta().insert('B');
final expected = Delta().insert('B').retain(1, {
'color': 'blue',
});
expect(a.compose(b), expected);
});
final b = Delta().retain(1, {
'bold': true,
'color': 'red',
test('retain + retain', () {
final a = Delta().retain(1, {
'color': 'blue',
});
final b = Delta().retain(1, {
'bold': true,
'color': 'red',
});
final expected = Delta().retain(1, {
'bold': true,
'color': 'red',
});
expect(a.compose(b), expected);
});
final expected = Delta().retain(1, {
'bold': true,
'color': 'red',
test('retain + delete', () {
final a = Delta().retain(1, {
'color': 'blue',
});
final b = Delta().delete(1);
final expected = Delta().delete(1);
expect(a.compose(b), expected);
});
expect(a.compose(b), expected);
});
test('retain + delete', () {
final a = Delta().retain(1, {
'color': 'blue',
test('insert in middle of text', () {
final a = Delta().insert('Hello');
final b = Delta().retain(3).insert('X');
final expected = Delta().insert('HelXlo');
expect(a.compose(b), expected);
});
test('insert and delete ordering', () {
final a = Delta().insert('Hello');
final b = Delta().insert('Hello');
final insertFirst = Delta().retain(3).insert('X').delete(1);
final deleteFirst = Delta().retain(3).delete(1).insert('X');
final expected = Delta().insert('HelXo');
expect(a.compose(insertFirst), expected);
expect(b.compose(deleteFirst), expected);
});
test('delete entire text', () {
final a = Delta().retain(4).insert('Hello');
final b = Delta().delete(9);
final expected = Delta().delete(4);
expect(a.compose(b), expected);
});
test('retain more than length of text', () {
final a = Delta().insert('Hello');
final b = Delta().retain(10);
final expected = Delta().insert('Hello');
expect(a.compose(b), expected);
});
test('retain start optimization', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.delete(1);
final b = Delta().retain(3).insert('D');
final expected = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.insert('D')
.delete(1);
expect(a.compose(b), expected);
});
test('retain end optimization', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true});
final b = Delta().delete(1);
final expected = Delta().insert('B').insert('C', {'bold': true});
expect(a.compose(b), expected);
});
test('retain end optimization join', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.insert('D')
.insert('E', {'bold': true})
.insert('F');
final b = Delta().retain(1).delete(1);
final expected = Delta()
.insert('AC', {'bold': true})
.insert('D')
.insert('E', {'bold': true})
.insert('F');
expect(a.compose(b), expected);
});
final b = Delta().delete(1);
final expected = Delta().delete(1);
expect(a.compose(b), expected);
});
test('insert in middle of text', () {
final a = Delta().insert('Hello');
final b = Delta().retain(3).insert('X');
final expected = Delta().insert('HelXlo');
expect(a.compose(b), expected);
});
test('insert and delete ordering', () {
final a = Delta().insert('Hello');
final b = Delta().insert('Hello');
final insertFirst = Delta().retain(3).insert('X').delete(1);
final deleteFirst = Delta().retain(3).delete(1).insert('X');
final expected = Delta().insert('HelXo');
expect(a.compose(insertFirst), expected);
expect(b.compose(deleteFirst), expected);
});
test('delete entire text', () {
final a = Delta().retain(4).insert('Hello');
final b = Delta().delete(9);
final expected = Delta().delete(4);
expect(a.compose(b), expected);
});
test('retain more than length of text', () {
final a = Delta().insert('Hello');
final b = Delta().retain(10);
final expected = Delta().insert('Hello');
expect(a.compose(b), expected);
});
test('retain start optimization', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.delete(1);
final b = Delta().retain(3).insert('D');
final expected = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.insert('D')
.delete(1);
expect(a.compose(b), expected);
});
test('retain end optimization', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true});
final b = Delta().delete(1);
final expected = Delta().insert('B').insert('C', {'bold': true});
expect(a.compose(b), expected);
});
test('retain end optimization join', () {
final a = Delta()
.insert('A', {'bold': true})
.insert('B')
.insert('C', {'bold': true})
.insert('D')
.insert('E', {'bold': true})
.insert('F');
final b = Delta().retain(1).delete(1);
final expected = Delta()
.insert('AC', {'bold': true})
.insert('D')
.insert('E', {'bold': true})
.insert('F');
expect(a.compose(b), expected);
group('invert', () {
test('insert', () {
final delta = Delta().retain(2).insert('A');
final base = Delta().insert('12346');
final expected = Delta().retain(2).delete(1);
final inverted = delta.invert(base);
expect(expected, inverted);
expect(base.compose(delta).compose(inverted), base);
});
test('delete', () {
final delta = Delta().retain(2).delete(3);
final base = Delta().insert('123456');
final expected = Delta().retain(2).insert('345');
final inverted = delta.invert(base);
expect(expected, inverted);
expect(base.compose(delta).compose(inverted), base);
});
// test('retain', () {
// final delta = Delta().retain(2).retain(3, {'bold': true});
// final base = Delta().insert('123456');
// final expected = Delta().retain(2).retain(3, {'bold': null});
// final inverted = delta.invert(base);
// expect(expected, inverted);
// expect(base.compose(delta).compose(inverted), base);
// });
});
}