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