From f3b17fb41074ca56bf0139ebf0f1ae3535a1d629 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 28 May 2026 11:00:53 +0300 Subject: [PATCH 1/3] feat(editor): SQL syntax highlighting via syntax_highlight (#49) Wire QueryaHighlightController into QueryaCodeEditor for SQL/JSON, map QueryaEditorTheme tokens to HighlighterTheme, and init grammars at startup. --- .flutter-plugins-dependencies | 2 +- .../editor/highlighter_theme_from_querya.dart | 82 +++++++ lib/core/editor/querya_code_editor.dart | 207 ++++++++++++++++-- .../editor/querya_highlight_controller.dart | 29 +++ lib/core/editor/syntax_highlight_service.dart | 71 ++++++ lib/main.dart | 2 + linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 6 + pubspec.yaml | 1 + .../highlighter_theme_from_querya_test.dart | 21 ++ test/core/editor/querya_code_editor_test.dart | 31 +++ .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 14 files changed, 448 insertions(+), 22 deletions(-) create mode 100644 lib/core/editor/highlighter_theme_from_querya.dart create mode 100644 lib/core/editor/querya_highlight_controller.dart create mode 100644 lib/core/editor/syntax_highlight_service.dart create mode 100644 test/core/editor/highlighter_theme_from_querya_test.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 1d9188ed..e58e7712 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"file_selector_ios","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_ios-0.5.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"file_selector_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_android-0.5.2+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_macos-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_linux-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_windows-0.1.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"file_selector_web","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_web-0.9.4+2/","dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"file_selector","dependencies":["file_selector_android","file_selector_ios","file_selector_linux","file_selector_macos","file_selector_web","file_selector_windows"]},{"name":"file_selector_android","dependencies":[]},{"name":"file_selector_ios","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-04-24 15:57:28.697051","version":"3.41.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_ios","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_ios-0.5.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"irondash_engine_context","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","native_build":true,"dependencies":["irondash_engine_context","device_info_plus"],"dev_dependency":false}],"android":[{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_android-0.5.2+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"irondash_engine_context","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","native_build":true,"dependencies":["irondash_engine_context","device_info_plus"],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_macos-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"irondash_engine_context","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","native_build":true,"dependencies":["irondash_engine_context","device_info_plus"],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_linux-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_selector_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"irondash_engine_context","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","native_build":true,"dependencies":["irondash_engine_context","device_info_plus"],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_windows-0.1.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_selector_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"irondash_engine_context","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","native_build":true,"dependencies":["irondash_engine_context","device_info_plus"],"dev_dependency":false}],"web":[{"name":"device_info_plus","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","dependencies":[],"dev_dependency":false},{"name":"file_selector_web","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/file_selector_web-0.9.4+2/","dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"super_native_extensions","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/super_native_extensions-0.9.1/","dependencies":["device_info_plus"],"dev_dependency":false}]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_selector","dependencies":["file_selector_android","file_selector_ios","file_selector_linux","file_selector_macos","file_selector_web","file_selector_windows"]},{"name":"file_selector_android","dependencies":[]},{"name":"file_selector_ios","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_web","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"irondash_engine_context","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"super_native_extensions","dependencies":["irondash_engine_context","device_info_plus"]}],"date_created":"2026-05-28 10:58:45.738032","version":"3.41.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/lib/core/editor/highlighter_theme_from_querya.dart b/lib/core/editor/highlighter_theme_from_querya.dart new file mode 100644 index 00000000..325a90f2 --- /dev/null +++ b/lib/core/editor/highlighter_theme_from_querya.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:querya_desktop/core/theme/querya_editor_theme.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +/// Builds a [HighlighterTheme] from [QueryaEditorTheme] token colors. +HighlighterTheme highlighterThemeFromQueryaEditor(QueryaEditorTheme editor) { + final wrapper = TextStyle( + color: editor.foreground, + fontFamily: editor.fontFamily, + fontSize: editor.fontSize, + ); + + final config = jsonEncode({ + 'settings': [ + { + 'scope': [ + 'comment', + 'comment.line', + 'comment.block', + ], + 'settings': {'foreground': _hex(editor.comment)}, + }, + { + 'scope': [ + 'keyword', + 'keyword.control', + 'keyword.operator', + 'storage.type', + ], + 'settings': {'foreground': _hex(editor.keyword)}, + }, + { + 'scope': [ + 'string', + 'string.quoted', + 'string.quoted.single', + 'string.quoted.double', + ], + 'settings': {'foreground': _hex(editor.string)}, + }, + { + 'scope': ['constant.numeric', 'number'], + 'settings': {'foreground': _hex(editor.number)}, + }, + { + 'scope': ['entity.name.function', 'support.function'], + 'settings': {'foreground': _hex(editor.function)}, + }, + { + 'scope': ['entity.name.type', 'support.type'], + 'settings': {'foreground': _hex(editor.type)}, + }, + { + 'scope': ['constant.language', 'variable.language'], + 'settings': {'foreground': _hex(editor.keyword)}, + }, + { + 'settings': {'foreground': _hex(editor.foreground)}, + }, + ], + }); + + return HighlighterTheme.fromConfiguration(config, wrapper); +} + +String _hex(Color c) { + final a = (c.a * 255).round().clamp(0, 255); + final r = (c.r * 255).round().clamp(0, 255); + final g = (c.g * 255).round().clamp(0, 255); + final b = (c.b * 255).round().clamp(0, 255); + if (a < 255) { + return '#${r.toRadixString(16).padLeft(2, '0')}' + '${g.toRadixString(16).padLeft(2, '0')}' + '${b.toRadixString(16).padLeft(2, '0')}' + '${a.toRadixString(16).padLeft(2, '0')}'; + } + return '#${r.toRadixString(16).padLeft(2, '0')}' + '${g.toRadixString(16).padLeft(2, '0')}' + '${b.toRadixString(16).padLeft(2, '0')}'; +} diff --git a/lib/core/editor/querya_code_editor.dart b/lib/core/editor/querya_code_editor.dart index 157aa8b1..4076c042 100644 --- a/lib/core/editor/querya_code_editor.dart +++ b/lib/core/editor/querya_code_editor.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/theme/querya_editor_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'querya_code_language.dart'; +import 'querya_highlight_controller.dart'; +import 'syntax_highlight_service.dart'; /// Shadcn vs Material [TextField] backend for different parent widgets. enum QueryaCodeEditorVariant { @@ -11,7 +14,7 @@ enum QueryaCodeEditorVariant { material, } -/// Unified code editor (MVP: plain [TextField]; highlighting in #49+). +/// Unified code editor with optional syntax highlighting (SQL/JSON). class QueryaCodeEditor extends StatefulWidget { const QueryaCodeEditor({ super.key, @@ -27,6 +30,7 @@ class QueryaCodeEditor extends StatefulWidget { this.hintText, this.contentPadding, this.textAlignVertical, + this.enableHighlighting = true, }); final material.TextEditingController? controller; @@ -44,53 +48,209 @@ class QueryaCodeEditor extends StatefulWidget { final material.EdgeInsetsGeometry? contentPadding; final material.TextAlignVertical? textAlignVertical; + /// When true and [language] is SQL/JSON, uses [syntax_highlight] if initialized. + final bool enableHighlighting; + @override State createState() => _QueryaCodeEditorState(); } class _QueryaCodeEditorState extends State { - late material.TextEditingController _controller; - bool _ownsController = false; + material.TextEditingController? _plainController; + QueryaHighlightController? _highlightController; + bool _ownsPlainController = false; + bool _ownsHighlightController = false; + bool _syncing = false; + QueryaEditorTheme? _highlightEditorTheme; + QueryaCodeLanguage? _highlightLanguage; + + material.TextEditingController get _activeController => + _highlightController ?? _plainController!; + + bool get _useHighlighting => + widget.enableHighlighting && + widget.language != QueryaCodeLanguage.plain && + SyntaxHighlightService.isInitialized; @override void initState() { super.initState(); - _initController(widget.controller); - _controller.addListener(_onTextChanged); + if (!_useHighlighting) { + _initPlainController(widget.controller); + _plainController!.addListener(_onTextChanged); + } } - void _initController(material.TextEditingController? external) { + void _initPlainController(material.TextEditingController? external) { if (external == null) { - _controller = material.TextEditingController(); - _ownsController = true; + _plainController = material.TextEditingController(); + _ownsPlainController = true; } else { - _controller = external; - _ownsController = false; + _plainController = external; + _ownsPlainController = false; } } void _onTextChanged() { - widget.onChanged?.call(_controller.text); + widget.onChanged?.call(_activeController.text); + } + + 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 _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() { + final highlight = _highlightController; + if (highlight == null) return; + highlight.removeListener(_onTextChanged); + highlight.removeListener(_syncToExternal); + widget.controller?.removeListener(_syncFromExternal); + if (_ownsHighlightController) { + highlight.dispose(); + } + _highlightController = null; + _highlightEditorTheme = null; + _highlightLanguage = null; + } + + void _ensureHighlightController(QueryaTheme queryaTheme) { + if (!_useHighlighting) return; + + final editor = queryaTheme.editor; + if (_highlightController != null && + _highlightEditorTheme == editor && + _highlightLanguage == widget.language) { + return; + } + + final pair = SyntaxHighlightService.createPair( + language: widget.language, + queryaTheme: queryaTheme, + ); + + final external = widget.controller; + final text = external?.text ?? _highlightController?.text ?? ''; + + _disposeHighlight(); + + _highlightController = QueryaHighlightController( + text: text, + lightHighlighter: pair.light, + darkHighlighter: pair.dark, + ); + _ownsHighlightController = external == null; + _highlightEditorTheme = editor; + _highlightLanguage = widget.language; + + _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); + } } @override void didUpdateWidget(QueryaCodeEditor oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { - oldWidget.controller?.removeListener(_onTextChanged); - if (_ownsController) { - _controller.dispose(); + _highlightController?.removeListener(_onTextChanged); + _plainController?.removeListener(_onTextChanged); + _disposeHighlight(); + if (_plainController != null) { + _plainController!.removeListener(_onTextChanged); + if (_ownsPlainController) { + _plainController!.dispose(); + } + _plainController = null; + } + if (_useHighlighting) { + _ensureHighlightController(context.queryaTheme); + _highlightController!.addListener(_onTextChanged); + } else { + _initPlainController(widget.controller); + _plainController!.addListener(_onTextChanged); + } + } else if (oldWidget.language != widget.language || + oldWidget.enableHighlighting != widget.enableHighlighting) { + _highlightEditorTheme = null; + _highlightLanguage = null; + if (_useHighlighting) { + _ensureHighlightController(context.queryaTheme); + } else { + _switchToPlain(widget.controller); } - _initController(widget.controller); - _controller.addListener(_onTextChanged); } } @override void dispose() { - _controller.removeListener(_onTextChanged); - if (_ownsController) { - _controller.dispose(); + _disposeHighlight(); + if (_plainController != null) { + _plainController!.removeListener(_onTextChanged); + if (_ownsPlainController) { + _plainController!.dispose(); + } } super.dispose(); } @@ -118,13 +278,18 @@ 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!; if (widget.variant == QueryaCodeEditorVariant.material) { return material.TextField( - controller: _controller, + controller: controller, readOnly: widget.readOnly, maxLines: widget.expands ? null : widget.maxLines, expands: widget.expands, @@ -141,7 +306,7 @@ class _QueryaCodeEditorState extends State { } return TextField( - controller: _controller, + controller: controller, readOnly: widget.readOnly, maxLines: widget.expands ? null : widget.maxLines, expands: widget.expands, diff --git a/lib/core/editor/querya_highlight_controller.dart b/lib/core/editor/querya_highlight_controller.dart new file mode 100644 index 00000000..88123627 --- /dev/null +++ b/lib/core/editor/querya_highlight_controller.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +/// [TextEditingController] that applies [Highlighter] in [buildTextSpan]. +class QueryaHighlightController extends TextEditingController { + QueryaHighlightController({ + super.text, + required this.lightHighlighter, + required this.darkHighlighter, + }); + + final Highlighter lightHighlighter; + final Highlighter darkHighlighter; + + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final highlighter = Theme.of(context).brightness == Brightness.light + ? lightHighlighter + : darkHighlighter; + return TextSpan( + style: style, + children: [highlighter.highlight(text)], + ); + } +} diff --git a/lib/core/editor/syntax_highlight_service.dart b/lib/core/editor/syntax_highlight_service.dart new file mode 100644 index 00000000..a22bd6e0 --- /dev/null +++ b/lib/core/editor/syntax_highlight_service.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:querya_desktop/core/theme/querya_editor_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +import 'highlighter_theme_from_querya.dart'; +import 'querya_code_language.dart'; + +/// Global syntax highlighter setup for [QueryaCodeEditor]. +abstract final class SyntaxHighlightService { + static bool _initialized = false; + + static Future ensureInitialized() async { + if (_initialized) return; + await Highlighter.initialize(['sql', 'json']); + _initialized = true; + } + + static bool get isInitialized => _initialized; + + static Highlighter createHighlighter({ + required QueryaCodeLanguage language, + required QueryaEditorTheme editorTheme, + required Brightness brightness, + }) { + _assertInitialized(); + final lang = switch (language) { + QueryaCodeLanguage.sql => 'sql', + QueryaCodeLanguage.json => 'json', + QueryaCodeLanguage.plain => 'sql', + }; + final theme = highlighterThemeFromQueryaEditor(editorTheme); + return Highlighter(language: lang, theme: theme); + } + + static HighlighterPair createPair({ + required QueryaCodeLanguage language, + required QueryaTheme queryaTheme, + }) { + return HighlighterPair( + light: createHighlighter( + language: language, + editorTheme: queryaTheme.editor, + brightness: Brightness.light, + ), + dark: createHighlighter( + language: language, + editorTheme: queryaTheme.editor, + brightness: Brightness.dark, + ), + ); + } + + static void _assertInitialized() { + assert( + _initialized, + 'Call SyntaxHighlightService.ensureInitialized() before use', + ); + } +} + +/// Light/dark highlighters for Material [Theme] brightness switching. +class HighlighterPair { + const HighlighterPair({required this.light, required this.dark}); + + final Highlighter light; + final Highlighter dark; + + Highlighter forBrightness(Brightness brightness) => + brightness == Brightness.light ? light : dark; +} diff --git a/lib/main.dart b/lib/main.dart index d6436b4c..14dbd026 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,12 +2,14 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'app/app.dart'; +import 'core/editor/syntax_highlight_service.dart'; import 'core/storage/local_db.dart'; import 'core/theme/theme_controller.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await LocalDb.initFfi(); + await SyntaxHighlightService.ensureInitialized(); await ThemeController.instance.load(); runApp(const QueryaApp()); doWhenWindowReady(() { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c3070bc4..49d2eb91 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,8 @@ #include #include #include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = @@ -20,4 +22,10 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); + irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); + g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); + super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d0c3733..0516a6e3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux file_selector_linux flutter_secure_storage_linux + irondash_engine_context + super_native_extensions ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1f85d46f..5644fad5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,19 @@ import FlutterMacOS import Foundation import bitsdojo_window_macos +import device_info_plus import file_selector_macos import flutter_secure_storage_macos +import irondash_engine_context import sqflite_darwin +import super_native_extensions func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) } diff --git a/pubspec.yaml b/pubspec.yaml index 1af0c98f..8219a245 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: mysql_client: ^0.0.27 flutter_secure_storage: ^9.2.4 file_selector: ^1.1.0 + syntax_highlight: ^0.5.0 dev_dependencies: flutter_test: diff --git a/test/core/editor/highlighter_theme_from_querya_test.dart b/test/core/editor/highlighter_theme_from_querya_test.dart new file mode 100644 index 00000000..ddb91c77 --- /dev/null +++ b/test/core/editor/highlighter_theme_from_querya_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/editor/highlighter_theme_from_querya.dart'; +import 'package:querya_desktop/core/editor/syntax_highlight_service.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + await SyntaxHighlightService.ensureInitialized(); + }); + + test('highlighterThemeFromQueryaEditor produces SQL spans', () { + final theme = highlighterThemeFromQueryaEditor(QueryaTheme.darkDefault.editor); + final highlighter = Highlighter(language: 'sql', theme: theme); + final span = highlighter.highlight('SELECT 1 -- comment'); + expect(span.children, isNotNull); + expect(span.children!.length, greaterThan(1)); + }); +} diff --git a/test/core/editor/querya_code_editor_test.dart b/test/core/editor/querya_code_editor_test.dart index 3830f830..33f1dc22 100644 --- a/test/core/editor/querya_code_editor_test.dart +++ b/test/core/editor/querya_code_editor_test.dart @@ -2,12 +2,17 @@ import 'package:flutter/material.dart' as material; import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/editor/querya_code_editor.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/core/theme/querya_theme.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import '../../support/querya_theme_test_shell.dart'; void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + await SyntaxHighlightService.ensureInitialized(); + }); testWidgets('QueryaCodeEditor shadcn applies fontSize from props', (tester) async { await tester.pumpWidget( queryaThemeTestShell( @@ -54,6 +59,32 @@ void main() { expect(field.style?.color, const Color(0xFFABCDEF)); }); + testWidgets('SQL highlighting keeps external controller in sync', (tester) async { + final external = material.TextEditingController(); + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: external, + language: QueryaCodeLanguage.sql, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(material.EditableText), 'SELECT 1'); + await tester.pump(); + expect(external.text, 'SELECT 1'); + external.text = 'UPDATE x'; + await tester.pump(); + expect( + tester.widget(find.byType(material.EditableText)).controller.text, + 'UPDATE x', + ); + }); + testWidgets('onChanged fires when text updates', (tester) async { var last = ''; await tester.pumpWidget( diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d1b0b32d..72592476 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,8 @@ #include #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { BitsdojoWindowPluginRegisterWithRegistrar( @@ -17,4 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + IrondashEngineContextPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); + SuperNativeExtensionsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 25b91502..69caccba 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows file_selector_windows flutter_secure_storage_windows + irondash_engine_context + super_native_extensions ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 385254d2ccecbdaaf63347bbd0ed5fd099179f10 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 28 May 2026 11:01:13 +0300 Subject: [PATCH 2/3] chore(editor): remove obsolete sql_syntax_highlighting stub --- lib/core/editor/sql_syntax_highlighting.dart | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 lib/core/editor/sql_syntax_highlighting.dart diff --git a/lib/core/editor/sql_syntax_highlighting.dart b/lib/core/editor/sql_syntax_highlighting.dart deleted file mode 100644 index 38286044..00000000 --- a/lib/core/editor/sql_syntax_highlighting.dart +++ /dev/null @@ -1,4 +0,0 @@ -// Future: syntax highlighting backend for [QueryaCodeEditor] (#49, #50). -// -// MVP uses plain TextField via [QueryaCodeEditor]. Candidates: syntax_highlight, -// re_editor, code_forge (see issue #48). From ea3dd9c9ed68327abd3c4678414f5db321369bdb Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 28 May 2026 11:03:47 +0300 Subject: [PATCH 3/3] fix(test): remove unused material import in highlighter test --- test/core/editor/highlighter_theme_from_querya_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core/editor/highlighter_theme_from_querya_test.dart b/test/core/editor/highlighter_theme_from_querya_test.dart index ddb91c77..1a249a37 100644 --- a/test/core/editor/highlighter_theme_from_querya_test.dart +++ b/test/core/editor/highlighter_theme_from_querya_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/editor/highlighter_theme_from_querya.dart'; import 'package:querya_desktop/core/editor/syntax_highlight_service.dart';