Skip to content

Commit e95092e

Browse files
Merge pull request #76 from QueryaHub/issue/46-token-colors
feat(theme): VS Code tokenColors and TextMate scope resolution (#46)
2 parents 629e113 + 775d2d8 commit e95092e

19 files changed

Lines changed: 838 additions & 105 deletions

docs/theme-import.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Querya can apply a **subset** of VS Code theme JSON / JSONC `colors` to
44
`QueryaWorkbenchTheme`, `QueryaEditorTheme`, and the shadcn `ColorScheme`.
55

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

89
## Supported `colors` keys
910

Lines changed: 18 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,33 @@
1-
import 'dart:convert';
2-
31
import 'package:flutter/material.dart';
2+
import 'package:querya_desktop/core/theme/parser/token_colors_highlighter_config.dart';
3+
import 'package:querya_desktop/core/theme/parser/vscode_theme_manifest.dart';
44
import 'package:querya_desktop/core/theme/querya_editor_theme.dart';
55
import 'package:syntax_highlight/syntax_highlight.dart';
66

7-
/// Builds a [HighlighterTheme] from [QueryaEditorTheme] token colors.
8-
HighlighterTheme highlighterThemeFromQueryaEditor(QueryaEditorTheme editor) {
7+
/// Builds a [HighlighterTheme] from [QueryaEditorTheme] and optional VS Code rules.
8+
HighlighterTheme highlighterThemeFromQueryaEditor(
9+
QueryaEditorTheme editor, {
10+
List<TokenColorRule> tokenColors = const [],
11+
}) {
912
final wrapper = TextStyle(
1013
color: editor.foreground,
1114
fontFamily: editor.fontFamily,
1215
fontSize: editor.fontSize,
1316
);
1417

15-
final config = jsonEncode({
16-
'settings': [
17-
{
18-
'scope': [
19-
'comment',
20-
'comment.line',
21-
'comment.block',
22-
],
23-
'settings': {'foreground': _hex(editor.comment)},
24-
},
25-
{
26-
'scope': [
27-
'keyword',
28-
'keyword.control',
29-
'keyword.operator',
30-
'storage.type',
31-
],
32-
'settings': {'foreground': _hex(editor.keyword)},
33-
},
34-
{
35-
'scope': [
36-
'string',
37-
'string.quoted',
38-
'string.quoted.single',
39-
'string.quoted.double',
40-
],
41-
'settings': {'foreground': _hex(editor.string)},
42-
},
43-
{
44-
'scope': [
45-
'constant.numeric',
46-
'constant.numeric.json',
47-
'number',
48-
],
49-
'settings': {'foreground': _hex(editor.number)},
50-
},
51-
{
52-
'scope': [
53-
'support.type.property-name',
54-
'support.type.property-name.json',
55-
],
56-
'settings': {'foreground': _hex(editor.type)},
57-
},
58-
{
59-
'scope': [
60-
'constant.language',
61-
'constant.language.json',
62-
],
63-
'settings': {'foreground': _hex(editor.keyword)},
64-
},
65-
{
66-
'scope': ['entity.name.function', 'support.function'],
67-
'settings': {'foreground': _hex(editor.function)},
68-
},
69-
{
70-
'scope': ['entity.name.type', 'support.type'],
71-
'settings': {'foreground': _hex(editor.type)},
72-
},
73-
{
74-
'scope': ['constant.language', 'variable.language'],
75-
'settings': {'foreground': _hex(editor.keyword)},
76-
},
77-
{
78-
'settings': {'foreground': _hex(editor.foreground)},
79-
},
80-
],
81-
});
18+
final config = tokenColors.isNotEmpty
19+
? buildHighlighterConfigFromTokenColors(tokenColors, editor.foreground)
20+
: buildDefaultEditorHighlighterConfig(editor);
8221

8322
return HighlighterTheme.fromConfiguration(config, wrapper);
8423
}
8524

86-
String _hex(Color c) {
87-
final a = (c.a * 255).round().clamp(0, 255);
88-
final r = (c.r * 255).round().clamp(0, 255);
89-
final g = (c.g * 255).round().clamp(0, 255);
90-
final b = (c.b * 255).round().clamp(0, 255);
91-
if (a < 255) {
92-
return '#${r.toRadixString(16).padLeft(2, '0')}'
93-
'${g.toRadixString(16).padLeft(2, '0')}'
94-
'${b.toRadixString(16).padLeft(2, '0')}'
95-
'${a.toRadixString(16).padLeft(2, '0')}';
96-
}
97-
return '#${r.toRadixString(16).padLeft(2, '0')}'
98-
'${g.toRadixString(16).padLeft(2, '0')}'
99-
'${b.toRadixString(16).padLeft(2, '0')}';
25+
/// JSON config for isolate/off-thread highlighting.
26+
String highlighterThemeConfigJson(
27+
QueryaEditorTheme editor, {
28+
List<TokenColorRule> tokenColors = const [],
29+
}) {
30+
return tokenColors.isNotEmpty
31+
? buildHighlighterConfigFromTokenColors(tokenColors, editor.foreground)
32+
: buildDefaultEditorHighlighterConfig(editor);
10033
}

lib/core/editor/querya_code_editor.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
6363
bool _syncing = false;
6464
QueryaEditorTheme? _highlightEditorTheme;
6565
QueryaCodeLanguage? _highlightLanguage;
66+
int _highlightTokenColorsHash = 0;
6667

6768
material.TextEditingController get _activeController =>
6869
_highlightController ?? _plainController!;
@@ -133,15 +134,18 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
133134
_highlightController = null;
134135
_highlightEditorTheme = null;
135136
_highlightLanguage = null;
137+
_highlightTokenColorsHash = 0;
136138
}
137139

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

141143
final editor = queryaTheme.editor;
144+
final tokenHash = Object.hashAll(queryaTheme.tokenColors);
142145
if (_highlightController != null &&
143146
_highlightEditorTheme == editor &&
144-
_highlightLanguage == widget.language) {
147+
_highlightLanguage == widget.language &&
148+
_highlightTokenColorsHash == tokenHash) {
145149
return;
146150
}
147151

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

158162
_highlightController = QueryaHighlightController(
159163
text: text,
164+
language: widget.language,
160165
lightHighlighter: pair.light,
161166
darkHighlighter: pair.dark,
167+
lightThemeConfig: pair.lightThemeConfig,
168+
darkThemeConfig: pair.darkThemeConfig,
169+
grammarJson: pair.grammarJson,
170+
wrapperColor: editor.foreground,
162171
);
163172
_ownsHighlightController = external == null;
164173
_highlightEditorTheme = editor;
165174
_highlightLanguage = widget.language;
175+
_highlightTokenColorsHash = tokenHash;
166176

167177
_highlightController!.addListener(_onTextChanged);
168178
if (external != null) {
@@ -235,6 +245,7 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
235245
oldWidget.enableHighlighting != widget.enableHighlighting) {
236246
_highlightEditorTheme = null;
237247
_highlightLanguage = null;
248+
_highlightTokenColorsHash = 0;
238249
if (_useHighlighting) {
239250
_ensureHighlightController(context.queryaTheme);
240251
} else {
Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,109 @@
11
import 'package:flutter/material.dart';
2+
import 'package:querya_desktop/core/editor/querya_code_language.dart';
23
import 'package:syntax_highlight/syntax_highlight.dart';
34

5+
import 'syntax_highlight_isolate.dart';
6+
47
/// [TextEditingController] that applies [Highlighter] in [buildTextSpan].
58
class QueryaHighlightController extends TextEditingController {
69
QueryaHighlightController({
710
super.text,
11+
required this.language,
812
required this.lightHighlighter,
913
required this.darkHighlighter,
14+
required this.lightThemeConfig,
15+
required this.darkThemeConfig,
16+
required this.grammarJson,
17+
required this.wrapperColor,
1018
});
1119

20+
final QueryaCodeLanguage language;
1221
final Highlighter lightHighlighter;
1322
final Highlighter darkHighlighter;
23+
final String lightThemeConfig;
24+
final String darkThemeConfig;
25+
final String grammarJson;
26+
final Color wrapperColor;
27+
28+
TextSpan? _cachedSpan;
29+
String? _cachedText;
30+
Brightness? _cachedBrightness;
31+
int _highlightGeneration = 0;
1432

1533
@override
1634
TextSpan buildTextSpan({
1735
required BuildContext context,
1836
TextStyle? style,
1937
required bool withComposing,
2038
}) {
21-
final highlighter = Theme.of(context).brightness == Brightness.light
39+
final brightness = Theme.of(context).brightness;
40+
final highlighter = brightness == Brightness.light
2241
? lightHighlighter
2342
: darkHighlighter;
24-
return TextSpan(
43+
final themeConfig = brightness == Brightness.light
44+
? lightThemeConfig
45+
: darkThemeConfig;
46+
47+
if (text.length < kSyntaxHighlightIsolateThreshold) {
48+
return TextSpan(
49+
style: style,
50+
children: [highlighter.highlight(text)],
51+
);
52+
}
53+
54+
if (_cachedText == text &&
55+
_cachedBrightness == brightness &&
56+
_cachedSpan != null) {
57+
return TextSpan(style: style, children: [_cachedSpan!]);
58+
}
59+
60+
_scheduleIsolateHighlight(
61+
text: text,
62+
brightness: brightness,
63+
themeConfig: themeConfig,
2564
style: style,
26-
children: [highlighter.highlight(text)],
2765
);
66+
67+
if (_cachedSpan != null && _cachedText == text) {
68+
return TextSpan(style: style, children: [_cachedSpan!]);
69+
}
70+
71+
return TextSpan(style: style, text: text);
72+
}
73+
74+
void _scheduleIsolateHighlight({
75+
required String text,
76+
required Brightness brightness,
77+
required String themeConfig,
78+
required TextStyle? style,
79+
}) {
80+
final generation = ++_highlightGeneration;
81+
final lang = switch (language) {
82+
QueryaCodeLanguage.sql => 'sql',
83+
QueryaCodeLanguage.json => 'json',
84+
QueryaCodeLanguage.plain => 'sql',
85+
};
86+
87+
highlightOffMainThread(
88+
SyntaxHighlightJob(
89+
code: text,
90+
language: lang,
91+
themeConfigJson: themeConfig,
92+
grammarJson: grammarJson,
93+
wrapperArgb: wrapperColor.toARGB32(),
94+
),
95+
).then((segments) {
96+
if (generation != _highlightGeneration) return;
97+
_cachedSpan = segmentsToTextSpan(segments, baseStyle: style);
98+
_cachedText = text;
99+
_cachedBrightness = brightness;
100+
notifyListeners();
101+
});
102+
}
103+
104+
@override
105+
void dispose() {
106+
_highlightGeneration++;
107+
super.dispose();
28108
}
29109
}

0 commit comments

Comments
 (0)