Pārlūkot izejas kodu

feat: undo manager

Vincent Chan 2 gadi atpakaļ
vecāks
revīzija
f4bbe77612

+ 11 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart

@@ -42,6 +42,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
     implements DeltaTextInputClient {
   TextNode get node => widget.node as TextNode;
   EditorState get editorState => widget.editorState;
+  bool _metaKeyDown = false;
 
   TextInputConnection? _textInputConnection;
 
@@ -86,6 +87,16 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
       } else if (event.logicalKey == LogicalKeyboardKey.delete) {
         _forwardDeleteTextAtSelection(sel);
         return KeyEventResult.handled;
+      } else if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
+          event.logicalKey == LogicalKeyboardKey.metaRight) {
+        _metaKeyDown = true;
+      } else if (event.logicalKey == LogicalKeyboardKey.keyZ && _metaKeyDown) {
+        editorState.undoManager.undo();
+      }
+    } else if (event is RawKeyUpEvent) {
+      if (event.logicalKey == LogicalKeyboardKey.metaLeft ||
+          event.logicalKey == LogicalKeyboardKey.metaRight) {
+        _metaKeyDown = false;
       }
     }
     return KeyEventResult.ignored;

+ 45 - 10
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -1,23 +1,31 @@
-import 'package:flowy_editor/document/node.dart';
-import 'package:flowy_editor/operation/operation.dart';
-import 'package:flowy_editor/document/attributes.dart';
+import 'dart:async';
+import 'package:flowy_editor/flowy_editor.dart';
+import 'package:flowy_editor/undo_manager.dart';
 import 'package:flutter/material.dart';
 
-import './document/state_tree.dart';
 import './document/selection.dart';
-import './operation/operation.dart';
-import './operation/transaction.dart';
-import './render/render_plugins.dart';
+
+class ApplyOptions {
+  final bool noLog;
+  const ApplyOptions({
+    this.noLog = false,
+  });
+}
 
 class EditorState {
   final StateTree document;
   final RenderPlugins renderPlugins;
+  final UndoManager undoManager = UndoManager();
   Selection? cursorSelection;
 
+  Timer? _debouncedSealHistoryItemTimer;
+
   EditorState({
     required this.document,
     required this.renderPlugins,
-  });
+  }) {
+    undoManager.state = this;
+  }
 
   /// TODO: move to a better place.
   Widget build(BuildContext context) {
@@ -30,14 +38,41 @@ class EditorState {
     );
   }
 
-  void apply(Transaction transaction) {
+  apply(Transaction transaction,
+      [ApplyOptions options = const ApplyOptions()]) {
     for (final op in transaction.operations) {
       _applyOperation(op);
     }
     cursorSelection = transaction.afterSelection;
+
+    if (options.noLog) {
+      return;
+    }
+
+    final undoItem = undoManager.getUndoHistoryItem();
+    undoItem.addAll(transaction.operations);
+    if (undoItem.beforeSelection == null &&
+        transaction.beforeSelection != null) {
+      undoItem.beforeSelection = transaction.beforeSelection;
+    }
+    undoItem.afterSelection = transaction.afterSelection;
+
+    _debouncedSealHistoryItem();
+  }
+
+  _debouncedSealHistoryItem() {
+    _debouncedSealHistoryItemTimer?.cancel();
+    _debouncedSealHistoryItemTimer =
+        Timer(const Duration(milliseconds: 1000), () {
+      if (undoManager.undoStack.isNonEmpty) {
+        debugPrint('Seal history item');
+        final last = undoManager.undoStack.last;
+        last.seal();
+      }
+    });
   }
 
-  void _applyOperation(Operation op) {
+  _applyOperation(Operation op) {
     if (op is InsertOperation) {
       document.insert(op.path, op.value);
     } else if (op is UpdateOperation) {

+ 2 - 2
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart

@@ -31,7 +31,7 @@ class TransactionBuilder {
 
   /// Commit the operations to the state
   commit() {
-    final transaction = _finish();
+    final transaction = finish();
     state.apply(transaction);
   }
 
@@ -99,7 +99,7 @@ class TransactionBuilder {
     operations.add(op);
   }
 
-  Transaction _finish() {
+  Transaction finish() {
     return Transaction(
       operations: UnmodifiableListView(operations),
       beforeSelection: beforeSelection,

+ 111 - 0
frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart

@@ -0,0 +1,111 @@
+import 'dart:collection';
+
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flowy_editor/operation/operation.dart';
+import 'package:flowy_editor/operation/transaction_builder.dart';
+import 'package:flowy_editor/operation/transaction.dart';
+import 'package:flowy_editor/editor_state.dart';
+
+class HistoryItem extends LinkedListEntry<HistoryItem> {
+  final List<Operation> operations = [];
+  Selection? beforeSelection;
+  Selection? afterSelection;
+  bool _sealed = false;
+
+  HistoryItem();
+
+  seal() {
+    _sealed = true;
+  }
+
+  add(Operation op) {
+    operations.add(op);
+  }
+
+  addAll(Iterable<Operation> iterable) {
+    operations.addAll(iterable);
+  }
+
+  bool get sealed {
+    return _sealed;
+  }
+
+  Transaction toTransaction(EditorState state) {
+    final builder = TransactionBuilder(state);
+    for (var i = operations.length - 1; i >= 0; i--) {
+      final operation = operations[i];
+      final inverted = operation.invert();
+      builder.add(inverted);
+    }
+    builder.afterSelection = beforeSelection;
+    builder.beforeSelection = afterSelection;
+    return builder.finish();
+  }
+}
+
+class FixedSizeStack {
+  final _list = LinkedList<HistoryItem>();
+  final int maxSize;
+
+  FixedSizeStack(this.maxSize);
+
+  push(HistoryItem stackItem) {
+    if (_list.length >= maxSize) {
+      _list.remove(_list.first);
+    }
+    _list.add(stackItem);
+  }
+
+  HistoryItem? pop() {
+    if (_list.isEmpty) {
+      return null;
+    }
+    final last = _list.last;
+
+    _list.remove(last);
+
+    return last;
+  }
+
+  HistoryItem get last {
+    return _list.last;
+  }
+
+  bool get isEmpty {
+    return _list.isEmpty;
+  }
+
+  bool get isNonEmpty {
+    return _list.isNotEmpty;
+  }
+}
+
+class UndoManager {
+  final undoStack = FixedSizeStack(20);
+  final redoStack = FixedSizeStack(20);
+  EditorState? state;
+
+  HistoryItem getUndoHistoryItem() {
+    if (undoStack.isEmpty) {
+      final item = HistoryItem();
+      undoStack.push(item);
+      return item;
+    }
+    final last = undoStack.last;
+    if (last.sealed) {
+      final item = HistoryItem();
+      undoStack.push(item);
+      return item;
+    }
+    return last;
+  }
+
+  undo() {
+    final historyItem = undoStack.pop();
+    if (historyItem == null) {
+      return;
+    }
+    final transaction = historyItem.toTransaction(state!);
+    state!.apply(transaction, const ApplyOptions(noLog: true));
+  }
+}