refactor: move transaction.dart to core/transform

This commit is contained in:
Lucas.Xu
2022-10-10 20:07:52 +08:00
parent 319875529f
commit 19bf8e3b7a
28 changed files with 566 additions and 585 deletions

View File

@ -26,9 +26,9 @@ ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
if (selection.isCollapsed) { if (selection.isCollapsed) {
TransactionBuilder(editorState) editorState.transaction
..insertText(codeBlockNode.first, selection.end.offset, '\n') .insertText(codeBlockNode.first, selection.end.offset, '\n');
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;
@ -61,16 +61,16 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
return; return;
} }
if (textNodes.first.toPlainText().isEmpty) { if (textNodes.first.toPlainText().isEmpty) {
TransactionBuilder(editorState) editorState.transaction
..updateNode(textNodes.first, { ..updateNode(textNodes.first, {
'subtype': 'code_block', 'subtype': 'code_block',
'theme': 'vs', 'theme': 'vs',
'language': null, 'language': null,
}) })
..afterSelection = selection ..afterSelection = selection;
..commit(); editorState.commit();
} else { } else {
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
TextNode( TextNode(
@ -83,8 +83,8 @@ SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
delta: Delta()..insert('\n'), delta: Delta()..insert('\n'),
), ),
) )
..afterSelection = selection ..afterSelection = selection;
..commit(); editorState.commit();
} }
}, },
); );
@ -181,11 +181,10 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
child: DropdownButton<String>( child: DropdownButton<String>(
value: _detectLanguage, value: _detectLanguage,
onChanged: (value) { onChanged: (value) {
TransactionBuilder(widget.editorState) widget.editorState.transaction.updateNode(widget.textNode, {
..updateNode(widget.textNode, {
'language': value, 'language': value,
}) });
..commit(); widget.editorState.commit();
}, },
items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) { items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(

View File

@ -18,7 +18,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
} }
final textNode = textNodes.first; final textNode = textNodes.first;
if (textNode.toPlainText() == '--') { if (textNode.toPlainText() == '--') {
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, 0, 2) ..deleteText(textNode, 0, 2)
..insertNode( ..insertNode(
textNode.path, textNode.path,
@ -29,8 +29,8 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
), ),
) )
..afterSelection = ..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0) Selection.single(path: textNode.path.next, startOffset: 0);
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;
@ -54,7 +54,7 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
} }
final textNode = textNodes.first; final textNode = textNodes.first;
if (textNode.toPlainText().isEmpty) { if (textNode.toPlainText().isEmpty) {
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
textNode.path, textNode.path,
Node( Node(
@ -64,10 +64,10 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
), ),
) )
..afterSelection = ..afterSelection =
Selection.single(path: textNode.path.next, startOffset: 0) Selection.single(path: textNode.path.next, startOffset: 0);
..commit(); editorState.commit();
} else { } else {
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
TextNode( TextNode(
@ -78,8 +78,8 @@ SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
delta: Delta()..insert('---'), delta: Delta()..insert('---'),
), ),
) )
..afterSelection = selection ..afterSelection = selection;
..commit(); editorState.commit();
} }
}, },
); );

View File

@ -23,7 +23,7 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
final Path texNodePath; final Path texNodePath;
if (textNodes.first.toPlainText().isEmpty) { if (textNodes.first.toPlainText().isEmpty) {
texNodePath = selection.end.path; texNodePath = selection.end.path;
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
selection.end.path, selection.end.path,
Node( Node(
@ -33,11 +33,11 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
), ),
) )
..deleteNode(textNodes.first) ..deleteNode(textNodes.first)
..afterSelection = selection ..afterSelection = selection;
..commit(); editorState.commit();
} else { } else {
texNodePath = selection.end.path.next; texNodePath = selection.end.path.next;
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
selection.end.path.next, selection.end.path.next,
Node( Node(
@ -46,8 +46,8 @@ SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
attributes: {'tex': ''}, attributes: {'tex': ''},
), ),
) )
..afterSelection = selection ..afterSelection = selection;
..commit(); editorState.commit();
} }
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final texState = final texState =
@ -142,9 +142,8 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
size: 16, size: 16,
), ),
onPressed: () { onPressed: () {
TransactionBuilder(widget.editorState) widget.editorState.transaction.deleteNode(widget.node);
..deleteNode(widget.node) widget.editorState.commit();
..commit();
}, },
), ),
); );
@ -175,12 +174,11 @@ class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
if (controller.text != _tex) { if (controller.text != _tex) {
TransactionBuilder(widget.editorState) widget.editorState.transaction.updateNode(
..updateNode(
widget.node, widget.node,
{'tex': controller.text}, {'tex': controller.text},
) );
..commit(); widget.editorState.commit();
} }
}, },
child: const Text('OK'), child: const Text('OK'),

View File

@ -31,7 +31,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
// Delete the previous 'underscore', // Delete the previous 'underscore',
// update the style of the text surrounded by the two underscores to 'italic', // update the style of the text surrounded by the two underscores to 'italic',
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, firstUnderscore, 1) ..deleteText(textNode, firstUnderscore, 1)
..formatText( ..formatText(
textNode, textNode,
@ -46,8 +46,8 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: selection.end.offset - 1, offset: selection.end.offset - 1,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -13,8 +13,7 @@ export 'src/core/document/attributes.dart';
export 'src/core/legacy/built_in_attribute_keys.dart'; export 'src/core/legacy/built_in_attribute_keys.dart';
export 'src/editor_state.dart'; export 'src/editor_state.dart';
export 'src/core/transform/operation.dart'; export 'src/core/transform/operation.dart';
export 'src/operation/transaction.dart'; export 'src/core/transform/transaction.dart';
export 'src/operation/transaction_builder.dart';
export 'src/render/selection/selectable.dart'; export 'src/render/selection/selectable.dart';
export 'src/service/editor_service.dart'; export 'src/service/editor_service.dart';
export 'src/service/render_plugin_service.dart'; export 'src/service/render_plugin_service.dart';

View File

@ -4,7 +4,7 @@ import 'package:appflowy_editor/src/commands/text_command_infra.dart';
import 'package:appflowy_editor/src/core/document/node.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/path.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
Future<void> insertContextInText( Future<void> insertContextInText(
@ -22,9 +22,8 @@ Future<void> insertContextInText(
final completer = Completer<void>(); final completer = Completer<void>();
TransactionBuilder(editorState) editorState.transaction.insertText(result, index, content);
..insertText(result, index, content) editorState.commit();
..commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(); completer.complete();

View File

@ -6,7 +6,7 @@ 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/path.dart';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
Future<void> updateTextNodeAttributes( Future<void> updateTextNodeAttributes(
@ -23,9 +23,8 @@ Future<void> updateTextNodeAttributes(
final completer = Completer<void>(); final completer = Completer<void>();
TransactionBuilder(editorState) editorState.transaction.updateNode(result, attributes);
..updateNode(result, attributes) editorState.commit();
..commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(); completer.complete();
@ -49,15 +48,13 @@ Future<void> updateTextNodeDeltaAttributes(
final newSelection = getSelection(editorState, selection: selection); final newSelection = getSelection(editorState, selection: selection);
final completer = Completer<void>(); final completer = Completer<void>();
editorState.transaction.formatText(
TransactionBuilder(editorState)
..formatText(
result, result,
newSelection.startIndex, newSelection.startIndex,
newSelection.length, newSelection.length,
attributes, attributes,
) );
..commit(); editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(); completer.complete();

View File

@ -0,0 +1,267 @@
import 'dart:math';
import 'package:appflowy_editor/src/core/document/attributes.dart';
import 'package:appflowy_editor/src/core/document/document.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';
import 'package:appflowy_editor/src/core/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart';
/// A [Transaction] has a list of [Operation] objects that will be applied
/// to the editor.
///
/// There will be several ways to consume the transaction:
/// 1. Apply to the state to update the UI.
/// 2. Send to the backend to store and do operation transforming.
class Transaction {
Transaction({
required this.document,
});
final Document document;
/// The operations to be applied.
final List<Operation> operations = [];
/// The selection to be applied.
Selection? afterSelection;
/// The before selection is to be recovered if needed.
Selection? beforeSelection;
/// Inserts the [Node] at the given [Path].
void insertNode(
Path path,
Node node, {
bool deepCopy = true,
}) {
insertNodes(path, [node], deepCopy: deepCopy);
}
/// Inserts a sequence of [Node]s at the given [Path].
void insertNodes(
Path path,
Iterable<Node> nodes, {
bool deepCopy = true,
}) {
if (deepCopy) {
add(InsertOperation(path, nodes.map((e) => e.copyWith())));
} else {
add(InsertOperation(path, nodes));
}
}
/// Updates the attributes of the [Node].
///
/// The [attributes] will be merged into the existing attributes.
void updateNode(Node node, Attributes attributes) {
final inverted = invertAttributes(node.attributes, attributes);
add(UpdateOperation(
node.path,
{...attributes},
inverted,
));
}
/// Deletes the [Node] in the document.
void deleteNode(Node node) {
deleteNodesAtPath(node.path);
}
/// Deletes the [Node]s in the document.
void deleteNodes(Iterable<Node> nodes) {
nodes.forEach(deleteNode);
}
/// Deletes the [Node]s at the given [Path].
///
/// The [length] indicates the number of consecutive deletions,
/// including the node of the current path.
void deleteNodesAtPath(Path path, [int length = 1]) {
if (path.isEmpty) return;
final nodes = <Node>[];
final parent = path.parent;
for (var i = 0; i < length; i++) {
final node = document.nodeAtPath(parent + [path.last + i]);
if (node == null) {
break;
}
nodes.add(node);
}
add(DeleteOperation(path, nodes));
}
/// Update the [TextNode]s with the given [Delta].
void updateText(TextNode textNode, Delta delta) {
final inverted = delta.invert(textNode.delta);
add(UpdateTextOperation(textNode.path, delta, inverted));
}
/// Returns the JSON representation of the transaction.
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (operations.isNotEmpty) {
json['operations'] = operations.map((o) => o.toJson());
}
if (afterSelection != null) {
json['after_selection'] = afterSelection!.toJson();
}
if (beforeSelection != null) {
json['before_selection'] = beforeSelection!.toJson();
}
return json;
}
/// Adds an operation to the transaction.
/// This method will merge operations if they are both TextEdits.
///
/// Also, this method will transform the path of the operations
/// to avoid conflicts.
add(Operation op, {bool transform = true}) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
if (op is UpdateTextOperation &&
last is UpdateTextOperation &&
op.path.equals(last.path)) {
final newOp = UpdateTextOperation(
op.path,
last.delta.compose(op.delta),
op.inverted.compose(last.inverted),
);
operations[operations.length - 1] = newOp;
return;
}
}
if (transform) {
for (var i = 0; i < operations.length; i++) {
op = transformOperation(operations[i], op);
}
}
if (op is UpdateTextOperation && op.delta.isEmpty) {
return;
}
operations.add(op);
}
}
extension TextTransaction on Transaction {
void mergeText(
TextNode first,
TextNode second, {
int? firstOffset,
int secondOffset = 0,
}) {
final firstLength = first.delta.length;
final secondLength = second.delta.length;
firstOffset ??= firstLength;
updateText(
first,
Delta()
..retain(firstOffset)
..delete(firstLength - firstOffset)
..addAll(second.delta.slice(secondOffset, secondLength)),
);
afterSelection = Selection.collapsed(Position(
path: first.path,
offset: firstOffset,
));
}
/// Inserts the text content at a specified index.
///
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
/// By default, the formatting attributes before the insert position will be reused.
void insertText(
TextNode textNode,
int index,
String text, {
Attributes? attributes,
}) {
var newAttributes = attributes;
if (index != 0 && attributes == null) {
newAttributes =
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
if (newAttributes != null) {
newAttributes = {...newAttributes}; // make a copy
}
}
updateText(
textNode,
Delta()
..retain(index)
..insert(text, attributes: newAttributes),
);
afterSelection = Selection.collapsed(
Position(path: textNode.path, offset: index + text.length),
);
}
/// Assigns a formatting attributes to a range of text.
formatText(
TextNode textNode,
int index,
int length,
Attributes attributes,
) {
afterSelection = beforeSelection;
updateText(
textNode,
Delta()
..retain(index)
..retain(length, attributes: attributes),
);
}
/// Deletes the text of specified length starting at index.
deleteText(
TextNode textNode,
int index,
int length,
) {
updateText(
textNode,
Delta()
..retain(index)
..delete(length),
);
afterSelection = Selection.collapsed(
Position(path: textNode.path, offset: index),
);
}
/// Replaces the text of specified length starting at index.
///
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
/// By default, the formatting attributes before the insert position will be reused.
replaceText(
TextNode textNode,
int index,
int length,
String text, {
Attributes? attributes,
}) {
var newAttributes = attributes;
if (index != 0 && attributes == null) {
newAttributes =
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
if (newAttributes != null) {
newAttributes = {...newAttributes}; // make a copy
}
}
updateText(
textNode,
Delta()
..retain(index)
..delete(length)
..insert(text, attributes: newAttributes),
);
afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: index + text.length,
),
);
}
}

View File

@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/core/document/document.dart'; import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/core/transform/operation.dart'; import 'package:appflowy_editor/src/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction.dart'; import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:appflowy_editor/src/undo_manager.dart'; import 'package:appflowy_editor/src/undo_manager.dart';
class ApplyOptions { class ApplyOptions {
@ -74,6 +74,24 @@ class EditorState {
bool editable = true; bool editable = true;
Transaction get transaction {
if (_transaction != null) {
return _transaction!;
}
_transaction = Transaction(document: document);
_transaction!.beforeSelection = _cursorSelection;
return _transaction!;
}
Transaction? _transaction;
void commit() {
if (_transaction != null) {
apply(_transaction!, const ApplyOptions(recordUndo: true));
_transaction = null;
}
}
Selection? get cursorSelection { Selection? get cursorSelection {
return _cursorSelection; return _cursorSelection;
} }

View File

@ -1,39 +0,0 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.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.
///
/// If you want to build a new [Transaction], use [TransactionBuilder] directly.
///
/// There will be several ways to consume the transaction:
/// 1. Apply to the state to update the UI.
/// 2. Send to the backend to store and do operation transforming.
/// 3. Used by the UndoManager to implement redo/undo.
@immutable
class Transaction {
final UnmodifiableListView<Operation> operations;
final Selection? beforeSelection;
final Selection? afterSelection;
const Transaction({
required this.operations,
this.beforeSelection,
this.afterSelection,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> result = {
"operations": operations.map((e) => e.toJson()),
};
if (beforeSelection != null) {
result["beforeSelection"] = beforeSelection!.toJson();
}
if (afterSelection != null) {
result["afterSelection"] = afterSelection!.toJson();
}
return result;
}
}

View File

@ -1,229 +0,0 @@
import 'dart:collection';
import 'dart:math';
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/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/core/transform/operation.dart';
import 'package:appflowy_editor/src/operation/transaction.dart';
/// A [TransactionBuilder] is used to build the transaction from the state.
/// It will save a snapshot of the cursor selection state automatically.
/// The cursor can be restored if the transaction is undo.
class TransactionBuilder {
final List<Operation> operations = [];
EditorState state;
Selection? beforeSelection;
Selection? afterSelection;
TransactionBuilder(this.state);
/// Commits the operations to the state
Future<void> commit() async {
final transaction = finish();
state.apply(transaction);
}
/// Inserts the nodes at the position of path.
insertNode(Path path, Node node) {
insertNodes(path, [node]);
}
/// Inserts a sequence of nodes at the position of path.
insertNodes(Path path, List<Node> nodes) {
beforeSelection = state.cursorSelection;
add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
}
/// Updates the attributes of nodes.
updateNode(Node node, Attributes attributes) {
beforeSelection = state.cursorSelection;
final inverted = invertAttributes(node.attributes, attributes);
add(UpdateOperation(
node.path,
{...attributes},
inverted,
));
}
/// Deletes a node in the document.
deleteNode(Node node) {
deleteNodesAtPath(node.path);
}
deleteNodes(List<Node> nodes) {
nodes.forEach(deleteNode);
}
/// Deletes a sequence of nodes at the path of the document.
/// The length specifies the length of the following nodes to delete(
/// including the start one).
deleteNodesAtPath(Path path, [int length = 1]) {
if (path.isEmpty) {
return;
}
final nodes = <Node>[];
final prefix = path.sublist(0, path.length - 1);
final last = path.last;
for (var i = 0; i < length; i++) {
final node = state.document.nodeAtPath(prefix + [last + i])!;
nodes.add(node);
}
add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
}
textEdit(TextNode node, Delta Function() f) {
beforeSelection = state.cursorSelection;
final path = node.path;
final delta = f();
final inverted = delta.invert(node.delta);
add(UpdateTextOperation(path, delta, inverted));
}
setAfterSelection(Selection sel) {
afterSelection = sel;
}
mergeText(TextNode firstNode, TextNode secondNode,
{int? firstOffset, int secondOffset = 0}) {
final firstLength = firstNode.delta.length;
final secondLength = secondNode.delta.length;
textEdit(
firstNode,
() => Delta()
..retain(firstOffset ?? firstLength)
..delete(firstLength - (firstOffset ?? firstLength))
..addAll(secondNode.delta.slice(secondOffset, secondLength)),
);
afterSelection = Selection.collapsed(
Position(
path: firstNode.path,
offset: firstOffset ?? firstLength,
),
);
}
/// Inserts content at a specified index.
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
/// By default, the formatting attributes before the insert position will be used.
insertText(
TextNode node,
int index,
String content, {
Attributes? attributes,
}) {
var newAttributes = attributes;
if (index != 0 && attributes == null) {
newAttributes =
node.delta.slice(max(index - 1, 0), index).first.attributes;
if (newAttributes != null) {
newAttributes = Attributes.from(newAttributes);
}
}
textEdit(
node,
() => Delta()
..retain(index)
..insert(
content,
attributes: newAttributes,
),
);
afterSelection = Selection.collapsed(
Position(path: node.path, offset: index + content.length),
);
}
/// Assigns formatting attributes to a range of text.
formatText(TextNode node, int index, int length, Attributes attributes) {
textEdit(
node,
() => Delta()
..retain(index)
..retain(length, attributes: attributes));
afterSelection = beforeSelection;
}
/// Deletes length characters starting from index.
deleteText(TextNode node, int index, int length) {
textEdit(
node,
() => Delta()
..retain(index)
..delete(length));
afterSelection =
Selection.collapsed(Position(path: node.path, offset: index));
}
replaceText(TextNode node, int index, int length, String content,
[Attributes? attributes]) {
var newAttributes = attributes;
if (attributes == null) {
final ops = node.delta.slice(index, index + length);
if (ops.isNotEmpty) {
newAttributes = ops.first.attributes;
}
}
textEdit(
node,
() => Delta()
..retain(index)
..delete(length)
..insert(content, attributes: newAttributes),
);
afterSelection = Selection.collapsed(
Position(
path: node.path,
offset: index + content.length,
),
);
}
/// Adds an operation to the transaction.
/// This method will merge operations if they are both TextEdits.
///
/// Also, this method will transform the path of the operations
/// to avoid conflicts.
add(Operation op, {bool transform = true}) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
if (op is UpdateTextOperation &&
last is UpdateTextOperation &&
op.path.equals(last.path)) {
final newOp = UpdateTextOperation(
op.path,
last.delta.compose(op.delta),
op.inverted.compose(last.inverted),
);
operations[operations.length - 1] = newOp;
return;
}
}
if (transform) {
for (var i = 0; i < operations.length; i++) {
op = transformOperation(operations[i], op);
}
}
if (op is UpdateTextOperation && op.delta.isEmpty) {
return;
}
operations.add(op);
}
/// Generates a immutable [Transaction] to apply or transmit.
Transaction finish() {
return Transaction(
operations: UnmodifiableListView(operations),
beforeSelection: beforeSelection,
afterSelection: afterSelection,
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart'; import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rich_clipboard/rich_clipboard.dart'; import 'package:rich_clipboard/rich_clipboard.dart';
@ -25,23 +24,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
RichClipboard.setData(RichClipboardData(text: src)); RichClipboard.setData(RichClipboardData(text: src));
}, },
onDelete: () { onDelete: () {
TransactionBuilder(context.editorState) context.editorState.transaction.deleteNode(context.node);
..deleteNode(context.node) context.editorState.commit();
..commit();
}, },
onAlign: (alignment) { onAlign: (alignment) {
TransactionBuilder(context.editorState) context.editorState.transaction.updateNode(context.node, {
..updateNode(context.node, {
'align': _alignmentToText(alignment), 'align': _alignmentToText(alignment),
}) });
..commit(); context.editorState.commit();
}, },
onResize: (width) { onResize: (width) {
TransactionBuilder(context.editorState) context.editorState.transaction.updateNode(context.node, {
..updateNode(context.node, {
'width': width, 'width': width,
}) });
..commit(); context.editorState.commit();
}, },
); );
} }

View File

@ -3,7 +3,7 @@ import 'dart:collection';
import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -192,11 +192,10 @@ extension on EditorState {
'align': 'center', 'align': 'center',
}, },
); );
TransactionBuilder(this) transaction.insertNode(
..insertNode(
selection.start.path, selection.start.path,
imageNode, imageNode,
) );
..commit(); commit();
} }
} }

View File

@ -45,13 +45,12 @@ class SelectionMenuItem {
final node = nodes.first as TextNode; final node = nodes.first as TextNode;
final end = selection.start.offset; final end = selection.start.offset;
final start = node.toPlainText().substring(0, end).lastIndexOf('/'); final start = node.toPlainText().substring(0, end).lastIndexOf('/');
TransactionBuilder(editorState) editorState.transaction.deleteText(
..deleteText(
node, node,
start, start,
selection.start.offset - start, selection.start.offset - start,
) );
..commit(); editorState.commit();
} }
} }
} }
@ -278,13 +277,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
final nodes = selectionService.currentSelectedNodes; final nodes = selectionService.currentSelectedNodes;
if (selection != null && nodes.length == 1) { if (selection != null && nodes.length == 1) {
widget.onSelectionUpdate(); widget.onSelectionUpdate();
TransactionBuilder(widget.editorState) widget.editorState.transaction.deleteText(
..deleteText(
nodes.first as TextNode, nodes.first as TextNode,
selection.start.offset - length, selection.start.offset - length,
length, length,
) );
..commit(); widget.editorState.commit();
} }
} }
@ -295,13 +293,12 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
widget.editorState.service.selectionService.currentSelectedNodes; widget.editorState.service.selectionService.currentSelectedNodes;
if (selection != null && nodes.length == 1) { if (selection != null && nodes.length == 1) {
widget.onSelectionUpdate(); widget.onSelectionUpdate();
TransactionBuilder(widget.editorState) widget.editorState.transaction.insertText(
..insertText(
nodes.first as TextNode, nodes.first as TextNode,
selection.end.offset, selection.end.offset,
text, text,
) );
..commit(); widget.editorState.commit();
} }
} }
} }

View File

@ -357,10 +357,9 @@ void showLinkMenu(
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onRemoveLink: () { onRemoveLink: () {
TransactionBuilder(editorState) editorState.transaction.formatText(
..formatText( textNode, index, length, {BuiltInAttributeKey.href: null});
textNode, index, length, {BuiltInAttributeKey.href: null}) editorState.commit();
..commit();
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onFocusChange: (value) { onFocusChange: (value) {

View File

@ -1,12 +1,5 @@
import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/appflowy_editor.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/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
void insertHeadingAfterSelection(EditorState editorState, String heading) { void insertHeadingAfterSelection(EditorState editorState, String heading) {
insertTextNodeAfterSelection(editorState, { insertTextNodeAfterSelection(editorState, {
@ -54,16 +47,15 @@ bool insertTextNodeAfterSelection(
formatTextNodes(editorState, attributes); formatTextNodes(editorState, attributes);
} else { } else {
final next = selection.end.path.next; final next = selection.end.path.next;
final builder = TransactionBuilder(editorState); editorState.transaction
builder
..insertNode( ..insertNode(
next, next,
TextNode.empty(attributes: attributes), TextNode.empty(attributes: attributes),
) )
..afterSelection = Selection.collapsed( ..afterSelection = Selection.collapsed(
Position(path: next, offset: 0), Position(path: next, offset: 0),
) );
..commit(); editorState.commit();
} }
return true; return true;
@ -107,7 +99,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
return false; return false;
} }
final builder = TransactionBuilder(editorState); final transaction = editorState.transaction;
for (final textNode in textNodes) { for (final textNode in textNodes) {
var newAttributes = {...textNode.attributes}; var newAttributes = {...textNode.attributes};
@ -117,7 +109,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
} }
} }
newAttributes.addAll(attributes); newAttributes.addAll(attributes);
builder transaction
..updateNode( ..updateNode(
textNode, textNode,
newAttributes, newAttributes,
@ -130,7 +122,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
); );
} }
builder.commit(); editorState.commit();
return true; return true;
} }
@ -216,13 +208,13 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
return false; return false;
} }
final builder = TransactionBuilder(editorState); final transaction = editorState.transaction;
// 1. All nodes are text nodes. // 1. All nodes are text nodes.
// 2. The first node is not TextNode. // 2. The first node is not TextNode.
// 3. The last node is not TextNode. // 3. The last node is not TextNode.
if (nodes.length == textNodes.length && textNodes.length == 1) { if (nodes.length == textNodes.length && textNodes.length == 1) {
builder.formatText( transaction.formatText(
textNodes.first, textNodes.first,
selection.start.offset, selection.start.offset,
selection.end.offset - selection.start.offset, selection.end.offset - selection.start.offset,
@ -239,7 +231,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
} else if (i == textNodes.length - 1 && textNode == nodes.last) { } else if (i == textNodes.length - 1 && textNode == nodes.last) {
length = selection.end.offset; length = selection.end.offset;
} }
builder.formatText( transaction.formatText(
textNode, textNode,
index, index,
length, length,
@ -248,7 +240,7 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
} }
} }
builder.commit(); editorState.commit();
return true; return true;
} }

View File

@ -1,4 +1,5 @@
import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -7,7 +8,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
/// [AppFlowyInputService] is responsible for processing text input, /// [AppFlowyInputService] is responsible for processing text input,
/// including text insertion, deletion and replacement. /// including text insertion, deletion and replacement.
@ -160,13 +160,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
} }
if (currentSelection.isSingle) { if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode; final textNode = selectionService.currentSelectedNodes.first as TextNode;
TransactionBuilder(_editorState) _editorState.transaction.insertText(
..insertText(
textNode, textNode,
delta.insertionOffset, delta.insertionOffset,
delta.textInserted, delta.textInserted,
) );
..commit(); _editorState.commit();
} else { } else {
// TODO: implement // TODO: implement
} }
@ -181,9 +180,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
if (currentSelection.isSingle) { if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode; final textNode = selectionService.currentSelectedNodes.first as TextNode;
final length = delta.deletedRange.end - delta.deletedRange.start; final length = delta.deletedRange.end - delta.deletedRange.start;
TransactionBuilder(_editorState) _editorState.transaction
..deleteText(textNode, delta.deletedRange.start, length) .deleteText(textNode, delta.deletedRange.start, length);
..commit(); _editorState.commit();
} else { } else {
// TODO: implement // TODO: implement
} }
@ -198,10 +197,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
if (currentSelection.isSingle) { if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode; final textNode = selectionService.currentSelectedNodes.first as TextNode;
final length = delta.replacedRange.end - delta.replacedRange.start; final length = delta.replacedRange.end - delta.replacedRange.start;
TransactionBuilder(_editorState) _editorState.transaction.replaceText(
..replaceText( textNode, delta.replacedRange.start, length, delta.replacementText);
textNode, delta.replacedRange.start, length, delta.replacementText) _editorState.commit();
..commit();
} else { } else {
// TODO: implement // TODO: implement
} }

View File

@ -28,11 +28,11 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
final List<Node> nonTextNodes = final List<Node> nonTextNodes =
nodes.where((node) => node is! TextNode).toList(growable: false); nodes.where((node) => node is! TextNode).toList(growable: false);
final transactionBuilder = TransactionBuilder(editorState); final transaction = editorState.transaction;
List<int>? cancelNumberListPath; List<int>? cancelNumberListPath;
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transactionBuilder.deleteNodes(nonTextNodes); transaction.deleteNodes(nonTextNodes);
} }
if (textNodes.length == 1) { if (textNodes.length == 1) {
@ -44,7 +44,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
if (textNode.subtype == BuiltInAttributeKey.numberList) { if (textNode.subtype == BuiltInAttributeKey.numberList) {
cancelNumberListPath = textNode.path; cancelNumberListPath = textNode.path;
} }
transactionBuilder transaction
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: null, BuiltInAttributeKey.subtype: null,
textNode.subtype!: null, textNode.subtype!: null,
@ -61,20 +61,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
return _backDeleteToPreviousTextNode( return _backDeleteToPreviousTextNode(
editorState, editorState,
textNode, textNode,
transactionBuilder, transaction,
nonTextNodes, nonTextNodes,
selection, selection,
); );
} }
} else { } else {
if (selection.isCollapsed) { if (selection.isCollapsed) {
transactionBuilder.deleteText( transaction.deleteText(
textNode, textNode,
index, index,
selection.start.offset - index, selection.start.offset - index,
); );
} else { } else {
transactionBuilder.deleteText( transaction.deleteText(
textNode, textNode,
selection.start.offset, selection.start.offset,
selection.end.offset - selection.start.offset, selection.end.offset - selection.start.offset,
@ -84,33 +84,32 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
} else { } else {
if (textNodes.isEmpty) { if (textNodes.isEmpty) {
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection = transaction.afterSelection = Selection.collapsed(selection.start);
Selection.collapsed(selection.start);
} }
transactionBuilder.commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
final startPosition = selection.start; final startPosition = selection.start;
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
_deleteTextNodes(transactionBuilder, textNodes, selection); _deleteTextNodes(transaction, textNodes, selection);
transactionBuilder.commit(); editorState.commit();
if (nodeAtStart is TextNode && if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) { nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental( makeFollowingNodesIncremental(
editorState, editorState,
startPosition.path, startPosition.path,
transactionBuilder.afterSelection!, transaction.afterSelection!,
); );
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} }
if (transactionBuilder.operations.isNotEmpty) { if (transaction.operations.isNotEmpty) {
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection = Selection.collapsed(selection.start); transaction.afterSelection = Selection.collapsed(selection.start);
} }
transactionBuilder.commit(); editorState.commit();
} }
if (cancelNumberListPath != null) { if (cancelNumberListPath != null) {
@ -128,20 +127,20 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
KeyEventResult _backDeleteToPreviousTextNode( KeyEventResult _backDeleteToPreviousTextNode(
EditorState editorState, EditorState editorState,
TextNode textNode, TextNode textNode,
TransactionBuilder transactionBuilder, Transaction transaction,
List<Node> nonTextNodes, List<Node> nonTextNodes,
Selection selection, Selection selection,
) { ) {
if (textNode.next == null && if (textNode.next == null &&
textNode.children.isEmpty && textNode.children.isEmpty &&
textNode.parent?.parent != null) { textNode.parent?.parent != null) {
transactionBuilder transaction
..deleteNode(textNode) ..deleteNode(textNode)
..insertNode(textNode.parent!.path.next, textNode) ..insertNode(textNode.parent!.path.next, textNode)
..afterSelection = Selection.collapsed( ..afterSelection = Selection.collapsed(
Position(path: textNode.parent!.path.next, offset: 0), Position(path: textNode.parent!.path.next, offset: 0),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -152,15 +151,15 @@ KeyEventResult _backDeleteToPreviousTextNode(
prevIsNumberList = true; prevIsNumberList = true;
} }
transactionBuilder.mergeText(previousTextNode, textNode); transaction.mergeText(previousTextNode, textNode);
if (textNode.children.isNotEmpty) { if (textNode.children.isNotEmpty) {
transactionBuilder.insertNodes( transaction.insertNodes(
previousTextNode.path.next, previousTextNode.path.next,
textNode.children.toList(growable: false), textNode.children.toList(growable: false),
); );
} }
transactionBuilder.deleteNode(textNode); transaction.deleteNode(textNode);
transactionBuilder.afterSelection = Selection.collapsed( transaction.afterSelection = Selection.collapsed(
Position( Position(
path: previousTextNode.path, path: previousTextNode.path,
offset: previousTextNode.toPlainText().length, offset: previousTextNode.toPlainText().length,
@ -168,16 +167,16 @@ KeyEventResult _backDeleteToPreviousTextNode(
); );
} }
if (transactionBuilder.operations.isNotEmpty) { if (transaction.operations.isNotEmpty) {
if (nonTextNodes.isNotEmpty) { if (nonTextNodes.isNotEmpty) {
transactionBuilder.afterSelection = Selection.collapsed(selection.start); transaction.afterSelection = Selection.collapsed(selection.start);
} }
transactionBuilder.commit(); editorState.commit();
} }
if (prevIsNumberList) { if (prevIsNumberList) {
makeFollowingNodesIncremental(editorState, previousTextNode!.path, makeFollowingNodesIncremental(
transactionBuilder.afterSelection!); editorState, previousTextNode!.path, transaction.afterSelection!);
} }
return KeyEventResult.handled; return KeyEventResult.handled;
@ -197,7 +196,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final transactionBuilder = TransactionBuilder(editorState); final transaction = editorState.transaction;
if (textNodes.length == 1) { if (textNodes.length == 1) {
final textNode = textNodes.first; final textNode = textNodes.first;
// The cursor is at the end of the line, // The cursor is at the end of the line,
@ -206,55 +205,52 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
return _mergeNextLineIntoThisLine( return _mergeNextLineIntoThisLine(
editorState, editorState,
textNode, textNode,
transactionBuilder, transaction,
selection, selection,
); );
} }
final index = textNode.delta.nextRunePosition(selection.start.offset); final index = textNode.delta.nextRunePosition(selection.start.offset);
if (selection.isCollapsed) { if (selection.isCollapsed) {
transactionBuilder.deleteText( transaction.deleteText(
textNode, textNode,
selection.start.offset, selection.start.offset,
index - selection.start.offset, index - selection.start.offset,
); );
} else { } else {
transactionBuilder.deleteText( transaction.deleteText(
textNode, textNode,
selection.start.offset, selection.start.offset,
selection.end.offset - selection.start.offset, selection.end.offset - selection.start.offset,
); );
} }
transactionBuilder.commit(); editorState.commit();
} else { } else {
final startPosition = selection.start; final startPosition = selection.start;
final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!; final nodeAtStart = editorState.document.nodeAtPath(startPosition.path)!;
_deleteTextNodes(transactionBuilder, textNodes, selection); _deleteTextNodes(transaction, textNodes, selection);
transactionBuilder.commit(); editorState.commit();
if (nodeAtStart is TextNode && if (nodeAtStart is TextNode &&
nodeAtStart.subtype == BuiltInAttributeKey.numberList) { nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental( makeFollowingNodesIncremental(
editorState, startPosition.path, transactionBuilder.afterSelection!); editorState, startPosition.path, transaction.afterSelection!);
} }
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} }
KeyEventResult _mergeNextLineIntoThisLine( KeyEventResult _mergeNextLineIntoThisLine(EditorState editorState,
EditorState editorState, TextNode textNode, Transaction transaction, Selection selection) {
TextNode textNode,
TransactionBuilder transactionBuilder,
Selection selection) {
final nextNode = textNode.next; final nextNode = textNode.next;
if (nextNode == null) { if (nextNode == null) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
if (nextNode is TextNode) { if (nextNode is TextNode) {
transactionBuilder.mergeText(textNode, nextNode); transaction.mergeText(textNode, nextNode);
} }
transactionBuilder.deleteNode(nextNode); transaction.deleteNode(nextNode);
transactionBuilder.commit(); editorState.commit();
if (textNode.subtype == BuiltInAttributeKey.numberList) { if (textNode.subtype == BuiltInAttributeKey.numberList) {
makeFollowingNodesIncremental(editorState, textNode.path, selection); makeFollowingNodesIncremental(editorState, textNode.path, selection);
@ -263,15 +259,15 @@ KeyEventResult _mergeNextLineIntoThisLine(
return KeyEventResult.handled; return KeyEventResult.handled;
} }
void _deleteTextNodes(TransactionBuilder transactionBuilder, void _deleteTextNodes(
List<TextNode> textNodes, Selection selection) { Transaction transaction, List<TextNode> textNodes, Selection selection) {
final first = textNodes.first; final first = textNodes.first;
final last = textNodes.last; final last = textNodes.last;
var content = textNodes.last.toPlainText(); var content = textNodes.last.toPlainText();
content = content.substring(selection.end.offset, content.length); content = content.substring(selection.end.offset, content.length);
// Merge the fist and the last text node content, // Merge the fist and the last text node content,
// and delete the all nodes expect for the first. // and delete the all nodes expect for the first.
transactionBuilder transaction
..deleteNodes(textNodes.sublist(1)) ..deleteNodes(textNodes.sublist(1))
..mergeText( ..mergeText(
first, first,

View File

@ -85,16 +85,16 @@ void _pasteHTML(EditorState editorState, String html) {
} else if (nodes.length == 1) { } else if (nodes.length == 1) {
final firstNode = nodes[0]; final firstNode = nodes[0];
final nodeAtPath = editorState.document.nodeAtPath(path)!; final nodeAtPath = editorState.document.nodeAtPath(path)!;
final tb = TransactionBuilder(editorState); final tb = editorState.transaction;
final startOffset = selection.start.offset; final startOffset = selection.start.offset;
if (nodeAtPath.type == "text" && firstNode.type == "text") { if (nodeAtPath.type == "text" && firstNode.type == "text") {
final textNodeAtPath = nodeAtPath as TextNode; final textNodeAtPath = nodeAtPath as TextNode;
final firstTextNode = firstNode as TextNode; final firstTextNode = firstNode as TextNode;
tb.textEdit(textNodeAtPath, tb.updateText(
() => (Delta()..retain(startOffset)) + firstTextNode.delta); textNodeAtPath, (Delta()..retain(startOffset)) + firstTextNode.delta);
tb.setAfterSelection(Selection.collapsed(Position( tb.afterSelection = (Selection.collapsed(Position(
path: path, offset: startOffset + firstTextNode.delta.length))); path: path, offset: startOffset + firstTextNode.delta.length)));
tb.commit(); editorState.commit();
return; return;
} }
} }
@ -104,7 +104,7 @@ void _pasteHTML(EditorState editorState, String html) {
void _pasteMultipleLinesInText( void _pasteMultipleLinesInText(
EditorState editorState, List<int> path, int offset, List<Node> nodes) { EditorState editorState, List<int> path, int offset, List<Node> nodes) {
final tb = TransactionBuilder(editorState); final tb = editorState.transaction;
final firstNode = nodes[0]; final firstNode = nodes[0];
final nodeAtPath = editorState.document.nodeAtPath(path)!; final nodeAtPath = editorState.document.nodeAtPath(path)!;
@ -120,9 +120,8 @@ void _pasteMultipleLinesInText(
final firstTextNode = firstNode as TextNode; final firstTextNode = firstNode as TextNode;
final remain = textNodeAtPath.delta.slice(offset); final remain = textNodeAtPath.delta.slice(offset);
tb.textEdit( tb.updateText(
textNodeAtPath, textNodeAtPath,
() =>
(Delta() (Delta()
..retain(offset) ..retain(offset)
..delete(remain.length)) + ..delete(remain.length)) +
@ -146,9 +145,9 @@ void _pasteMultipleLinesInText(
tailNodes.add(TextNode(delta: remain)); tailNodes.add(TextNode(delta: remain));
} }
tb.setAfterSelection(afterSelection); tb.afterSelection = afterSelection;
tb.insertNodes(path, tailNodes); tb.insertNodes(path, tailNodes);
tb.commit(); editorState.commit();
if (startNumber != null) { if (startNumber != null) {
makeFollowingNodesIncremental(editorState, originalPath, afterSelection, makeFollowingNodesIncremental(editorState, originalPath, afterSelection,
@ -161,9 +160,9 @@ void _pasteMultipleLinesInText(
_computeSelectionAfterPasteMultipleNodes(editorState, nodes); _computeSelectionAfterPasteMultipleNodes(editorState, nodes);
path[path.length - 1]++; path[path.length - 1]++;
tb.setAfterSelection(afterSelection); tb.afterSelection = afterSelection;
tb.insertNodes(path, nodes); tb.insertNodes(path, nodes);
tb.commit(); editorState.commit();
} }
void _handlePaste(EditorState editorState) async { void _handlePaste(EditorState editorState) async {
@ -196,15 +195,15 @@ void _pasteSingleLine(
EditorState editorState, Selection selection, String line) { EditorState editorState, Selection selection, String line) {
final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode; final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode;
final beginOffset = selection.end.offset; final beginOffset = selection.end.offset;
TransactionBuilder(editorState) editorState.transaction
..textEdit( ..updateText(
node, node,
() => Delta() Delta()
..retain(beginOffset) ..retain(beginOffset)
..addAll(_lineContentToDelta(line))) ..addAll(_lineContentToDelta(line)))
..setAfterSelection(Selection.collapsed( ..afterSelection = (Selection.collapsed(
Position(path: selection.end.path, offset: beginOffset + line.length))) Position(path: selection.end.path, offset: beginOffset + line.length)));
..commit(); editorState.commit();
} }
/// parse url from the line text /// parse url from the line text
@ -264,7 +263,7 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
final insertedLineSuffix = node.delta.slice(beginOffset); final insertedLineSuffix = node.delta.slice(beginOffset);
path[path.length - 1]++; path[path.length - 1]++;
final tb = TransactionBuilder(editorState); final tb = editorState.transaction;
final List<TextNode> nodes = final List<TextNode> nodes =
remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList(); remains.map((e) => TextNode(delta: _lineContentToDelta(e))).toList();
@ -279,16 +278,16 @@ void _handlePastePlainText(EditorState editorState, String plainText) {
} }
// insert first line // insert first line
tb.textEdit( tb.updateText(
node, node,
() => Delta() Delta()
..retain(beginOffset) ..retain(beginOffset)
..insert(firstLine) ..insert(firstLine)
..delete(node.delta.length - beginOffset)); ..delete(node.delta.length - beginOffset));
// insert remains // insert remains
tb.insertNodes(path, nodes); tb.insertNodes(path, nodes);
tb.setAfterSelection(afterSelection); tb.afterSelection = afterSelection;
tb.commit(); editorState.commit();
} }
} }
@ -309,15 +308,15 @@ void _deleteSelectedContent(EditorState editorState) {
if (selection.start.path.equals(selection.end.path) && if (selection.start.path.equals(selection.end.path) &&
beginNode.type == "text") { beginNode.type == "text") {
final textItem = beginNode as TextNode; final textItem = beginNode as TextNode;
final tb = TransactionBuilder(editorState); final tb = editorState.transaction;
final len = selection.end.offset - selection.start.offset; final len = selection.end.offset - selection.start.offset;
tb.textEdit( tb.updateText(
textItem, textItem,
() => Delta() Delta()
..retain(selection.start.offset) ..retain(selection.start.offset)
..delete(len)); ..delete(len));
tb.setAfterSelection(Selection.collapsed(selection.start)); tb.afterSelection = Selection.collapsed(selection.start);
tb.commit(); editorState.commit();
return; return;
} }
final traverser = NodeIterator( final traverser = NodeIterator(
@ -325,13 +324,13 @@ void _deleteSelectedContent(EditorState editorState) {
startNode: beginNode, startNode: beginNode,
endNode: endNode, endNode: endNode,
); );
final tb = TransactionBuilder(editorState); final tb = editorState.transaction;
while (traverser.moveNext()) { while (traverser.moveNext()) {
final item = traverser.current; final item = traverser.current;
if (item.type == "text" && beginNode == item) { if (item.type == "text" && beginNode == item) {
final textItem = item as TextNode; final textItem = item as TextNode;
final deleteLen = textItem.delta.length - selection.start.offset; final deleteLen = textItem.delta.length - selection.start.offset;
tb.textEdit(textItem, () { tb.updateText(textItem, () {
final delta = Delta() final delta = Delta()
..retain(selection.start.offset) ..retain(selection.start.offset)
..delete(deleteLen); ..delete(deleteLen);
@ -342,13 +341,13 @@ void _deleteSelectedContent(EditorState editorState) {
} }
return delta; return delta;
}); }());
} else { } else {
tb.deleteNode(item); tb.deleteNode(item);
} }
} }
tb.setAfterSelection(Selection.collapsed(selection.start)); tb.afterSelection = Selection.collapsed(selection.start);
tb.commit(); editorState.commit();
} }
ShortcutEventHandler copyEventHandler = (editorState, event) { ShortcutEventHandler copyEventHandler = (editorState, event) {

View File

@ -39,7 +39,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
final afterSelection = Selection.collapsed( final afterSelection = Selection.collapsed(
Position(path: textNodes.first.path.next, offset: 0), Position(path: textNodes.first.path.next, offset: 0),
); );
TransactionBuilder(editorState) editorState.transaction
..deleteText( ..deleteText(
textNodes.first, textNodes.first,
selection.start.offset, selection.start.offset,
@ -51,8 +51,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
0, 0,
selection.end.offset, selection.end.offset,
) )
..afterSelection = afterSelection ..afterSelection = afterSelection;
..commit(); editorState.commit();
if (startNode is TextNode && if (startNode is TextNode &&
startNode.subtype == BuiltInAttributeKey.numberList) { startNode.subtype == BuiltInAttributeKey.numberList) {
@ -77,12 +77,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
final afterSelection = Selection.collapsed( final afterSelection = Selection.collapsed(
Position(path: textNode.path, offset: 0), Position(path: textNode.path, offset: 0),
); );
TransactionBuilder(editorState) editorState.transaction
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: null, BuiltInAttributeKey.subtype: null,
}) })
..afterSelection = afterSelection ..afterSelection = afterSelection;
..commit(); editorState.commit();
final nextNode = textNode.next; final nextNode = textNode.next;
if (nextNode is TextNode && if (nextNode is TextNode &&
@ -105,13 +105,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
BuiltInAttributeKey.numberList; BuiltInAttributeKey.numberList;
newNode.attributes[BuiltInAttributeKey.number] = prevNumber; newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
final insertPath = textNode.path; final insertPath = textNode.path;
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
insertPath, insertPath,
newNode, newNode,
) )
..afterSelection = afterSelection ..afterSelection = afterSelection;
..commit(); editorState.commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection, makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
beginNum: prevNumber); beginNum: prevNumber);
@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
BuiltInAttributeKey.heading, BuiltInAttributeKey.heading,
BuiltInAttributeKey.quote, BuiltInAttributeKey.quote,
].contains(subtype); ].contains(subtype);
TransactionBuilder(editorState) editorState.transaction
..insertNode( ..insertNode(
textNode.path, textNode.path,
textNode.copyWith( textNode.copyWith(
@ -129,8 +129,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
attributes: needCopyAttributes ? null : {}, attributes: needCopyAttributes ? null : {},
), ),
) )
..afterSelection = afterSelection ..afterSelection = afterSelection;
..commit(); editorState.commit();
} }
} }
return KeyEventResult.handled; return KeyEventResult.handled;
@ -145,25 +145,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
Position(path: nextPath, offset: 0), Position(path: nextPath, offset: 0),
); );
final transactionBuilder = TransactionBuilder(editorState); final transaction = editorState.transaction;
transactionBuilder.insertNode( transaction.insertNode(
textNode.path.next, textNode.path.next,
textNode.copyWith( textNode.copyWith(
attributes: attributes, attributes: attributes,
delta: textNode.delta.slice(selection.end.offset), delta: textNode.delta.slice(selection.end.offset),
), ),
); );
transactionBuilder.deleteText( transaction.deleteText(
textNode, textNode,
selection.start.offset, selection.start.offset,
textNode.toPlainText().length - selection.start.offset, textNode.toPlainText().length - selection.start.offset,
); );
if (textNode.children.isNotEmpty) { if (textNode.children.isNotEmpty) {
final children = textNode.children.toList(growable: false); final children = textNode.children.toList(growable: false);
transactionBuilder.deleteNodes(children); transaction.deleteNodes(children);
} }
transactionBuilder.afterSelection = afterSelection; transaction.afterSelection = afterSelection;
transactionBuilder.commit(); editorState.commit();
// If the new type of a text node is number list, // If the new type of a text node is number list,
// the numbers of the following nodes should be incremental. // the numbers of the following nodes should be incremental.

View File

@ -1,6 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
bool _isCodeStyle(TextNode textNode, int index) { bool _isCodeStyle(TextNode textNode, int index) {
@ -72,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, lastBackquoteIndex, 1) ..deleteText(textNode, lastBackquoteIndex, 1)
..deleteText(textNode, firstBackquoteIndex, 2) ..deleteText(textNode, firstBackquoteIndex, 2)
..formatText( ..formatText(
@ -88,8 +89,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: endIndex - 3, offset: endIndex - 3,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -103,7 +104,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
// delete the backquote. // delete the backquote.
// update the style of the text surround by ` ` to code. // update the style of the text surround by ` ` to code.
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, startIndex, 1) ..deleteText(textNode, startIndex, 1)
..formatText( ..formatText(
textNode, textNode,
@ -118,8 +119,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: endIndex - 1, offset: endIndex - 1,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
@ -165,7 +166,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
// delete the last three tildes. // delete the last three tildes.
// update the style of the text surround by `~~ ~~` to strikethrough. // update the style of the text surround by `~~ ~~` to strikethrough.
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, lastTildeIndex, 1) ..deleteText(textNode, lastTildeIndex, 1)
..deleteText(textNode, thirdToLastTildeIndex, 2) ..deleteText(textNode, thirdToLastTildeIndex, 2)
..formatText( ..formatText(
@ -181,8 +182,8 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: selection.end.offset - 3, offset: selection.end.offset - 3,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
@ -219,7 +220,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
// update the href attribute of the text surrounded by [ ] to the url, // update the href attribute of the text surrounded by [ ] to the url,
// delete everything after the text, // delete everything after the text,
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, firstOpeningBracket, 1) ..deleteText(textNode, firstOpeningBracket, 1)
..formatText( ..formatText(
textNode, textNode,
@ -236,8 +237,8 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: firstOpeningBracket + linkText!.length, offset: firstOpeningBracket + linkText!.length,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -42,7 +42,7 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
// delete the last three asterisks. // delete the last three asterisks.
// update the style of the text surround by `** **` to bold. // update the style of the text surround by `** **` to bold.
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1) ..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastAsteriskIndex, 2) ..deleteText(textNode, thirdToLastAsteriskIndex, 2)
..formatText( ..formatText(
@ -59,8 +59,8 @@ ShortcutEventHandler doubleAsterisksToBold = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: selection.end.offset - 3, offset: selection.end.offset - 3,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
@ -108,7 +108,7 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
// delete the last three underscores. // delete the last three underscores.
// update the style of the text surround by `__ __` to bold. // update the style of the text surround by `__ __` to bold.
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, lastAsterisIndex, 1) ..deleteText(textNode, lastAsterisIndex, 1)
..deleteText(textNode, thirdToLastUnderscoreIndex, 2) ..deleteText(textNode, thirdToLastUnderscoreIndex, 2)
..formatText( ..formatText(
@ -125,8 +125,8 @@ ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) {
path: textNode.path, path: textNode.path,
offset: selection.end.offset - 3, offset: selection.end.offset - 3,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/core/document/attributes.dart'; import 'package:appflowy_editor/src/core/document/attributes.dart';
void makeFollowingNodesIncremental( void makeFollowingNodesIncremental(
@ -16,7 +15,7 @@ void makeFollowingNodesIncremental(
int numPtr = beginNum + 1; int numPtr = beginNum + 1;
var ptr = insertNode.next; var ptr = insertNode.next;
final builder = TransactionBuilder(editorState); final builder = editorState.transaction;
while (ptr != null) { while (ptr != null) {
if (ptr.subtype != BuiltInAttributeKey.numberList) { if (ptr.subtype != BuiltInAttributeKey.numberList) {
@ -34,5 +33,5 @@ void makeFollowingNodesIncremental(
} }
builder.afterSelection = afterSelection; builder.afterSelection = afterSelection;
builder.commit(); editorState.commit();
} }

View File

@ -1,5 +1,5 @@
import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:appflowy_editor/src/extensions/node_extensions.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
@ -25,10 +25,9 @@ ShortcutEventHandler slashShortcutHandler = (editorState, event) {
if (selection == null || context == null || selectable == null) { if (selection == null || context == null || selectable == null) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
TransactionBuilder(editorState) editorState.transaction.replaceText(textNode, selection.start.offset,
..replaceText(textNode, selection.start.offset, selection.end.offset - selection.start.offset, event.character ?? '');
selection.end.offset - selection.start.offset, event.character ?? '') editorState.commit();
..commit();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_selectionMenuService = _selectionMenuService =

View File

@ -15,9 +15,8 @@ ShortcutEventHandler tabHandler = (editorState, event) {
final previous = textNode.previous; final previous = textNode.previous;
if (textNode.subtype != BuiltInAttributeKey.bulletedList) { if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
TransactionBuilder(editorState) editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4);
..insertText(textNode, selection.end.offset, ' ' * 4) editorState.commit();
..commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -31,11 +30,11 @@ ShortcutEventHandler tabHandler = (editorState, event) {
start: selection.start.copyWith(path: path), start: selection.start.copyWith(path: path),
end: selection.end.copyWith(path: path), end: selection.end.copyWith(path: path),
); );
TransactionBuilder(editorState) editorState.transaction
..deleteNode(textNode) ..deleteNode(textNode)
..insertNode(path, textNode) ..insertNode(path, textNode)
..setAfterSelection(afterSelection) ..afterSelection = afterSelection;
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
}; };

View File

@ -1,3 +1,4 @@
import 'package:appflowy_editor/src/core/transform/transaction.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -6,7 +7,6 @@ import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/location/position.dart'; import 'package:appflowy_editor/src/core/location/position.dart';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import './number_list_helper.dart'; import './number_list_helper.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
@ -99,15 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
)); ));
final insertPath = textNode.path; final insertPath = textNode.path;
editorState.transaction
TransactionBuilder(editorState)
..deleteText(textNode, 0, matchText.length) ..deleteText(textNode, 0, matchText.length)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList, BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: numValue BuiltInAttributeKey.number: numValue
}) })
..afterSelection = afterSelection ..afterSelection = afterSelection;
..commit(); editorState.commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection); makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
@ -118,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
if (textNode.subtype == BuiltInAttributeKey.bulletedList) { if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, 0, 1) ..deleteText(textNode, 0, 1)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
@ -128,8 +127,8 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
path: textNode.path, path: textNode.path,
offset: 0, offset: 0,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -151,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
check = false; check = false;
} }
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, 0, symbol.length) ..deleteText(textNode, 0, symbol.length)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
@ -162,8 +161,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
path: textNode.path, path: textNode.path,
offset: 0, offset: 0,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }
@ -177,7 +176,7 @@ KeyEventResult _toHeadingStyle(
if (textNode.attributes.heading == hX) { if (textNode.attributes.heading == hX) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
TransactionBuilder(editorState) editorState.transaction
..deleteText(textNode, 0, x) ..deleteText(textNode, 0, x)
..updateNode(textNode, { ..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
@ -188,8 +187,8 @@ KeyEventResult _toHeadingStyle(
path: textNode.path, path: textNode.path,
offset: 0, offset: 0,
), ),
) );
..commit(); editorState.commit();
return KeyEventResult.handled; return KeyEventResult.handled;
} }

View File

@ -3,8 +3,7 @@ import 'dart:collection';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/infra/log.dart';
import 'package:appflowy_editor/src/core/transform/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/core/transform/transaction.dart';
import 'package:appflowy_editor/src/operation/transaction.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
/// A [HistoryItem] contains list of operations committed by users. /// A [HistoryItem] contains list of operations committed by users.
@ -39,7 +38,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
/// Create a new [Transaction] by inverting the operations. /// Create a new [Transaction] by inverting the operations.
Transaction toTransaction(EditorState state) { Transaction toTransaction(EditorState state) {
final builder = TransactionBuilder(state); final builder = Transaction(document: state.document);
for (var i = operations.length - 1; i >= 0; i--) { for (var i = operations.length - 1; i >= 0; i--) {
final operation = operations[i]; final operation = operations[i];
final inverted = operation.invert(); final inverted = operation.invert();
@ -47,7 +46,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
} }
builder.afterSelection = beforeSelection; builder.afterSelection = beforeSelection;
builder.beforeSelection = afterSelection; builder.beforeSelection = afterSelection;
return builder.finish(); return builder;
} }
} }

View File

@ -3,7 +3,6 @@ import 'dart:collection';
import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/core/transform/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/editor_state.dart';
import 'package:appflowy_editor/src/core/document/document.dart'; import 'package:appflowy_editor/src/core/document/document.dart';
@ -55,18 +54,19 @@ void main() {
item1, item1,
item2, item2,
item3, item3,
])); ]),
);
final state = EditorState(document: Document(root: root)); final state = EditorState(document: Document(root: root));
expect(item1.path, [0]); expect(item1.path, [0]);
expect(item2.path, [1]); expect(item2.path, [1]);
expect(item3.path, [2]); expect(item3.path, [2]);
final tb = TransactionBuilder(state); final transaction = state.transaction;
tb.deleteNode(item1); transaction.deleteNode(item1);
tb.deleteNode(item2); transaction.deleteNode(item2);
tb.deleteNode(item3); transaction.deleteNode(item3);
final transaction = tb.finish(); state.commit();
expect(transaction.operations[0].path, [0]); expect(transaction.operations[0].path, [0]);
expect(transaction.operations[1].path, [0]); expect(transaction.operations[1].path, [0]);
expect(transaction.operations[2].path, [0]); expect(transaction.operations[2].path, [0]);
@ -77,10 +77,9 @@ void main() {
final state = EditorState(document: Document(root: root)); final state = EditorState(document: Document(root: root));
final item1 = Node(type: "node", attributes: {}, children: LinkedList()); final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final tb = TransactionBuilder(state); final transaction = state.transaction;
tb.insertNode([0], item1); transaction.insertNode([0], item1);
state.commit();
final transaction = tb.finish();
expect(transaction.toJson(), { expect(transaction.toJson(), {
"operations": [ "operations": [
{ {
@ -99,11 +98,12 @@ void main() {
children: LinkedList() children: LinkedList()
..addAll([ ..addAll([
item1, item1,
])); ]),
);
final state = EditorState(document: Document(root: root)); final state = EditorState(document: Document(root: root));
final tb = TransactionBuilder(state); final transaction = state.transaction;
tb.deleteNode(item1); transaction.deleteNode(item1);
final transaction = tb.finish(); state.commit();
expect(transaction.toJson(), { expect(transaction.toJson(), {
"operations": [ "operations": [
{ {