mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: move operation.dart to core/transform
This commit is contained in:
parent
805bdc9d32
commit
319875529f
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user