Merge pull request #831 from AppFlowy-IO/doc/transaction-and-deltas

Doc: transaction and deltas
This commit is contained in:
Vincent Chan 2022-08-12 15:44:50 +08:00 committed by GitHub
commit 68a1acc9f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 29 deletions

View File

@ -256,7 +256,12 @@ TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
return result;
}
// basically copy from: https://github.com/quilljs/delta
/// Deltas are a simple, yet expressive format that can be used to describe contents and changes.
/// The format is JSON based, and is human readable, yet easily parsible by machines.
/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML.
///
/// Basically borrowed from: https://github.com/quilljs/delta
class Delta extends Iterable<TextOperation> {
final List<TextOperation> _operations;
String? _rawString;
@ -316,6 +321,9 @@ class Delta extends Iterable<TextOperation> {
_operations.add(textOp);
}
/// The slice() method does not change the original string.
/// The start and end parameters specifies the part of the string to extract.
/// The end position is optional.
Delta slice(int start, [int? end]) {
final result = Delta();
final iterator = _OpIterator(_operations);
@ -336,19 +344,29 @@ class Delta extends Iterable<TextOperation> {
return result;
}
/// Insert operations have an `insert` key defined.
/// A String value represents inserting text.
void insert(String content, [Attributes? attributes]) =>
add(TextInsert(content, attributes));
/// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip).
/// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range.
/// A value of `null` in the `attributes` Object represents removal of that key.
///
/// *Note: It is not necessary to retain the last characters of a document as this is implied.*
void retain(int length, [Attributes? attributes]) =>
add(TextRetain(length, attributes));
/// Delete operations have a Number `delete` key defined representing the number of characters to delete.
void delete(int length) => add(TextDelete(length));
/// The length of the string fo the [Delta].
int get length {
return _operations.fold(
0, (previousValue, element) => previousValue + element.length);
}
/// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta.
Delta compose(Delta other) {
final thisIter = _OpIterator(_operations);
final otherIter = _OpIterator(other._operations);
@ -412,6 +430,7 @@ class Delta extends Iterable<TextOperation> {
return delta..chop();
}
/// This method joins two Delta together.
Delta operator +(Delta other) {
var ops = [..._operations];
if (other._operations.isNotEmpty) {
@ -445,6 +464,7 @@ class Delta extends Iterable<TextOperation> {
return hashList(_operations);
}
/// Returned an inverted delta that has the opposite effect of against a base document delta.
Delta invert(Delta base) {
final inverted = Delta();
_operations.fold(0, (int previousValue, op) {
@ -475,6 +495,13 @@ class Delta extends Iterable<TextOperation> {
return _operations.map((e) => e.toJson()).toList();
}
/// This method will return the position of the previous rune.
///
/// Since the encoding of the [String] in Dart is UTF-16.
/// If you want to find the previous character of a position,
/// you can' just use the `position - 1` simply.
///
/// This method can help you to compute the position of the previous character.
int prevRunePosition(int pos) {
if (pos == 0) {
return pos - 1;
@ -485,6 +512,13 @@ class Delta extends Iterable<TextOperation> {
return _runeIndexes![pos - 1];
}
/// This method will return the position of the next rune.
///
/// Since the encoding of the [String] in Dart is UTF-16.
/// If you want to find the previous character of a position,
/// you can' just use the `position + 1` simply.
///
/// This method can help you to compute the position of the next character.
int nextRunePosition(int pos) {
final stringContent = toRawString();
if (pos >= stringContent.length - 1) {

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flowy_editor/src/service/service.dart';
import 'package:flutter/material.dart';
import 'package:flowy_editor/src/document/node.dart';
import 'package:flowy_editor/src/document/selection.dart';
import 'package:flowy_editor/src/document/state_tree.dart';
import 'package:flowy_editor/src/operation/operation.dart';
@ -26,11 +25,26 @@ enum CursorUpdateReason {
others,
}
/// The state of the editor.
///
/// The state including:
/// - The document to render
/// - The state of the selection.
///
/// [EditorState] also includes the services of the editor:
/// - Selection service
/// - Scroll service
/// - Keyboard service
/// - Input service
/// - Toolbar service
///
/// In consideration of collaborative editing.
/// All the mutations should be applied through [Transaction].
///
/// Mutating the document with document's API is not recommended.
class EditorState {
final StateTree document;
List<Node> selectedNodes = [];
// Service reference.
final service = FlowyService();
@ -41,7 +55,6 @@ class EditorState {
return _cursorSelection;
}
/// add the set reason in the future, don't use setter
updateCursorSelection(Selection? cursorSelection,
[CursorUpdateReason reason = CursorUpdateReason.others]) {
// broadcast to other users here
@ -59,8 +72,13 @@ class EditorState {
undoManager.state = this;
}
/// Apply the transaction to the state.
///
/// The options can be used to determine whether the editor
/// should record the transaction in undo/redo stack.
apply(Transaction transaction,
[ApplyOptions options = const ApplyOptions()]) {
// TODO: validate the transation.
for (final op in transaction.operations) {
_applyOperation(op);
}

View File

@ -14,7 +14,6 @@ import 'package:flowy_editor/src/operation/transaction.dart';
/// A [TransactionBuilder] is used to build the transaction from the state.
/// It will save make a snapshot of the cursor selection state automatically.
/// The cursor can be resorted if the transaction is undo.
class TransactionBuilder {
final List<Operation> operations = [];
EditorState state;
@ -29,15 +28,18 @@ class TransactionBuilder {
state.apply(transaction);
}
/// Insert the nodes at the position of path.
insertNode(Path path, Node node) {
insertNodes(path, [node]);
}
/// Insert a sequence of nodes at the position of path.
insertNodes(Path path, List<Node> nodes) {
beforeSelection = state.cursorSelection;
add(InsertOperation(path, nodes));
}
/// Update the attributes of nodes.
updateNode(Node node, Attributes attributes) {
beforeSelection = state.cursorSelection;
@ -49,6 +51,7 @@ class TransactionBuilder {
));
}
/// Delete a node in the document.
deleteNode(Node node) {
deleteNodesAtPath(node.path);
}
@ -57,6 +60,9 @@ class TransactionBuilder {
nodes.forEach(deleteNode);
}
/// Delete a sequence of nodes at the path of the document.
/// The length specific the length of the following nodes to delete(
/// including the start one).
deleteNodesAtPath(Path path, [int length = 1]) {
if (path.isEmpty) {
return;
@ -106,6 +112,9 @@ class TransactionBuilder {
);
}
/// Insert 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;
@ -126,6 +135,7 @@ class TransactionBuilder {
Position(path: node.path, offset: index + content.length));
}
/// Assign formatting attributes to a range of text.
formatText(TextNode node, int index, int length, Attributes attributes) {
textEdit(
node,
@ -135,6 +145,7 @@ class TransactionBuilder {
afterSelection = beforeSelection;
}
/// Delete length characters starting from index.
deleteText(TextNode node, int index, int length) {
textEdit(
node,
@ -169,6 +180,11 @@ class TransactionBuilder {
);
}
/// Add 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) {
final Operation? last = operations.isEmpty ? null : operations.last;
if (last != null) {
@ -190,6 +206,7 @@ class TransactionBuilder {
operations.add(op);
}
/// Generate a immutable [Transaction] to apply or transmit.
Transaction finish() {
return Transaction(
operations: UnmodifiableListView(operations),

View File

@ -1,6 +1,5 @@
import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
@ -14,7 +13,6 @@ import 'package:flowy_editor/src/service/keyboard_service.dart';
List<FlowyKeyEventHandler> defaultKeyEventHandlers = [
deleteTextHandler,
slashShortcutHandler,
flowyDeleteNodesHandler,
arrowKeysHandler,
copyPasteKeysHandler,
redoUndoKeysHandler,

View File

@ -1,21 +0,0 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/src/service/keyboard_service.dart';
import 'package:flutter/material.dart';
FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) {
// Handle delete nodes.
final nodes = editorState.selectedNodes;
if (nodes.length <= 1) {
return KeyEventResult.ignored;
}
debugPrint('delete nodes = $nodes');
nodes
.fold<TransactionBuilder>(
TransactionBuilder(editorState),
(previousValue, node) => previousValue..deleteNode(node),
)
.commit();
return KeyEventResult.handled;
};

View File

@ -18,6 +18,11 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
HistoryItem();
/// Seal the history item.
/// When an item is sealed, no more operations can be added
/// to the item.
///
/// The caller should create a new [HistoryItem].
seal() {
_sealed = true;
}
@ -32,6 +37,7 @@ class HistoryItem extends LinkedListEntry<HistoryItem> {
operations.addAll(iterable);
}
/// Create a new [Transaction] by inverting the operations.
Transaction toTransaction(EditorState state) {
final builder = TransactionBuilder(state);
for (var i = operations.length - 1; i >= 0; i--) {