Skip to content

Commit

Permalink
feat: add diff document/nodes function (AppFlowy-IO#748)
Browse files Browse the repository at this point in the history
* feat: transform operations

* feat: add document diff function
  • Loading branch information
LucasXu0 authored Mar 19, 2024
1 parent 2493d9b commit cbb3346
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 29 deletions.
9 changes: 9 additions & 0 deletions example/lib/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/pages/collab_editor.dart';
import 'package:example/pages/customize_theme_for_editor.dart';
import 'package:example/pages/editor.dart';
import 'package:example/pages/editor_list.dart';
Expand Down Expand Up @@ -159,6 +160,14 @@ class _HomePageState extends State<HomePage> {

// Theme Demo
_buildSeparator(context, 'Showcases'),
_buildListTile(context, 'Collab Editor', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CollabEditor(),
),
);
}),
_buildListTile(context, 'Custom Theme', () {
Navigator.push(
context,
Expand Down
66 changes: 66 additions & 0 deletions example/lib/pages/collab_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

// Not completed yet
class CollabEditor extends StatefulWidget {
const CollabEditor({super.key});

@override
State<CollabEditor> createState() => _CollabEditorState();
}

class _CollabEditorState extends State<CollabEditor> {
final EditorState editorStateA =
EditorState(document: Document.blank(withInitialText: true));
final EditorState editorStateB =
EditorState(document: Document.blank(withInitialText: true));

@override
void initState() {
super.initState();

editorStateA.transactionStream.listen((event) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (event.$1 == TransactionTime.before) {
editorStateB.apply(event.$2, isRemote: true);
}
});
});

editorStateB.transactionStream.listen((event) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (event.$1 == TransactionTime.before) {
editorStateA.apply(event.$2, isRemote: true);
}
});
});
}

@override
void dispose() {
editorStateA.dispose();
editorStateB.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: AppFlowyEditor(
editorState: editorStateA,
),
),
const VerticalDivider(),
Expanded(
child: AppFlowyEditor(
editorState: editorStateB,
),
),
],
),
);
}
}
8 changes: 4 additions & 4 deletions example/lib/pages/editor.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'dart:convert';

import 'package:flutter/material.dart';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:example/pages/desktop_editor.dart';
import 'package:example/pages/mobile_editor.dart';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

class Editor extends StatefulWidget {
const Editor({
Expand Down Expand Up @@ -105,6 +103,8 @@ class _EditorState extends State<Editor> {
}
});

widget.onEditorStateChange(editorState);

this.editorState = editorState;
registerWordCounter();
}
Expand Down
1 change: 1 addition & 0 deletions lib/src/core/core.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export 'document/attributes.dart';
export 'document/deprecated/document.dart';
export 'document/deprecated/node.dart';
export 'document/diff.dart';
export 'document/document.dart';
export 'document/node.dart';
export 'document/node_iterator.dart';
Expand Down
47 changes: 47 additions & 0 deletions lib/src/core/document/diff.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';

const _equality = DeepCollectionEquality();

List<Operation> diffDocuments(Document oldDocument, Document newDocument) {
return diffNodes(oldDocument.root, newDocument.root);
}

List<Operation> diffNodes(Node oldNode, Node newNode) {
List<Operation> operations = [];

if (!_equality.equals(oldNode.attributes, newNode.attributes)) {
operations.add(
UpdateOperation(oldNode.path, newNode.attributes, oldNode.attributes),
);
}

final oldChildrenById = {
for (final child in oldNode.children) child.id: child,
};
final newChildrenById = {
for (final child in newNode.children) child.id: child,
};

// Identify insertions and updates
for (final newChild in newNode.children) {
final oldChild = oldChildrenById[newChild.id];
if (oldChild == null) {
// Insert operation
operations.add(InsertOperation(newChild.path, [newChild]));
} else {
// Recursive diff for updates
operations.addAll(diffNodes(oldChild, newChild));
}
}

// Identify deletions
oldChildrenById.keys
.where((id) => !newChildrenById.containsKey(id))
.forEach((id) {
final oldChild = oldChildrenById[id]!;
operations.add(DeleteOperation(oldChild.path, [oldChild]));
});

return operations;
}
22 changes: 22 additions & 0 deletions lib/src/core/document/path.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ extension PathExtensions on Path {
..add(last + 1);
}

Path nextNPath(int n) {
Path nextPath = Path.from(this, growable: true);
if (isEmpty) {
return nextPath;
}
final last = nextPath.last;
return nextPath
..removeLast()
..add(last + n);
}

Path get previous {
Path previousPath = Path.from(this, growable: true);
if (isEmpty) {
Expand All @@ -82,6 +93,17 @@ extension PathExtensions on Path {
..add(max(0, last - 1));
}

Path previousNPath(int n) {
Path previousPath = Path.from(this, growable: true);
if (isEmpty) {
return previousPath;
}
final last = previousPath.last;
return previousPath
..removeLast()
..add(max(0, last - n));
}

Path get parent {
if (isEmpty) {
return this;
Expand Down
97 changes: 73 additions & 24 deletions lib/src/editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -286,25 +286,26 @@ class EditorState {

final completer = Completer<void>();

// broadcast to other users here, before applying the transaction
_observer.add((TransactionTime.before, transaction));
if (isRemote) {
selection = _applyTransactionFromRemote(transaction);
} else {
// broadcast to other users here, before applying the transaction
_observer.add((TransactionTime.before, transaction));

for (final operation in transaction.operations) {
Log.editor.debug('apply op: ${operation.toJson()}');
_applyOperation(operation);
}
_applyTransactionInLocal(transaction);

// broadcast to other users here, after applying the transaction
_observer.add((TransactionTime.after, transaction));
// broadcast to other users here, after applying the transaction
_observer.add((TransactionTime.after, transaction));

_recordRedoOrUndo(options, transaction);
_recordRedoOrUndo(options, transaction);

if (withUpdateSelection) {
_selectionUpdateReason = SelectionUpdateReason.transaction;
if (transaction.selectionExtraInfo != null) {
selectionExtraInfo = transaction.selectionExtraInfo;
if (withUpdateSelection) {
_selectionUpdateReason = SelectionUpdateReason.transaction;
if (transaction.selectionExtraInfo != null) {
selectionExtraInfo = transaction.selectionExtraInfo;
}
selection = transaction.afterSelection;
}
selection = transaction.afterSelection;
}

completer.complete();
Expand Down Expand Up @@ -530,18 +531,66 @@ class EditorState {
});
}

void _applyOperation(Operation op) {
if (op is InsertOperation) {
document.insert(op.path, op.nodes);
} else if (op is UpdateOperation) {
// ignore the update operation if the attributes are the same.
if (!mapEquals(op.attributes, op.oldAttributes)) {
void _applyTransactionInLocal(Transaction transaction) {
for (final op in transaction.operations) {
Log.editor.debug('apply op (local): ${op.toJson()}');

if (op is InsertOperation) {
document.insert(op.path, op.nodes);
} else if (op is UpdateOperation) {
// ignore the update operation if the attributes are the same.
if (!mapEquals(op.attributes, op.oldAttributes)) {
document.update(op.path, op.attributes);
}
} else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length);
} else if (op is UpdateTextOperation) {
document.updateText(op.path, op.delta);
}
}
}

Selection? _applyTransactionFromRemote(Transaction transaction) {
var selection = this.selection;

for (final op in transaction.operations) {
Log.editor.debug('apply op (remote): ${op.toJson()}');

if (op is InsertOperation) {
document.insert(op.path, op.nodes);
if (selection != null) {
if (op.path <= selection.start.path) {
selection = Selection(
start: selection.start.copyWith(
path: selection.start.path.nextNPath(op.nodes.length),
),
end: selection.end.copyWith(
path: selection.end.path.nextNPath(op.nodes.length),
),
);
}
}
} else if (op is UpdateOperation) {
document.update(op.path, op.attributes);
} else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length);
if (selection != null) {
if (op.path <= selection.start.path) {
selection = Selection(
start: selection.start.copyWith(
path: selection.start.path.previous,
),
end: selection.end.copyWith(
path: selection.end.path.previous,
),
);
}
}
} else if (op is UpdateTextOperation) {
document.updateText(op.path, op.delta);
}
} else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length);
} else if (op is UpdateTextOperation) {
document.updateText(op.path, op.delta);
}

return selection;
}
}
Loading

0 comments on commit cbb3346

Please sign in to comment.