Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/standalone/browser/standaloneThemeService.ts
3294 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 } 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 readonly semanticHighlighting = false;
183
}
184
185
function isBuiltinTheme(themeName: string): themeName is BuiltinTheme {
186
return (
187
themeName === VS_LIGHT_THEME_NAME
188
|| themeName === VS_DARK_THEME_NAME
189
|| themeName === HC_BLACK_THEME_NAME
190
|| themeName === HC_LIGHT_THEME_NAME
191
);
192
}
193
194
function getBuiltinRules(builtinTheme: BuiltinTheme): IStandaloneThemeData {
195
switch (builtinTheme) {
196
case VS_LIGHT_THEME_NAME:
197
return vs;
198
case VS_DARK_THEME_NAME:
199
return vs_dark;
200
case HC_BLACK_THEME_NAME:
201
return hc_black;
202
case HC_LIGHT_THEME_NAME:
203
return hc_light;
204
}
205
}
206
207
function newBuiltInTheme(builtinTheme: BuiltinTheme): StandaloneTheme {
208
const themeData = getBuiltinRules(builtinTheme);
209
return new StandaloneTheme(builtinTheme, themeData);
210
}
211
212
export class StandaloneThemeService extends Disposable implements IStandaloneThemeService {
213
214
declare readonly _serviceBrand: undefined;
215
216
private readonly _onColorThemeChange = this._register(new Emitter<IStandaloneTheme>());
217
public readonly onDidColorThemeChange = this._onColorThemeChange.event;
218
219
private readonly _onFileIconThemeChange = this._register(new Emitter<IFileIconTheme>());
220
public readonly onDidFileIconThemeChange = this._onFileIconThemeChange.event;
221
222
private readonly _onProductIconThemeChange = this._register(new Emitter<IProductIconTheme>());
223
public readonly onDidProductIconThemeChange = this._onProductIconThemeChange.event;
224
225
private readonly _environment: IEnvironmentService = Object.create(null);
226
private readonly _knownThemes: Map<string, StandaloneTheme>;
227
private _autoDetectHighContrast: boolean;
228
private _codiconCSS: string;
229
private _themeCSS: string;
230
private _allCSS: string;
231
private _globalStyleElement: HTMLStyleElement | null;
232
private _styleElements: HTMLStyleElement[];
233
private _colorMapOverride: Color[] | null;
234
private _theme!: IStandaloneTheme;
235
236
private _builtInProductIconTheme = new UnthemedProductIconTheme();
237
238
constructor() {
239
super();
240
241
this._autoDetectHighContrast = true;
242
243
this._knownThemes = new Map<string, StandaloneTheme>();
244
this._knownThemes.set(VS_LIGHT_THEME_NAME, newBuiltInTheme(VS_LIGHT_THEME_NAME));
245
this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
246
this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
247
this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME));
248
249
const iconsStyleSheet = this._register(getIconsStyleSheet(this));
250
251
this._codiconCSS = iconsStyleSheet.getCSS();
252
this._themeCSS = '';
253
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
254
this._globalStyleElement = null;
255
this._styleElements = [];
256
this._colorMapOverride = null;
257
this.setTheme(VS_LIGHT_THEME_NAME);
258
this._onOSSchemeChanged();
259
260
this._register(iconsStyleSheet.onDidChange(() => {
261
this._codiconCSS = iconsStyleSheet.getCSS();
262
this._updateCSS();
263
}));
264
265
addMatchMediaChangeListener(mainWindow, '(forced-colors: active)', () => {
266
this._onOSSchemeChanged();
267
});
268
}
269
270
public registerEditorContainer(domNode: HTMLElement): IDisposable {
271
if (dom.isInShadowDOM(domNode)) {
272
return this._registerShadowDomContainer(domNode);
273
}
274
return this._registerRegularEditorContainer();
275
}
276
277
private _registerRegularEditorContainer(): IDisposable {
278
if (!this._globalStyleElement) {
279
this._globalStyleElement = domStylesheetsJs.createStyleSheet(undefined, style => {
280
style.className = 'monaco-colors';
281
style.textContent = this._allCSS;
282
});
283
this._styleElements.push(this._globalStyleElement);
284
}
285
return Disposable.None;
286
}
287
288
private _registerShadowDomContainer(domNode: HTMLElement): IDisposable {
289
const styleElement = domStylesheetsJs.createStyleSheet(domNode, style => {
290
style.className = 'monaco-colors';
291
style.textContent = this._allCSS;
292
});
293
this._styleElements.push(styleElement);
294
return {
295
dispose: () => {
296
for (let i = 0; i < this._styleElements.length; i++) {
297
if (this._styleElements[i] === styleElement) {
298
this._styleElements.splice(i, 1);
299
return;
300
}
301
}
302
}
303
};
304
}
305
306
public defineTheme(themeName: string, themeData: IStandaloneThemeData): void {
307
if (!/^[a-z0-9\-]+$/i.test(themeName)) {
308
throw new Error('Illegal theme name!');
309
}
310
if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {
311
throw new Error('Illegal theme base!');
312
}
313
// set or replace theme
314
this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));
315
316
if (isBuiltinTheme(themeName)) {
317
this._knownThemes.forEach(theme => {
318
if (theme.base === themeName) {
319
theme.notifyBaseUpdated();
320
}
321
});
322
}
323
if (this._theme.themeName === themeName) {
324
this.setTheme(themeName); // refresh theme
325
}
326
}
327
328
public getColorTheme(): IStandaloneTheme {
329
return this._theme;
330
}
331
332
public setColorMapOverride(colorMapOverride: Color[] | null): void {
333
this._colorMapOverride = colorMapOverride;
334
this._updateThemeOrColorMap();
335
}
336
337
public setTheme(themeName: string): void {
338
let theme: StandaloneTheme | undefined;
339
if (this._knownThemes.has(themeName)) {
340
theme = this._knownThemes.get(themeName);
341
} else {
342
theme = this._knownThemes.get(VS_LIGHT_THEME_NAME);
343
}
344
this._updateActualTheme(theme);
345
}
346
347
private _updateActualTheme(desiredTheme: IStandaloneTheme | undefined): void {
348
if (!desiredTheme || this._theme === desiredTheme) {
349
// Nothing to do
350
return;
351
}
352
this._theme = desiredTheme;
353
this._updateThemeOrColorMap();
354
}
355
356
private _onOSSchemeChanged() {
357
if (this._autoDetectHighContrast) {
358
const wantsHighContrast = mainWindow.matchMedia(`(forced-colors: active)`).matches;
359
if (wantsHighContrast !== isHighContrast(this._theme.type)) {
360
// switch to high contrast or non-high contrast but stick to dark or light
361
let newThemeName;
362
if (isDark(this._theme.type)) {
363
newThemeName = wantsHighContrast ? HC_BLACK_THEME_NAME : VS_DARK_THEME_NAME;
364
} else {
365
newThemeName = wantsHighContrast ? HC_LIGHT_THEME_NAME : VS_LIGHT_THEME_NAME;
366
}
367
this._updateActualTheme(this._knownThemes.get(newThemeName));
368
}
369
}
370
}
371
372
public setAutoDetectHighContrast(autoDetectHighContrast: boolean): void {
373
this._autoDetectHighContrast = autoDetectHighContrast;
374
this._onOSSchemeChanged();
375
}
376
377
private _updateThemeOrColorMap(): void {
378
const cssRules: string[] = [];
379
const hasRule: { [rule: string]: boolean } = {};
380
const ruleCollector: ICssStyleCollector = {
381
addRule: (rule: string) => {
382
if (!hasRule[rule]) {
383
cssRules.push(rule);
384
hasRule[rule] = true;
385
}
386
}
387
};
388
themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));
389
390
const colorVariables: string[] = [];
391
for (const item of colorRegistry.getColors()) {
392
const color = this._theme.getColor(item.id, true);
393
if (color) {
394
colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
395
}
396
}
397
ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { ${colorVariables.join('\n')} }`);
398
399
const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();
400
ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));
401
402
this._themeCSS = cssRules.join('\n');
403
this._updateCSS();
404
405
TokenizationRegistry.setColorMap(colorMap);
406
this._onColorThemeChange.fire(this._theme);
407
}
408
409
private _updateCSS(): void {
410
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
411
this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);
412
}
413
414
public getFileIconTheme(): IFileIconTheme {
415
return {
416
hasFileIcons: false,
417
hasFolderIcons: false,
418
hidesExplorerArrows: false
419
};
420
}
421
422
public getProductIconTheme(): IProductIconTheme {
423
return this._builtInProductIconTheme;
424
}
425
426
}
427
428