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
3 changes: 2 additions & 1 deletion docs/theme-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Querya can apply a **subset** of VS Code theme JSON / JSONC `colors` to
`QueryaWorkbenchTheme`, `QueryaEditorTheme`, and the shadcn `ColorScheme`.

Syntax highlighting (`tokenColors`) is tracked separately (issue #46).
Imported `tokenColors` are persisted with the theme file and applied to SQL/JSON
syntax highlighting via `TokenStyleResolver` → `HighlighterTheme` (issue #46).

## Supported `colors` keys

Expand Down
103 changes: 18 additions & 85 deletions lib/core/editor/highlighter_theme_from_querya.dart
Original file line number Diff line number Diff line change
@@ -1,100 +1,33 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:querya_desktop/core/theme/parser/token_colors_highlighter_config.dart';
import 'package:querya_desktop/core/theme/parser/vscode_theme_manifest.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) {
/// Builds a [HighlighterTheme] from [QueryaEditorTheme] and optional VS Code rules.
HighlighterTheme highlighterThemeFromQueryaEditor(
QueryaEditorTheme editor, {
List<TokenColorRule> tokenColors = const [],
}) {
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',
'constant.numeric.json',
'number',
],
'settings': {'foreground': _hex(editor.number)},
},
{
'scope': [
'support.type.property-name',
'support.type.property-name.json',
],
'settings': {'foreground': _hex(editor.type)},
},
{
'scope': [
'constant.language',
'constant.language.json',
],
'settings': {'foreground': _hex(editor.keyword)},
},
{
'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)},
},
],
});
final config = tokenColors.isNotEmpty
? buildHighlighterConfigFromTokenColors(tokenColors, editor.foreground)
: buildDefaultEditorHighlighterConfig(editor);

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')}';
/// JSON config for isolate/off-thread highlighting.
String highlighterThemeConfigJson(
QueryaEditorTheme editor, {
List<TokenColorRule> tokenColors = const [],
}) {
return tokenColors.isNotEmpty
? buildHighlighterConfigFromTokenColors(tokenColors, editor.foreground)
: buildDefaultEditorHighlighterConfig(editor);
}
13 changes: 12 additions & 1 deletion lib/core/editor/querya_code_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
bool _syncing = false;
QueryaEditorTheme? _highlightEditorTheme;
QueryaCodeLanguage? _highlightLanguage;
int _highlightTokenColorsHash = 0;

material.TextEditingController get _activeController =>
_highlightController ?? _plainController!;
Expand Down Expand Up @@ -133,15 +134,18 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
_highlightController = null;
_highlightEditorTheme = null;
_highlightLanguage = null;
_highlightTokenColorsHash = 0;
}

void _ensureHighlightController(QueryaTheme queryaTheme) {
if (!_useHighlighting) return;

final editor = queryaTheme.editor;
final tokenHash = Object.hashAll(queryaTheme.tokenColors);
if (_highlightController != null &&
_highlightEditorTheme == editor &&
_highlightLanguage == widget.language) {
_highlightLanguage == widget.language &&
_highlightTokenColorsHash == tokenHash) {
return;
}

Expand All @@ -157,12 +161,18 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {

_highlightController = QueryaHighlightController(
text: text,
language: widget.language,
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) {
Expand Down Expand Up @@ -235,6 +245,7 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
oldWidget.enableHighlighting != widget.enableHighlighting) {
_highlightEditorTheme = null;
_highlightLanguage = null;
_highlightTokenColorsHash = 0;
if (_useHighlighting) {
_ensureHighlightController(context.queryaTheme);
} else {
Expand Down
86 changes: 83 additions & 3 deletions lib/core/editor/querya_highlight_controller.dart
Original file line number Diff line number Diff line change
@@ -1,29 +1,109 @@
import 'package:flutter/material.dart';
import 'package:querya_desktop/core/editor/querya_code_language.dart';
import 'package:syntax_highlight/syntax_highlight.dart';

import 'syntax_highlight_isolate.dart';

/// [TextEditingController] that applies [Highlighter] in [buildTextSpan].
class QueryaHighlightController extends TextEditingController {
QueryaHighlightController({
super.text,
required this.language,
required this.lightHighlighter,
required this.darkHighlighter,
required this.lightThemeConfig,
required this.darkThemeConfig,
required this.grammarJson,
required this.wrapperColor,
});

final QueryaCodeLanguage language;
final Highlighter lightHighlighter;
final Highlighter darkHighlighter;
final String lightThemeConfig;
final String darkThemeConfig;
final String grammarJson;
final Color wrapperColor;

TextSpan? _cachedSpan;
String? _cachedText;
Brightness? _cachedBrightness;
int _highlightGeneration = 0;

@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
final highlighter = Theme.of(context).brightness == Brightness.light
final brightness = Theme.of(context).brightness;
final highlighter = brightness == Brightness.light
? lightHighlighter
: darkHighlighter;
return TextSpan(
final themeConfig = brightness == Brightness.light
? lightThemeConfig
: darkThemeConfig;

if (text.length < kSyntaxHighlightIsolateThreshold) {
return TextSpan(
style: style,
children: [highlighter.highlight(text)],
);
}

if (_cachedText == text &&
_cachedBrightness == brightness &&
_cachedSpan != null) {
return TextSpan(style: style, children: [_cachedSpan!]);
}

_scheduleIsolateHighlight(
text: text,
brightness: brightness,
themeConfig: themeConfig,
style: style,
children: [highlighter.highlight(text)],
);

if (_cachedSpan != null && _cachedText == text) {
return TextSpan(style: style, children: [_cachedSpan!]);
}

return TextSpan(style: style, text: text);
}

void _scheduleIsolateHighlight({
required String text,
required Brightness brightness,
required String themeConfig,
required TextStyle? style,
}) {
final generation = ++_highlightGeneration;
final lang = switch (language) {
QueryaCodeLanguage.sql => 'sql',
QueryaCodeLanguage.json => 'json',
QueryaCodeLanguage.plain => 'sql',
};

highlightOffMainThread(
SyntaxHighlightJob(
code: text,
language: lang,
themeConfigJson: themeConfig,
grammarJson: grammarJson,
wrapperArgb: wrapperColor.toARGB32(),
),
).then((segments) {
if (generation != _highlightGeneration) return;
_cachedSpan = segmentsToTextSpan(segments, baseStyle: style);
_cachedText = text;
_cachedBrightness = brightness;
notifyListeners();
});
}

@override
void dispose() {
_highlightGeneration++;
super.dispose();
}
}
Loading
Loading