mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #615 from vincentdchan/feat/invert-delta
Feat: invert delta
This commit is contained in:
commit
0ef92a0897
@ -1,6 +1,7 @@
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
|
|
||||||
class ImageNodeBuilder extends NodeWidgetBuilder {
|
class ImageNodeBuilder extends NodeWidgetBuilder {
|
||||||
ImageNodeBuilder.create({
|
ImageNodeBuilder.create({
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
|
|
||||||
class TextNodeBuilder extends NodeWidgetBuilder {
|
class TextNodeBuilder extends NodeWidgetBuilder {
|
||||||
TextNodeBuilder.create({
|
TextNodeBuilder.create({
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:flowy_editor/document/path.dart';
|
import 'package:flowy_editor/document/path.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import './attributes.dart';
|
||||||
typedef Attributes = Map<String, dynamic>;
|
|
||||||
|
|
||||||
class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
Node? parent;
|
Node? parent;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
import 'package:flowy_editor/document/path.dart';
|
import 'package:flowy_editor/document/path.dart';
|
||||||
|
import './attributes.dart';
|
||||||
|
|
||||||
class StateTree {
|
class StateTree {
|
||||||
final Node root;
|
final Node root;
|
||||||
|
@ -3,7 +3,7 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import './node.dart';
|
import './attributes.dart';
|
||||||
|
|
||||||
// constant number: 2^53 - 1
|
// constant number: 2^53 - 1
|
||||||
const int _maxInt = 9007199254740991;
|
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 {
|
class TextInsert extends TextOperation {
|
||||||
String content;
|
String content;
|
||||||
final Attributes? _attributes;
|
final Attributes? _attributes;
|
||||||
@ -60,7 +52,7 @@ class TextInsert extends TextOperation {
|
|||||||
final contentHash = content.hashCode;
|
final contentHash = content.hashCode;
|
||||||
final attrs = _attributes;
|
final attrs = _attributes;
|
||||||
return Object.hash(
|
return Object.hash(
|
||||||
contentHash, attrs == null ? null : _hashAttributes(attrs));
|
contentHash, attrs == null ? null : hashAttributes(attrs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +96,7 @@ class TextRetain extends TextOperation {
|
|||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
final attrs = _attributes;
|
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 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 = _composeMap(thisOp.attributes, otherOp.attributes);
|
final attributes =
|
||||||
|
composeAttributes(thisOp.attributes, otherOp.attributes);
|
||||||
if (otherOp is TextRetain && otherOp.length > 0) {
|
if (otherOp is TextRetain && otherOp.length > 0) {
|
||||||
TextOperation? newOp;
|
TextOperation? newOp;
|
||||||
if (thisOp is TextRetain) {
|
if (thisOp is TextRetain) {
|
||||||
@ -404,23 +397,30 @@ class Delta {
|
|||||||
int get hashCode {
|
int get hashCode {
|
||||||
return hashList(operations);
|
return hashList(operations);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Delta invert(Delta base) {
|
||||||
Attributes? _composeMap(Attributes? a, Attributes? b) {
|
final inverted = Delta();
|
||||||
a ??= {};
|
operations.fold(0, (int previousValue, op) {
|
||||||
b ??= {};
|
if (op is TextInsert) {
|
||||||
final Attributes attributes = {};
|
inverted.delete(op.length);
|
||||||
attributes.addAll(b);
|
} else if (op is TextRetain && op.attributes == null) {
|
||||||
|
inverted.retain(op.length);
|
||||||
for (final entry in a.entries) {
|
return previousValue + op.length;
|
||||||
if (!b.containsKey(entry.key)) {
|
} else if (op is TextDelete || op is TextRetain) {
|
||||||
attributes[entry.key] = entry.value;
|
final length = op.length;
|
||||||
}
|
final slice = base.slice(previousValue, previousValue + length);
|
||||||
}
|
for (final baseOp in slice.operations) {
|
||||||
|
if (op is TextDelete) {
|
||||||
if (attributes.isEmpty) {
|
inverted.add(baseOp);
|
||||||
return null;
|
} else if (op is TextRetain && op.attributes != null) {
|
||||||
}
|
inverted.retain(baseOp.length,
|
||||||
|
invertAttributes(op.attributes, baseOp.attributes));
|
||||||
return attributes;
|
}
|
||||||
|
}
|
||||||
|
return previousValue + length;
|
||||||
|
}
|
||||||
|
return previousValue;
|
||||||
|
});
|
||||||
|
return inverted.chop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
import 'package:flowy_editor/operation/operation.dart';
|
import 'package:flowy_editor/operation/operation.dart';
|
||||||
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import './document/state_tree.dart';
|
import './document/state_tree.dart';
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:flowy_editor/document/path.dart';
|
import 'package:flowy_editor/document/path.dart';
|
||||||
import 'package:flowy_editor/document/node.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 {
|
abstract class Operation {
|
||||||
Operation invert();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:flowy_editor/document/text_delta.dart';
|
import 'package:flowy_editor/document/text_delta.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
group('compose', () {
|
||||||
test('test delta', () {
|
test('test delta', () {
|
||||||
final delta = Delta(<TextOperation>[
|
final delta = Delta(<TextOperation>[
|
||||||
TextInsert('Gandalf', {
|
TextInsert('Gandalf', {
|
||||||
@ -170,4 +170,31 @@ void main() {
|
|||||||
.insert('F');
|
.insert('F');
|
||||||
expect(a.compose(b), expected);
|
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);
|
||||||
|
// });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user