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
37 changes: 29 additions & 8 deletions lib/core/editor/syntax_highlight_isolate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, TextStyle> _tokenStyles = <int, TextStyle>{};
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,
);
}

Expand Down
67 changes: 67 additions & 0 deletions test/core/editor/syntax_highlight_isolate_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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));
}
});
});
}
Loading