Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/languages/supports/tokenization.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Color } from '../../../../base/common/color.js';
7
import { IFontTokenOptions } from '../../../../platform/theme/common/themeService.js';
8
import { LanguageId, FontStyle, ColorId, StandardTokenType, MetadataConsts } from '../../encodedTokenAttributes.js';
9
10
export interface ITokenThemeRule {
11
token: string;
12
foreground?: string;
13
background?: string;
14
fontStyle?: string;
15
}
16
17
export class ParsedTokenThemeRule {
18
_parsedThemeRuleBrand: void = undefined;
19
20
readonly token: string;
21
readonly index: number;
22
23
/**
24
* -1 if not set. An or mask of `FontStyle` otherwise.
25
*/
26
readonly fontStyle: FontStyle;
27
readonly foreground: string | null;
28
readonly background: string | null;
29
30
constructor(
31
token: string,
32
index: number,
33
fontStyle: number,
34
foreground: string | null,
35
background: string | null,
36
) {
37
this.token = token;
38
this.index = index;
39
this.fontStyle = fontStyle;
40
this.foreground = foreground;
41
this.background = background;
42
}
43
}
44
45
/**
46
* Parse a raw theme into rules.
47
*/
48
export function parseTokenTheme(source: ITokenThemeRule[]): ParsedTokenThemeRule[] {
49
if (!source || !Array.isArray(source)) {
50
return [];
51
}
52
const result: ParsedTokenThemeRule[] = [];
53
let resultLen = 0;
54
for (let i = 0, len = source.length; i < len; i++) {
55
const entry = source[i];
56
57
let fontStyle: number = FontStyle.NotSet;
58
if (typeof entry.fontStyle === 'string') {
59
fontStyle = FontStyle.None;
60
61
const segments = entry.fontStyle.split(' ');
62
for (let j = 0, lenJ = segments.length; j < lenJ; j++) {
63
const segment = segments[j];
64
switch (segment) {
65
case 'italic':
66
fontStyle = fontStyle | FontStyle.Italic;
67
break;
68
case 'bold':
69
fontStyle = fontStyle | FontStyle.Bold;
70
break;
71
case 'underline':
72
fontStyle = fontStyle | FontStyle.Underline;
73
break;
74
case 'strikethrough':
75
fontStyle = fontStyle | FontStyle.Strikethrough;
76
break;
77
}
78
}
79
}
80
81
let foreground: string | null = null;
82
if (typeof entry.foreground === 'string') {
83
foreground = entry.foreground;
84
}
85
86
let background: string | null = null;
87
if (typeof entry.background === 'string') {
88
background = entry.background;
89
}
90
91
result[resultLen++] = new ParsedTokenThemeRule(
92
entry.token || '',
93
i,
94
fontStyle,
95
foreground,
96
background
97
);
98
}
99
100
return result;
101
}
102
103
/**
104
* Resolve rules (i.e. inheritance).
105
*/
106
function resolveParsedTokenThemeRules(parsedThemeRules: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {
107
108
// Sort rules lexicographically, and then by index if necessary
109
parsedThemeRules.sort((a, b) => {
110
const r = strcmp(a.token, b.token);
111
if (r !== 0) {
112
return r;
113
}
114
return a.index - b.index;
115
});
116
117
// Determine defaults
118
let defaultFontStyle = FontStyle.None;
119
let defaultForeground = '000000';
120
let defaultBackground = 'ffffff';
121
while (parsedThemeRules.length >= 1 && parsedThemeRules[0].token === '') {
122
const incomingDefaults = parsedThemeRules.shift()!;
123
if (incomingDefaults.fontStyle !== FontStyle.NotSet) {
124
defaultFontStyle = incomingDefaults.fontStyle;
125
}
126
if (incomingDefaults.foreground !== null) {
127
defaultForeground = incomingDefaults.foreground;
128
}
129
if (incomingDefaults.background !== null) {
130
defaultBackground = incomingDefaults.background;
131
}
132
}
133
const colorMap = new ColorMap();
134
135
// start with token colors from custom token themes
136
for (const color of customTokenColors) {
137
colorMap.getId(color);
138
}
139
140
141
const foregroundColorId = colorMap.getId(defaultForeground);
142
const backgroundColorId = colorMap.getId(defaultBackground);
143
144
const defaults = new ThemeTrieElementRule(defaultFontStyle, foregroundColorId, backgroundColorId);
145
const root = new ThemeTrieElement(defaults);
146
for (let i = 0, len = parsedThemeRules.length; i < len; i++) {
147
const rule = parsedThemeRules[i];
148
root.insert(rule.token, rule.fontStyle, colorMap.getId(rule.foreground), colorMap.getId(rule.background));
149
}
150
151
return new TokenTheme(colorMap, root);
152
}
153
154
const colorRegExp = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/;
155
156
export class ColorMap {
157
158
private _lastColorId: number;
159
private readonly _id2color: Color[];
160
private readonly _color2id: Map<string, ColorId>;
161
162
constructor() {
163
this._lastColorId = 0;
164
this._id2color = [];
165
this._color2id = new Map<string, ColorId>();
166
}
167
168
public getId(color: string | null): ColorId {
169
if (color === null) {
170
return 0;
171
}
172
const match = color.match(colorRegExp);
173
if (!match) {
174
throw new Error('Illegal value for token color: ' + color);
175
}
176
color = match[1].toUpperCase();
177
let value = this._color2id.get(color);
178
if (value) {
179
return value;
180
}
181
value = ++this._lastColorId;
182
this._color2id.set(color, value);
183
this._id2color[value] = Color.fromHex('#' + color);
184
return value;
185
}
186
187
public getColorMap(): Color[] {
188
return this._id2color.slice(0);
189
}
190
191
}
192
193
export class TokenTheme {
194
195
public static createFromRawTokenTheme(source: ITokenThemeRule[], customTokenColors: string[]): TokenTheme {
196
return this.createFromParsedTokenTheme(parseTokenTheme(source), customTokenColors);
197
}
198
199
public static createFromParsedTokenTheme(source: ParsedTokenThemeRule[], customTokenColors: string[]): TokenTheme {
200
return resolveParsedTokenThemeRules(source, customTokenColors);
201
}
202
203
private readonly _colorMap: ColorMap;
204
private readonly _root: ThemeTrieElement;
205
private readonly _cache: Map<string, number>;
206
207
constructor(colorMap: ColorMap, root: ThemeTrieElement) {
208
this._colorMap = colorMap;
209
this._root = root;
210
this._cache = new Map<string, number>();
211
}
212
213
public getColorMap(): Color[] {
214
return this._colorMap.getColorMap();
215
}
216
217
/**
218
* used for testing purposes
219
*/
220
public getThemeTrieElement(): ExternalThemeTrieElement {
221
return this._root.toExternalThemeTrieElement();
222
}
223
224
public _match(token: string): ThemeTrieElementRule {
225
return this._root.match(token);
226
}
227
228
public match(languageId: LanguageId, token: string): number {
229
// The cache contains the metadata without the language bits set.
230
let result = this._cache.get(token);
231
if (typeof result === 'undefined') {
232
const rule = this._match(token);
233
const standardToken = toStandardTokenType(token);
234
result = (
235
rule.metadata
236
| (standardToken << MetadataConsts.TOKEN_TYPE_OFFSET)
237
) >>> 0;
238
this._cache.set(token, result);
239
}
240
241
return (
242
result
243
| (languageId << MetadataConsts.LANGUAGEID_OFFSET)
244
) >>> 0;
245
}
246
}
247
248
const STANDARD_TOKEN_TYPE_REGEXP = /\b(comment|string|regex|regexp)\b/;
249
export function toStandardTokenType(tokenType: string): StandardTokenType {
250
const m = tokenType.match(STANDARD_TOKEN_TYPE_REGEXP);
251
if (!m) {
252
return StandardTokenType.Other;
253
}
254
switch (m[1]) {
255
case 'comment':
256
return StandardTokenType.Comment;
257
case 'string':
258
return StandardTokenType.String;
259
case 'regex':
260
return StandardTokenType.RegEx;
261
case 'regexp':
262
return StandardTokenType.RegEx;
263
}
264
throw new Error('Unexpected match for standard token type!');
265
}
266
267
export function strcmp(a: string, b: string): number {
268
if (a < b) {
269
return -1;
270
}
271
if (a > b) {
272
return 1;
273
}
274
return 0;
275
}
276
277
export class ThemeTrieElementRule {
278
_themeTrieElementRuleBrand: void = undefined;
279
280
private _fontStyle: FontStyle;
281
private _foreground: ColorId;
282
private _background: ColorId;
283
public metadata: number;
284
285
constructor(fontStyle: FontStyle, foreground: ColorId, background: ColorId) {
286
this._fontStyle = fontStyle;
287
this._foreground = foreground;
288
this._background = background;
289
this.metadata = (
290
(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)
291
| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)
292
| (this._background << MetadataConsts.BACKGROUND_OFFSET)
293
) >>> 0;
294
}
295
296
public clone(): ThemeTrieElementRule {
297
return new ThemeTrieElementRule(this._fontStyle, this._foreground, this._background);
298
}
299
300
public acceptOverwrite(fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {
301
if (fontStyle !== FontStyle.NotSet) {
302
this._fontStyle = fontStyle;
303
}
304
if (foreground !== ColorId.None) {
305
this._foreground = foreground;
306
}
307
if (background !== ColorId.None) {
308
this._background = background;
309
}
310
this.metadata = (
311
(this._fontStyle << MetadataConsts.FONT_STYLE_OFFSET)
312
| (this._foreground << MetadataConsts.FOREGROUND_OFFSET)
313
| (this._background << MetadataConsts.BACKGROUND_OFFSET)
314
) >>> 0;
315
}
316
}
317
318
export class ExternalThemeTrieElement {
319
320
public readonly mainRule: ThemeTrieElementRule;
321
public readonly children: Map<string, ExternalThemeTrieElement>;
322
323
constructor(
324
mainRule: ThemeTrieElementRule,
325
children: Map<string, ExternalThemeTrieElement> | { [key: string]: ExternalThemeTrieElement } = new Map<string, ExternalThemeTrieElement>()
326
) {
327
this.mainRule = mainRule;
328
if (children instanceof Map) {
329
this.children = children;
330
} else {
331
this.children = new Map<string, ExternalThemeTrieElement>();
332
for (const key in children) {
333
this.children.set(key, children[key]);
334
}
335
}
336
}
337
}
338
339
export class ThemeTrieElement {
340
_themeTrieElementBrand: void = undefined;
341
342
private readonly _mainRule: ThemeTrieElementRule;
343
private readonly _children: Map<string, ThemeTrieElement>;
344
345
constructor(mainRule: ThemeTrieElementRule) {
346
this._mainRule = mainRule;
347
this._children = new Map<string, ThemeTrieElement>();
348
}
349
350
/**
351
* used for testing purposes
352
*/
353
public toExternalThemeTrieElement(): ExternalThemeTrieElement {
354
const children = new Map<string, ExternalThemeTrieElement>();
355
this._children.forEach((element, index) => {
356
children.set(index, element.toExternalThemeTrieElement());
357
});
358
return new ExternalThemeTrieElement(this._mainRule, children);
359
}
360
361
public match(token: string): ThemeTrieElementRule {
362
if (token === '') {
363
return this._mainRule;
364
}
365
366
const dotIndex = token.indexOf('.');
367
let head: string;
368
let tail: string;
369
if (dotIndex === -1) {
370
head = token;
371
tail = '';
372
} else {
373
head = token.substring(0, dotIndex);
374
tail = token.substring(dotIndex + 1);
375
}
376
377
const child = this._children.get(head);
378
if (typeof child !== 'undefined') {
379
return child.match(tail);
380
}
381
382
return this._mainRule;
383
}
384
385
public insert(token: string, fontStyle: FontStyle, foreground: ColorId, background: ColorId): void {
386
if (token === '') {
387
// Merge into the main rule
388
this._mainRule.acceptOverwrite(fontStyle, foreground, background);
389
return;
390
}
391
392
const dotIndex = token.indexOf('.');
393
let head: string;
394
let tail: string;
395
if (dotIndex === -1) {
396
head = token;
397
tail = '';
398
} else {
399
head = token.substring(0, dotIndex);
400
tail = token.substring(dotIndex + 1);
401
}
402
403
let child = this._children.get(head);
404
if (typeof child === 'undefined') {
405
child = new ThemeTrieElement(this._mainRule.clone());
406
this._children.set(head, child);
407
}
408
409
child.insert(tail, fontStyle, foreground, background);
410
}
411
}
412
413
export function generateTokensCSSForColorMap(colorMap: readonly Color[]): string {
414
const rules: string[] = [];
415
for (let i = 1, len = colorMap.length; i < len; i++) {
416
const color = colorMap[i];
417
rules[i] = `.mtk${i} { color: ${color}; }`;
418
}
419
rules.push('.mtki { font-style: italic; }');
420
rules.push('.mtkb { font-weight: bold; }');
421
rules.push('.mtku { text-decoration: underline; text-underline-position: under; }');
422
rules.push('.mtks { text-decoration: line-through; }');
423
rules.push('.mtks.mtku { text-decoration: underline line-through; text-underline-position: under; }');
424
return rules.join('\n');
425
}
426
427
export function generateTokensCSSForFontMap(fontMap: readonly IFontTokenOptions[]): string {
428
const rules: string[] = [];
429
const fonts = new Set<string>();
430
for (let i = 1, len = fontMap.length; i < len; i++) {
431
const font = fontMap[i];
432
if (!font.fontFamily && !font.fontSizeMultiplier) {
433
continue;
434
}
435
const className = classNameForFontTokenDecorations(font.fontFamily ?? '', font.fontSizeMultiplier ?? 0);
436
if (fonts.has(className)) {
437
continue;
438
}
439
fonts.add(className);
440
let rule = `.${className} {`;
441
if (font.fontFamily) {
442
rule += `font-family: ${font.fontFamily};`;
443
}
444
if (font.fontSizeMultiplier) {
445
rule += `font-size: calc(var(--editor-font-size)*${font.fontSizeMultiplier});`;
446
}
447
rule += `}`;
448
rules.push(rule);
449
}
450
return rules.join('\n');
451
}
452
453
export function classNameForFontTokenDecorations(fontFamily: string, fontSize: number): string {
454
const safeFontFamily = sanitizeFontFamilyForClassName(fontFamily);
455
return cleanClassName(`font-decoration-${safeFontFamily}-${fontSize}`);
456
}
457
458
function sanitizeFontFamilyForClassName(fontFamily: string): string {
459
const normalized = fontFamily.toLowerCase().trim();
460
if (!normalized) {
461
return 'default';
462
}
463
return cleanClassName(normalized);
464
}
465
466
function cleanClassName(className: string): string {
467
return className.replace(/[^a-z0-9_-]/gi, '-');
468
}
469
470