feat: debounce on word count service (#711)

* feat: debounce on word count service

* test: add pump for debounce in test
This commit is contained in:
Mathias Mogensen 2024-02-12 00:27:10 +01:00 • committed by GitHub
commit 66762ef9a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 15 deletions

View file

@ -48,10 +48,26 @@ class Counters {
/// the counter stats. /// the counter stats.
/// ///
class WordCountService with ChangeNotifier { class WordCountService with ChangeNotifier {
WordCountService({required this.editorState}); WordCountService({
required this.editorState,
this.debounceDuration = const Duration(milliseconds: 300),
});
final EditorState editorState; final EditorState editorState;
/// The time to wait before input stops, to recalculate
/// word and character count.
///
/// This duration is used for debouncing both document
/// and selection changes.
///
/// If 0 no debouncing will occur
///
final Duration debounceDuration;
Timer? _selectionTimer;
Timer? _documentTimer;
/// Number of words and characters in the [Document]. /// Number of words and characters in the [Document].
/// ///
Counters get documentCounters => _documentCounters; Counters get documentCounters => _documentCounters;
@ -111,9 +127,8 @@ class WordCountService with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
_streamSubscription = _streamSubscription = editorState.transactionStream.listen(_onDocUpdate);
editorState.transactionStream.listen(_recountOnTransactionUpdate); editorState.selectionNotifier.addListener(_onSelUpdate);
editorState.selectionNotifier.addListener(_recountOnSelectionUpdate);
} }
/// Stops the Word Counter and resets the counts. /// Stops the Word Counter and resets the counts.
@ -123,6 +138,10 @@ class WordCountService with ChangeNotifier {
return; return;
} }
_documentTimer?.cancel();
_documentTimer = null;
_selectionTimer?.cancel();
_selectionTimer = null;
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_documentCounters = const Counters(); _documentCounters = const Counters();
_selectionCounters = const Counters(); _selectionCounters = const Counters();
@ -133,11 +152,27 @@ class WordCountService with ChangeNotifier {
@override @override
void dispose() { void dispose() {
editorState.selectionNotifier.removeListener(_recountOnSelectionUpdate); editorState.selectionNotifier.removeListener(_onSelUpdate);
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_documentTimer?.cancel();
_selectionTimer?.cancel();
_documentTimer = null;
_selectionTimer = null;
super.dispose(); super.dispose();
} }
void _onSelUpdate() {
if (debounceDuration.inMilliseconds == 0) {
return _recountOnSelectionUpdate();
}
if (_selectionTimer?.isActive ?? false) {
_selectionTimer!.cancel();
}
_selectionTimer = Timer(debounceDuration, _recountOnSelectionUpdate);
}
void _recountOnSelectionUpdate() { void _recountOnSelectionUpdate() {
// If collapsed or null, reset count // If collapsed or null, reset count
if (editorState.selection?.isCollapsed ?? true) { if (editorState.selection?.isCollapsed ?? true) {
@ -146,7 +181,6 @@ class WordCountService with ChangeNotifier {
} }
_selectionCounters = const Counters(); _selectionCounters = const Counters();
return notifyListeners(); return notifyListeners();
} }
@ -169,16 +203,26 @@ class WordCountService with ChangeNotifier {
charCount += counters.charCount; charCount += counters.charCount;
} }
return Counters( return Counters(wordCount: wordCount, charCount: charCount);
wordCount: wordCount, }
charCount: charCount,
void _onDocUpdate((TransactionTime time, Transaction t) event) {
if (debounceDuration.inMilliseconds == 0) {
return _recountOnTransactionUpdate(event.$1);
}
if (_documentTimer?.isActive ?? false) {
_documentTimer!.cancel();
}
_documentTimer = Timer(
debounceDuration,
() => _recountOnTransactionUpdate(event.$1),
); );
} }
void _recountOnTransactionUpdate( void _recountOnTransactionUpdate(TransactionTime time) {
(TransactionTime time, Transaction t) event, if (time != TransactionTime.after) {
) {
if (event.$1 != TransactionTime.after) {
return; return;
} }

View file

@ -1,6 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import '../../new/infra/testable_editor.dart'; import '../../new/infra/testable_editor.dart';
void main() async { void main() async {
@ -44,7 +45,7 @@ void main() async {
await editor.editorState.apply(transaction); await editor.editorState.apply(transaction);
await tester.pumpAndSettle(); await tester.pumpAndSettle(const Duration(milliseconds: 300));
expect(service.documentCounters.wordCount, 3 * 4); expect(service.documentCounters.wordCount, 3 * 4);
expect(service.documentCounters.charCount, text.length * 4); expect(service.documentCounters.charCount, text.length * 4);
@ -53,6 +54,8 @@ void main() async {
service.stop(); service.stop();
await tester.pumpAndSettle(const Duration(milliseconds: 300));
expect(service.documentCounters.wordCount, 0); expect(service.documentCounters.wordCount, 0);
expect(service.documentCounters.charCount, 0); expect(service.documentCounters.charCount, 0);
expect(wordCount, 0); expect(wordCount, 0);