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 []), + ); + }); + }); +}