Skip to content

Commit

Permalink
Cache offset and length in document nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Amir-P committed Aug 23, 2024
1 parent e725636 commit d17a52e
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 22 deletions.
10 changes: 4 additions & 6 deletions packages/parchment/lib/src/document/embeds.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:collection/collection.dart';
import 'package:quiver/core.dart';

const _dataEquality = DeepCollectionEquality();

Expand Down Expand Up @@ -60,12 +59,11 @@ class EmbeddableObject {

@override
int get hashCode {
if (_data.isEmpty) return hash2(type, inline);
if (_data.isEmpty) return Object.hash(type, inline);

final dataHash = hashObjects(
_data.entries.map((e) => hash2(e.key, e.value)),
);
return hash3(type, inline, dataHash);
final dataHash =
Object.hashAll(_data.entries.map((e) => Object.hash(e.key, e.value)));
return Object.hash(type, inline, dataHash);
}

Map<String, dynamic> toJson() {
Expand Down
12 changes: 9 additions & 3 deletions packages/parchment/lib/src/document/leaf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ abstract base class LeafNode extends Node with StyledNode {
Object get value => _value;
Object _value;

void _setValue(Object newValue) {
_value = newValue;
parent.invalidateLength();
next?.invalidateOffset();
}

/// Splits this leaf node at [index] and returns new node.
///
/// If this is the last node in its list and [index] equals this node's
Expand All @@ -48,7 +54,7 @@ abstract base class LeafNode extends Node with StyledNode {

if (this is TextNode) {
final text = _value as String;
_value = text.substring(0, index);
_setValue(text.substring(0, index));
final split = LeafNode(text.substring(index));
split.applyStyle(style);
insertAfter(split);
Expand Down Expand Up @@ -205,7 +211,7 @@ abstract base class LeafNode extends Node with StyledNode {
var mergeWith = node.previous as TextNode;
if (mergeWith.style == node.style) {
final combinedValue = mergeWith.value + node.value;
mergeWith._value = combinedValue;
mergeWith._setValue(combinedValue);
node.unlink();
node = mergeWith;
}
Expand All @@ -214,7 +220,7 @@ abstract base class LeafNode extends Node with StyledNode {
var mergeWith = node.next as TextNode;
if (mergeWith.style == node.style) {
final combinedValue = node.value + mergeWith.value;
node._value = combinedValue;
node._setValue(combinedValue);
mergeWith.unlink();
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/parchment/lib/src/document/line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ final class LineNode extends ContainerNode<LeafNode> with StyledNode {
@override
LeafNode get defaultChild => TextNode();

// TODO: should be able to cache length and invalidate on any child-related operation
@override
int get length => super.length + 1;

Expand Down
69 changes: 64 additions & 5 deletions packages/parchment/lib/src/document/node.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:collection';

import 'package:meta/meta.dart';
import 'package:parchment_delta/parchment_delta.dart';

import 'attributes.dart';
Expand Down Expand Up @@ -33,24 +34,32 @@ abstract base class Node extends LinkedListEntry<Node> {
/// `null`.
bool get mounted => _parent != null;

int? _offsetCache;

/// Offset in characters of this node relative to [parent] node.
///
/// To get offset of this node in the document see [documentOffset].
int get offset {
if (isFirst) return 0;
if (_offsetCache != null) return _offsetCache!;

if (isFirst) return _offsetCache = 0;
var offset = 0;
var node = this;
do {
node = node.previous!;
offset += node.length;
} while (!node.isFirst);
return offset;
return _offsetCache = offset;
}

int? _documentOffsetCache;

/// Offset in characters of this node in the document.
int get documentOffset {
if (_documentOffsetCache != null) return _documentOffsetCache!;

final parentOffset = (_parent is! RootNode) ? _parent!.documentOffset : 0;
return parentOffset + offset;
return _documentOffsetCache = parentOffset + offset;
}

/// Returns `true` if this node contains character at specified [offset] in
Expand Down Expand Up @@ -86,20 +95,40 @@ abstract base class Node extends LinkedListEntry<Node> {
assert(entry._parent == null && _parent != null);
entry._parent = _parent;
super.insertBefore(entry);
_parent?.invalidateLength();
invalidateOffset();
}

@override
void insertAfter(Node entry) {
assert(entry._parent == null && _parent != null);
entry._parent = _parent;
super.insertAfter(entry);
parent?.invalidateLength();
entry.invalidateOffset();
}

@override
void unlink() {
assert(_parent != null);
final oldParent = _parent;
_parent = null;
final oldNext = next;
super.unlink();
oldNext?.invalidateOffset();
oldParent?.invalidateLength();
}

@mustCallSuper
void invalidateOffset() {
_offsetCache = null;
invalidateDocumentOffset();
next?.invalidateOffset();
}

@mustCallSuper
void invalidateDocumentOffset() {
_documentOffsetCache = null;
}
}

Expand Down Expand Up @@ -163,20 +192,29 @@ abstract base class ContainerNode<T extends Node> extends Node {
assert(node._parent == null);
node._parent = this;
_children.add(node);
node.next?.invalidateOffset();
invalidateLength();
}

/// Adds [node] to the beginning of this container children list.
void addFirst(T node) {
assert(node._parent == null);
node._parent = this;
_children.addFirst(node);
node.next?.invalidateOffset();
invalidateLength();
}

/// Removes [node] from this container.
void remove(T node) {
assert(node._parent == this);
node._parent = null;
_children.remove(node);
final oldNext = node.next;
final removed = _children.remove(node);
if (removed) {
invalidateLength();
oldNext?.invalidateOffset();
}
}

/// Moves children of this node to [newParent].
Expand All @@ -189,6 +227,8 @@ abstract base class ContainerNode<T extends Node> extends Node {
newParent.add(child);
}

invalidateLength();

/// In case [newParent] already had children we need to make sure
/// combined list is optimized.
if (toBeOptimized != null) toBeOptimized.optimize();
Expand Down Expand Up @@ -222,10 +262,13 @@ abstract base class ContainerNode<T extends Node> extends Node {
@override
String toPlainText() => children.map((child) => child.toPlainText()).join();

int? _length;

/// Content length of this node's children. To get number of children in this
/// node use [childCount].
@override
int get length => _children.fold(0, (current, node) => current + node.length);
int get length => _length ??=
_children.fold<int>(0, (current, node) => current + node.length);

@override
void insert(int index, Object data, ParchmentStyle? style) {
Expand All @@ -240,6 +283,7 @@ abstract base class ContainerNode<T extends Node> extends Node {
final result = lookup(index);
result.node!.insert(result.offset, data, style);
}
invalidateLength();
}

@override
Expand All @@ -254,10 +298,25 @@ abstract base class ContainerNode<T extends Node> extends Node {
assert(isNotEmpty);
final res = lookup(index);
res.node!.delete(res.offset, length);
invalidateLength();
}

@override
String toString() => _children.join('\n');

@override
void invalidateDocumentOffset() {
super.invalidateDocumentOffset();
for (var child in children) {
child.invalidateDocumentOffset();
}
}

void invalidateLength() {
_length = null;
next?.invalidateOffset();
parent?.invalidateLength();
}
}

/// Mixin used by nodes that wish to implement [StyledNode] interface.
Expand Down
4 changes: 2 additions & 2 deletions packages/parchment/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ environment:
dependencies:
collection: ^1.18.0
parchment_delta: ^1.0.0
quiver: ^3.2.1
html: ^0.15.4
meta: ^1.12.0

dev_dependencies:
test: ^1.25.5
test: ^1.25.8
lints: ^4.0.0
2 changes: 2 additions & 0 deletions packages/parchment/test/document/leaf_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ void main() {
test('insert in formatted node', () {
line.retain(0, 6, boldStyle);
expect(line.childCount, 2);
expect(line.children.last.offset, 6);
line.insert(3, 'don', null);
expect(line.childCount, 4);
expect(line.children.last.offset, 9);
final b = boldStyle.toJson();
expect(
line.children.elementAt(0).toDelta(),
Expand Down
3 changes: 3 additions & 0 deletions packages/parchment/test/document/line_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ void main() {
root.retain(0, 4, boldStyle);
root.retain(16, 6, boldStyle);
final line = root.first as LineNode;
final lastTextSegment = line.children.last;
expect(lastTextSegment.offset, 16);
final newLine = line.splitAt(10);
expect(lastTextSegment.offset, 6);
expect(line.toPlainText(), 'This house\n');
expect(newLine.toPlainText(), ' is a circus\n');
});
Expand Down
19 changes: 14 additions & 5 deletions packages/parchment/test/document/node_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ void main() {
});

test('documentOffset', () {
root.insert(0, 'First line\nSecond line', null);
final line = root.children.last as LineNode;
final text = line.first as TextNode;
expect(line.documentOffset, 11);
expect(text.documentOffset, 11);
root.insert(0, 'First line\nSecond line\nThird line', null);
final secondLine = root.children.first.next as LineNode;
final thirdLine = root.children.last as LineNode;
expect(thirdLine.documentOffset, 23);
secondLine.insert(6, ' styled', ParchmentStyle.fromJson({'b': true}));
final styledText = secondLine.first.next as TextNode;
final lastText = secondLine.last as TextNode;
expect(secondLine.documentOffset, 11);
expect(thirdLine.documentOffset, 30);
expect(styledText.documentOffset, 17);
expect(lastText.documentOffset, 24);
secondLine.remove(styledText);
expect(lastText.documentOffset, 17);
expect(thirdLine.documentOffset, 23);
});

test('containsOffset', () {
Expand Down

0 comments on commit d17a52e

Please sign in to comment.