Skip to content

Commit 2f686d4

Browse files
CatHood0CatHood0EchoEllet
authored
feat: cache for toPlainText in Document to avoid unnecessary text computing (singerdmx#2482)
feat: cache for toPlainText in Document to avoid unnecessary text computing (singerdmx#2482) test: basic unit tests --------- Co-authored-by: CatHood0 <[email protected]> Co-authored-by: Ellet <[email protected]>
1 parent 5922b99 commit 2f686d4

File tree

3 files changed

+97
-5
lines changed

3 files changed

+97
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
1111
## [Unreleased]
1212

13+
### Added
14+
15+
- Cache for `toPlainText` in `Document` class to avoid unnecessary text computing [#2482](https://github.com/singerdmx/flutter-quill/pull/2482).
16+
1317
## [11.1.2] - 2025-03-24
1418

1519
### Fixed

lib/src/document/document.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,26 @@ import 'style.dart';
2525
class Document {
2626
/// Creates new empty document.
2727
Document() : _delta = Delta()..insert('\n') {
28-
_loadDocument(_delta);
28+
loadDocument(_delta);
2929
}
3030

3131
/// Creates new document from provided JSON `data`.
3232
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
33-
_loadDocument(_delta);
33+
loadDocument(_delta);
3434
}
3535

3636
/// Creates new document from provided `delta`.
3737
Document.fromDelta(Delta delta) : _delta = delta {
38-
_loadDocument(delta);
38+
loadDocument(delta);
3939
}
4040

41+
/// Stores the plain text content of the entire document in memory for quick access.
42+
///
43+
/// It acts as a cache to avoid repeatedly extracting or generating the plain text.
44+
@visibleForTesting
45+
@internal
46+
String? cachedPlainText;
47+
4148
/// The root node of the document tree
4249
final Root _root = Root();
4350

@@ -465,16 +472,19 @@ class Document {
465472
throw StateError('_delta compose failed');
466473
}
467474
assert(_delta == _root.toDelta(), 'Compose failed');
475+
cachedPlainText = null;
468476
final change = DocChange(originalDelta, delta, changeSource);
469477
documentChangeObserver.add(change);
470478
history.handleDocChange(change);
471479
}
472480

473481
HistoryChanged undo() {
482+
cachedPlainText = null;
474483
return history.undo(this);
475484
}
476485

477486
HistoryChanged redo() {
487+
cachedPlainText = null;
478488
return history.redo(this);
479489
}
480490

@@ -539,11 +549,13 @@ class Document {
539549
Iterable<EmbedBuilder>? embedBuilders,
540550
EmbedBuilder? unknownEmbedBuilder,
541551
]) =>
542-
_root.children
552+
cachedPlainText ??= _root.children
543553
.map((e) => e.toPlainText(embedBuilders, unknownEmbedBuilder))
544554
.join();
545555

546-
void _loadDocument(Delta doc) {
556+
@visibleForTesting
557+
@internal
558+
void loadDocument(Delta doc) {
547559
if (doc.isEmpty) {
548560
throw ArgumentError.value(
549561
doc.toString(), 'Document Delta cannot be empty.');
@@ -570,6 +582,7 @@ class Document {
570582
_root.childCount > 1) {
571583
_root.remove(node);
572584
}
585+
cachedPlainText = null;
573586
}
574587

575588
bool isEmpty() {

test/document/document_test.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,79 @@ void main() {
182182
reason: 'start of blank line');
183183
});
184184
});
185+
group('cachedPlainText', () {
186+
late Document document;
187+
188+
setUp(() {
189+
document = Document();
190+
});
191+
192+
test('is null initially', () {
193+
expect(document.cachedPlainText, isNull);
194+
});
195+
196+
test('updates to null when undo is called', () {
197+
document.cachedPlainText = 'Non-null cached plain text';
198+
expect(document.cachedPlainText, isNotNull);
199+
200+
document.undo();
201+
expect(document.cachedPlainText, isNull);
202+
});
203+
204+
test('updates to null when redo is called', () {
205+
final document = Document()
206+
..cachedPlainText = 'Non-null cached plain text';
207+
expect(document.cachedPlainText, isNotNull);
208+
209+
document.redo();
210+
expect(document.cachedPlainText, isNull);
211+
});
212+
213+
test('returns cachedPlainText if not null', () {
214+
const example = 'Hello, World!';
215+
document.cachedPlainText = example;
216+
217+
expect(document.cachedPlainText, example);
218+
});
219+
220+
test('sets cachedPlainText if null when toPlainText is called', () {
221+
document.toPlainText();
222+
expect(document.cachedPlainText, isNotNull);
223+
});
224+
225+
test('sets cachedPlainText correctly when toPlainText is called', () {
226+
const example = 'Hello\n';
227+
document = Document.fromDelta(Delta()..insert(example))..toPlainText();
228+
229+
expect(document.cachedPlainText, isNotNull);
230+
expect(document.cachedPlainText, example);
231+
});
232+
233+
test('sets cachedPlainText to null when loadDocument is called', () {
234+
document.cachedPlainText = 'Not null';
235+
expect(document.cachedPlainText, isNotNull);
236+
237+
document.loadDocument(Delta()..insert('Hello\n'));
238+
239+
expect(document.cachedPlainText, isNull);
240+
});
241+
242+
test('sets cachedPlainText to null when compose is called', () {
243+
document.cachedPlainText = 'Not null';
244+
expect(document.cachedPlainText, isNotNull);
245+
246+
document.compose(Delta()..insert('Hello\n'), ChangeSource.local);
247+
248+
expect(document.cachedPlainText, isNull);
249+
});
250+
251+
test('sets cachedPlainText correctly', () {
252+
// This test is useful in case this property has a getter and setter.
253+
document.cachedPlainText = 'Not null';
254+
expect(document.cachedPlainText, isNotNull);
255+
256+
document.cachedPlainText = null;
257+
expect(document.cachedPlainText, isNull);
258+
});
259+
});
185260
}

0 commit comments

Comments
 (0)