From 9ea12cde34e3a317385bc5113e737979f6f227e8 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 10 Oct 2026 23:29:54 +0300 Subject: [PATCH] perf(theme): cache the hash of QueryaTheme token colours QueryaCodeEditor and SyntaxHighlightService hashed every TokenColorRule on each call. The hash is now computed once per tokenColors list (an Expando keyed by the list, so lerp/copyWith themes sharing it reuse it). Closes #1353. --- lib/core/editor/querya_code_editor.dart | 2 +- lib/core/editor/syntax_highlight_service.dart | 2 +- lib/core/theme/querya_theme.dart | 11 ++++ .../querya_theme_token_colors_hash_test.dart | 51 +++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 test/core/theme/querya_theme_token_colors_hash_test.dart diff --git a/lib/core/editor/querya_code_editor.dart b/lib/core/editor/querya_code_editor.dart index 566c07d0..bf908898 100644 --- a/lib/core/editor/querya_code_editor.dart +++ b/lib/core/editor/querya_code_editor.dart @@ -97,7 +97,7 @@ class _QueryaCodeEditorState extends State { if (controller is! QueryaHighlightController) return; final editor = queryaTheme.editor; - final tokenHash = Object.hashAll(queryaTheme.tokenColors); + final tokenHash = queryaTheme.tokenColorsHash; if (_lastEditorTheme == editor && _lastTokenColorsHash == tokenHash) { return; } diff --git a/lib/core/editor/syntax_highlight_service.dart b/lib/core/editor/syntax_highlight_service.dart index d8fb06ea..e02da0ab 100644 --- a/lib/core/editor/syntax_highlight_service.dart +++ b/lib/core/editor/syntax_highlight_service.dart @@ -64,7 +64,7 @@ abstract final class SyntaxHighlightService { final cacheKey = Object.hash( language, queryaTheme.editor, - Object.hashAll(queryaTheme.tokenColors), + queryaTheme.tokenColorsHash, ); final cached = _pairCache[cacheKey]; if (cached != null) return cached; diff --git a/lib/core/theme/querya_theme.dart b/lib/core/theme/querya_theme.dart index 0215fee7..2a96df06 100644 --- a/lib/core/theme/querya_theme.dart +++ b/lib/core/theme/querya_theme.dart @@ -24,6 +24,17 @@ class QueryaTheme { /// VS Code `tokenColors` for syntax highlighting (imported themes). final List tokenColors; + /// Hashes of token colour lists, keyed by the list itself: themes built by + /// [copyWith] and [lerp] (one per animation frame) share the list, so each + /// list is hashed once however many themes point at it. + static final Expando _tokenColorsHashes = + Expando('QueryaTheme.tokenColorsHash'); + + /// Hash of [tokenColors], computed once per list (#1353): the editor asks + /// for it on every build, and a full VS Code theme has hundreds of rules. + int get tokenColorsHash => + _tokenColorsHashes[tokenColors] ??= Object.hashAll(tokenColors); + static const QueryaTheme darkDefault = QueryaTheme( workbench: QueryaWorkbenchTheme.darkDefault, editor: QueryaEditorTheme.darkDefault, diff --git a/test/core/theme/querya_theme_token_colors_hash_test.dart b/test/core/theme/querya_theme_token_colors_hash_test.dart new file mode 100644 index 00000000..2c352442 --- /dev/null +++ b/test/core/theme/querya_theme_token_colors_hash_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/theme/parser/vscode_theme_manifest.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; + +void main() { + const rules = [ + TokenColorRule(scopes: ['keyword'], foreground: '#ff0000'), + TokenColorRule(scopes: ['string', 'string.quoted'], foreground: '#00ff00'), + ]; + + group('QueryaTheme.tokenColorsHash (#1353)', () { + test('equals the hash of the token colours', () { + final theme = QueryaTheme.darkDefault.copyWith(tokenColors: rules); + + expect(theme.tokenColorsHash, Object.hashAll(rules)); + }); + + test('is stable across reads', () { + final theme = QueryaTheme.darkDefault.copyWith(tokenColors: rules); + + expect(theme.tokenColorsHash, theme.tokenColorsHash); + }); + + test('themes sharing a list share the hash', () { + final a = QueryaTheme.darkDefault.copyWith(tokenColors: rules); + final b = a.copyWith(brightness: a.brightness); + final mid = QueryaTheme.lerp(a, b, 0.25); + + expect(b.tokenColorsHash, a.tokenColorsHash); + expect(mid.tokenColorsHash, a.tokenColorsHash); + }); + + test('different rules give a different hash', () { + final a = QueryaTheme.darkDefault.copyWith(tokenColors: rules); + final b = QueryaTheme.darkDefault.copyWith( + tokenColors: const [ + TokenColorRule(scopes: ['comment'], foreground: '#888888'), + ], + ); + + expect(a.tokenColorsHash, isNot(b.tokenColorsHash)); + }); + + test('a theme without token colours has the empty-list hash', () { + expect( + QueryaTheme.darkDefault.tokenColorsHash, + Object.hashAll(const []), + ); + }); + }); +}