Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 39 additions & 162 deletions lib/core/editor/querya_code_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,212 +60,93 @@ class QueryaCodeEditor extends StatefulWidget {
}

class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
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();
}

Expand Down Expand Up @@ -293,14 +174,10 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {

@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(
Expand Down
29 changes: 24 additions & 5 deletions lib/core/editor/querya_highlight_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 25 additions & 0 deletions lib/core/editor/syntax_highlight_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -112,6 +113,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,
Expand Down
9 changes: 8 additions & 1 deletion lib/features/workspace/sql_query_tab_session.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<double>(initialFraction) {
controller.addListener(_onTextChanged);
Expand Down
13 changes: 8 additions & 5 deletions test/core/editor/querya_code_editor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
),
Expand All @@ -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(
Expand Down
Loading