refactor: move operation.dart to core/transform

This commit is contained in:
Lucas.Xu 2022-10-10 16:28:07 +08:00
parent 805bdc9d32
commit 319875529f
10 changed files with 230 additions and 232 deletions

View File

@ -12,7 +12,7 @@ export 'src/core/document/text_delta.dart';
export 'src/core/document/attributes.dart';
export 'src/core/legacy/built_in_attribute_keys.dart';
export 'src/editor_state.dart';
export 'src/operation/operation.dart';
export 'src/core/transform/operation.dart';
export 'src/operation/transaction.dart';
export 'src/operation/transaction_builder.dart';
export 'src/render/selection/selectable.dart';

View File

@ -42,7 +42,7 @@ class Document {
}
/// Inserts a [Node]s at the given [Path].
bool insert(Path path, List<Node> nodes) {
bool insert(Path path, Iterable<Node> nodes) {
if (path.isEmpty || nodes.isEmpty) {
return false;
}

View File

@ -0,0 +1,216 @@
import 'package:appflowy_editor/src/core/document/attributes.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart';
/// [Operation] represents a change to a [Document].
abstract class Operation {
Operation(
this.path,
);
factory Operation.fromJson() => throw UnimplementedError();
final Path path;
/// Inverts the operation.
///
/// Returns the inverted operation.
Operation invert();
/// Returns the JSON representation of the operation.
Map<String, dynamic> toJson();
Operation copyWith({Path? path});
}
/// [InsertOperation] represents an insert operation.
class InsertOperation extends Operation {
InsertOperation(
super.path,
this.nodes,
);
factory InsertOperation.fromJson(Map<String, dynamic> json) {
final path = json['path'] as Path;
final nodes = (json['nodes'] as List).map((n) => Node.fromJson(n));
return InsertOperation(path, nodes);
}
final Iterable<Node> nodes;
@override
Operation invert() => DeleteOperation(path, nodes);
@override
Map<String, dynamic> toJson() {
return {
'op': 'insert',
'path': path,
'nodes': nodes.map((n) => n.toJson()),
};
}
@override
Operation copyWith({Path? path}) {
return InsertOperation(path ?? this.path, nodes);
}
}
/// [DeleteOperation] represents a delete operation.
class DeleteOperation extends Operation {
DeleteOperation(
super.path,
this.nodes,
);
factory DeleteOperation.fromJson(Map<String, dynamic> json) {
final path = json['path'] as Path;
final nodes = (json['nodes'] as List).map((n) => Node.fromJson(n));
return DeleteOperation(path, nodes);
}
final Iterable<Node> nodes;
@override
Operation invert() => InsertOperation(path, nodes);
@override
Map<String, dynamic> toJson() {
return {
'op': 'delete',
'path': path,
'nodes': nodes.map((n) => n.toJson()),
};
}
@override
Operation copyWith({Path? path}) {
return DeleteOperation(path ?? this.path, nodes);
}
}
/// [UpdateOperation] represents an attributes update operation.
class UpdateOperation extends Operation {
UpdateOperation(
super.path,
this.attributes,
this.oldAttributes,
);
factory UpdateOperation.fromJson(Map<String, dynamic> json) {
final path = json['path'] as Path;
final oldAttributes = json['oldAttributes'] as Attributes;
final attributes = json['attributes'] as Attributes;
return UpdateOperation(
path,
attributes,
oldAttributes,
);
}
final Attributes attributes;
final Attributes oldAttributes;
@override
Operation invert() => UpdateOperation(
path,
oldAttributes,
attributes,
);
@override
Map<String, dynamic> toJson() {
return {
'op': 'update',
'path': path,
'attributes': {...attributes},
'oldAttributes': {...oldAttributes},
};
}
@override
Operation copyWith({Path? path}) {
return UpdateOperation(
path ?? this.path,
{...attributes},
{...oldAttributes},
);
}
}
/// [UpdateTextOperation] represents a text update operation.
class UpdateTextOperation extends Operation {
UpdateTextOperation(
super.path,
this.delta,
this.inverted,
);
factory UpdateTextOperation.fromJson(Map<String, dynamic> json) {
final path = json['path'] as Path;
final delta = Delta.fromJson(json['delta']);
final inverted = Delta.fromJson(json['invert']);
return UpdateTextOperation(path, delta, inverted);
}
final Delta delta;
final Delta inverted;
@override
Operation invert() => UpdateTextOperation(path, inverted, delta);
@override
Map<String, dynamic> toJson() {
return {
'op': 'update_text',
'path': path,
'delta': delta.toJson(),
'inverted': inverted.toJson(),
};
}
@override
Operation copyWith({Path? path}) {
return UpdateTextOperation(path ?? this.path, delta, inverted);
}
}
// TODO(Lucas.Xu): refactor this part
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);
} else {
prefix.add(bAtIndex);
}
prefix.addAll(suffix);
return prefix;
}
Operation transformOperation(Operation a, Operation b) {
if (a is InsertOperation) {
final newPath = transformPath(a.path, b.path, a.nodes.length);
return b.copyWith(path: newPath);
} else if (a is DeleteOperation) {
final newPath = transformPath(a.path, b.path, -1 * a.nodes.length);
return b.copyWith(path: newPath);
}
// TODO: transform update and textedit
return b;
}

View File

@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction.dart';
import 'package:appflowy_editor/src/undo_manager.dart';
@ -166,7 +166,7 @@ class EditorState {
document.update(op.path, op.attributes);
} else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length);
} else if (op is TextEditOperation) {
} else if (op is UpdateTextOperation) {
document.updateText(op.path, op.delta);
}
_observer.add(op);

View File

@ -1,218 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
abstract class Operation {
factory Operation.fromJson(Map<String, dynamic> map) {
String t = map["op"] as String;
if (t == "insert") {
return InsertOperation.fromJson(map);
} else if (t == "update") {
return UpdateOperation.fromJson(map);
} else if (t == "delete") {
return DeleteOperation.fromJson(map);
} else if (t == "text-edit") {
return TextEditOperation.fromJson(map);
}
throw ArgumentError('unexpected type $t');
}
final Path path;
Operation(this.path);
Operation copyWithPath(Path path);
Operation invert();
Map<String, dynamic> toJson();
}
class InsertOperation extends Operation {
final List<Node> nodes;
factory InsertOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final value =
(map["nodes"] as List<dynamic>).map((n) => Node.fromJson(n)).toList();
return InsertOperation(path, value);
}
InsertOperation(Path path, this.nodes) : super(path);
InsertOperation copyWith({Path? path, List<Node>? nodes}) =>
InsertOperation(path ?? this.path, nodes ?? this.nodes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return DeleteOperation(
path,
nodes,
);
}
@override
Map<String, dynamic> toJson() {
return {
"op": "insert",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
}
}
class UpdateOperation extends Operation {
final Attributes attributes;
final Attributes oldAttributes;
factory UpdateOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final attributes = map["attributes"] as Map<String, dynamic>;
final oldAttributes = map["oldAttributes"] as Map<String, dynamic>;
return UpdateOperation(path, attributes, oldAttributes);
}
UpdateOperation(
Path path,
this.attributes,
this.oldAttributes,
) : super(path);
UpdateOperation copyWith(
{Path? path, Attributes? attributes, Attributes? oldAttributes}) =>
UpdateOperation(path ?? this.path, attributes ?? this.attributes,
oldAttributes ?? this.oldAttributes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return UpdateOperation(
path,
oldAttributes,
attributes,
);
}
@override
Map<String, dynamic> toJson() {
return {
"op": "update",
"path": path.toList(),
"attributes": {...attributes},
"oldAttributes": {...oldAttributes},
};
}
}
class DeleteOperation extends Operation {
final List<Node> nodes;
factory DeleteOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final List<Node> nodes =
(map["nodes"] as List<dynamic>).map((e) => Node.fromJson(e)).toList();
return DeleteOperation(path, nodes);
}
DeleteOperation(
Path path,
this.nodes,
) : super(path);
DeleteOperation copyWith({Path? path, List<Node>? nodes}) =>
DeleteOperation(path ?? this.path, nodes ?? this.nodes);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return InsertOperation(path, nodes);
}
@override
Map<String, dynamic> toJson() {
return {
"op": "delete",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
}
}
class TextEditOperation extends Operation {
final Delta delta;
final Delta inverted;
factory TextEditOperation.fromJson(Map<String, dynamic> map) {
final path = map["path"] as List<int>;
final delta = Delta.fromJson(map["delta"]);
final invert = Delta.fromJson(map["invert"]);
return TextEditOperation(path, delta, invert);
}
TextEditOperation(
Path path,
this.delta,
this.inverted,
) : super(path);
TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) =>
TextEditOperation(
path ?? this.path, delta ?? this.delta, inverted ?? this.inverted);
@override
Operation copyWithPath(Path path) => copyWith(path: path);
@override
Operation invert() {
return TextEditOperation(path, inverted, delta);
}
@override
Map<String, dynamic> toJson() {
return {
"op": "text-edit",
"path": path.toList(),
"delta": delta.toJson(),
"invert": inverted.toJson(),
};
}
}
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);
} else {
prefix.add(bAtIndex);
}
prefix.addAll(suffix);
return prefix;
}
Operation transformOperation(Operation a, Operation b) {
if (a is InsertOperation) {
final newPath = transformPath(a.path, b.path, a.nodes.length);
return b.copyWithPath(newPath);
} else if (a is DeleteOperation) {
final newPath = transformPath(a.path, b.path, -1 * a.nodes.length);
return b.copyWithPath(newPath);
}
// TODO: transform update and textedit
return b;
}

View File

@ -1,7 +1,7 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import './operation.dart';
import '../core/transform/operation.dart';
/// A [Transaction] has a list of [Operation] objects that will be applied
/// to the editor. It is an immutable class and used to store and transmit.

View File

@ -8,7 +8,7 @@ import 'package:appflowy_editor/src/core/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction.dart';
/// A [TransactionBuilder] is used to build the transaction from the state.
@ -85,7 +85,7 @@ class TransactionBuilder {
final inverted = delta.invert(node.delta);
add(TextEditOperation(path, delta, inverted));
add(UpdateTextOperation(path, delta, inverted));
}
setAfterSelection(Selection sel) {
@ -195,10 +195,10 @@ class TransactionBuilder {
add(Operation op, {bool transform = true}) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
if (op is TextEditOperation &&
last is TextEditOperation &&
if (op is UpdateTextOperation &&
last is UpdateTextOperation &&
op.path.equals(last.path)) {
final newOp = TextEditOperation(
final newOp = UpdateTextOperation(
op.path,
last.delta.compose(op.delta),
op.inverted.compose(last.inverted),
@ -212,7 +212,7 @@ class TransactionBuilder {
op = transformOperation(operations[i], op);
}
}
if (op is TextEditOperation && op.delta.isEmpty) {
if (op is UpdateTextOperation && op.delta.isEmpty) {
return;
}
operations.add(op);

View File

@ -2,7 +2,7 @@ import 'dart:collection';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/operation/transaction.dart';
import 'package:appflowy_editor/src/editor_state.dart';

View File

@ -2,7 +2,7 @@ import 'dart:collection';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/core/document/document.dart';

View File

@ -63,7 +63,7 @@ bool isInsertAndPathEqual(Operation operation, Path path, [String? content]) {
return false;
}
final firstNode = operation.nodes[0];
final firstNode = operation.nodes.first;
if (firstNode is! TextNode) {
return false;
}