From 598b077d27365569dfcfb519a21ca11f3f52dd79 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 1 Jun 2026 21:11:25 +0300 Subject: [PATCH 1/5] fix: clear mistakes that fall out of new text range fix: prevent notifying listeners after dispose --- .../controllers/language_tool_controller.dart | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index 0e7e999..cea310a 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -12,6 +12,7 @@ import 'package:languagetool_textfield/src/utils/keep_latest_response_service.da /// marked TextSpans with tap recognizer class LanguageToolController extends TextEditingController { bool _isEnabled; + bool _isDisposed = false; /// Color scheme to highlight mistakes final HighlightStyle highlightStyle; @@ -59,7 +60,7 @@ class LanguageToolController extends TextEditingController { bool get isEnabled => _isEnabled; set isEnabled(bool value) { - if (value == _isEnabled) return; + if (value == _isEnabled || _isDisposed) return; _isEnabled = value; @@ -168,6 +169,7 @@ class LanguageToolController extends TextEditingController { @override void dispose() { + _isDisposed = true; _languageCheckService?.dispose(); super.dispose(); } @@ -198,8 +200,7 @@ class LanguageToolController extends TextEditingController { ///set value triggers each time, even when cursor changes its location ///so this check avoid cleaning Mistake list when text wasn't really changed if (spellCheckSameText || newText != text && newText.isNotEmpty) { - final filteredMistakes = _filterMistakesOnChanged(newText); - _mistakes = filteredMistakes.toList(); + _mistakes = _filterMistakesOnChanged(_mistakes, newText).toList(); // If we have a text change and we have a popup on hold // it will close the popup @@ -215,7 +216,11 @@ class LanguageToolController extends TextEditingController { () => _languageCheckService?.findMistakes(newText) ?? Future(() => null), ); - if (mistakesWrapper == null || !mistakesWrapper.hasResult) return; + if (mistakesWrapper == null || + !mistakesWrapper.hasResult || + _isDisposed) { + return; + } final mistakes = mistakesWrapper.result(); _fetchError = mistakesWrapper.error; @@ -310,27 +315,31 @@ class LanguageToolController extends TextEditingController { ); } - /// Filters the list of mistakes based on the changes - /// in the text when it is changed. - Iterable _filterMistakesOnChanged(String newText) sync* { + List _filterMistakesOnChanged( + List mistakes, + String newText, + ) { final isSelectionRangeEmpty = selection.end == selection.start; final lengthDiscrepancy = newText.length - text.length; - for (final mistake in _mistakes) { - Mistake? newMistake; - - newMistake = isSelectionRangeEmpty - ? _adjustMistakeOffsetWithCaretCursor( - mistake: mistake, - lengthDiscrepancy: lengthDiscrepancy, - ) - : _adjustMistakeOffsetWithSelectionRange( - mistake: mistake, - lengthDiscrepancy: lengthDiscrepancy, - ); - - if (newMistake != null) yield newMistake; - } + return mistakes + .map( + (mistake) => isSelectionRangeEmpty + ? _adjustMistakeOffsetWithCaretCursor( + mistake: mistake, + lengthDiscrepancy: lengthDiscrepancy, + ) + : _adjustMistakeOffsetWithSelectionRange( + mistake: mistake, + lengthDiscrepancy: lengthDiscrepancy, + ), + ) + .nonNulls + .where( + (mistake) => + mistake.offset >= 0 && mistake.endOffset <= newText.length, + ) + .toList(); } /// Adjusts the mistake offset when the selection is a caret cursor. From 423fe043c9fc7a31aae6e45548c787ea34a0451f Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 1 Jun 2026 21:15:06 +0300 Subject: [PATCH 2/5] doc: add 1.0.1 changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84cc605..870e0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 1.0.1 - Fix: recheck text on changing language +- Fixes a bug where changing text programmatically can sometimes throw a RangeError +- Fixes notifying listeners after async gap when disposed ## 1.0.0 From 4a7136debaba5a9b6c62d90d45abe3b15db43ee3 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 1 Jun 2026 21:15:59 +0300 Subject: [PATCH 3/5] fix: remove unnecessary toList --- lib/src/core/controllers/language_tool_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index cea310a..9196003 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -200,7 +200,7 @@ class LanguageToolController extends TextEditingController { ///set value triggers each time, even when cursor changes its location ///so this check avoid cleaning Mistake list when text wasn't really changed if (spellCheckSameText || newText != text && newText.isNotEmpty) { - _mistakes = _filterMistakesOnChanged(_mistakes, newText).toList(); + _mistakes = _filterMistakesOnChanged(_mistakes, newText); // If we have a text change and we have a popup on hold // it will close the popup From 70c27010414172f9d8187c116ed0ddc7f89e36b9 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 1 Jun 2026 21:59:19 +0300 Subject: [PATCH 4/5] style: fix lints --- lib/src/core/controllers/language_tool_controller.dart | 8 +++++--- lib/src/utils/mistake_popup.dart | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index 9196003..f9131ea 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -127,9 +127,11 @@ class LanguageToolController extends TextEditingController { }) { final languageToolService = LanguageToolService(languageToolClient); - // false positive, the same variable is used beyond the return statement - // ignore: avoid_unnecessary_return_variable - if (delay == Duration.zero) return languageToolService; + if (delay == Duration.zero) { + // false positive, the variable might be used after the if statement + // ignore: avoid_unnecessary_return_variable + return languageToolService; + } switch (delayType) { case DelayType.debouncing: diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 14a1f92..f16d6b6 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -90,6 +90,7 @@ class LanguageToolMistakePopup extends StatelessWidget { required this.mistake, required this.controller, required this.mistakePosition, + super.key, this.maxWidth = _defaultMaxWidth, this.maxHeight = double.infinity, this.horizontalMargin = _defaultHorizontalMargin, From ba39d5f97e42e877a25d1c3fa052f3a78f33acda Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 8 Jun 2026 22:51:45 +0300 Subject: [PATCH 5/5] fix: handle mistakes filtering when selection is invalid or unknown --- lib/src/core/controllers/language_tool_controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index f9131ea..82ad00e 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -321,6 +321,12 @@ class LanguageToolController extends TextEditingController { List mistakes, String newText, ) { + if (!selection.isValid) { + return mistakes + .where((mistake) => mistake.endOffset <= newText.length) + .toList(); + } + final isSelectionRangeEmpty = selection.end == selection.start; final lengthDiscrepancy = newText.length - text.length;