mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: operation transforming
This commit is contained in:
parent
b967453047
commit
c72fead19c
@ -1,21 +1,27 @@
|
|||||||
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';
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
|
||||||
abstract class Operation {
|
abstract class Operation {
|
||||||
|
final Path path;
|
||||||
|
Operation({required this.path});
|
||||||
|
Operation copyWithPath(Path path);
|
||||||
Operation invert();
|
Operation invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
class InsertOperation extends Operation {
|
class InsertOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Node value;
|
final Node value;
|
||||||
|
|
||||||
InsertOperation({
|
InsertOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.value,
|
required this.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
InsertOperation copyWith({Path? path, Node? value}) =>
|
||||||
|
InsertOperation(path: path ?? this.path, value: value ?? this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Operation copyWithPath(Path path) => copyWith(path: path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return DeleteOperation(
|
return DeleteOperation(
|
||||||
@ -26,16 +32,25 @@ class InsertOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UpdateOperation extends Operation {
|
class UpdateOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Attributes attributes;
|
final Attributes attributes;
|
||||||
final Attributes oldAttributes;
|
final Attributes oldAttributes;
|
||||||
|
|
||||||
UpdateOperation({
|
UpdateOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.attributes,
|
required this.attributes,
|
||||||
required this.oldAttributes,
|
required this.oldAttributes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UpdateOperation copyWith(
|
||||||
|
{Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
|
||||||
|
UpdateOperation(
|
||||||
|
path: path ?? this.path,
|
||||||
|
attributes: attributes ?? this.attributes,
|
||||||
|
oldAttributes: oldAttributes ?? this.oldAttributes);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Operation copyWithPath(Path path) => copyWith(path: path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return UpdateOperation(
|
return UpdateOperation(
|
||||||
@ -47,14 +62,19 @@ class UpdateOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DeleteOperation extends Operation {
|
class DeleteOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Node removedValue;
|
final Node removedValue;
|
||||||
|
|
||||||
DeleteOperation({
|
DeleteOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.removedValue,
|
required this.removedValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation(
|
||||||
|
path: path ?? this.path, removedValue: removedValue ?? this.removedValue);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Operation copyWithPath(Path path) => copyWith(path: path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return InsertOperation(
|
return InsertOperation(
|
||||||
@ -65,18 +85,61 @@ class DeleteOperation extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TextEditOperation extends Operation {
|
class TextEditOperation extends Operation {
|
||||||
final Path path;
|
|
||||||
final Delta delta;
|
final Delta delta;
|
||||||
final Delta inverted;
|
final Delta inverted;
|
||||||
|
|
||||||
TextEditOperation({
|
TextEditOperation({
|
||||||
required this.path,
|
required super.path,
|
||||||
required this.delta,
|
required this.delta,
|
||||||
required this.inverted,
|
required this.inverted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
|
||||||
|
TextEditOperation(
|
||||||
|
path: path ?? this.path,
|
||||||
|
delta: delta ?? this.delta,
|
||||||
|
inverted: inverted ?? this.inverted);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Operation copyWithPath(Path path) => copyWith(path: path);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Operation invert() {
|
Operation invert() {
|
||||||
return TextEditOperation(path: path, delta: inverted, inverted: delta);
|
return TextEditOperation(path: path, delta: inverted, inverted: delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
|
||||||
|
if (preInsertPath.length > b.length) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
if (preInsertPath.isEmpty || b.isEmpty) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
// check the prefix
|
||||||
|
for (var i = 0; i < preInsertPath.length - 1; i++) {
|
||||||
|
if (preInsertPath[i] != b[i]) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final prefix = preInsertPath.sublist(0, preInsertPath.length - 1);
|
||||||
|
final suffix = b.sublist(preInsertPath.length);
|
||||||
|
final preInsertLast = preInsertPath.last;
|
||||||
|
final bAtIndex = b[preInsertPath.length - 1];
|
||||||
|
if (preInsertLast <= bAtIndex) {
|
||||||
|
prefix.add(bAtIndex + delta);
|
||||||
|
}
|
||||||
|
prefix.addAll(suffix);
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation transformOperation(Operation a, Operation b) {
|
||||||
|
if (a is InsertOperation) {
|
||||||
|
final newPath = transformPath(a.path, b.path);
|
||||||
|
return b.copyWithPath(newPath);
|
||||||
|
} else if (b is DeleteOperation) {
|
||||||
|
final newPath = transformPath(a.path, b.path, -1);
|
||||||
|
return b.copyWithPath(newPath);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flowy_editor/operation/operation.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('transform path', () {
|
||||||
|
test('transform path changed', () {
|
||||||
|
expect(transformPath([0, 1], [0, 1]), [0, 2]);
|
||||||
|
expect(transformPath([0, 1], [0, 2]), [0, 3]);
|
||||||
|
expect(transformPath([0, 1], [0, 2, 7, 8, 9]), [0, 3, 7, 8, 9]);
|
||||||
|
expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
|
||||||
|
});
|
||||||
|
test("transform path not changed", () {
|
||||||
|
expect(transformPath([0, 1, 2], [0, 0, 7, 8, 9]), [0, 0, 7, 8, 9]);
|
||||||
|
expect(transformPath([0, 1, 2], [0, 1]), [0, 1]);
|
||||||
|
});
|
||||||
|
test("transform path delta", () {
|
||||||
|
expect(transformPath([0, 1], [0, 1], 5), [0, 6]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
group('transform operation', () {
|
||||||
|
test('insert + insert', () {
|
||||||
|
final t = transformOperation(
|
||||||
|
InsertOperation(path: [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
], value: Node(type: "node", attributes: {}, children: LinkedList())),
|
||||||
|
InsertOperation(
|
||||||
|
path: [0, 1],
|
||||||
|
value:
|
||||||
|
Node(type: "node", attributes: {}, children: LinkedList())));
|
||||||
|
expect(t.path, [0, 2]);
|
||||||
|
});
|
||||||
|
test('delete + delete', () {
|
||||||
|
final t = transformOperation(
|
||||||
|
DeleteOperation(
|
||||||
|
path: [0, 1],
|
||||||
|
removedValue:
|
||||||
|
Node(type: "node", attributes: {}, children: LinkedList())),
|
||||||
|
DeleteOperation(
|
||||||
|
path: [0, 2],
|
||||||
|
removedValue:
|
||||||
|
Node(type: "node", attributes: {}, children: LinkedList())));
|
||||||
|
expect(t.path, [0, 1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user