From 6f05c1547ad96eadace2fc372322eb12b21dd8eb Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:22:27 +0300 Subject: [PATCH 01/22] style: run dart formatter --- lib/src/utils/mistake_popup.dart | 1 + 1 file changed, 1 insertion(+) 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 d701a6abfe883491a60e6aeecdaea20d3b4f7268 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:49:54 +0300 Subject: [PATCH 02/22] feat: add actions builder to allow adding actions --- lib/src/utils/mistake_popup.dart | 66 +++++++++++++++++--------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index f16d6b6..9c1693e 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -84,18 +84,21 @@ class LanguageToolMistakePopup extends StatelessWidget { /// Mistake suggestion style. final ButtonStyle? mistakeStyle; - /// [LanguageToolMistakePopup] constructor + /// Optional builder that adds additional actions to the header. + final List Function(BuildContext context)? actionsBuilder; + + /// Creates a [LanguageToolMistakePopup]. const LanguageToolMistakePopup({ required this.popupRenderer, required this.mistake, required this.controller, required this.mistakePosition, - super.key, this.maxWidth = _defaultMaxWidth, this.maxHeight = double.infinity, this.horizontalMargin = _defaultHorizontalMargin, this.verticalMargin = _defaultVerticalMargin, this.mistakeStyle, + this.actionsBuilder, super.key, }); @@ -150,38 +153,39 @@ class LanguageToolMistakePopup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only(left: 4), - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5.0), - child: Image.asset( - LangToolImages.logo, - width: _iconSize, - height: _iconSize, - package: 'languagetool_textfield', + child: IconTheme( + data: const IconThemeData(size: 12), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 5.0), + child: Image.asset( + LangToolImages.logo, + width: _iconSize, + height: _iconSize, + package: 'languagetool_textfield', + ), ), - ), - const Text('Correct'), - ], + const Text('Correct'), + ], + ), ), - ), - IconButton( - icon: const Icon( - Icons.close, - size: 12, + ...?actionsBuilder?.call(context), + IconButton( + icon: const Icon(Icons.close), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + splashRadius: dismissSplashRadius, + onPressed: () { + _dismissDialog(); + controller.onClosePopup(); + }, ), - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - splashRadius: dismissSplashRadius, - onPressed: () { - _dismissDialog(); - controller.onClosePopup(); - }, - ), - ], + ], + ), ), ), Container( From bcb02b5521c09cdc1456beaab81be3baf66aa163 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 9 Sep 2025 15:59:47 +0300 Subject: [PATCH 03/22] refactor: avoid magic numbers style: move all constants to class static members --- lib/src/utils/mistake_popup.dart | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 9c1693e..22ae41f 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -52,7 +52,18 @@ class LanguageToolMistakePopup extends StatelessWidget { static const double _defaultVerticalMargin = 25.0; static const double _defaultHorizontalMargin = 10.0; static const double _defaultMaxWidth = 250.0; - static const _iconSize = 25.0; + static const double _logoSize = 25; + static const double _headerIconSize = 12; + + static const double _borderRadius = 10.0; + static const double _mistakeNameFontSize = 11.0; + static const double _mistakeMessageFontSize = 13.0; + static const double _replacementButtonsSpacing = 4.0; + static const double _replacementButtonsSpacingMobile = -6.0; + static const double _paddingBetweenTitle = 14.0; + static const double _titleLetterSpacing = 0.56; + static const double _dismissSplashRadius = 2.0; + static const double _padding = 10.0; /// Renderer used to display this window. final PopupOverlayRenderer popupRenderer; @@ -104,17 +115,6 @@ class LanguageToolMistakePopup extends StatelessWidget { @override Widget build(BuildContext context) { - const borderRadius = 10.0; - const mistakeNameFontSize = 11.0; - const mistakeMessageFontSize = 13.0; - const replacementButtonsSpacing = 4.0; - const replacementButtonsSpacingMobile = -6.0; - const paddingBetweenTitle = 14.0; - const titleLetterSpacing = 0.56; - const dismissSplashRadius = 2.0; - - const padding = 10.0; - final availableSpace = _calculateAvailableSpace(context); final colorScheme = Theme.of(context).colorScheme; @@ -132,7 +132,7 @@ class LanguageToolMistakePopup extends StatelessWidget { ), decoration: BoxDecoration( color: colorScheme.surface.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.circular(_borderRadius), boxShadow: [ BoxShadow( color: colorScheme.onSurface.withValues(alpha: 0.5), @@ -154,7 +154,7 @@ class LanguageToolMistakePopup extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 4), child: IconTheme( - data: const IconThemeData(size: 12), + data: const IconThemeData(size: _headerIconSize), child: Row( children: [ Expanded( @@ -164,8 +164,8 @@ class LanguageToolMistakePopup extends StatelessWidget { padding: const EdgeInsets.only(right: 5.0), child: Image.asset( LangToolImages.logo, - width: _iconSize, - height: _iconSize, + width: _logoSize, + height: _logoSize, package: 'languagetool_textfield', ), ), @@ -178,7 +178,7 @@ class LanguageToolMistakePopup extends StatelessWidget { icon: const Icon(Icons.close), constraints: const BoxConstraints(), padding: EdgeInsets.zero, - splashRadius: dismissSplashRadius, + splashRadius: _dismissSplashRadius, onPressed: () { _dismissDialog(); controller.onClosePopup(); @@ -190,10 +190,10 @@ class LanguageToolMistakePopup extends StatelessWidget { ), Container( margin: const EdgeInsets.only(top: 8), - padding: const EdgeInsets.all(padding), + padding: const EdgeInsets.all(_padding), decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.circular(_borderRadius), ), child: SingleChildScrollView( child: Column( @@ -201,33 +201,33 @@ class LanguageToolMistakePopup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only( - bottom: paddingBetweenTitle, + bottom: _paddingBetweenTitle, ), child: Text( mistake.type.name.capitalize(), style: TextStyle( color: colorScheme.onSurface.withValues(alpha: 0.7), - fontSize: mistakeNameFontSize, + fontSize: _mistakeNameFontSize, fontWeight: FontWeight.w600, - letterSpacing: titleLetterSpacing, + letterSpacing: _titleLetterSpacing, ), ), ), Padding( - padding: const EdgeInsets.only(bottom: padding), + padding: const EdgeInsets.only(bottom: _padding), child: Text( mistake.message, style: const TextStyle( - fontSize: mistakeMessageFontSize, + fontSize: _mistakeMessageFontSize, ), ), ), Wrap( - spacing: replacementButtonsSpacing, + spacing: _replacementButtonsSpacing, runSpacing: kIsWeb - ? replacementButtonsSpacing - : replacementButtonsSpacingMobile, + ? _replacementButtonsSpacing + : _replacementButtonsSpacingMobile, children: mistake.replacements .map( (replacement) => ElevatedButton( From 9de4a153a381959713ef971932afd8cef83a69cc Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 17:28:34 +0300 Subject: [PATCH 04/22] feat: add "add to dictionary" feature ui: use default icon button padding export result to allow custom language check service implementations --- example/lib/main.dart | 163 +++++++++++++++--- lib/languagetool_textfield.dart | 2 + .../controllers/language_tool_controller.dart | 12 +- lib/src/utils/mistake_popup.dart | 22 ++- ...ory_dictionary_language_check_service.dart | 51 ++++++ 5 files changed, 221 insertions(+), 29 deletions(-) create mode 100644 lib/src/wrappers/in_memory_dictionary_language_check_service.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e65dab..72050be 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -20,45 +20,116 @@ class App extends StatefulWidget { } class _AppState extends State { - /// Initialize LanguageToolController - final LanguageToolController _controller = LanguageToolController(); + Set _dictionary = {}; + final _addWordController = TextEditingController(); - static const List alignments = [ - MainAxisAlignment.center, - MainAxisAlignment.start, - MainAxisAlignment.end, - ]; - int currentAlignmentIndex = 0; + LanguageToolController? _spellCheckController; + + LanguageToolController _nonNullController() { + return _spellCheckController ??= LanguageToolController( + languageCheckService: InMemoryDictionaryLanguageCheckService( + getDictionary: () => _dictionary, + ), + ); + } @override Widget build(BuildContext context) { + final spellCheckController = _nonNullController(); + return Material( child: Scaffold( body: Column( - mainAxisAlignment: alignments[currentAlignmentIndex], + mainAxisAlignment: MainAxisAlignment.start, children: [ LanguageToolTextField( - controller: _controller, + controller: spellCheckController, language: 'en-US', + mistakePopup: MistakePopup( + popupRenderer: PopupOverlayRenderer(), + mistakeBuilder: _mistakeBuilder, + ), ), ValueListenableBuilder( - valueListenable: _controller, + valueListenable: spellCheckController, builder: (_, __, ___) => CheckboxListTile( title: const Text("Enable spell checking"), - value: _controller.isEnabled, - onChanged: (value) => _controller.isEnabled = value ?? false, + value: spellCheckController.isEnabled, + onChanged: (value) => + spellCheckController.isEnabled = value ?? false, ), ), - DropdownMenu( - hintText: "Select alignment...", - onSelected: (value) => setState(() { - currentAlignmentIndex = value ?? 0; - }), - dropdownMenuEntries: const [ - DropdownMenuEntry(value: 0, label: "Center alignment"), - DropdownMenuEntry(value: 1, label: "Top alignment"), - DropdownMenuEntry(value: 2, label: "Bottom alignment"), - ], + const SizedBox(height: 20), + Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Dictionary', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _addWordController, + decoration: const InputDecoration( + labelText: 'Add word to dictionary', + border: OutlineInputBorder(), + ), + onSubmitted: (_) => _addWord(), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addWord, + child: const Text('Add'), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Dictionary Words (${_dictionary.length})', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + if (_dictionary.isNotEmpty) + TextButton( + onPressed: _clearAllWords, + child: const Text('Clear All'), + ), + ], + ), + const SizedBox(height: 8), + if (_dictionary.isEmpty) + const Center( + child: Text( + 'No words in dictionary', + style: TextStyle(color: Colors.grey), + ), + ) + else + for (final word in _dictionary) + ListTile( + title: Text(word), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _removeWord(word), + ), + ), + ], + ), + ), ), ], ), @@ -66,9 +137,53 @@ class _AppState extends State { ); } + void _addWord() { + final word = _addWordController.text.trim(); + + if (word.isNotEmpty && !_dictionary.contains(word)) { + setState(() { + _dictionary = {..._dictionary, word}; + _addWordController.clear(); + _spellCheckController?.recheckText(); + }); + } + } + + void _removeWord(String word) { + setState(() { + _dictionary = _dictionary.difference({word}); + _spellCheckController?.recheckText(); + }); + } + + void _clearAllWords() { + setState(() { + _dictionary = {}; + _spellCheckController?.recheckText(); + }); + } + + Widget _mistakeBuilder({ + required LanguageToolController controller, + required Mistake mistake, + required Offset mistakePosition, + required PopupOverlayRenderer popupRenderer, + }) { + return LanguageToolMistakePopup( + popupRenderer: popupRenderer, + mistake: mistake, + mistakePosition: mistakePosition, + controller: controller, + addWordToDictionary: (word) async { + setState(() => _dictionary = {..._dictionary, word}); + }, + ); + } + @override void dispose() { - _controller.dispose(); + _spellCheckController?.dispose(); + _addWordController.dispose(); super.dispose(); } } diff --git a/lib/languagetool_textfield.dart b/lib/languagetool_textfield.dart index fa751af..b7e5828 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -16,5 +16,7 @@ export 'src/language_check_services/language_tool_service.dart'; export 'src/presentation/language_tool_text_field.dart'; export 'src/utils/mistake_popup.dart'; export 'src/utils/popup_overlay_renderer.dart'; +export 'src/utils/result.dart'; export 'src/wrappers/debounce_language_check_service.dart'; +export 'src/wrappers/in_memory_dictionary_language_check_service.dart'; export 'src/wrappers/throttling_language_check_service.dart'; diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index 0e7e999..52496b5 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -64,7 +64,7 @@ class LanguageToolController extends TextEditingController { _isEnabled = value; if (_isEnabled) { - _handleTextChange(text, spellCheckSameText: true); + recheckText(); } else { _mistakes = []; for (final recognizer in _recognizers) { @@ -189,6 +189,16 @@ class LanguageToolController extends TextEditingController { }); } + /// Rechecks the current text for spelling and grammar errors. + /// + /// This method forces a recheck of the existing text + /// This is useful when you want to re-evaluate the text without any actual + /// text changes, such as after changing language settings or updating + /// spell check configurations. + void recheckText() { + _handleTextChange(text, spellCheckSameText: true); + } + /// Clear mistakes list when text mas modified and get a new list of mistakes /// via API Future _handleTextChange( diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 22ae41f..a4595d3 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -96,7 +96,7 @@ class LanguageToolMistakePopup extends StatelessWidget { final ButtonStyle? mistakeStyle; /// Optional builder that adds additional actions to the header. - final List Function(BuildContext context)? actionsBuilder; + final Future Function(String)? addWordToDictionary; /// Creates a [LanguageToolMistakePopup]. const LanguageToolMistakePopup({ @@ -109,7 +109,7 @@ class LanguageToolMistakePopup extends StatelessWidget { this.horizontalMargin = _defaultHorizontalMargin, this.verticalMargin = _defaultVerticalMargin, this.mistakeStyle, - this.actionsBuilder, + this.addWordToDictionary, super.key, }); @@ -173,11 +173,25 @@ class LanguageToolMistakePopup extends StatelessWidget { ], ), ), - ...?actionsBuilder?.call(context), + if (addWordToDictionary case final addWordToDictionary?) + IconButton( + icon: const Icon(Icons.menu_book), + constraints: const BoxConstraints(), + splashRadius: _dismissSplashRadius, + onPressed: () async { + final word = controller.text.substring( + mistake.offset, + mistake.endOffset, + ); + + await addWordToDictionary(word); + + _fixTheMistake(word); + }, + ), IconButton( icon: const Icon(Icons.close), constraints: const BoxConstraints(), - padding: EdgeInsets.zero, splashRadius: _dismissSplashRadius, onPressed: () { _dismissDialog(); diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart new file mode 100644 index 0000000..d56b534 --- /dev/null +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -0,0 +1,51 @@ +import 'package:languagetool_textfield/languagetool_textfield.dart'; + +/// A language-check service that filters LanguageTool suggestions using an in-memory dictionary. +/// +/// This class wraps a LanguageToolService and extends ThrottlingLanguageCheckService to +/// limit the frequency of requests. After performing a check with the underlying service, +/// it removes any reported mistakes whose corresponding word is present in the provided +/// in-memory dictionary (so user-defined or domain-specific words can be treated as correct). +/// +/// The filtering is performed by extracting the substring of the input text using each +/// mistake's offset and endOffset and checking membership against the dictionary returned +/// by [getDictionary]. +/// +/// Note: the underlying service is throttled to avoid excessive requests; the throttling +/// behavior is provided by the superclass. +class InMemoryDictionaryLanguageCheckService + extends ThrottlingLanguageCheckService { + /// Callback that supplies the current set of words to be treated as correct. + /// + /// This function is invoked for each text check so the dictionary can be dynamic + /// (for example, reflecting user edits or settings). It must return a Set + /// containing the words that should be ignored by the language checker. + final Set Function() getDictionary; + + /// Creates an InMemoryDictionaryLanguageCheckService that uses [getDictionary] to filter mistakes. + /// + /// The [getDictionary] callback is required and will be called for every check operation. + /// The service delegates checking to an internal LanguageToolService and then filters + /// the results based on the returned dictionary. + InMemoryDictionaryLanguageCheckService({required this.getDictionary}) + : super( + LanguageToolService(LanguageToolClient()), + const Duration(milliseconds: 250), + ); + + @override + Future>?> findMistakes(String text) async { + final result = await super.findMistakes(text); + final dictionary = getDictionary(); + + return result?.map( + (mistakes) => mistakes.where( + (mistake) { + final word = text.substring(mistake.offset, mistake.endOffset); + + return !dictionary.contains(word); + }, + ).toList(), + ); + } +} From 4280367aafce016db1b447336dfafe88270839dd Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 17:32:36 +0300 Subject: [PATCH 05/22] doc: update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84cc605..6178db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ - potentially BREAKING: hide baseService and debouncing/throttling from Debouncing and Throttling LanguageService wrappers - potentially BREAKING: rename `DebounceLanguageToolService` to `DebounceLanguageCheckService` - potentially BREAKING: rename `ThrottleLanguageToolService` to `ThrottleLanguageCheckService` +- Support adding words to dictionary through `addWordToDictionary` callback in `LanguageToolMistakePopup` - Allow overriding `languageCheckService` - Add `isEnabled` to toggle spell check +- Use default IconButton padding for mistake popup - Add missing properties from flutter's `TextField` - autofillHints - autofocus From 5e53b8beb78cc7c889a05b4c68454de16f97b76a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:09:56 +0300 Subject: [PATCH 06/22] fix: add scroll view to scroll the ui --- example/lib/main.dart | 169 +++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 72050be..3b9851d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -39,99 +39,100 @@ class _AppState extends State { return Material( child: Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - LanguageToolTextField( - controller: spellCheckController, - language: 'en-US', - mistakePopup: MistakePopup( - popupRenderer: PopupOverlayRenderer(), - mistakeBuilder: _mistakeBuilder, + body: SingleChildScrollView( + child: Column( + children: [ + LanguageToolTextField( + controller: spellCheckController, + language: 'en-US', + mistakePopup: MistakePopup( + popupRenderer: PopupOverlayRenderer(), + mistakeBuilder: _mistakeBuilder, + ), ), - ), - ValueListenableBuilder( - valueListenable: spellCheckController, - builder: (_, __, ___) => CheckboxListTile( - title: const Text("Enable spell checking"), - value: spellCheckController.isEnabled, - onChanged: (value) => - spellCheckController.isEnabled = value ?? false, + ValueListenableBuilder( + valueListenable: spellCheckController, + builder: (_, __, ___) => CheckboxListTile( + title: const Text("Enable spell checking"), + value: spellCheckController.isEnabled, + onChanged: (value) => + spellCheckController.isEnabled = value ?? false, + ), ), - ), - const SizedBox(height: 20), - Card( - margin: const EdgeInsets.all(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Dictionary', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + const SizedBox(height: 20), + Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Dictionary', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: TextField( - controller: _addWordController, - decoration: const InputDecoration( - labelText: 'Add word to dictionary', - border: OutlineInputBorder(), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _addWordController, + decoration: const InputDecoration( + labelText: 'Add word to dictionary', + border: OutlineInputBorder(), + ), + onSubmitted: (_) => _addWord(), ), - onSubmitted: (_) => _addWord(), ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _addWord, - child: const Text('Add'), - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Dictionary Words (${_dictionary.length})', - style: const TextStyle(fontWeight: FontWeight.w500), - ), - if (_dictionary.isNotEmpty) - TextButton( - onPressed: _clearAllWords, - child: const Text('Clear All'), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addWord, + child: const Text('Add'), ), - ], - ), - const SizedBox(height: 8), - if (_dictionary.isEmpty) - const Center( - child: Text( - 'No words in dictionary', - style: TextStyle(color: Colors.grey), - ), - ) - else - for (final word in _dictionary) - ListTile( - title: Text(word), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _removeWord(word), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Dictionary Words (${_dictionary.length})', + style: const TextStyle(fontWeight: FontWeight.w500), ), - ), - ], + if (_dictionary.isNotEmpty) + TextButton( + onPressed: _clearAllWords, + child: const Text('Clear All'), + ), + ], + ), + const SizedBox(height: 8), + if (_dictionary.isEmpty) + const Center( + child: Text( + 'No words in dictionary', + style: TextStyle(color: Colors.grey), + ), + ) + else + for (final word in _dictionary) + ListTile( + title: Text(word), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _removeWord(word), + ), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ); From 7075f62b646b8b371defa3834216bcd7230627a6 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:10:20 +0300 Subject: [PATCH 07/22] reactor: allow changing language tool service and throttling duration --- .../in_memory_dictionary_language_check_service.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart index d56b534..871bfa2 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -27,10 +27,13 @@ class InMemoryDictionaryLanguageCheckService /// The [getDictionary] callback is required and will be called for every check operation. /// The service delegates checking to an internal LanguageToolService and then filters /// the results based on the returned dictionary. - InMemoryDictionaryLanguageCheckService({required this.getDictionary}) - : super( - LanguageToolService(LanguageToolClient()), - const Duration(milliseconds: 250), + InMemoryDictionaryLanguageCheckService({ + required this.getDictionary, + LanguageCheckService? languageToolService, + Duration? throttlingDuration, + }) : super( + languageToolService ?? LanguageToolService(LanguageToolClient()), + throttlingDuration ?? const Duration(milliseconds: 250), ); @override From 68d4d695be2bebe7c5f9faeb6b0cd73eaa77480f Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Sep 2025 18:38:49 +0300 Subject: [PATCH 08/22] refactor: avoid widget returning methods --- example/lib/main.dart | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 3b9851d..2feed06 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -47,7 +47,22 @@ class _AppState extends State { language: 'en-US', mistakePopup: MistakePopup( popupRenderer: PopupOverlayRenderer(), - mistakeBuilder: _mistakeBuilder, + mistakeBuilder: ({ + required LanguageToolController controller, + required Mistake mistake, + required Offset mistakePosition, + required PopupOverlayRenderer popupRenderer, + }) { + return LanguageToolMistakePopup( + popupRenderer: popupRenderer, + mistake: mistake, + mistakePosition: mistakePosition, + controller: controller, + addWordToDictionary: (word) async { + setState(() => _dictionary = {..._dictionary, word}); + }, + ); + }, ), ), ValueListenableBuilder( @@ -164,23 +179,6 @@ class _AppState extends State { }); } - Widget _mistakeBuilder({ - required LanguageToolController controller, - required Mistake mistake, - required Offset mistakePosition, - required PopupOverlayRenderer popupRenderer, - }) { - return LanguageToolMistakePopup( - popupRenderer: popupRenderer, - mistake: mistake, - mistakePosition: mistakePosition, - controller: controller, - addWordToDictionary: (word) async { - setState(() => _dictionary = {..._dictionary, word}); - }, - ); - } - @override void dispose() { _spellCheckController?.dispose(); From 216d4fabd36792c51f82c5a756adf8d4b57d336b Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 2 Jun 2026 22:28:40 +0300 Subject: [PATCH 09/22] fix: handle invalid mistake offsets refactor: move getDictionary call to be inside success result --- ...ory_dictionary_language_check_service.dart | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart index 871bfa2..d8bca76 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -39,16 +39,26 @@ class InMemoryDictionaryLanguageCheckService @override Future>?> findMistakes(String text) async { final result = await super.findMistakes(text); - final dictionary = getDictionary(); return result?.map( - (mistakes) => mistakes.where( - (mistake) { - final word = text.substring(mistake.offset, mistake.endOffset); + (mistakes) { + final dictionary = getDictionary(); - return !dictionary.contains(word); - }, - ).toList(), + return mistakes.where( + (mistake) { + final mistakeHasInvalidOffset = mistake.offset < 0 || + mistake.offset >= text.length || + mistake.endOffset < 0 || + mistake.endOffset > text.length; + + if (mistakeHasInvalidOffset) return false; + + final word = text.substring(mistake.offset, mistake.endOffset); + + return !dictionary.contains(word); + }, + ).toList(); + }, ); } } From 4b9ca366bd76ed0175c22a901339319e2a8ffa0c Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 2 Jun 2026 22:24:42 +0300 Subject: [PATCH 10/22] refactor: extract method --- lib/src/utils/mistake_popup.dart | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index a4595d3..abb2ce8 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -173,21 +173,13 @@ class LanguageToolMistakePopup extends StatelessWidget { ], ), ), - if (addWordToDictionary case final addWordToDictionary?) + if (addWordToDictionary != null) IconButton( icon: const Icon(Icons.menu_book), constraints: const BoxConstraints(), splashRadius: _dismissSplashRadius, - onPressed: () async { - final word = controller.text.substring( - mistake.offset, - mistake.endOffset, - ); - - await addWordToDictionary(word); - - _fixTheMistake(word); - }, + onPressed: () => + _addWordToDictionaryAndFix(mistake), ), IconButton( icon: const Icon(Icons.close), @@ -280,6 +272,17 @@ class LanguageToolMistakePopup extends StatelessWidget { return min(max(availableSpaceBottom, availableSpaceTop), maxHeight); } + Future _addWordToDictionaryAndFix(Mistake mistake) async { + final word = controller.text.substring( + mistake.offset, + mistake.endOffset, + ); + + await addWordToDictionary?.call(word); + + _fixTheMistake(word); + } + void _dismissDialog() { popupRenderer.dismiss(); } From 869eb322ae4a2d4d0105263daaf1a328f0583d3c Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 1 Jun 2026 21:59:19 +0300 Subject: [PATCH 11/22] style: fix lints --- .../controllers/language_tool_controller.dart | 8 ++-- lib/src/utils/mistake_popup.dart | 47 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/src/core/controllers/language_tool_controller.dart b/lib/src/core/controllers/language_tool_controller.dart index 52496b5..20428cf 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -126,9 +126,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 abb2ce8..75fdefc 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -55,16 +55,6 @@ class LanguageToolMistakePopup extends StatelessWidget { static const double _logoSize = 25; static const double _headerIconSize = 12; - static const double _borderRadius = 10.0; - static const double _mistakeNameFontSize = 11.0; - static const double _mistakeMessageFontSize = 13.0; - static const double _replacementButtonsSpacing = 4.0; - static const double _replacementButtonsSpacingMobile = -6.0; - static const double _paddingBetweenTitle = 14.0; - static const double _titleLetterSpacing = 0.56; - static const double _dismissSplashRadius = 2.0; - static const double _padding = 10.0; - /// Renderer used to display this window. final PopupOverlayRenderer popupRenderer; @@ -115,6 +105,17 @@ class LanguageToolMistakePopup extends StatelessWidget { @override Widget build(BuildContext context) { + const borderRadius = 10.0; + const mistakeNameFontSize = 11.0; + const mistakeMessageFontSize = 13.0; + const replacementButtonsSpacing = 4.0; + const replacementButtonsSpacingMobile = -6.0; + const paddingBetweenTitle = 14.0; + const titleLetterSpacing = 0.56; + const dismissSplashRadius = 2.0; + + const padding = 10.0; + final availableSpace = _calculateAvailableSpace(context); final colorScheme = Theme.of(context).colorScheme; @@ -132,7 +133,7 @@ class LanguageToolMistakePopup extends StatelessWidget { ), decoration: BoxDecoration( color: colorScheme.surface.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(_borderRadius), + borderRadius: BorderRadius.circular(borderRadius), boxShadow: [ BoxShadow( color: colorScheme.onSurface.withValues(alpha: 0.5), @@ -177,14 +178,14 @@ class LanguageToolMistakePopup extends StatelessWidget { IconButton( icon: const Icon(Icons.menu_book), constraints: const BoxConstraints(), - splashRadius: _dismissSplashRadius, + splashRadius: dismissSplashRadius, onPressed: () => _addWordToDictionaryAndFix(mistake), ), IconButton( icon: const Icon(Icons.close), constraints: const BoxConstraints(), - splashRadius: _dismissSplashRadius, + splashRadius: dismissSplashRadius, onPressed: () { _dismissDialog(); controller.onClosePopup(); @@ -196,10 +197,10 @@ class LanguageToolMistakePopup extends StatelessWidget { ), Container( margin: const EdgeInsets.only(top: 8), - padding: const EdgeInsets.all(_padding), + padding: const EdgeInsets.all(padding), decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: BorderRadius.circular(_borderRadius), + borderRadius: BorderRadius.circular(borderRadius), ), child: SingleChildScrollView( child: Column( @@ -207,33 +208,33 @@ class LanguageToolMistakePopup extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only( - bottom: _paddingBetweenTitle, + bottom: paddingBetweenTitle, ), child: Text( mistake.type.name.capitalize(), style: TextStyle( color: colorScheme.onSurface.withValues(alpha: 0.7), - fontSize: _mistakeNameFontSize, + fontSize: mistakeNameFontSize, fontWeight: FontWeight.w600, - letterSpacing: _titleLetterSpacing, + letterSpacing: titleLetterSpacing, ), ), ), Padding( - padding: const EdgeInsets.only(bottom: _padding), + padding: const EdgeInsets.only(bottom: padding), child: Text( mistake.message, style: const TextStyle( - fontSize: _mistakeMessageFontSize, + fontSize: mistakeMessageFontSize, ), ), ), Wrap( - spacing: _replacementButtonsSpacing, + spacing: replacementButtonsSpacing, runSpacing: kIsWeb - ? _replacementButtonsSpacing - : _replacementButtonsSpacingMobile, + ? replacementButtonsSpacing + : replacementButtonsSpacingMobile, children: mistake.replacements .map( (replacement) => ElevatedButton( From 587d5dcd2dbc94ab7cdac5e014f085d1db8cb53c Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 8 Jun 2026 23:30:20 +0300 Subject: [PATCH 12/22] refactor: use recheckText --- 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 20428cf..1e1863b 100644 --- a/lib/src/core/controllers/language_tool_controller.dart +++ b/lib/src/core/controllers/language_tool_controller.dart @@ -52,7 +52,7 @@ class LanguageToolController extends TextEditingController { if (!_isEnabled) return; - _handleTextChange(text, spellCheckSameText: true); + recheckText(); } /// Indicates whether spell checking is enabled From 8a957fb27b9fa8ab7ac4a6767f1076946a680b9b Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 16 Jun 2026 16:24:17 +0300 Subject: [PATCH 13/22] refactor: allow chaining of LanguageCheckServices refactor: decouple in memory dictionary from throttling services refactor: move invalid mistakes offset check to ThrottlingLanguageCheckService --- example/lib/main.dart | 10 +- ...ory_dictionary_language_check_service.dart | 91 +++++++++---------- .../throttling_language_check_service.dart | 15 ++- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2feed06..73c2523 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,7 +28,15 @@ class _AppState extends State { LanguageToolController _nonNullController() { return _spellCheckController ??= LanguageToolController( languageCheckService: InMemoryDictionaryLanguageCheckService( - getDictionary: () => _dictionary, + languageCheckService: ThrottlingLanguageCheckService( + LanguageToolService(LanguageToolClient()), + const Duration(milliseconds: 250), + ), + shouldIgnoreMistake: (mistake, text) { + final word = text.substring(mistake.offset, mistake.endOffset); + + return _dictionary.contains(word); + }, ), ); } diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart index d8bca76..95dd55b 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -1,64 +1,59 @@ import 'package:languagetool_textfield/languagetool_textfield.dart'; -/// A language-check service that filters LanguageTool suggestions using an in-memory dictionary. +/// A [LanguageToolService] that post-filters mistakes reported by LanguageTool. /// -/// This class wraps a LanguageToolService and extends ThrottlingLanguageCheckService to -/// limit the frequency of requests. After performing a check with the underlying service, -/// it removes any reported mistakes whose corresponding word is present in the provided -/// in-memory dictionary (so user-defined or domain-specific words can be treated as correct). +/// After delegating to [LanguageToolService.findMistakes], this service keeps +/// only mistakes that pass [_shouldIgnoreMistake]. A common use case is hiding +/// domain-specific vocabulary the user has marked as correct. /// -/// The filtering is performed by extracting the substring of the input text using each -/// mistake's offset and endOffset and checking membership against the dictionary returned -/// by [getDictionary]. -/// -/// Note: the underlying service is throttled to avoid excessive requests; the throttling -/// behavior is provided by the superclass. -class InMemoryDictionaryLanguageCheckService - extends ThrottlingLanguageCheckService { - /// Callback that supplies the current set of words to be treated as correct. +/// This class does not throttle or debounce requests. Wrap it in +/// [ThrottlingLanguageCheckService] or [DebounceLanguageCheckService] when +/// needed. +class InMemoryDictionaryLanguageCheckService extends LanguageCheckService { + /// Predicate applied to each mistake returned by LanguageTool. /// - /// This function is invoked for each text check so the dictionary can be dynamic - /// (for example, reflecting user edits or settings). It must return a Set - /// containing the words that should be ignored by the language checker. - final Set Function() getDictionary; + /// Invoked once per [Mistake]. Only mistakes for which this returns `true` + /// are included in the final result. + final bool Function(Mistake, String) _shouldIgnoreMistake; + + /// The underlying language check service that is used to find mistakes. + final LanguageCheckService _languageCheckService; + + @override + String get language => _languageCheckService.language; + + @override + set language(String language) { + _languageCheckService.language = language; + } - /// Creates an InMemoryDictionaryLanguageCheckService that uses [getDictionary] to filter mistakes. + /// Creates an [InMemoryDictionaryLanguageCheckService]. /// - /// The [getDictionary] callback is required and will be called for every check operation. - /// The service delegates checking to an internal LanguageToolService and then filters - /// the results based on the returned dictionary. + /// [shouldIgnoreMistake] is required and evaluated for every mistake returned + /// by LanguageTool. + /// An optional [languageCheckService] overrides the default service. InMemoryDictionaryLanguageCheckService({ - required this.getDictionary, - LanguageCheckService? languageToolService, - Duration? throttlingDuration, - }) : super( - languageToolService ?? LanguageToolService(LanguageToolClient()), - throttlingDuration ?? const Duration(milliseconds: 250), - ); + required bool Function(Mistake, String) shouldIgnoreMistake, + LanguageCheckService? languageCheckService, + }) : _shouldIgnoreMistake = shouldIgnoreMistake, + _languageCheckService = + languageCheckService ?? LanguageToolService(LanguageToolClient()); @override Future>?> findMistakes(String text) async { - final result = await super.findMistakes(text); + final result = await _languageCheckService.findMistakes(text); return result?.map( - (mistakes) { - final dictionary = getDictionary(); - - return mistakes.where( - (mistake) { - final mistakeHasInvalidOffset = mistake.offset < 0 || - mistake.offset >= text.length || - mistake.endOffset < 0 || - mistake.endOffset > text.length; - - if (mistakeHasInvalidOffset) return false; - - final word = text.substring(mistake.offset, mistake.endOffset); - - return !dictionary.contains(word); - }, - ).toList(); - }, + (mistakes) => mistakes + .where((mistake) => !_shouldIgnoreMistake(mistake, text)) + .toList(growable: false), ); } + + // ignore: proper_super_calls + @override + Future dispose() async { + await _languageCheckService.dispose(); + await super.dispose(); + } } diff --git a/lib/src/wrappers/throttling_language_check_service.dart b/lib/src/wrappers/throttling_language_check_service.dart index 2d637db..c84f138 100644 --- a/lib/src/wrappers/throttling_language_check_service.dart +++ b/lib/src/wrappers/throttling_language_check_service.dart @@ -28,8 +28,21 @@ class ThrottlingLanguageCheckService extends LanguageCheckService { @override Future>?> findMistakes(String text) async { - return await _throttling + final result = await _throttling .throttle(() => _languageCheckService.findMistakes(text)); + + return result?.map( + (mistakes) => mistakes.where( + (mistake) { + final mistakeHasInvalidOffset = mistake.offset < 0 || + mistake.offset >= text.length || + mistake.endOffset < 0 || + mistake.endOffset > text.length; + + return !mistakeHasInvalidOffset; + }, + ).toList(growable: false), + ); } // ignore: proper_super_calls From c56ac644e95f2a8939e2cacaab782591f86ba6fd Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 16 Jun 2026 16:42:28 +0300 Subject: [PATCH 14/22] docs: fix code comment documentation --- .../wrappers/in_memory_dictionary_language_check_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart index 95dd55b..5104279 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/in_memory_dictionary_language_check_service.dart @@ -12,8 +12,8 @@ import 'package:languagetool_textfield/languagetool_textfield.dart'; class InMemoryDictionaryLanguageCheckService extends LanguageCheckService { /// Predicate applied to each mistake returned by LanguageTool. /// - /// Invoked once per [Mistake]. Only mistakes for which this returns `true` - /// are included in the final result. + /// Invoked once per [Mistake]. Mistakes for which this returns `true` + /// are ignored and excluded from the final result. final bool Function(Mistake, String) _shouldIgnoreMistake; /// The underlying language check service that is used to find mistakes. From 529a632ae2e7104a240aadec5f1f451bfbf4ffaa Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 16 Jun 2026 16:43:23 +0300 Subject: [PATCH 15/22] fix: recheck text after adding a word to dictionary --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 73c2523..b70a922 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -68,6 +68,7 @@ class _AppState extends State { controller: controller, addWordToDictionary: (word) async { setState(() => _dictionary = {..._dictionary, word}); + spellCheckController.recheckText(); }, ); }, From 9e4efd49bb8498b80bb572d3e733edaeb96f4f26 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 16 Jun 2026 16:47:57 +0300 Subject: [PATCH 16/22] fix: recheck that the mistake still exists before fixing --- lib/src/utils/mistake_popup.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index 75fdefc..ff856f1 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -238,7 +238,8 @@ class LanguageToolMistakePopup extends StatelessWidget { children: mistake.replacements .map( (replacement) => ElevatedButton( - onPressed: () => _fixTheMistake(replacement), + onPressed: () => + _fixTheMistake(mistake, replacement), style: mistakeStyle ?? ElevatedButton.styleFrom( elevation: 0, @@ -281,14 +282,20 @@ class LanguageToolMistakePopup extends StatelessWidget { await addWordToDictionary?.call(word); - _fixTheMistake(word); + final mistakeStillExists = + controller.text.substring(mistake.offset, mistake.endOffset) == word; + if (mistakeStillExists) { + _fixTheMistake(mistake, word); + } else { + _dismissDialog(); + } } void _dismissDialog() { popupRenderer.dismiss(); } - void _fixTheMistake(String replacement) { + void _fixTheMistake(Mistake mistake, String replacement) { controller.replaceMistake(mistake, replacement); _dismissDialog(); } From 49c39cf7d30c4a4ab687719c3a474eec8fdae6f4 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 13:27:08 +0300 Subject: [PATCH 17/22] add clarifying comment --- lib/src/utils/mistake_popup.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/utils/mistake_popup.dart b/lib/src/utils/mistake_popup.dart index ff856f1..f58adb8 100644 --- a/lib/src/utils/mistake_popup.dart +++ b/lib/src/utils/mistake_popup.dart @@ -285,6 +285,7 @@ class LanguageToolMistakePopup extends StatelessWidget { final mistakeStillExists = controller.text.substring(mistake.offset, mistake.endOffset) == word; if (mistakeStillExists) { + // Clear the mistake without actually changing the text _fixTheMistake(mistake, word); } else { _dismissDialog(); From 4e76986eb9605a54baa9a72dbac01301fa522dcb Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 13:30:41 +0300 Subject: [PATCH 18/22] refactor: rename to FilteredLanguageCheckService --- example/lib/main.dart | 2 +- ...ck_service.dart => filtered_language_check_service.dart} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/src/wrappers/{in_memory_dictionary_language_check_service.dart => filtered_language_check_service.dart} (91%) diff --git a/example/lib/main.dart b/example/lib/main.dart index b70a922..032757d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,7 +27,7 @@ class _AppState extends State { LanguageToolController _nonNullController() { return _spellCheckController ??= LanguageToolController( - languageCheckService: InMemoryDictionaryLanguageCheckService( + languageCheckService: FilteredLanguageCheckService( languageCheckService: ThrottlingLanguageCheckService( LanguageToolService(LanguageToolClient()), const Duration(milliseconds: 250), diff --git a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart b/lib/src/wrappers/filtered_language_check_service.dart similarity index 91% rename from lib/src/wrappers/in_memory_dictionary_language_check_service.dart rename to lib/src/wrappers/filtered_language_check_service.dart index 5104279..f4fa1a5 100644 --- a/lib/src/wrappers/in_memory_dictionary_language_check_service.dart +++ b/lib/src/wrappers/filtered_language_check_service.dart @@ -9,7 +9,7 @@ import 'package:languagetool_textfield/languagetool_textfield.dart'; /// This class does not throttle or debounce requests. Wrap it in /// [ThrottlingLanguageCheckService] or [DebounceLanguageCheckService] when /// needed. -class InMemoryDictionaryLanguageCheckService extends LanguageCheckService { +class FilteredLanguageCheckService extends LanguageCheckService { /// Predicate applied to each mistake returned by LanguageTool. /// /// Invoked once per [Mistake]. Mistakes for which this returns `true` @@ -27,12 +27,12 @@ class InMemoryDictionaryLanguageCheckService extends LanguageCheckService { _languageCheckService.language = language; } - /// Creates an [InMemoryDictionaryLanguageCheckService]. + /// Creates an [FilteredLanguageCheckService]. /// /// [shouldIgnoreMistake] is required and evaluated for every mistake returned /// by LanguageTool. /// An optional [languageCheckService] overrides the default service. - InMemoryDictionaryLanguageCheckService({ + FilteredLanguageCheckService({ required bool Function(Mistake, String) shouldIgnoreMistake, LanguageCheckService? languageCheckService, }) : _shouldIgnoreMistake = shouldIgnoreMistake, From cd6a84e5efdafbfd3a247a23d30bac6c91450474 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 13:36:47 +0300 Subject: [PATCH 19/22] fix: import path --- lib/languagetool_textfield.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/languagetool_textfield.dart b/lib/languagetool_textfield.dart index b7e5828..69161a7 100644 --- a/lib/languagetool_textfield.dart +++ b/lib/languagetool_textfield.dart @@ -18,5 +18,5 @@ export 'src/utils/mistake_popup.dart'; export 'src/utils/popup_overlay_renderer.dart'; export 'src/utils/result.dart'; export 'src/wrappers/debounce_language_check_service.dart'; -export 'src/wrappers/in_memory_dictionary_language_check_service.dart'; +export 'src/wrappers/filtered_language_check_service.dart'; export 'src/wrappers/throttling_language_check_service.dart'; From aa676f5ba89818fcfb55fab8c1d469fd7c39a80b Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 16:06:28 +0300 Subject: [PATCH 20/22] refactor: initialize _spellCheckController lazily --- example/lib/main.dart | 50 ++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 032757d..6852193 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,35 +23,31 @@ class _AppState extends State { Set _dictionary = {}; final _addWordController = TextEditingController(); - LanguageToolController? _spellCheckController; - - LanguageToolController _nonNullController() { - return _spellCheckController ??= LanguageToolController( - languageCheckService: FilteredLanguageCheckService( - languageCheckService: ThrottlingLanguageCheckService( - LanguageToolService(LanguageToolClient()), - const Duration(milliseconds: 250), - ), - shouldIgnoreMistake: (mistake, text) { - final word = text.substring(mistake.offset, mistake.endOffset); - - return _dictionary.contains(word); - }, + // ignore: avoid-late-keyword + late final LanguageToolController _spellCheckController = + LanguageToolController( + languageCheckService: FilteredLanguageCheckService( + languageCheckService: ThrottlingLanguageCheckService( + LanguageToolService(LanguageToolClient()), + const Duration(milliseconds: 250), ), - ); - } + shouldIgnoreMistake: (mistake, text) { + final word = text.substring(mistake.offset, mistake.endOffset); + + return _dictionary.contains(word); + }, + ), + ); @override Widget build(BuildContext context) { - final spellCheckController = _nonNullController(); - return Material( child: Scaffold( body: SingleChildScrollView( child: Column( children: [ LanguageToolTextField( - controller: spellCheckController, + controller: _spellCheckController, language: 'en-US', mistakePopup: MistakePopup( popupRenderer: PopupOverlayRenderer(), @@ -68,19 +64,19 @@ class _AppState extends State { controller: controller, addWordToDictionary: (word) async { setState(() => _dictionary = {..._dictionary, word}); - spellCheckController.recheckText(); + _spellCheckController.recheckText(); }, ); }, ), ), ValueListenableBuilder( - valueListenable: spellCheckController, + valueListenable: _spellCheckController, builder: (_, __, ___) => CheckboxListTile( title: const Text("Enable spell checking"), - value: spellCheckController.isEnabled, + value: _spellCheckController.isEnabled, onChanged: (value) => - spellCheckController.isEnabled = value ?? false, + _spellCheckController.isEnabled = value ?? false, ), ), const SizedBox(height: 20), @@ -169,7 +165,7 @@ class _AppState extends State { setState(() { _dictionary = {..._dictionary, word}; _addWordController.clear(); - _spellCheckController?.recheckText(); + _spellCheckController.recheckText(); }); } } @@ -177,20 +173,20 @@ class _AppState extends State { void _removeWord(String word) { setState(() { _dictionary = _dictionary.difference({word}); - _spellCheckController?.recheckText(); + _spellCheckController.recheckText(); }); } void _clearAllWords() { setState(() { _dictionary = {}; - _spellCheckController?.recheckText(); + _spellCheckController.recheckText(); }); } @override void dispose() { - _spellCheckController?.dispose(); + _spellCheckController.dispose(); _addWordController.dispose(); super.dispose(); } From 6938fd5b47d885347faa1bae552ac864d507d0d5 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 20:31:05 +0300 Subject: [PATCH 21/22] style: allow initialized late vars chore: upgrade solid lints in example --- example/analysis_options.yaml | 5 +++++ example/lib/main.dart | 1 - example/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 21bc10a..120f856 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,6 @@ include: package:solid_lints/analysis_options.yaml + +custom_lint: + rules: + - avoid_late_keyword: + allow_initialized: true diff --git a/example/lib/main.dart b/example/lib/main.dart index 6852193..9828396 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -23,7 +23,6 @@ class _AppState extends State { Set _dictionary = {}; final _addWordController = TextEditingController(); - // ignore: avoid-late-keyword late final LanguageToolController _spellCheckController = LanguageToolController( languageCheckService: FilteredLanguageCheckService( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6762603..a1541dd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - solid_lints: ^0.0.14 + solid_lints: ^0.3.1 flutter: uses-material-design: true From e0de60aad7dfff3c30527479c2eb0e57020aa7b0 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 23 Jun 2026 15:43:06 +0300 Subject: [PATCH 22/22] style: inline var --- .../throttling_language_check_service.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/src/wrappers/throttling_language_check_service.dart b/lib/src/wrappers/throttling_language_check_service.dart index c84f138..9173250 100644 --- a/lib/src/wrappers/throttling_language_check_service.dart +++ b/lib/src/wrappers/throttling_language_check_service.dart @@ -32,16 +32,15 @@ class ThrottlingLanguageCheckService extends LanguageCheckService { .throttle(() => _languageCheckService.findMistakes(text)); return result?.map( - (mistakes) => mistakes.where( - (mistake) { - final mistakeHasInvalidOffset = mistake.offset < 0 || - mistake.offset >= text.length || - mistake.endOffset < 0 || - mistake.endOffset > text.length; - - return !mistakeHasInvalidOffset; - }, - ).toList(growable: false), + (mistakes) => mistakes + .where( + (mistake) => + mistake.offset >= 0 && + mistake.offset < text.length && + mistake.endOffset >= 0 && + mistake.endOffset <= text.length, + ) + .toList(growable: false), ); }