From 64b81eeaef4a037b56ba08f6ed7343474f94397e Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 17 Oct 2026 11:15:03 +0300 Subject: [PATCH] perf(editor): share token TextStyles between segments instead of copyWith per token (#1543) --- lib/core/editor/syntax_highlight_isolate.dart | 37 +++++++--- .../editor/syntax_highlight_isolate_test.dart | 67 +++++++++++++++++++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/lib/core/editor/syntax_highlight_isolate.dart b/lib/core/editor/syntax_highlight_isolate.dart index 7250b005..b9dab902 100644 --- a/lib/core/editor/syntax_highlight_isolate.dart +++ b/lib/core/editor/syntax_highlight_isolate.dart @@ -212,17 +212,38 @@ FontWeight? _fontWeightFromValue(int? value) { return null; } +/// Token styles built for [_cachedBase] (#1543). A theme has a handful of +/// distinct token looks, but a document has hundreds of tokens, and the spans +/// are rebuilt on every keystroke: the styles are made once per base style and +/// shared by every segment that looks the same. +const Object _noBase = Object(); +Object? _cachedBase = _noBase; +final Map _tokenStyles = {}; +const int _maxTokenStyles = 256; + TextStyle? _styleFromSegment(HighlightSegment s, TextStyle? base) { - if (s.colorArgb == null && - s.fontWeightValue == null && - s.fontStyleIndex == null) { + final argb = s.colorArgb; + final weight = s.fontWeightValue; + final fontStyle = s.fontStyleIndex; + if (argb == null && weight == null && fontStyle == null) { return null; } - return (base ?? const TextStyle()).copyWith( - color: s.colorArgb != null ? Color(s.colorArgb!) : null, - fontWeight: _fontWeightFromValue(s.fontWeightValue), - fontStyle: - s.fontStyleIndex != null ? FontStyle.values[s.fontStyleIndex!] : null, + if (!identical(_cachedBase, base)) { + _cachedBase = base; + _tokenStyles.clear(); + } else if (_tokenStyles.length >= _maxTokenStyles) { + _tokenStyles.clear(); + } + // Colour 32 bits, +1 bit "has a colour", weight 100..900 as 0..9, style as + // 0 (none) or index + 1. + final key = (argb ?? 0) | + ((argb != null ? 1 : 0) << 32) | + (((weight ?? 0) ~/ 100) << 33) | + ((fontStyle != null ? fontStyle + 1 : 0) << 37); + return _tokenStyles[key] ??= (base ?? const TextStyle()).copyWith( + color: argb != null ? Color(argb) : null, + fontWeight: _fontWeightFromValue(weight), + fontStyle: fontStyle != null ? FontStyle.values[fontStyle] : null, ); } diff --git a/test/core/editor/syntax_highlight_isolate_test.dart b/test/core/editor/syntax_highlight_isolate_test.dart index 601e6407..32974c59 100644 --- a/test/core/editor/syntax_highlight_isolate_test.dart +++ b/test/core/editor/syntax_highlight_isolate_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/editor/syntax_highlight_isolate.dart'; import 'package:querya_desktop/core/editor/querya_code_language.dart'; @@ -56,4 +57,70 @@ void main() { expect(segments, isNotEmpty); expect(segments.map((s) => s.text).join(), code); }); + + group('segmentsToTextSpan style sharing (#1543)', () { + const keyword = HighlightSegment( + text: 'SELECT', + colorArgb: 0xFF2196F3, + fontWeightValue: 700, + ); + const keywordAgain = HighlightSegment( + text: 'FROM', + colorArgb: 0xFF2196F3, + fontWeightValue: 700, + ); + const comment = HighlightSegment( + text: '-- x', + colorArgb: 0xFF808080, + fontStyleIndex: 1, + ); + const plain = HighlightSegment(text: ' '); + + TextStyle? styleOf(TextSpan span, int i) => + (span.children![i] as TextSpan).style; + + test('segments that look the same share one TextStyle', () { + const base = TextStyle(fontSize: 14); + final span = segmentsToTextSpan( + [keyword, plain, keywordAgain, comment], + baseStyle: base, + ); + expect(identical(styleOf(span, 0), styleOf(span, 2)), isTrue); + expect(identical(styleOf(span, 0), styleOf(span, 3)), isFalse); + expect(styleOf(span, 1), isNull); // no colour, weight or style + }); + + test('styles keep the base style and the segment attributes', () { + const base = TextStyle(fontSize: 14); + final span = segmentsToTextSpan([keyword, comment], baseStyle: base); + final kw = styleOf(span, 0)!; + expect(kw.fontSize, 14); + expect(kw.color, const Color(0xFF2196F3)); + expect(kw.fontWeight, FontWeight.w700); + expect(kw.fontStyle, isNull); + final cm = styleOf(span, 1)!; + expect(cm.color, const Color(0xFF808080)); + expect(cm.fontStyle, FontStyle.italic); + expect(cm.fontWeight, isNull); + }); + + test('a different base style is not served the old styles', () { + final a = segmentsToTextSpan([keyword], baseStyle: const TextStyle(fontSize: 12)); + final b = segmentsToTextSpan([keyword], baseStyle: const TextStyle(fontSize: 18)); + expect(styleOf(a, 0)!.fontSize, 12); + expect(styleOf(b, 0)!.fontSize, 18); + }); + + test('a null base style works and more looks than the cache holds do not break', + () { + final segments = [ + for (var i = 0; i < 600; i++) + HighlightSegment(text: 't', colorArgb: 0xFF000000 + i), + ]; + final span = segmentsToTextSpan(segments); + for (var i = 0; i < segments.length; i++) { + expect(styleOf(span, i)!.color, Color(0xFF000000 + i)); + } + }); + }); }