Path: blob/main/src/vs/editor/standalone/browser/standaloneThemeService.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../base/browser/dom.js';6import * as domStylesheetsJs from '../../../base/browser/domStylesheets.js';7import { addMatchMediaChangeListener } from '../../../base/browser/browser.js';8import { Color } from '../../../base/common/color.js';9import { Emitter } from '../../../base/common/event.js';10import { TokenizationRegistry } from '../../common/languages.js';11import { FontStyle, TokenMetadata } from '../../common/encodedTokenAttributes.js';12import { ITokenThemeRule, TokenTheme, generateTokensCSSForColorMap } from '../../common/languages/supports/tokenization.js';13import { BuiltinTheme, IStandaloneTheme, IStandaloneThemeData, IStandaloneThemeService } from '../common/standaloneTheme.js';14import { hc_black, hc_light, vs, vs_dark } from '../common/themes.js';15import { IEnvironmentService } from '../../../platform/environment/common/environment.js';16import { Registry } from '../../../platform/registry/common/platform.js';17import { asCssVariableName, ColorIdentifier, Extensions, IColorRegistry } from '../../../platform/theme/common/colorRegistry.js';18import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IProductIconTheme, IThemingRegistry, ITokenStyle } from '../../../platform/theme/common/themeService.js';19import { IDisposable, Disposable } from '../../../base/common/lifecycle.js';20import { ColorScheme, isDark, isHighContrast } from '../../../platform/theme/common/theme.js';21import { getIconsStyleSheet, UnthemedProductIconTheme } from '../../../platform/theme/browser/iconsStyleSheet.js';22import { mainWindow } from '../../../base/browser/window.js';2324export const VS_LIGHT_THEME_NAME = 'vs';25export const VS_DARK_THEME_NAME = 'vs-dark';26export const HC_BLACK_THEME_NAME = 'hc-black';27export const HC_LIGHT_THEME_NAME = 'hc-light';2829const colorRegistry = Registry.as<IColorRegistry>(Extensions.ColorContribution);30const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);3132class StandaloneTheme implements IStandaloneTheme {3334public readonly id: string;35public readonly themeName: string;3637private readonly themeData: IStandaloneThemeData;38private colors: Map<string, Color> | null;39private readonly defaultColors: { [colorId: string]: Color | undefined };40private _tokenTheme: TokenTheme | null;4142constructor(name: string, standaloneThemeData: IStandaloneThemeData) {43this.themeData = standaloneThemeData;44const base = standaloneThemeData.base;45if (name.length > 0) {46if (isBuiltinTheme(name)) {47this.id = name;48} else {49this.id = base + ' ' + name;50}51this.themeName = name;52} else {53this.id = base;54this.themeName = base;55}56this.colors = null;57this.defaultColors = Object.create(null);58this._tokenTheme = null;59}6061public get label(): string {62return this.themeName;63}6465public get base(): string {66return this.themeData.base;67}6869public notifyBaseUpdated() {70if (this.themeData.inherit) {71this.colors = null;72this._tokenTheme = null;73}74}7576private getColors(): Map<string, Color> {77if (!this.colors) {78const colors = new Map<string, Color>();79for (const id in this.themeData.colors) {80colors.set(id, Color.fromHex(this.themeData.colors[id]));81}82if (this.themeData.inherit) {83const baseData = getBuiltinRules(this.themeData.base);84for (const id in baseData.colors) {85if (!colors.has(id)) {86colors.set(id, Color.fromHex(baseData.colors[id]));87}88}89}90this.colors = colors;91}92return this.colors;93}9495public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {96const color = this.getColors().get(colorId);97if (color) {98return color;99}100if (useDefault !== false) {101return this.getDefault(colorId);102}103return undefined;104}105106private getDefault(colorId: ColorIdentifier): Color | undefined {107let color = this.defaultColors[colorId];108if (color) {109return color;110}111color = colorRegistry.resolveDefaultColor(colorId, this);112this.defaultColors[colorId] = color;113return color;114}115116public defines(colorId: ColorIdentifier): boolean {117return this.getColors().has(colorId);118}119120public get type(): ColorScheme {121switch (this.base) {122case VS_LIGHT_THEME_NAME: return ColorScheme.LIGHT;123case HC_BLACK_THEME_NAME: return ColorScheme.HIGH_CONTRAST_DARK;124case HC_LIGHT_THEME_NAME: return ColorScheme.HIGH_CONTRAST_LIGHT;125default: return ColorScheme.DARK;126}127}128129public get tokenTheme(): TokenTheme {130if (!this._tokenTheme) {131let rules: ITokenThemeRule[] = [];132let encodedTokensColors: string[] = [];133if (this.themeData.inherit) {134const baseData = getBuiltinRules(this.themeData.base);135rules = baseData.rules;136if (baseData.encodedTokensColors) {137encodedTokensColors = baseData.encodedTokensColors;138}139}140// Pick up default colors from `editor.foreground` and `editor.background` if available141const editorForeground = this.themeData.colors['editor.foreground'];142const editorBackground = this.themeData.colors['editor.background'];143if (editorForeground || editorBackground) {144const rule: ITokenThemeRule = { token: '' };145if (editorForeground) {146rule.foreground = editorForeground;147}148if (editorBackground) {149rule.background = editorBackground;150}151rules.push(rule);152}153rules = rules.concat(this.themeData.rules);154if (this.themeData.encodedTokensColors) {155encodedTokensColors = this.themeData.encodedTokensColors;156}157this._tokenTheme = TokenTheme.createFromRawTokenTheme(rules, encodedTokensColors);158}159return this._tokenTheme;160}161162public getTokenStyleMetadata(type: string, modifiers: string[], modelLanguage: string): ITokenStyle | undefined {163// use theme rules match164const style = this.tokenTheme._match([type].concat(modifiers).join('.'));165const metadata = style.metadata;166const foreground = TokenMetadata.getForeground(metadata);167const fontStyle = TokenMetadata.getFontStyle(metadata);168return {169foreground: foreground,170italic: Boolean(fontStyle & FontStyle.Italic),171bold: Boolean(fontStyle & FontStyle.Bold),172underline: Boolean(fontStyle & FontStyle.Underline),173strikethrough: Boolean(fontStyle & FontStyle.Strikethrough)174};175}176177public get tokenColorMap(): string[] {178return [];179}180181public readonly semanticHighlighting = false;182}183184function isBuiltinTheme(themeName: string): themeName is BuiltinTheme {185return (186themeName === VS_LIGHT_THEME_NAME187|| themeName === VS_DARK_THEME_NAME188|| themeName === HC_BLACK_THEME_NAME189|| themeName === HC_LIGHT_THEME_NAME190);191}192193function getBuiltinRules(builtinTheme: BuiltinTheme): IStandaloneThemeData {194switch (builtinTheme) {195case VS_LIGHT_THEME_NAME:196return vs;197case VS_DARK_THEME_NAME:198return vs_dark;199case HC_BLACK_THEME_NAME:200return hc_black;201case HC_LIGHT_THEME_NAME:202return hc_light;203}204}205206function newBuiltInTheme(builtinTheme: BuiltinTheme): StandaloneTheme {207const themeData = getBuiltinRules(builtinTheme);208return new StandaloneTheme(builtinTheme, themeData);209}210211export class StandaloneThemeService extends Disposable implements IStandaloneThemeService {212213declare readonly _serviceBrand: undefined;214215private readonly _onColorThemeChange = this._register(new Emitter<IStandaloneTheme>());216public readonly onDidColorThemeChange = this._onColorThemeChange.event;217218private readonly _onFileIconThemeChange = this._register(new Emitter<IFileIconTheme>());219public readonly onDidFileIconThemeChange = this._onFileIconThemeChange.event;220221private readonly _onProductIconThemeChange = this._register(new Emitter<IProductIconTheme>());222public readonly onDidProductIconThemeChange = this._onProductIconThemeChange.event;223224private readonly _environment: IEnvironmentService = Object.create(null);225private readonly _knownThemes: Map<string, StandaloneTheme>;226private _autoDetectHighContrast: boolean;227private _codiconCSS: string;228private _themeCSS: string;229private _allCSS: string;230private _globalStyleElement: HTMLStyleElement | null;231private _styleElements: HTMLStyleElement[];232private _colorMapOverride: Color[] | null;233private _theme!: IStandaloneTheme;234235private _builtInProductIconTheme = new UnthemedProductIconTheme();236237constructor() {238super();239240this._autoDetectHighContrast = true;241242this._knownThemes = new Map<string, StandaloneTheme>();243this._knownThemes.set(VS_LIGHT_THEME_NAME, newBuiltInTheme(VS_LIGHT_THEME_NAME));244this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));245this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));246this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME));247248const iconsStyleSheet = this._register(getIconsStyleSheet(this));249250this._codiconCSS = iconsStyleSheet.getCSS();251this._themeCSS = '';252this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;253this._globalStyleElement = null;254this._styleElements = [];255this._colorMapOverride = null;256this.setTheme(VS_LIGHT_THEME_NAME);257this._onOSSchemeChanged();258259this._register(iconsStyleSheet.onDidChange(() => {260this._codiconCSS = iconsStyleSheet.getCSS();261this._updateCSS();262}));263264addMatchMediaChangeListener(mainWindow, '(forced-colors: active)', () => {265this._onOSSchemeChanged();266});267}268269public registerEditorContainer(domNode: HTMLElement): IDisposable {270if (dom.isInShadowDOM(domNode)) {271return this._registerShadowDomContainer(domNode);272}273return this._registerRegularEditorContainer();274}275276private _registerRegularEditorContainer(): IDisposable {277if (!this._globalStyleElement) {278this._globalStyleElement = domStylesheetsJs.createStyleSheet(undefined, style => {279style.className = 'monaco-colors';280style.textContent = this._allCSS;281});282this._styleElements.push(this._globalStyleElement);283}284return Disposable.None;285}286287private _registerShadowDomContainer(domNode: HTMLElement): IDisposable {288const styleElement = domStylesheetsJs.createStyleSheet(domNode, style => {289style.className = 'monaco-colors';290style.textContent = this._allCSS;291});292this._styleElements.push(styleElement);293return {294dispose: () => {295for (let i = 0; i < this._styleElements.length; i++) {296if (this._styleElements[i] === styleElement) {297this._styleElements.splice(i, 1);298return;299}300}301}302};303}304305public defineTheme(themeName: string, themeData: IStandaloneThemeData): void {306if (!/^[a-z0-9\-]+$/i.test(themeName)) {307throw new Error('Illegal theme name!');308}309if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {310throw new Error('Illegal theme base!');311}312// set or replace theme313this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));314315if (isBuiltinTheme(themeName)) {316this._knownThemes.forEach(theme => {317if (theme.base === themeName) {318theme.notifyBaseUpdated();319}320});321}322if (this._theme.themeName === themeName) {323this.setTheme(themeName); // refresh theme324}325}326327public getColorTheme(): IStandaloneTheme {328return this._theme;329}330331public setColorMapOverride(colorMapOverride: Color[] | null): void {332this._colorMapOverride = colorMapOverride;333this._updateThemeOrColorMap();334}335336public setTheme(themeName: string): void {337let theme: StandaloneTheme | undefined;338if (this._knownThemes.has(themeName)) {339theme = this._knownThemes.get(themeName);340} else {341theme = this._knownThemes.get(VS_LIGHT_THEME_NAME);342}343this._updateActualTheme(theme);344}345346private _updateActualTheme(desiredTheme: IStandaloneTheme | undefined): void {347if (!desiredTheme || this._theme === desiredTheme) {348// Nothing to do349return;350}351this._theme = desiredTheme;352this._updateThemeOrColorMap();353}354355private _onOSSchemeChanged() {356if (this._autoDetectHighContrast) {357const wantsHighContrast = mainWindow.matchMedia(`(forced-colors: active)`).matches;358if (wantsHighContrast !== isHighContrast(this._theme.type)) {359// switch to high contrast or non-high contrast but stick to dark or light360let newThemeName;361if (isDark(this._theme.type)) {362newThemeName = wantsHighContrast ? HC_BLACK_THEME_NAME : VS_DARK_THEME_NAME;363} else {364newThemeName = wantsHighContrast ? HC_LIGHT_THEME_NAME : VS_LIGHT_THEME_NAME;365}366this._updateActualTheme(this._knownThemes.get(newThemeName));367}368}369}370371public setAutoDetectHighContrast(autoDetectHighContrast: boolean): void {372this._autoDetectHighContrast = autoDetectHighContrast;373this._onOSSchemeChanged();374}375376private _updateThemeOrColorMap(): void {377const cssRules: string[] = [];378const hasRule: { [rule: string]: boolean } = {};379const ruleCollector: ICssStyleCollector = {380addRule: (rule: string) => {381if (!hasRule[rule]) {382cssRules.push(rule);383hasRule[rule] = true;384}385}386};387themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));388389const colorVariables: string[] = [];390for (const item of colorRegistry.getColors()) {391const color = this._theme.getColor(item.id, true);392if (color) {393colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);394}395}396ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { ${colorVariables.join('\n')} }`);397398const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();399ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));400401this._themeCSS = cssRules.join('\n');402this._updateCSS();403404TokenizationRegistry.setColorMap(colorMap);405this._onColorThemeChange.fire(this._theme);406}407408private _updateCSS(): void {409this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;410this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);411}412413public getFileIconTheme(): IFileIconTheme {414return {415hasFileIcons: false,416hasFolderIcons: false,417hidesExplorerArrows: false418};419}420421public getProductIconTheme(): IProductIconTheme {422return this._builtInProductIconTheme;423}424425}426427428