From ec292a337998de152433de85f6fa1f960fc2af6c Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 10 Oct 2026 20:48:09 +0300 Subject: [PATCH 1/2] refactor(editor): decouple QueryaCodeEditor from highlight controller lifecycle and manage at session level Closes #1366 --- lib/core/editor/querya_code_editor.dart | 201 ++++-------------- .../editor/querya_highlight_controller.dart | 29 ++- lib/core/editor/syntax_highlight_service.dart | 24 +++ .../workspace/sql_query_tab_session.dart | 9 +- test/core/editor/querya_code_editor_test.dart | 13 +- 5 files changed, 103 insertions(+), 173 deletions(-) diff --git a/lib/core/editor/querya_code_editor.dart b/lib/core/editor/querya_code_editor.dart index 057b58e8..566c07d0 100644 --- a/lib/core/editor/querya_code_editor.dart +++ b/lib/core/editor/querya_code_editor.dart @@ -60,212 +60,93 @@ class QueryaCodeEditor extends StatefulWidget { } class _QueryaCodeEditorState extends State { - material.TextEditingController? _plainController; - QueryaHighlightController? _highlightController; - bool _ownsPlainController = false; - bool _ownsHighlightController = false; - bool _syncing = false; - QueryaEditorTheme? _highlightEditorTheme; - QueryaCodeLanguage? _highlightLanguage; - int _highlightTokenColorsHash = 0; + material.TextEditingController? _internalController; + QueryaEditorTheme? _lastEditorTheme; + int _lastTokenColorsHash = 0; - material.TextEditingController get _activeController => - _highlightController ?? _plainController!; - - bool get _useHighlighting => - widget.enableHighlighting && - widget.language != QueryaCodeLanguage.plain && - SyntaxHighlightService.isInitialized; + material.TextEditingController get _effectiveController => + widget.controller ?? _internalController!; @override void initState() { super.initState(); - if (!_useHighlighting) { - _initPlainController(widget.controller); - _plainController!.addListener(_onTextChanged); - } + _initControllerIfNeeded(); + _attachListener(); } - void _initPlainController(material.TextEditingController? external) { - if (external == null) { - _plainController = material.TextEditingController(); - _ownsPlainController = true; - } else { - _plainController = external; - _ownsPlainController = false; + void _initControllerIfNeeded() { + if (widget.controller == null) { + _internalController = material.TextEditingController(); } } - void _onTextChanged() { - widget.onChanged?.call(_activeController.text); + void _attachListener() { + _effectiveController.addListener(_onTextChanged); } - void _syncFromExternal() { - final external = widget.controller; - final highlight = _highlightController; - if (external == null || highlight == null || _syncing) return; - if (external.text == highlight.text && - external.selection == highlight.selection) { - return; - } - _syncing = true; - highlight.value = external.value; - _syncing = false; + void _detachListener([material.TextEditingController? target]) { + (target ?? _effectiveController).removeListener(_onTextChanged); } - void _syncToExternal() { - final external = widget.controller; - final highlight = _highlightController; - if (external == null || highlight == null || _syncing) return; - if (external.text == highlight.text && - external.selection == highlight.selection) { - return; - } - _syncing = true; - external.value = highlight.value; - _syncing = false; - } - - void _disposeHighlight([material.TextEditingController? externalToDetach]) { - final highlight = _highlightController; - if (highlight == null) return; - highlight.removeListener(_onTextChanged); - highlight.removeListener(_syncToExternal); - (externalToDetach ?? widget.controller)?.removeListener(_syncFromExternal); - if (_ownsHighlightController) { - highlight.dispose(); - } - _highlightController = null; - _highlightEditorTheme = null; - _highlightLanguage = null; - _highlightTokenColorsHash = 0; + void _onTextChanged() { + widget.onChanged?.call(_effectiveController.text); } - void _ensureHighlightController(QueryaTheme queryaTheme) { - if (!_useHighlighting) return; + void _syncThemeIfNeeded(QueryaTheme queryaTheme) { + final controller = _effectiveController; + if (controller is! QueryaHighlightController) return; final editor = queryaTheme.editor; final tokenHash = Object.hashAll(queryaTheme.tokenColors); - if (_highlightController != null && - _highlightEditorTheme == editor && - _highlightLanguage == widget.language && - _highlightTokenColorsHash == tokenHash) { + if (_lastEditorTheme == editor && _lastTokenColorsHash == tokenHash) { return; } + _lastEditorTheme = editor; + _lastTokenColorsHash = tokenHash; + final pair = SyntaxHighlightService.createPair( - language: widget.language, + language: controller.language, queryaTheme: queryaTheme, ); - final external = widget.controller; - final text = external?.text ?? _highlightController?.text ?? ''; - - _disposeHighlight(); - - _highlightController = QueryaHighlightController( - text: text, - language: widget.language, + controller.updateTheme( lightHighlighter: pair.light, darkHighlighter: pair.dark, lightThemeConfig: pair.lightThemeConfig, darkThemeConfig: pair.darkThemeConfig, - grammarJson: pair.grammarJson, wrapperColor: editor.foreground, ); - _ownsHighlightController = external == null; - _highlightEditorTheme = editor; - _highlightLanguage = widget.language; - _highlightTokenColorsHash = tokenHash; - - _highlightController!.addListener(_onTextChanged); - if (external != null) { - external.addListener(_syncFromExternal); - _highlightController!.addListener(_syncToExternal); - } - } - - void _switchToPlain(material.TextEditingController? external) { - _disposeHighlight(); - final text = external?.text ?? _plainController?.text ?? ''; - if (_plainController != null) { - _plainController!.removeListener(_onTextChanged); - if (_ownsPlainController) { - _plainController!.dispose(); - } - } - _initPlainController(external); - if (_ownsPlainController && text.isNotEmpty) { - _plainController!.text = text; - } - _plainController!.addListener(_onTextChanged); } @override void didChangeDependencies() { super.didChangeDependencies(); - final queryaTheme = context.queryaTheme; - if (_useHighlighting) { - if (_plainController != null) { - _plainController!.removeListener(_onTextChanged); - if (_ownsPlainController) { - _plainController!.dispose(); - } - _plainController = null; - _ownsPlainController = false; - } - _ensureHighlightController(queryaTheme); - } else if (_highlightController != null) { - _highlightController!.removeListener(_onTextChanged); - _switchToPlain(widget.controller); - } else if (_plainController == null) { - _initPlainController(widget.controller); - _plainController!.addListener(_onTextChanged); - } + _syncThemeIfNeeded(context.queryaTheme); } @override void didUpdateWidget(QueryaCodeEditor oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { - _highlightController?.removeListener(_onTextChanged); - _plainController?.removeListener(_onTextChanged); - _disposeHighlight(oldWidget.controller); - if (_plainController != null) { - _plainController!.removeListener(_onTextChanged); - if (_ownsPlainController) { - _plainController!.dispose(); - } - _plainController = null; - } - if (_useHighlighting) { - _ensureHighlightController(context.queryaTheme); - } else { - _initPlainController(widget.controller); - _plainController!.addListener(_onTextChanged); - } - } else if (oldWidget.language != widget.language || - oldWidget.enableHighlighting != widget.enableHighlighting) { - _highlightEditorTheme = null; - _highlightLanguage = null; - _highlightTokenColorsHash = 0; - if (_useHighlighting) { - _ensureHighlightController(context.queryaTheme); - } else { - _switchToPlain(widget.controller); + _detachListener(oldWidget.controller ?? _internalController); + if (oldWidget.controller == null && widget.controller != null) { + _internalController?.dispose(); + _internalController = null; + } else if (widget.controller == null && _internalController == null) { + _internalController = material.TextEditingController(); } + _lastEditorTheme = null; + _lastTokenColorsHash = 0; + _attachListener(); + _syncThemeIfNeeded(context.queryaTheme); } } @override void dispose() { - _disposeHighlight(); - if (_plainController != null) { - _plainController!.removeListener(_onTextChanged); - if (_ownsPlainController) { - _plainController!.dispose(); - } - } + _detachListener(); + _internalController?.dispose(); super.dispose(); } @@ -293,14 +174,10 @@ class _QueryaCodeEditorState extends State { @override Widget build(BuildContext context) { - if (_useHighlighting) { - _ensureHighlightController(context.queryaTheme); - } - final editor = context.editorTheme; final style = _textStyle(editor); final placeholder = _resolvedPlaceholder(); - final controller = _highlightController ?? _plainController!; + final controller = _effectiveController; if (widget.variant == QueryaCodeEditorVariant.material) { return material.TextField( diff --git a/lib/core/editor/querya_highlight_controller.dart b/lib/core/editor/querya_highlight_controller.dart index 793d78a5..4a3912ae 100644 --- a/lib/core/editor/querya_highlight_controller.dart +++ b/lib/core/editor/querya_highlight_controller.dart @@ -23,12 +23,31 @@ class QueryaHighlightController extends TextEditingController { }); final QueryaCodeLanguage language; - final Highlighter lightHighlighter; - final Highlighter darkHighlighter; - final String lightThemeConfig; - final String darkThemeConfig; + Highlighter lightHighlighter; + Highlighter darkHighlighter; + String lightThemeConfig; + String darkThemeConfig; final String grammarJson; - final Color wrapperColor; + Color wrapperColor; + + /// Updates highlighters and theme configs when application theme changes. + void updateTheme({ + required Highlighter lightHighlighter, + required Highlighter darkHighlighter, + required String lightThemeConfig, + required String darkThemeConfig, + required Color wrapperColor, + }) { + this.lightHighlighter = lightHighlighter; + this.darkHighlighter = darkHighlighter; + this.lightThemeConfig = lightThemeConfig; + this.darkThemeConfig = darkThemeConfig; + this.wrapperColor = wrapperColor; + _cachedSpan = null; + _cachedText = null; + _cachedBrightness = null; + notifyListeners(); + } TextSpan? _cachedSpan; String? _cachedText; diff --git a/lib/core/editor/syntax_highlight_service.dart b/lib/core/editor/syntax_highlight_service.dart index 62a3b2a9..4232fb61 100644 --- a/lib/core/editor/syntax_highlight_service.dart +++ b/lib/core/editor/syntax_highlight_service.dart @@ -112,6 +112,30 @@ abstract final class SyntaxHighlightService { ); } + /// Creates a [QueryaHighlightController] initialized with highlighters and grammar for [language]. + /// If syntax highlighting is not initialized or [language] is plain, falls back to a plain [TextEditingController]. + static TextEditingController createHighlightController({ + required QueryaCodeLanguage language, + String? text, + QueryaTheme? queryaTheme, + }) { + if (!_initialized || language == QueryaCodeLanguage.plain) { + return TextEditingController(text: text); + } + final theme = queryaTheme ?? QueryaTheme.darkDefault; + final pair = createPair(language: language, queryaTheme: theme); + return QueryaHighlightController( + text: text, + language: language, + lightHighlighter: pair.light, + darkHighlighter: pair.dark, + lightThemeConfig: pair.lightThemeConfig, + darkThemeConfig: pair.darkThemeConfig, + grammarJson: pair.grammarJson, + wrapperColor: theme.editor.foreground, + ); + } + static void _assertInitialized() { assert( _initialized, diff --git a/lib/features/workspace/sql_query_tab_session.dart b/lib/features/workspace/sql_query_tab_session.dart index bd1560bf..62f94278 100644 --- a/lib/features/workspace/sql_query_tab_session.dart +++ b/lib/features/workspace/sql_query_tab_session.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/actions/sql_script_format.dart'; import 'package:querya_desktop/core/database/table_schema_meta.dart'; +import 'package:querya_desktop/core/editor/querya_code_language.dart'; +import 'package:querya_desktop/core/editor/syntax_highlight_service.dart'; import 'package:querya_desktop/features/workspace/query_plan.dart'; import 'package:querya_desktop/core/unsaved_work_registry.dart'; import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart'; @@ -17,7 +19,12 @@ class SqlQueryTabSession { this.filePath, double initialFraction = 0.65, this.isDiagram = false, - }) : controller = material.TextEditingController(text: initialSql ?? ''), + material.TextEditingController? customController, + }) : controller = customController ?? + SyntaxHighlightService.createHighlightController( + language: QueryaCodeLanguage.sql, + text: initialSql ?? '', + ), _savedSql = initialSql ?? '', topFraction = material.ValueNotifier(initialFraction) { controller.addListener(_onTextChanged); diff --git a/test/core/editor/querya_code_editor_test.dart b/test/core/editor/querya_code_editor_test.dart index 3d78133f..5102cd5f 100644 --- a/test/core/editor/querya_code_editor_test.dart +++ b/test/core/editor/querya_code_editor_test.dart @@ -63,16 +63,19 @@ void main() { expect(field.style?.color, const Color(0xFFABCDEF)); }); - testWidgets('SQL highlighting keeps external controller in sync', + testWidgets('SQL highlight controller works directly in QueryaCodeEditor', (tester) async { - final external = material.TextEditingController(); + final controller = SyntaxHighlightService.createHighlightController( + language: QueryaCodeLanguage.sql, + text: '', + ); await tester.pumpWidget( queryaThemeTestShell( child: material.SizedBox( width: 400, height: 200, child: QueryaCodeEditor( - controller: external, + controller: controller, language: QueryaCodeLanguage.sql, ), ), @@ -83,8 +86,8 @@ void main() { await tester.enterText(find.byType(material.EditableText), 'SELECT 1'); await tester.pump(); await pumpSyntaxHighlightDebounce(tester); - expect(external.text, 'SELECT 1'); - external.text = 'UPDATE x'; + expect(controller.text, 'SELECT 1'); + controller.text = 'UPDATE x'; await tester.pump(); await pumpSyntaxHighlightDebounce(tester); expect( From 497f6e0f55cbd7271c38c1deab68f76d1d24f1a4 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 10 Oct 2026 21:14:42 +0300 Subject: [PATCH 2/2] fix(editor): import QueryaHighlightController in SyntaxHighlightService createHighlightController referenced the class without importing it, so the whole app failed to compile. --- lib/core/editor/syntax_highlight_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/editor/syntax_highlight_service.dart b/lib/core/editor/syntax_highlight_service.dart index 4232fb61..d8fb06ea 100644 --- a/lib/core/editor/syntax_highlight_service.dart +++ b/lib/core/editor/syntax_highlight_service.dart @@ -7,6 +7,7 @@ import 'package:syntax_highlight/syntax_highlight.dart'; import 'highlighter_theme_from_querya.dart'; import 'querya_code_language.dart'; +import 'querya_highlight_controller.dart'; /// Global syntax highlighter setup for [QueryaCodeEditor]. abstract final class SyntaxHighlightService {