Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/standalone/browser/standaloneThemeService.ts
5240 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 * as dom from '../../../base/browser/dom.js';
7
import * as domStylesheetsJs from '../../../base/browser/domStylesheets.js';
8
import { addMatchMediaChangeListener } from '../../../base/browser/browser.js';
9
import { Color } from '../../../base/common/color.js';
10
import { Emitter } from '../../../base/common/event.js';
11
import { TokenizationRegistry } from '../../common/languages.js';
12
import { FontStyle, TokenMetadata } from '../../common/encodedTokenAttributes.js';
13
import { ITokenThemeRule, TokenTheme, generateTokensCSSForColorMap } from '../../common/languages/supports/tokenization.js';
14
import { BuiltinTheme, IStandaloneTheme, IStandaloneThemeData, IStandaloneThemeService } from '../common/standaloneTheme.js';
15
import { hc_black, hc_light, vs, vs_dark } from '../common/themes.js';
16
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
17
import { Registry } from '../../../platform/registry/common/platform.js';
18
import { asCssVariableName, ColorIdentifier, Extensions, IColorRegistry } from '../../../platform/theme/common/colorRegistry.js';
19
import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IProductIconTheme, IThemingRegistry, ITokenStyle, IFontTokenOptions } from '../../../platform/theme/common/themeService.js';
20
import { IDisposable, Disposable } from '../../../base/common/lifecycle.js';
21
import { ColorScheme, isDark, isHighContrast } from '../../../platform/theme/common/theme.js';
22
import { getIconsStyleSheet, UnthemedProductIconTheme } from '../../../platform/theme/browser/iconsStyleSheet.js';
23
import { mainWindow } from '../../../base/browser/window.js';
24
25
export const VS_LIGHT_THEME_NAME = 'vs';
26
export const VS_DARK_THEME_NAME = 'vs-dark';
27
export const HC_BLACK_THEME_NAME = 'hc-black';
28
export const HC_LIGHT_THEME_NAME = 'hc-light';
29
30
const colorRegistry = Registry.as<IColorRegistry>(Extensions.ColorContribution);
31
const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);
32
33
class StandaloneTheme implements IStandaloneTheme {
34
35
public readonly id: string;
36
public readonly themeName: string;
37
38
private readonly themeData: IStandaloneThemeData;
39
private colors: Map<string, Color> | null;
40
private readonly defaultColors: { [colorId: string]: Color | undefined };
41
private _tokenTheme: TokenTheme | null;
42
43
constructor(name: string, standaloneThemeData: IStandaloneThemeData) {
44
this.themeData = standaloneThemeData;
45
const base = standaloneThemeData.base;
46
if (name.length > 0) {
47
if (isBuiltinTheme(name)) {
48
this.id = name;
49
} else {
50
this.id = base + ' ' + name;
51
}
52
this.themeName = name;
53
} else {
54
this.id = base;
55
this.themeName = base;
56
}
57
this.colors = null;
58
this.defaultColors = Object.create(null);
59
this._tokenTheme = null;
60
}
61
62
public get label(): string {
63
return this.themeName;
64
}
65
66
public get base(): string {
67
return this.themeData.base;
68
}
69
70
public notifyBaseUpdated() {
71
if (this.themeData.inherit) {
72
this.colors = null;
73
this._tokenTheme = null;
74
}
75
}
76
77
private getColors(): Map<string, Color> {
78
if (!this.colors) {
79
const colors = new Map<string, Color>();
80
for (const id in this.themeData.colors) {
81
colors.set(id, Color.fromHex(this.themeData.colors[id]));
82
}
83
if (this.themeData.inherit) {
84
const baseData = getBuiltinRules(this.themeData.base);
85
for (const id in baseData.colors) {
86
if (!colors.has(id)) {
87
colors.set(id, Color.fromHex(baseData.colors[id]));
88
}
89
}
90
}
91
this.colors = colors;
92
}
93
return this.colors;
94
}
95
96
public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {
97
const color = this.getColors().get(colorId);
98
if (color) {
99
return color;
100
}
101
if (useDefault !== false) {
102
return this.getDefault(colorId);
103
}
104
return undefined;
105
}
106
107
private getDefault(colorId: ColorIdentifier): Color | undefined {
108
let color = this.defaultColors[colorId];
109
if (color) {
110
return color;
111
}
112
color = colorRegistry.resolveDefaultColor(colorId, this);
113
this.defaultColors[colorId] = color;
114
return color;
115
}
116
117
public defines(colorId: ColorIdentifier): boolean {
118
return this.getColors().has(colorId);
119
}
120
121
public get type(): ColorScheme {
122
switch (this.base) {
123
case VS_LIGHT_THEME_NAME: return ColorScheme.LIGHT;
124
case HC_BLACK_THEME_NAME: return ColorScheme.HIGH_CONTRAST_DARK;
125
case HC_LIGHT_THEME_NAME: return ColorScheme.HIGH_CONTRAST_LIGHT;
126
default: return ColorScheme.DARK;
127
}
128
}
129
130
public get tokenTheme(): TokenTheme {
131
if (!this._tokenTheme) {
132
let rules: ITokenThemeRule[] = [];
133
let encodedTokensColors: string[] = [];
134
if (this.themeData.inherit) {
135
const baseData = getBuiltinRules(this.themeData.base);
136
rules = baseData.rules;
137
if (baseData.encodedTokensColors) {
138
encodedTokensColors = baseData.encodedTokensColors;
139
}
140
}
141
// Pick up default colors from `editor.foreground` and `editor.background` if available
142
const editorForeground = this.themeData.colors['editor.foreground'];
143
const editorBackground = this.themeData.colors['editor.background'];
144
if (editorForeground || editorBackground) {
145
const rule: ITokenThemeRule = { token: '' };
146
if (editorForeground) {
147
rule.foreground = editorForeground;
148
}
149
if (editorBackground) {
150
rule.background = editorBackground;
151
}
152
rules.push(rule);
153
}
154
rules = rules.concat(this.themeData.rules);
155
if (this.themeData.encodedTokensColors) {
156
encodedTokensColors = this.themeData.encodedTokensColors;
157
}
158
this._tokenTheme = TokenTheme.createFromRawTokenTheme(rules, encodedTokensColors);
159
}
160
return this._tokenTheme;
161
}
162
163
public getTokenStyleMetadata(type: string, modifiers: string[], modelLanguage: string): ITokenStyle | undefined {
164
// use theme rules match
165
const style = this.tokenTheme._match([type].concat(modifiers).join('.'));
166
const metadata = style.metadata;
167
const foreground = TokenMetadata.getForeground(metadata);
168
const fontStyle = TokenMetadata.getFontStyle(metadata);
169
return {
170
foreground: foreground,
171
italic: Boolean(fontStyle & FontStyle.Italic),
172
bold: Boolean(fontStyle & FontStyle.Bold),
173
underline: Boolean(fontStyle & FontStyle.Underline),
174
strikethrough: Boolean(fontStyle & FontStyle.Strikethrough)
175
};
176
}
177
178
public get tokenColorMap(): string[] {
179
return [];
180
}
181
182
public get tokenFontMap(): IFontTokenOptions[] {
183
return [];
184
}
185
186
public readonly semanticHighlighting = false;
187
}
188
189
function isBuiltinTheme(themeName: string): themeName is BuiltinTheme {
190
return (
191
themeName === VS_LIGHT_THEME_NAME
192
|| themeName === VS_DARK_THEME_NAME
193
|| themeName === HC_BLACK_THEME_NAME
194
|| themeName === HC_LIGHT_THEME_NAME
195
);
196
}
197
198
function getBuiltinRules(builtinTheme: BuiltinTheme): IStandaloneThemeData {
199
switch (builtinTheme) {
200
case VS_LIGHT_THEME_NAME:
201
return vs;
202
case VS_DARK_THEME_NAME:
203
return vs_dark;
204
case HC_BLACK_THEME_NAME:
205
return hc_black;
206
case HC_LIGHT_THEME_NAME:
207
return hc_light;
208
}
209
}
210
211
function newBuiltInTheme(builtinTheme: BuiltinTheme): StandaloneTheme {
212
const themeData = getBuiltinRules(builtinTheme);
213
return new StandaloneTheme(builtinTheme, themeData);
214
}
215
216
export class StandaloneThemeService extends Disposable implements IStandaloneThemeService {
217
218
declare readonly _serviceBrand: undefined;
219
220
private readonly _onColorThemeChange = this._register(new Emitter<IStandaloneTheme>());
221
public readonly onDidColorThemeChange = this._onColorThemeChange.event;
222
223
private readonly _onFileIconThemeChange = this._register(new Emitter<IFileIconTheme>());
224
public readonly onDidFileIconThemeChange = this._onFileIconThemeChange.event;
225
226
private readonly _onProductIconThemeChange = this._register(new Emitter<IProductIconTheme>());
227
public readonly onDidProductIconThemeChange = this._onProductIconThemeChange.event;
228
229
private readonly _environment: IEnvironmentService = Object.create(null);
230
private readonly _knownThemes: Map<string, StandaloneTheme>;
231
private _autoDetectHighContrast: boolean;
232
private _codiconCSS: string;
233
private _themeCSS: string;
234
private _allCSS: string;
235
private _globalStyleElement: HTMLStyleElement | null;
236
private _styleElements: HTMLStyleElement[];
237
private _colorMapOverride: Color[] | null;
238
private _theme!: IStandaloneTheme;
239
240
private _builtInProductIconTheme = new UnthemedProductIconTheme();
241
242
constructor() {
243
super();
244
245
this._autoDetectHighContrast = true;
246
247
this._knownThemes = new Map<string, StandaloneTheme>();
248
this._knownThemes.set(VS_LIGHT_THEME_NAME, newBuiltInTheme(VS_LIGHT_THEME_NAME));
249
this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
250
this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
251
this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME));
252
253
const iconsStyleSheet = this._register(getIconsStyleSheet(this));
254
255
this._codiconCSS = iconsStyleSheet.getCSS();
256
this._themeCSS = '';
257
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
258
this._globalStyleElement = null;
259
this._styleElements = [];
260
this._colorMapOverride = null;
261
this.setTheme(VS_LIGHT_THEME_NAME);
262
this._onOSSchemeChanged();
263
264
this._register(iconsStyleSheet.onDidChange(() => {
265
this._codiconCSS = iconsStyleSheet.getCSS();
266
this._updateCSS();
267
}));
268
269
addMatchMediaChangeListener(mainWindow, '(forced-colors: active)', () => {
270
// Update theme selection for auto-detecting high contrast
271
this._onOSSchemeChanged();
272
});
273
}
274
275
public registerEditorContainer(domNode: HTMLElement): IDisposable {
276
if (dom.isInShadowDOM(domNode)) {
277
return this._registerShadowDomContainer(domNode);
278
}
279
return this._registerRegularEditorContainer();
280
}
281
282
private _registerRegularEditorContainer(): IDisposable {
283
if (!this._globalStyleElement) {
284
this._globalStyleElement = domStylesheetsJs.createStyleSheet(undefined, style => {
285
style.className = 'monaco-colors';
286
style.textContent = this._allCSS;
287
});
288
this._styleElements.push(this._globalStyleElement);
289
}
290
return Disposable.None;
291
}
292
293
private _registerShadowDomContainer(domNode: HTMLElement): IDisposable {
294
const styleElement = domStylesheetsJs.createStyleSheet(domNode, style => {
295
style.className = 'monaco-colors';
296
style.textContent = this._allCSS;
297
});
298
this._styleElements.push(styleElement);
299
return {
300
dispose: () => {
301
for (let i = 0; i < this._styleElements.length; i++) {
302
if (this._styleElements[i] === styleElement) {
303
this._styleElements.splice(i, 1);
304
return;
305
}
306
}
307
}
308
};
309
}
310
311
public defineTheme(themeName: string, themeData: IStandaloneThemeData): void {
312
if (!/^[a-z0-9\-]+$/i.test(themeName)) {
313
throw new Error('Illegal theme name!');
314
}
315
if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {
316
throw new Error('Illegal theme base!');
317
}
318
// set or replace theme
319
this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));
320
321
if (isBuiltinTheme(themeName)) {
322
this._knownThemes.forEach(theme => {
323
if (theme.base === themeName) {
324
theme.notifyBaseUpdated();
325
}
326
});
327
}
328
if (this._theme.themeName === themeName) {
329
this.setTheme(themeName); // refresh theme
330
}
331
}
332
333
public getColorTheme(): IStandaloneTheme {
334
return this._theme;
335
}
336
337
public setColorMapOverride(colorMapOverride: Color[] | null): void {
338
this._colorMapOverride = colorMapOverride;
339
this._updateThemeOrColorMap();
340
}
341
342
public setTheme(themeName: string): void {
343
let theme: StandaloneTheme | undefined;
344
if (this._knownThemes.has(themeName)) {
345
theme = this._knownThemes.get(themeName);
346
} else {
347
theme = this._knownThemes.get(VS_LIGHT_THEME_NAME);
348
}
349
this._updateActualTheme(theme);
350
}
351
352
private _updateActualTheme(desiredTheme: IStandaloneTheme | undefined): void {
353
if (!desiredTheme || this._theme === desiredTheme) {
354
// Nothing to do
355
return;
356
}
357
this._theme = desiredTheme;
358
this._updateThemeOrColorMap();
359
}
360
361
private _onOSSchemeChanged() {
362
if (this._autoDetectHighContrast) {
363
const wantsHighContrast = mainWindow.matchMedia(`(forced-colors: active)`).matches;
364
if (wantsHighContrast !== isHighContrast(this._theme.type)) {
365
// switch to high contrast or non-high contrast but stick to dark or light
366
let newThemeName;
367
if (isDark(this._theme.type)) {
368
newThemeName = wantsHighContrast ? HC_BLACK_THEME_NAME : VS_DARK_THEME_NAME;
369
} else {
370
newThemeName = wantsHighContrast ? HC_LIGHT_THEME_NAME : VS_LIGHT_THEME_NAME;
371
}
372
this._updateActualTheme(this._knownThemes.get(newThemeName));
373
}
374
}
375
}
376
377
public setAutoDetectHighContrast(autoDetectHighContrast: boolean): void {
378
this._autoDetectHighContrast = autoDetectHighContrast;
379
this._onOSSchemeChanged();
380
}
381
382
private _updateThemeOrColorMap(): void {
383
const cssRules: string[] = [];
384
const hasRule: { [rule: string]: boolean } = {};
385
const ruleCollector: ICssStyleCollector = {
386
addRule: (rule: string) => {
387
if (!hasRule[rule]) {
388
cssRules.push(rule);
389
hasRule[rule] = true;
390
}
391
}
392
};
393
themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));
394
395
const colorVariables: string[] = [];
396
for (const item of colorRegistry.getColors()) {
397
const color = this._theme.getColor(item.id, true);
398
if (color) {
399
colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
400
}
401
}
402
ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { ${colorVariables.join('\n')} }`);
403
404
const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();
405
ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));
406
407
// If the OS has forced-colors active, disable forced color adjustment for
408
// Monaco editor elements so that VS Code's built-in high contrast themes
409
// (hc-black / hc-light) are used instead of the OS forcing system colors.
410
ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { forced-color-adjust: none; }`);
411
412
this._themeCSS = cssRules.join('\n');
413
this._updateCSS();
414
415
TokenizationRegistry.setColorMap(colorMap);
416
this._onColorThemeChange.fire(this._theme);
417
}
418
419
private _updateCSS(): void {
420
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
421
this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);
422
}
423
424
public getFileIconTheme(): IFileIconTheme {
425
return {
426
hasFileIcons: false,
427
hasFolderIcons: false,
428
hidesExplorerArrows: false
429
};
430
}
431
432
public getProductIconTheme(): IProductIconTheme {
433
return this._builtInProductIconTheme;
434
}
435
436
}
437
438