diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index e3710ddb3c..7534b03427 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -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/flowy_editor.dart'; abstract class Operation { + final Path path; + Operation({required this.path}); + Operation copyWithPath(Path path); Operation invert(); } class InsertOperation extends Operation { - final Path path; final Node value; InsertOperation({ - required this.path, + required super.path, 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 Operation invert() { return DeleteOperation( @@ -26,16 +32,25 @@ class InsertOperation extends Operation { } class UpdateOperation extends Operation { - final Path path; final Attributes attributes; final Attributes oldAttributes; UpdateOperation({ - required this.path, + required super.path, required this.attributes, 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 Operation invert() { return UpdateOperation( @@ -47,14 +62,19 @@ class UpdateOperation extends Operation { } class DeleteOperation extends Operation { - final Path path; final Node removedValue; DeleteOperation({ - required this.path, + required super.path, 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 Operation invert() { return InsertOperation( @@ -65,18 +85,61 @@ class DeleteOperation extends Operation { } class TextEditOperation extends Operation { - final Path path; final Delta delta; final Delta inverted; TextEditOperation({ - required this.path, + required super.path, required this.delta, 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 Operation invert() { 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; +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart new file mode 100644 index 0000000000..53c2a243b9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -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]); + }); + }); +}