From de53249339420a0e94ca24675586b8d8dde3526c Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 12:17:31 +0300 Subject: [PATCH 1/2] fix(editor): prevent length mismatch assertion error in QueryaHighlightController for scripts > 8KB (Closes #909) --- .../editor/querya_highlight_controller.dart | 2 +- .../querya_highlight_controller_test.dart | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 test/core/editor/querya_highlight_controller_test.dart diff --git a/lib/core/editor/querya_highlight_controller.dart b/lib/core/editor/querya_highlight_controller.dart index 5b463a50..793d78a5 100644 --- a/lib/core/editor/querya_highlight_controller.dart +++ b/lib/core/editor/querya_highlight_controller.dart @@ -74,7 +74,7 @@ class QueryaHighlightController extends TextEditingController { style: style, ); - if (_cachedSpan != null) { + if (_cachedSpan != null && _cachedText == text) { return TextSpan(style: style, children: [_cachedSpan!]); } diff --git a/test/core/editor/querya_highlight_controller_test.dart b/test/core/editor/querya_highlight_controller_test.dart new file mode 100644 index 00000000..9c941a50 --- /dev/null +++ b/test/core/editor/querya_highlight_controller_test.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/editor/querya_code_language.dart'; +import 'package:querya_desktop/core/editor/querya_highlight_controller.dart'; +import 'package:querya_desktop/core/editor/syntax_highlight_isolate.dart'; +import 'package:querya_desktop/core/editor/syntax_highlight_service.dart'; +import 'package:querya_desktop/core/theme/parser/token_colors_highlighter_config.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; + +void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + await SyntaxHighlightService.ensureInitialized(); + }); + + testWidgets('buildTextSpan ensures span.toPlainText() == text when text changes for > 8KB script', + (tester) async { + final pair = SyntaxHighlightService.createPair( + language: QueryaCodeLanguage.sql, + queryaTheme: QueryaTheme.darkDefault, + ); + final lightHighlighter = pair.light; + final darkHighlighter = pair.dark; + final lightConfig = buildDefaultEditorHighlighterConfig( + QueryaTheme.lightDefault.editor, + ); + final darkConfig = buildDefaultEditorHighlighterConfig( + QueryaTheme.darkDefault.editor, + ); + final grammarJson = SyntaxHighlightService.grammarJsonFor( + QueryaCodeLanguage.sql, + ); + + final initialLargeText = List.filled(500, 'SELECT id FROM users; -- row').join('\n'); + expect(initialLargeText.length, greaterThan(kSyntaxHighlightIsolateThreshold)); + + final controller = QueryaHighlightController( + text: initialLargeText, + language: QueryaCodeLanguage.sql, + lightHighlighter: lightHighlighter, + darkHighlighter: darkHighlighter, + lightThemeConfig: lightConfig, + darkThemeConfig: darkConfig, + grammarJson: grammarJson, + wrapperColor: Colors.black, + ); + + late BuildContext buildContext; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light(), + home: Builder( + builder: (ctx) { + buildContext = ctx; + return const SizedBox(); + }, + ), + ), + ); + + // Initial buildTextSpan while isolate is pending + var span = controller.buildTextSpan( + context: buildContext, + withComposing: false, + ); + expect(span.toPlainText(), initialLargeText); + + // Wait for isolate highlight and debounce to finish + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + + // Now cached span is present + span = controller.buildTextSpan( + context: buildContext, + withComposing: false, + ); + expect(span.toPlainText(), initialLargeText); + expect(span.children, isNotEmpty); + + // User edits text by adding a character or typing + final modifiedLargeText = '$initialLargeText\n-- extra line'; + controller.text = modifiedLargeText; + + // Immediately before isolate finishes, buildTextSpan MUST match modifiedLargeText + span = controller.buildTextSpan( + context: buildContext, + withComposing: false, + ); + expect(span.toPlainText(), modifiedLargeText); + + // User deletes characters + final deletedText = modifiedLargeText.substring(0, modifiedLargeText.length - 20); + controller.text = deletedText; + + span = controller.buildTextSpan( + context: buildContext, + withComposing: false, + ); + expect(span.toPlainText(), deletedText); + + controller.dispose(); + }); +} From a08830c4d67d49a98b458970bfccd394ab6d9872 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 12:24:54 +0300 Subject: [PATCH 2/2] test(editor): update highlight controller test for length mismatch assertions --- .../querya_highlight_controller_test.dart | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/core/editor/querya_highlight_controller_test.dart b/test/core/editor/querya_highlight_controller_test.dart index 9c941a50..2fe0bcb6 100644 --- a/test/core/editor/querya_highlight_controller_test.dart +++ b/test/core/editor/querya_highlight_controller_test.dart @@ -31,11 +31,10 @@ void main() { QueryaCodeLanguage.sql, ); - final initialLargeText = List.filled(500, 'SELECT id FROM users; -- row').join('\n'); - expect(initialLargeText.length, greaterThan(kSyntaxHighlightIsolateThreshold)); - + // 1. Initialize controller with small text (< threshold) to populate _cachedSpan synchronously + const smallText = 'SELECT id FROM users;'; final controller = QueryaHighlightController( - text: initialLargeText, + text: smallText, language: QueryaCodeLanguage.sql, lightHighlighter: lightHighlighter, darkHighlighter: darkHighlighter, @@ -58,37 +57,40 @@ void main() { ), ); - // Initial buildTextSpan while isolate is pending + // Initial highlight for small text populates _cachedSpan var span = controller.buildTextSpan( context: buildContext, withComposing: false, ); - expect(span.toPlainText(), initialLargeText); + expect(span.toPlainText(), smallText); + expect(span.children, isNotNull); - // Wait for isolate highlight and debounce to finish - await tester.pumpAndSettle(const Duration(milliseconds: 300)); + // 2. Switch to large text (> 8KB). Stale _cachedSpan from smallText must NOT be returned. + final largeText = List.filled(500, 'SELECT id FROM users; -- row').join('\n'); + expect(largeText.length, greaterThan(kSyntaxHighlightIsolateThreshold)); + controller.text = largeText; - // Now cached span is present span = controller.buildTextSpan( context: buildContext, withComposing: false, ); - expect(span.toPlainText(), initialLargeText); - expect(span.children, isNotEmpty); + // span.toPlainText() MUST match largeText, never stale smallText + expect(span.toPlainText(), largeText); + expect(span.toPlainText() == controller.text, isTrue); - // User edits text by adding a character or typing - final modifiedLargeText = '$initialLargeText\n-- extra line'; + // 3. User edits text while isolate highlight is pending (typing characters) + final modifiedLargeText = '$largeText\n-- extra query'; controller.text = modifiedLargeText; - // Immediately before isolate finishes, buildTextSpan MUST match modifiedLargeText span = controller.buildTextSpan( context: buildContext, withComposing: false, ); expect(span.toPlainText(), modifiedLargeText); + expect(span.toPlainText() == controller.text, isTrue); - // User deletes characters - final deletedText = modifiedLargeText.substring(0, modifiedLargeText.length - 20); + // 4. User deletes characters + final deletedText = modifiedLargeText.substring(0, modifiedLargeText.length - 25); controller.text = deletedText; span = controller.buildTextSpan( @@ -96,6 +98,7 @@ void main() { withComposing: false, ); expect(span.toPlainText(), deletedText); + expect(span.toPlainText() == controller.text, isTrue); controller.dispose(); });