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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
// Delete the previous 'underscore',
// update the style of the text surrounded by the two underscores to 'italic',
// and update the cursor position.
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, firstUnderscore, 1)
..formatText(
textNode,
@ -46,8 +46,8 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
path: textNode.path,
offset: selection.end.offset - 1,
),
)
..commit();
);
editorState.commit();
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/editor_state.dart';
export 'src/core/transform/operation.dart';
export 'src/operation/transaction.dart';
export 'src/operation/transaction_builder.dart';
export 'src/core/transform/transaction.dart';
export 'src/render/selection/selectable.dart';
export 'src/service/editor_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/path.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';
Future<void> insertContextInText(
@ -22,9 +22,8 @@ Future<void> insertContextInText(
final completer = Completer<void>();
TransactionBuilder(editorState)
..insertText(result, index, content)
..commit();
editorState.transaction.insertText(result, index, content);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
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/location/selection.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';
Future<void> updateTextNodeAttributes(
@ -23,9 +23,8 @@ Future<void> updateTextNodeAttributes(
final completer = Completer<void>();
TransactionBuilder(editorState)
..updateNode(result, attributes)
..commit();
editorState.transaction.updateNode(result, attributes);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
@ -49,15 +48,13 @@ Future<void> updateTextNodeDeltaAttributes(
final newSelection = getSelection(editorState, selection: selection);
final completer = Completer<void>();
TransactionBuilder(editorState)
..formatText(
result,
newSelection.startIndex,
newSelection.length,
attributes,
)
..commit();
editorState.transaction.formatText(
result,
newSelection.startIndex,
newSelection.length,
attributes,
);
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
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/document/document.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';
class ApplyOptions {
@ -74,6 +74,24 @@ class EditorState {
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 {
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/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/service/render_plugin_service.dart';
import 'package:flutter/material.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
@ -25,23 +24,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
RichClipboard.setData(RichClipboardData(text: src));
},
onDelete: () {
TransactionBuilder(context.editorState)
..deleteNode(context.node)
..commit();
context.editorState.transaction.deleteNode(context.node);
context.editorState.commit();
},
onAlign: (alignment) {
TransactionBuilder(context.editorState)
..updateNode(context.node, {
'align': _alignmentToText(alignment),
})
..commit();
context.editorState.transaction.updateNode(context.node, {
'align': _alignmentToText(alignment),
});
context.editorState.commit();
},
onResize: (width) {
TransactionBuilder(context.editorState)
..updateNode(context.node, {
'width': width,
})
..commit();
context.editorState.transaction.updateNode(context.node, {
'width': width,
});
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/editor_state.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:flutter/material.dart';
@ -192,11 +192,10 @@ extension on EditorState {
'align': 'center',
},
);
TransactionBuilder(this)
..insertNode(
selection.start.path,
imageNode,
)
..commit();
transaction.insertNode(
selection.start.path,
imageNode,
);
commit();
}
}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
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/material.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/editor_state.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,
/// including text insertion, deletion and replacement.
@ -160,13 +160,12 @@ class _AppFlowyInputState extends State<AppFlowyInput>
}
if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode;
TransactionBuilder(_editorState)
..insertText(
textNode,
delta.insertionOffset,
delta.textInserted,
)
..commit();
_editorState.transaction.insertText(
textNode,
delta.insertionOffset,
delta.textInserted,
);
_editorState.commit();
} else {
// TODO: implement
}
@ -181,9 +180,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode;
final length = delta.deletedRange.end - delta.deletedRange.start;
TransactionBuilder(_editorState)
..deleteText(textNode, delta.deletedRange.start, length)
..commit();
_editorState.transaction
.deleteText(textNode, delta.deletedRange.start, length);
_editorState.commit();
} else {
// TODO: implement
}
@ -198,10 +197,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
if (currentSelection.isSingle) {
final textNode = selectionService.currentSelectedNodes.first as TextNode;
final length = delta.replacedRange.end - delta.replacedRange.start;
TransactionBuilder(_editorState)
..replaceText(
textNode, delta.replacedRange.start, length, delta.replacementText)
..commit();
_editorState.transaction.replaceText(
textNode, delta.replacedRange.start, length, delta.replacementText);
_editorState.commit();
} else {
// TODO: implement
}

View File

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

View File

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

View File

@ -39,7 +39,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
final afterSelection = Selection.collapsed(
Position(path: textNodes.first.path.next, offset: 0),
);
TransactionBuilder(editorState)
editorState.transaction
..deleteText(
textNodes.first,
selection.start.offset,
@ -51,8 +51,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
0,
selection.end.offset,
)
..afterSelection = afterSelection
..commit();
..afterSelection = afterSelection;
editorState.commit();
if (startNode is TextNode &&
startNode.subtype == BuiltInAttributeKey.numberList) {
@ -77,12 +77,12 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
final afterSelection = Selection.collapsed(
Position(path: textNode.path, offset: 0),
);
TransactionBuilder(editorState)
editorState.transaction
..updateNode(textNode, {
BuiltInAttributeKey.subtype: null,
})
..afterSelection = afterSelection
..commit();
..afterSelection = afterSelection;
editorState.commit();
final nextNode = textNode.next;
if (nextNode is TextNode &&
@ -105,13 +105,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
BuiltInAttributeKey.numberList;
newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
final insertPath = textNode.path;
TransactionBuilder(editorState)
editorState.transaction
..insertNode(
insertPath,
newNode,
)
..afterSelection = afterSelection
..commit();
..afterSelection = afterSelection;
editorState.commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
beginNum: prevNumber);
@ -120,7 +120,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
BuiltInAttributeKey.heading,
BuiltInAttributeKey.quote,
].contains(subtype);
TransactionBuilder(editorState)
editorState.transaction
..insertNode(
textNode.path,
textNode.copyWith(
@ -129,8 +129,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
attributes: needCopyAttributes ? null : {},
),
)
..afterSelection = afterSelection
..commit();
..afterSelection = afterSelection;
editorState.commit();
}
}
return KeyEventResult.handled;
@ -145,25 +145,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
Position(path: nextPath, offset: 0),
);
final transactionBuilder = TransactionBuilder(editorState);
transactionBuilder.insertNode(
final transaction = editorState.transaction;
transaction.insertNode(
textNode.path.next,
textNode.copyWith(
attributes: attributes,
delta: textNode.delta.slice(selection.end.offset),
),
);
transactionBuilder.deleteText(
transaction.deleteText(
textNode,
selection.start.offset,
textNode.toPlainText().length - selection.start.offset,
);
if (textNode.children.isNotEmpty) {
final children = textNode.children.toList(growable: false);
transactionBuilder.deleteNodes(children);
transaction.deleteNodes(children);
}
transactionBuilder.afterSelection = afterSelection;
transactionBuilder.commit();
transaction.afterSelection = afterSelection;
editorState.commit();
// If the new type of a text node is number list,
// 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/src/extensions/text_node_extensions.dart';
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
import 'package:flutter/material.dart';
bool _isCodeStyle(TextNode textNode, int index) {
@ -72,7 +73,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, lastBackquoteIndex, 1)
..deleteText(textNode, firstBackquoteIndex, 2)
..formatText(
@ -88,8 +89,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
path: textNode.path,
offset: endIndex - 3,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
}
@ -103,7 +104,7 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
// delete the backquote.
// update the style of the text surround by ` ` to code.
// and update the cursor position.
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, startIndex, 1)
..formatText(
textNode,
@ -118,8 +119,8 @@ ShortcutEventHandler backquoteToCodeHandler = (editorState, event) {
path: textNode.path,
offset: endIndex - 1,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
};
@ -165,7 +166,7 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
// delete the last three tildes.
// update the style of the text surround by `~~ ~~` to strikethrough.
// and update the cursor position.
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, lastTildeIndex, 1)
..deleteText(textNode, thirdToLastTildeIndex, 2)
..formatText(
@ -181,8 +182,8 @@ ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) {
path: textNode.path,
offset: selection.end.offset - 3,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
};
@ -219,7 +220,7 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
// update the href attribute of the text surrounded by [ ] to the url,
// delete everything after the text,
// and update the cursor position.
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, firstOpeningBracket, 1)
..formatText(
textNode,
@ -236,8 +237,8 @@ ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) {
path: textNode.path,
offset: firstOpeningBracket + linkText!.length,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
};

View File

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

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/src/core/location/selection.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/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/core/document/attributes.dart';
void makeFollowingNodesIncremental(
@ -16,7 +15,7 @@ void makeFollowingNodesIncremental(
int numPtr = beginNum + 1;
var ptr = insertNode.next;
final builder = TransactionBuilder(editorState);
final builder = editorState.transaction;
while (ptr != null) {
if (ptr.subtype != BuiltInAttributeKey.numberList) {
@ -34,5 +33,5 @@ void makeFollowingNodesIncremental(
}
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/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/extensions/node_extensions.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) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
..replaceText(textNode, selection.start.offset,
selection.end.offset - selection.start.offset, event.character ?? '')
..commit();
editorState.transaction.replaceText(textNode, selection.start.offset,
selection.end.offset - selection.start.offset, event.character ?? '');
editorState.commit();
WidgetsBinding.instance.addPostFrameCallback((_) {
_selectionMenuService =

View File

@ -15,9 +15,8 @@ ShortcutEventHandler tabHandler = (editorState, event) {
final previous = textNode.previous;
if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
TransactionBuilder(editorState)
..insertText(textNode, selection.end.offset, ' ' * 4)
..commit();
editorState.transaction.insertText(textNode, selection.end.offset, ' ' * 4);
editorState.commit();
return KeyEventResult.handled;
}
@ -31,11 +30,11 @@ ShortcutEventHandler tabHandler = (editorState, event) {
start: selection.start.copyWith(path: path),
end: selection.end.copyWith(path: path),
);
TransactionBuilder(editorState)
editorState.transaction
..deleteNode(textNode)
..insertNode(path, textNode)
..setAfterSelection(afterSelection)
..commit();
..afterSelection = afterSelection;
editorState.commit();
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:flutter/material.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/selection.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import './number_list_helper.dart';
import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
@ -99,15 +99,14 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
));
final insertPath = textNode.path;
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, 0, matchText.length)
..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: numValue
})
..afterSelection = afterSelection
..commit();
..afterSelection = afterSelection;
editorState.commit();
makeFollowingNodesIncremental(editorState, insertPath, afterSelection);
@ -118,7 +117,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, 0, 1)
..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
@ -128,8 +127,8 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
path: textNode.path,
offset: 0,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
}
@ -151,7 +150,7 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
check = false;
}
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, 0, symbol.length)
..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
@ -162,8 +161,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
path: textNode.path,
offset: 0,
),
)
..commit();
);
editorState.commit();
return KeyEventResult.handled;
}
@ -177,7 +176,7 @@ KeyEventResult _toHeadingStyle(
if (textNode.attributes.heading == hX) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
editorState.transaction
..deleteText(textNode, 0, x)
..updateNode(textNode, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
@ -188,8 +187,8 @@ KeyEventResult _toHeadingStyle(
path: textNode.path,
offset: 0,
),
)
..commit();
);
editorState.commit();
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/infra/log.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/core/transform/transaction.dart';
import 'package:appflowy_editor/src/editor_state.dart';
/// 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.
Transaction toTransaction(EditorState state) {
final builder = TransactionBuilder(state);
final builder = Transaction(document: state.document);
for (var i = operations.length - 1; i >= 0; i--) {
final operation = operations[i];
final inverted = operation.invert();
@ -47,7 +46,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
}
builder.afterSelection = beforeSelection;
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:flutter_test/flutter_test.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';
@ -48,25 +47,26 @@ void main() {
final item2 = Node(type: "node", attributes: {}, children: LinkedList());
final item3 = Node(type: "node", attributes: {}, children: LinkedList());
final root = Node(
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
item2,
item3,
]));
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
item2,
item3,
]),
);
final state = EditorState(document: Document(root: root));
expect(item1.path, [0]);
expect(item2.path, [1]);
expect(item3.path, [2]);
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
tb.deleteNode(item2);
tb.deleteNode(item3);
final transaction = tb.finish();
final transaction = state.transaction;
transaction.deleteNode(item1);
transaction.deleteNode(item2);
transaction.deleteNode(item3);
state.commit();
expect(transaction.operations[0].path, [0]);
expect(transaction.operations[1].path, [0]);
expect(transaction.operations[2].path, [0]);
@ -77,10 +77,9 @@ void main() {
final state = EditorState(document: Document(root: root));
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final tb = TransactionBuilder(state);
tb.insertNode([0], item1);
final transaction = tb.finish();
final transaction = state.transaction;
transaction.insertNode([0], item1);
state.commit();
expect(transaction.toJson(), {
"operations": [
{
@ -94,16 +93,17 @@ void main() {
test("delete", () {
final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final root = Node(
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
]));
type: "root",
attributes: {},
children: LinkedList()
..addAll([
item1,
]),
);
final state = EditorState(document: Document(root: root));
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
final transaction = tb.finish();
final transaction = state.transaction;
transaction.deleteNode(item1);
state.commit();
expect(transaction.toJson(), {
"operations": [
{