feat: implement transform
This commit is contained in:
parent
212928321f
commit
4f4c93e92f
29 changed files with 311 additions and 228 deletions
|
|
@ -55,3 +55,4 @@ export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
|||
|
||||
export 'src/editor/block_component/block_component.dart';
|
||||
export 'src/editor/editor_component/editor_component.dart';
|
||||
export 'src/editor/transform/transform.dart';
|
||||
|
|
|
|||
|
|
@ -95,18 +95,18 @@ extension TextCommands on EditorState {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> insertNewLine({
|
||||
Path? path,
|
||||
}) async {
|
||||
final p = path ?? getSelection(null).start.path.next;
|
||||
final transaction = this.transaction;
|
||||
transaction.insertNode(p, TextNode.empty());
|
||||
transaction.afterSelection = Selection.single(
|
||||
path: p,
|
||||
startOffset: 0,
|
||||
);
|
||||
return apply(transaction);
|
||||
}
|
||||
// Future<void> insertNewLine({
|
||||
// Path? path,
|
||||
// }) async {
|
||||
// final p = path ?? getSelection(null).start.path.next;
|
||||
// final transaction = this.transaction;
|
||||
// transaction.insertNode(p, TextNode.empty());
|
||||
// transaction.afterSelection = Selection.single(
|
||||
// path: p,
|
||||
// startOffset: 0,
|
||||
// );
|
||||
// return apply(transaction);
|
||||
// }
|
||||
|
||||
Future<void> insertNewLineAtCurrentSelection() async {
|
||||
final selection = getSelection(null);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ class Document {
|
|||
);
|
||||
}
|
||||
|
||||
factory Document.blank() {
|
||||
final root = Node(
|
||||
type: 'editor',
|
||||
);
|
||||
return Document(
|
||||
root: root,
|
||||
);
|
||||
}
|
||||
|
||||
final Node root;
|
||||
|
||||
/// Returns the node at the given [path].
|
||||
|
|
|
|||
|
|
@ -3,14 +3,20 @@ import 'package:appflowy_editor/src/core/document/document.dart';
|
|||
|
||||
/// [NodeIterator] is used to traverse the nodes in visual order.
|
||||
class NodeIterator implements Iterator<Node> {
|
||||
/// Creates a NodeIterator.
|
||||
NodeIterator({
|
||||
required this.document,
|
||||
required this.startNode,
|
||||
this.endNode,
|
||||
});
|
||||
|
||||
/// The document to iterate.
|
||||
final Document document;
|
||||
|
||||
/// The node to start the iteration with.
|
||||
final Node startNode;
|
||||
|
||||
/// The node to end the iteration with.
|
||||
final Node? endNode;
|
||||
|
||||
Node? _currentNode;
|
||||
|
|
@ -58,7 +64,7 @@ class NodeIterator implements Iterator<Node> {
|
|||
}
|
||||
|
||||
List<Node> toList() {
|
||||
final result = <Node>[];
|
||||
final List<Node> result = [];
|
||||
while (moveNext()) {
|
||||
result.add(current);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,25 +230,37 @@ extension TextTransaction on Transaction {
|
|||
}
|
||||
|
||||
void mergeText(
|
||||
TextNode first,
|
||||
TextNode second, {
|
||||
int? firstOffset,
|
||||
int secondOffset = 0,
|
||||
Node left,
|
||||
Node right, {
|
||||
int? leftOffset,
|
||||
int rightOffset = 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)),
|
||||
);
|
||||
final leftDelta = left.delta;
|
||||
final rightDelta = right.delta;
|
||||
if (leftDelta == null || rightDelta == null) {
|
||||
return;
|
||||
}
|
||||
final leftLength = leftDelta.length;
|
||||
final rightLength = rightDelta.length;
|
||||
leftOffset ??= leftLength;
|
||||
|
||||
final composed = leftDelta
|
||||
.compose(
|
||||
Delta()
|
||||
..retain(leftOffset)
|
||||
..delete(leftLength - leftOffset)
|
||||
..addAll(rightDelta.slice(rightOffset, rightLength)),
|
||||
)
|
||||
.toJson();
|
||||
|
||||
updateNode(left, {
|
||||
'delta': composed,
|
||||
});
|
||||
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: first.path,
|
||||
offset: firstOffset,
|
||||
path: left.path,
|
||||
offset: leftOffset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
||||
|
||||
extension DeleteSelection on EditorState {
|
||||
Future<void> deleteSelection(Selection? selection) async {
|
||||
final transaction = this.transaction;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalized = selection.normalized;
|
||||
final nodes = this.selection.getNodesInSelection(normalized);
|
||||
|
||||
transaction.afterSelection = normalized.collapse(atStart: true);
|
||||
|
||||
// single line
|
||||
if (nodes.length == 1) {
|
||||
final node = nodes.first;
|
||||
if (node.delta != null) {
|
||||
transaction.deleteText2(node, normalized.startIndex, normalized.length);
|
||||
} else {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
return apply(transaction);
|
||||
} else {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension InsertNewLine on EditorState {
|
||||
Future<void> insertNewLine2(Selection? selection) async {
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final transaction = this.transaction;
|
||||
final path = selection.start.path.next;
|
||||
|
||||
transaction.insertNode(
|
||||
path,
|
||||
Node(
|
||||
type: 'paragraph',
|
||||
attributes: {
|
||||
'delta': Delta().toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
offset: 0,
|
||||
),
|
||||
);
|
||||
|
||||
return apply(transaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// transaction
|
||||
export 'transaction_delete_text.dart';
|
||||
export 'transaction_insert_text.dart';
|
||||
|
||||
// editor state
|
||||
export 'editor_state_delete_selection.dart';
|
||||
export 'editor_state_insert_new_line.dart';
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension Extension on Transaction {
|
||||
void deleteText2(
|
||||
Node node,
|
||||
int index,
|
||||
int length,
|
||||
) {
|
||||
final delta = node.delta;
|
||||
if (delta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final now = delta.compose(
|
||||
Delta()
|
||||
..retain(index)
|
||||
..delete(length),
|
||||
);
|
||||
|
||||
updateNode(node, {
|
||||
'delta': now.toJson(),
|
||||
});
|
||||
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(path: node.path, offset: index),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> onDelete(
|
||||
|
|
@ -8,18 +7,18 @@ Future<void> onDelete(
|
|||
) async {
|
||||
Log.input.debug('onDelete: $deletion');
|
||||
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// single line
|
||||
if (selection.isSingle) {
|
||||
final node = editorState.selection.currentSelectedNodes.first;
|
||||
final node = editorState.selectionService.currentSelectedNodes.first;
|
||||
assert(node.delta != null);
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..deleteText2(
|
||||
..deleteText(
|
||||
node,
|
||||
deletion.deletedRange.start,
|
||||
deletion.textDeleted.length,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Future<void> onInsert(
|
|||
}
|
||||
}
|
||||
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ Future<void> onInsert(
|
|||
// IME
|
||||
// single line
|
||||
if (selection.isCollapsed) {
|
||||
final node = editorState.selection.currentSelectedNodes.first;
|
||||
final node = editorState.selectionService.currentSelectedNodes.first;
|
||||
assert(node.delta != null);
|
||||
|
||||
final transaction = editorState.transaction
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
|||
super.initState();
|
||||
|
||||
editorState = Provider.of<EditorState>(context, listen: false);
|
||||
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
||||
|
||||
textInputService = DeltaTextInputService(
|
||||
onInsert: (insertion) => onInsert(insertion, editorState),
|
||||
|
|
@ -34,15 +35,11 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
|||
onNonTextUpdate: onNonTextUpdate,
|
||||
onPerformAction: (action) => onPerformAction(action, editorState),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
editorState.selection.currentSelection.addListener(_onSelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
editorState.selection.currentSelection.removeListener(_onSelectionChanged);
|
||||
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +50,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
|||
|
||||
void _onSelectionChanged() {
|
||||
// attach the delta text input service if needed
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
textInputService.close();
|
||||
} else {
|
||||
|
|
@ -73,10 +70,11 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
|||
}
|
||||
|
||||
TextEditingValue? _getCurrentTextEditingValue(Selection selection) {
|
||||
final editableNodes = editorState.selection.currentSelectedNodes.where(
|
||||
final editableNodes =
|
||||
editorState.selectionService.currentSelectedNodes.where(
|
||||
(element) => element.delta != null,
|
||||
);
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
final selection = editorState.selection;
|
||||
final composingTextRange = textInputService.composingTextRange;
|
||||
if (editableNodes.isNotEmpty && selection != null) {
|
||||
var text = editableNodes.fold<String>(
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
||||
import 'package:appflowy_editor/src/editor/block_component/block_component.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../util/util.dart';
|
||||
|
||||
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
||||
if (PlatformExtension.isDesktop && RawKeyboard.instance.isShiftPressed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
|
||||
// delete the selection
|
||||
await editorState.deleteSelection(selection);
|
||||
|
||||
// insert a new line
|
||||
await editorState.insertNewLine2(selection);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/// insert a new line block
|
||||
///
|
||||
/// - on desktop or web: enter
|
||||
/// - on mobile: enter
|
||||
///
|
||||
CharacterShortcutEvent insertNewLine = CharacterShortcutEvent(
|
||||
key: 'insert a new line',
|
||||
character: '\n',
|
||||
handler: _insertNewLineHandler,
|
||||
);
|
||||
|
||||
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
||||
// on desktop or web, shift + enter to insert a '\n' character to the same line.
|
||||
// so, we should return the false to let the system handle it.
|
||||
if (PlatformExtension.isNotMobile && RawKeyboard.instance.isShiftPressed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete the selection
|
||||
await editorState.deleteSelection(selection);
|
||||
// insert a new line
|
||||
await editorState.insertNewLine(selection.start);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:appflowy_editor/src/editor/block_component/block_component.dart';
|
||||
|
||||
CharacterShortcutEventHandler _markdownBlockHandler = (editorState) async {
|
||||
final selection = editorState.selection.currentSelection.value;
|
||||
final selection = editorState.selection;
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class MobileToolbar extends StatelessWidget {
|
|||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
editorState.selection.updateSelection(null);
|
||||
editorState.selectionService.updateSelection(null);
|
||||
},
|
||||
icon: const Icon(Icons.keyboard_hide),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,43 +1,37 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension SelectionTransform on EditorState {
|
||||
Future<void> deleteSelection(Selection selection) async {
|
||||
Future<bool> deleteSelection(Selection selection) async {
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
selection = selection.normalized;
|
||||
final transaction = this.transaction;
|
||||
final normalized = selection.normalized;
|
||||
final nodes = this.selection.getNodesInSelection(normalized);
|
||||
|
||||
transaction.afterSelection = normalized.collapse(atStart: true);
|
||||
final nodes = getNodesInSelection(selection);
|
||||
|
||||
if (nodes.length == 1) {
|
||||
final node = nodes.first;
|
||||
if (node.delta != null) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
normalized.startIndex,
|
||||
normalized.length,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
);
|
||||
} else {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
} else {
|
||||
assert(nodes.first.path < nodes.last.path);
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
final node = nodes[i];
|
||||
if (node.delta != null) {
|
||||
if (i == 0) {
|
||||
transaction.deleteText(
|
||||
transaction.mergeText(
|
||||
node,
|
||||
normalized.startIndex,
|
||||
node.delta!.length - normalized.startIndex,
|
||||
);
|
||||
} else if (i == nodes.length - 1) {
|
||||
transaction.deleteText(
|
||||
node,
|
||||
0,
|
||||
normalized.endIndex,
|
||||
nodes.last,
|
||||
leftOffset: selection.startIndex,
|
||||
rightOffset: selection.endIndex,
|
||||
);
|
||||
} else {
|
||||
transaction.deleteNode(node);
|
||||
|
|
@ -48,6 +42,9 @@ extension SelectionTransform on EditorState {
|
|||
}
|
||||
}
|
||||
|
||||
return apply(transaction);
|
||||
transaction.afterSelection = selection.collapse(atStart: true);
|
||||
|
||||
await apply(transaction);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ extension TextTransforms on EditorState {
|
|||
///
|
||||
/// Then it inserts a new paragraph node. After that, it sets the selection to be at the
|
||||
/// beginning of the new paragraph.
|
||||
Future<void> insertNewLine({
|
||||
Position? at,
|
||||
}) async {
|
||||
Future<void> insertNewLine(
|
||||
Position? position,
|
||||
) async {
|
||||
// If the position is not passed in, use the current selection.
|
||||
final position = at ?? selection.currentSelection.value?.start;
|
||||
position = position ?? selectionService.currentSelection.value?.start;
|
||||
|
||||
// If there is no position, or if the selection is not collapsed, do nothing.
|
||||
if (position == null ||
|
||||
!(selection.currentSelection.value?.isCollapsed ?? false)) {
|
||||
!(selectionService.currentSelection.value?.isCollapsed ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
2
lib/src/editor/transform/transform.dart
Normal file
2
lib/src/editor/transform/transform.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'selection_transform.dart';
|
||||
export 'text_transform.dart';
|
||||
|
|
@ -4,4 +4,5 @@ extension PlatformExtension on Platform {
|
|||
static bool get isDesktop =>
|
||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||
static bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||
static bool get isNotMobile => !isMobile;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,10 +43,17 @@ enum CursorUpdateReason {
|
|||
class EditorState {
|
||||
final Document document;
|
||||
|
||||
final ValueNotifier<Selection?> selectionNotifier =
|
||||
ValueNotifier<Selection?>(null);
|
||||
Selection? get selection => selectionNotifier.value;
|
||||
set selection(Selection? value) {
|
||||
selectionNotifier.value = value;
|
||||
}
|
||||
|
||||
// Service reference.
|
||||
final service = FlowyService();
|
||||
|
||||
AppFlowySelectionService get selection => service.selectionService;
|
||||
AppFlowySelectionService get selectionService => service.selectionService;
|
||||
AppFlowyRenderPluginService get renderer => service.renderPluginService;
|
||||
|
||||
List<CharacterShortcutEvent> characterShortcutEvents = [
|
||||
|
|
@ -134,33 +141,72 @@ class EditorState {
|
|||
///
|
||||
/// The options can be used to determine whether the editor
|
||||
/// should record the transaction in undo/redo stack.
|
||||
///
|
||||
/// The maximumRuleApplyLoop is used to prevent infinite loop.
|
||||
///
|
||||
/// The withUpdateSelection is used to determine whether the editor
|
||||
/// should update the selection after applying the transaction.
|
||||
Future<void> apply(
|
||||
Transaction transaction, {
|
||||
bool isRemote = false,
|
||||
ApplyOptions options = const ApplyOptions(recordUndo: true),
|
||||
ruleCount = 0,
|
||||
withUpdateCursor = true,
|
||||
bool withUpdateSelection = true,
|
||||
}) async {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
if (!editable) {
|
||||
completer.complete();
|
||||
return completer.future;
|
||||
}
|
||||
// TODO: validate the transaction.
|
||||
for (final op in transaction.operations) {
|
||||
_applyOperation(op);
|
||||
for (final operation in transaction.operations) {
|
||||
_applyOperation(operation);
|
||||
}
|
||||
|
||||
// broadcast to other users here
|
||||
_observer.add(transaction);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
_applyRules(ruleCount);
|
||||
if (withUpdateCursor) {
|
||||
await updateCursorSelection(transaction.afterSelection);
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
_recordRedoOrUndo(options, transaction);
|
||||
|
||||
if (withUpdateSelection) {
|
||||
selection = transaction.afterSelection;
|
||||
}
|
||||
|
||||
// TODO: execute this line after the UI has been updated.
|
||||
{
|
||||
completer.complete();
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// get nodes in selection
|
||||
///
|
||||
/// if selection is backward, return nodes in order
|
||||
/// if selection is forward, return nodes in reverse order
|
||||
///
|
||||
List<Node> getNodesInSelection(Selection selection) {
|
||||
// Normalize the selection.
|
||||
final normalized = selection.normalized;
|
||||
|
||||
// Get the start and end nodes.
|
||||
final startNode = document.nodeAtPath(normalized.start.path);
|
||||
final endNode = document.nodeAtPath(normalized.end.path);
|
||||
|
||||
// If we have both nodes, we can find the nodes in the selection.
|
||||
if (startNode != null && endNode != null) {
|
||||
final nodes = NodeIterator(
|
||||
document: document,
|
||||
startNode: startNode,
|
||||
endNode: endNode,
|
||||
).toList();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// If we don't have both nodes, we can't find the nodes in the selection.
|
||||
return [];
|
||||
}
|
||||
|
||||
void _recordRedoOrUndo(ApplyOptions options, Transaction transaction) {
|
||||
if (options.recordUndo) {
|
||||
final undoItem = undoManager.getUndoHistoryItem();
|
||||
undoItem.addAll(transaction.operations);
|
||||
|
|
@ -177,33 +223,6 @@ class EditorState {
|
|||
redoItem.afterSelection = transaction.afterSelection;
|
||||
undoManager.redoStack.push(redoItem);
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// get nodes in selection
|
||||
///
|
||||
List<Node> getNodesInSelection(Selection selection) {
|
||||
final start =
|
||||
selection.isBackward ? selection.start.path : selection.end.path;
|
||||
final end =
|
||||
selection.isBackward ? selection.end.path : selection.start.path;
|
||||
assert(start <= end);
|
||||
final startNode = editorState.document.nodeAtPath(start);
|
||||
final endNode = editorState.document.nodeAtPath(end);
|
||||
if (startNode != null && endNode != null) {
|
||||
final nodes = NodeIterator(
|
||||
document: editorState.document,
|
||||
startNode: startNode,
|
||||
endNode: endNode,
|
||||
).toList();
|
||||
if (selection.isBackward) {
|
||||
return nodes;
|
||||
} else {
|
||||
return nodes.reversed.toList(growable: false);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
void _debouncedSealHistoryItem() {
|
||||
|
|
@ -233,9 +252,9 @@ class EditorState {
|
|||
}
|
||||
}
|
||||
|
||||
void _applyRules(int ruleCount) {
|
||||
void _applyRules(int maximumRuleApplyLoop) {
|
||||
// Set a maximum count to prevent a dead loop.
|
||||
if (ruleCount >= 5 || disableRules) {
|
||||
if (maximumRuleApplyLoop >= 5 || disableRules) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +262,10 @@ class EditorState {
|
|||
_insureLastNodeEditable(transaction);
|
||||
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
apply(transaction, ruleCount: ruleCount + 1, withUpdateCursor: false);
|
||||
apply(
|
||||
transaction,
|
||||
withUpdateSelection: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue