Path: blob/main/src/vs/editor/standalone/browser/standaloneThemeService.ts
5240 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, IFontTokenOptions } 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 get tokenFontMap(): IFontTokenOptions[] {182return [];183}184185public readonly semanticHighlighting = false;186}187188function isBuiltinTheme(themeName: string): themeName is BuiltinTheme {189return (190themeName === VS_LIGHT_THEME_NAME191|| themeName === VS_DARK_THEME_NAME192|| themeName === HC_BLACK_THEME_NAME193|| themeName === HC_LIGHT_THEME_NAME194);195}196197function getBuiltinRules(builtinTheme: BuiltinTheme): IStandaloneThemeData {198switch (builtinTheme) {199case VS_LIGHT_THEME_NAME:200return vs;201case VS_DARK_THEME_NAME:202return vs_dark;203case HC_BLACK_THEME_NAME:204return hc_black;205case HC_LIGHT_THEME_NAME:206return hc_light;207}208}209210function newBuiltInTheme(builtinTheme: BuiltinTheme): StandaloneTheme {211const themeData = getBuiltinRules(builtinTheme);212return new StandaloneTheme(builtinTheme, themeData);213}214215export class StandaloneThemeService extends Disposable implements IStandaloneThemeService {216217declare readonly _serviceBrand: undefined;218219private readonly _onColorThemeChange = this._register(new Emitter<IStandaloneTheme>());220public readonly onDidColorThemeChange = this._onColorThemeChange.event;221222private readonly _onFileIconThemeChange = this._register(new Emitter<IFileIconTheme>());223public readonly onDidFileIconThemeChange = this._onFileIconThemeChange.event;224225private readonly _onProductIconThemeChange = this._register(new Emitter<IProductIconTheme>());226public readonly onDidProductIconThemeChange = this._onProductIconThemeChange.event;227228private readonly _environment: IEnvironmentService = Object.create(null);229private readonly _knownThemes: Map<string, StandaloneTheme>;230private _autoDetectHighContrast: boolean;231private _codiconCSS: string;232private _themeCSS: string;233private _allCSS: string;234private _globalStyleElement: HTMLStyleElement | null;235private _styleElements: HTMLStyleElement[];236private _colorMapOverride: Color[] | null;237private _theme!: IStandaloneTheme;238239private _builtInProductIconTheme = new UnthemedProductIconTheme();240241constructor() {242super();243244this._autoDetectHighContrast = true;245246this._knownThemes = new Map<string, StandaloneTheme>();247this._knownThemes.set(VS_LIGHT_THEME_NAME, newBuiltInTheme(VS_LIGHT_THEME_NAME));248this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));249this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));250this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME));251252const iconsStyleSheet = this._register(getIconsStyleSheet(this));253254this._codiconCSS = iconsStyleSheet.getCSS();255this._themeCSS = '';256this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;257this._globalStyleElement = null;258this._styleElements = [];259this._colorMapOverride = null;260this.setTheme(VS_LIGHT_THEME_NAME);261this._onOSSchemeChanged();262263this._register(iconsStyleSheet.onDidChange(() => {264this._codiconCSS = iconsStyleSheet.getCSS();265this._updateCSS();266}));267268addMatchMediaChangeListener(mainWindow, '(forced-colors: active)', () => {269// Update theme selection for auto-detecting high contrast270this._onOSSchemeChanged();271});272}273274public registerEditorContainer(domNode: HTMLElement): IDisposable {275if (dom.isInShadowDOM(domNode)) {276return this._registerShadowDomContainer(domNode);277}278return this._registerRegularEditorContainer();279}280281private _registerRegularEditorContainer(): IDisposable {282if (!this._globalStyleElement) {283this._globalStyleElement = domStylesheetsJs.createStyleSheet(undefined, style => {284style.className = 'monaco-colors';285style.textContent = this._allCSS;286});287this._styleElements.push(this._globalStyleElement);288}289return Disposable.None;290}291292private _registerShadowDomContainer(domNode: HTMLElement): IDisposable {293const styleElement = domStylesheetsJs.createStyleSheet(domNode, style => {294style.className = 'monaco-colors';295style.textContent = this._allCSS;296});297this._styleElements.push(styleElement);298return {299dispose: () => {300for (let i = 0; i < this._styleElements.length; i++) {301if (this._styleElements[i] === styleElement) {302this._styleElements.splice(i, 1);303return;304}305}306}307};308}309310public defineTheme(themeName: string, themeData: IStandaloneThemeData): void {311if (!/^[a-z0-9\-]+$/i.test(themeName)) {312throw new Error('Illegal theme name!');313}314if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {315throw new Error('Illegal theme base!');316}317// set or replace theme318this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));319320if (isBuiltinTheme(themeName)) {321this._knownThemes.forEach(theme => {322if (theme.base === themeName) {323theme.notifyBaseUpdated();324}325});326}327if (this._theme.themeName === themeName) {328this.setTheme(themeName); // refresh theme329}330}331332public getColorTheme(): IStandaloneTheme {333return this._theme;334}335336public setColorMapOverride(colorMapOverride: Color[] | null): void {337this._colorMapOverride = colorMapOverride;338this._updateThemeOrColorMap();339}340341public setTheme(themeName: string): void {342let theme: StandaloneTheme | undefined;343if (this._knownThemes.has(themeName)) {344theme = this._knownThemes.get(themeName);345} else {346theme = this._knownThemes.get(VS_LIGHT_THEME_NAME);347}348this._updateActualTheme(theme);349}350351private _updateActualTheme(desiredTheme: IStandaloneTheme | undefined): void {352if (!desiredTheme || this._theme === desiredTheme) {353// Nothing to do354return;355}356this._theme = desiredTheme;357this._updateThemeOrColorMap();358}359360private _onOSSchemeChanged() {361if (this._autoDetectHighContrast) {362const wantsHighContrast = mainWindow.matchMedia(`(forced-colors: active)`).matches;363if (wantsHighContrast !== isHighContrast(this._theme.type)) {364// switch to high contrast or non-high contrast but stick to dark or light365let newThemeName;366if (isDark(this._theme.type)) {367newThemeName = wantsHighContrast ? HC_BLACK_THEME_NAME : VS_DARK_THEME_NAME;368} else {369newThemeName = wantsHighContrast ? HC_LIGHT_THEME_NAME : VS_LIGHT_THEME_NAME;370}371this._updateActualTheme(this._knownThemes.get(newThemeName));372}373}374}375376public setAutoDetectHighContrast(autoDetectHighContrast: boolean): void {377this._autoDetectHighContrast = autoDetectHighContrast;378this._onOSSchemeChanged();379}380381private _updateThemeOrColorMap(): void {382const cssRules: string[] = [];383const hasRule: { [rule: string]: boolean } = {};384const ruleCollector: ICssStyleCollector = {385addRule: (rule: string) => {386if (!hasRule[rule]) {387cssRules.push(rule);388hasRule[rule] = true;389}390}391};392themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));393394const colorVariables: string[] = [];395for (const item of colorRegistry.getColors()) {396const color = this._theme.getColor(item.id, true);397if (color) {398colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);399}400}401ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { ${colorVariables.join('\n')} }`);402403const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();404ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));405406// If the OS has forced-colors active, disable forced color adjustment for407// Monaco editor elements so that VS Code's built-in high contrast themes408// (hc-black / hc-light) are used instead of the OS forcing system colors.409ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { forced-color-adjust: none; }`);410411this._themeCSS = cssRules.join('\n');412this._updateCSS();413414TokenizationRegistry.setColorMap(colorMap);415this._onColorThemeChange.fire(this._theme);416}417418private _updateCSS(): void {419this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;420this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);421}422423public getFileIconTheme(): IFileIconTheme {424return {425hasFileIcons: false,426hasFolderIcons: false,427hidesExplorerArrows: false428};429}430431public getProductIconTheme(): IProductIconTheme {432return this._builtInProductIconTheme;433}434435}436437438