824 lines
36 KiB
TypeScript
824 lines
36 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as nls from 'vs/nls';
|
|
import * as types from 'vs/base/common/types';
|
|
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
|
import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, ThemeSettings, IWorkbenchProductIconTheme, ThemeSettingTarget } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
|
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
import { Registry } from 'vs/platform/registry/common/platform';
|
|
import * as errors from 'vs/base/common/errors';
|
|
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
|
import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData';
|
|
import { IColorTheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService';
|
|
import { Event, Emitter } from 'vs/base/common/event';
|
|
import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema';
|
|
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
|
import { FileIconThemeData } from 'vs/workbench/services/themes/browser/fileIconThemeData';
|
|
import { createStyleSheet } from 'vs/base/browser/dom';
|
|
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
|
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import * as resources from 'vs/base/common/resources';
|
|
import { registerColorThemeSchemas } from 'vs/workbench/services/themes/common/colorThemeSchema';
|
|
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
|
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
|
|
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
|
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
|
|
import { ThemeRegistry, registerColorThemeExtensionPoint, registerFileIconThemeExtensionPoint, registerProductIconThemeExtensionPoint } from 'vs/workbench/services/themes/common/themeExtensionPoints';
|
|
import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationSchemas, ThemeConfiguration, updateProductIconThemeConfigurationSchemas } from 'vs/workbench/services/themes/common/themeConfiguration';
|
|
import { ProductIconThemeData, DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData';
|
|
import { registerProductIconThemeSchemas } from 'vs/workbench/services/themes/common/productIconThemeSchema';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { isWeb } from 'vs/base/common/platform';
|
|
import { ColorScheme } from 'vs/platform/theme/common/theme';
|
|
import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService';
|
|
import { RunOnceScheduler, Sequencer } from 'vs/base/common/async';
|
|
import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
|
|
import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet';
|
|
import { asCssVariableName, getColorRegistry } from 'vs/platform/theme/common/colorRegistry';
|
|
|
|
// implementation
|
|
|
|
const DEFAULT_COLOR_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json';
|
|
const DEFAULT_LIGHT_COLOR_THEME_ID = 'vs vscode-theme-defaults-themes-light_plus-json';
|
|
|
|
const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme';
|
|
|
|
const defaultThemeExtensionId = 'vscode-theme-defaults';
|
|
|
|
const DEFAULT_FILE_ICON_THEME_ID = 'vscode.vscode-theme-seti-vs-seti';
|
|
const fileIconsEnabledClass = 'file-icons-enabled';
|
|
|
|
const colorThemeRulesClassName = 'contributedColorTheme';
|
|
const fileIconThemeRulesClassName = 'contributedFileIconTheme';
|
|
const productIconThemeRulesClassName = 'contributedProductIconTheme';
|
|
|
|
const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);
|
|
|
|
function validateThemeId(theme: string): string {
|
|
// migrations
|
|
switch (theme) {
|
|
case VS_LIGHT_THEME: return `vs ${defaultThemeExtensionId}-themes-light_vs-json`;
|
|
case VS_DARK_THEME: return `vs-dark ${defaultThemeExtensionId}-themes-dark_vs-json`;
|
|
case VS_HC_THEME: return `hc-black ${defaultThemeExtensionId}-themes-hc_black-json`;
|
|
}
|
|
return theme;
|
|
}
|
|
|
|
const colorThemesExtPoint = registerColorThemeExtensionPoint();
|
|
const fileIconThemesExtPoint = registerFileIconThemeExtensionPoint();
|
|
const productIconThemesExtPoint = registerProductIconThemeExtensionPoint();
|
|
|
|
export class WorkbenchThemeService implements IWorkbenchThemeService {
|
|
declare readonly _serviceBrand: undefined;
|
|
|
|
private readonly container: HTMLElement;
|
|
private settings: ThemeConfiguration;
|
|
|
|
private readonly colorThemeRegistry: ThemeRegistry<ColorThemeData>;
|
|
private currentColorTheme: ColorThemeData;
|
|
private readonly onColorThemeChange: Emitter<IWorkbenchColorTheme>;
|
|
private readonly colorThemeWatcher: ThemeFileWatcher;
|
|
private colorThemingParticipantChangeListener: IDisposable | undefined;
|
|
private readonly colorThemeSequencer: Sequencer;
|
|
|
|
private readonly fileIconThemeRegistry: ThemeRegistry<FileIconThemeData>;
|
|
private currentFileIconTheme: FileIconThemeData;
|
|
private readonly onFileIconThemeChange: Emitter<IWorkbenchFileIconTheme>;
|
|
private readonly fileIconThemeWatcher: ThemeFileWatcher;
|
|
private readonly fileIconThemeSequencer: Sequencer;
|
|
|
|
private readonly productIconThemeRegistry: ThemeRegistry<ProductIconThemeData>;
|
|
private currentProductIconTheme: ProductIconThemeData;
|
|
private readonly onProductIconThemeChange: Emitter<IWorkbenchProductIconTheme>;
|
|
private readonly productIconThemeWatcher: ThemeFileWatcher;
|
|
private readonly productIconThemeSequencer: Sequencer;
|
|
|
|
private themeSettingIdBeforeSchemeSwitch: string | undefined;
|
|
|
|
constructor(
|
|
@IExtensionService extensionService: IExtensionService,
|
|
@IStorageService private readonly storageService: IStorageService,
|
|
@IConfigurationService private readonly configurationService: IConfigurationService,
|
|
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
@IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService,
|
|
@IFileService fileService: IFileService,
|
|
@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,
|
|
@IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService,
|
|
@ILogService private readonly logService: ILogService,
|
|
@IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService,
|
|
@IUserDataInitializationService readonly userDataInitializationService: IUserDataInitializationService
|
|
) {
|
|
this.container = layoutService.container;
|
|
this.settings = new ThemeConfiguration(configurationService);
|
|
|
|
this.colorThemeRegistry = new ThemeRegistry(colorThemesExtPoint, ColorThemeData.fromExtensionTheme);
|
|
this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this));
|
|
this.onColorThemeChange = new Emitter<IWorkbenchColorTheme>({ leakWarningThreshold: 400 });
|
|
this.currentColorTheme = ColorThemeData.createUnloadedTheme('');
|
|
this.colorThemeSequencer = new Sequencer();
|
|
|
|
this.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this));
|
|
this.fileIconThemeRegistry = new ThemeRegistry(fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme);
|
|
this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>();
|
|
this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme('');
|
|
this.fileIconThemeSequencer = new Sequencer();
|
|
|
|
this.productIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentProductIconTheme.bind(this));
|
|
this.productIconThemeRegistry = new ThemeRegistry(productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme);
|
|
this.onProductIconThemeChange = new Emitter<IWorkbenchProductIconTheme>();
|
|
this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme('');
|
|
this.productIconThemeSequencer = new Sequencer();
|
|
|
|
// In order to avoid paint flashing for tokens, because
|
|
// themes are loaded asynchronously, we need to initialize
|
|
// a color theme document with good defaults until the theme is loaded
|
|
let themeData: ColorThemeData | undefined = ColorThemeData.fromStorageData(this.storageService);
|
|
if (themeData && this.settings.colorTheme !== themeData.settingsId && this.settings.isDefaultColorTheme()) {
|
|
// the web has different defaults than the desktop, therefore do not restore when the setting is the default theme and the storage doesn't match that.
|
|
themeData = undefined;
|
|
}
|
|
|
|
// the preferred color scheme (high contrast, light, dark) has changed since the last start
|
|
const preferredColorScheme = this.getPreferredColorScheme();
|
|
|
|
if (preferredColorScheme && themeData?.type !== preferredColorScheme && this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL) !== preferredColorScheme) {
|
|
themeData = ColorThemeData.createUnloadedThemeForThemeType(preferredColorScheme);
|
|
}
|
|
if (!themeData) {
|
|
const initialColorTheme = environmentService.options?.initialColorTheme;
|
|
if (initialColorTheme) {
|
|
themeData = ColorThemeData.createUnloadedThemeForThemeType(initialColorTheme.themeType, initialColorTheme.colors);
|
|
}
|
|
}
|
|
if (!themeData) {
|
|
themeData = ColorThemeData.createUnloadedThemeForThemeType(isWeb ? ColorScheme.LIGHT : ColorScheme.DARK);
|
|
}
|
|
themeData.setCustomizations(this.settings);
|
|
this.applyTheme(themeData, undefined, true);
|
|
|
|
const fileIconData = FileIconThemeData.fromStorageData(this.storageService);
|
|
if (fileIconData) {
|
|
this.applyAndSetFileIconTheme(fileIconData, true);
|
|
}
|
|
|
|
const productIconData = ProductIconThemeData.fromStorageData(this.storageService);
|
|
if (productIconData) {
|
|
this.applyAndSetProductIconTheme(productIconData, true);
|
|
}
|
|
|
|
Promise.all([extensionService.whenInstalledExtensionsRegistered(), userDataInitializationService.whenInitializationFinished()]).then(_ => {
|
|
this.installConfigurationListener();
|
|
this.installPreferredSchemeListener();
|
|
this.installRegistryListeners();
|
|
this.initialize().catch(errors.onUnexpectedError);
|
|
});
|
|
|
|
const codiconStyleSheet = createStyleSheet();
|
|
codiconStyleSheet.id = 'codiconStyles';
|
|
|
|
const iconsStyleSheet = getIconsStyleSheet();
|
|
function updateAll() {
|
|
codiconStyleSheet.textContent = iconsStyleSheet.getCSS();
|
|
}
|
|
|
|
const delayer = new RunOnceScheduler(updateAll, 0);
|
|
iconsStyleSheet.onDidChange(() => delayer.schedule());
|
|
delayer.schedule();
|
|
}
|
|
|
|
private initialize(): Promise<[IWorkbenchColorTheme | null, IWorkbenchFileIconTheme | null, IWorkbenchProductIconTheme | null]> {
|
|
const extDevLocs = this.environmentService.extensionDevelopmentLocationURI;
|
|
const extDevLoc = extDevLocs && extDevLocs.length === 1 ? extDevLocs[0] : undefined; // in dev mode, switch to a theme provided by the extension under dev.
|
|
|
|
const initializeColorTheme = async () => {
|
|
const devThemes = this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
|
if (devThemes.length) {
|
|
return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
|
}
|
|
const fallbackTheme = this.currentColorTheme.type === ColorScheme.LIGHT ? DEFAULT_LIGHT_COLOR_THEME_ID : DEFAULT_COLOR_THEME_ID;
|
|
const theme = this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, fallbackTheme);
|
|
|
|
const preferredColorScheme = this.getPreferredColorScheme();
|
|
const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL);
|
|
if (preferredColorScheme !== prevScheme) {
|
|
this.storageService.store(PERSISTED_OS_COLOR_SCHEME, preferredColorScheme, StorageScope.GLOBAL, StorageTarget.USER);
|
|
if (preferredColorScheme && theme?.type !== preferredColorScheme) {
|
|
return this.applyPreferredColorTheme(preferredColorScheme);
|
|
}
|
|
}
|
|
return this.setColorTheme(theme && theme.id, undefined);
|
|
};
|
|
|
|
const initializeFileIconTheme = async () => {
|
|
const devThemes = this.fileIconThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
|
if (devThemes.length) {
|
|
return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
|
}
|
|
const theme = this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme);
|
|
return this.setFileIconTheme(theme ? theme.id : DEFAULT_FILE_ICON_THEME_ID, undefined);
|
|
};
|
|
|
|
const initializeProductIconTheme = async () => {
|
|
const devThemes = this.productIconThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
|
if (devThemes.length) {
|
|
return this.setProductIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
|
}
|
|
const theme = this.productIconThemeRegistry.findThemeBySettingsId(this.settings.productIconTheme);
|
|
return this.setProductIconTheme(theme ? theme.id : DEFAULT_PRODUCT_ICON_THEME_ID, undefined);
|
|
};
|
|
|
|
|
|
return Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]);
|
|
}
|
|
|
|
private installConfigurationListener() {
|
|
this.configurationService.onDidChangeConfiguration(e => {
|
|
if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)) {
|
|
this.restoreColorTheme();
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME) || e.affectsConfiguration(ThemeSettings.DETECT_HC)) {
|
|
this.handlePreferredSchemeUpdated();
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.PREFERRED_DARK_THEME) && this.getPreferredColorScheme() === ColorScheme.DARK) {
|
|
this.applyPreferredColorTheme(ColorScheme.DARK);
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.PREFERRED_LIGHT_THEME) && this.getPreferredColorScheme() === ColorScheme.LIGHT) {
|
|
this.applyPreferredColorTheme(ColorScheme.LIGHT);
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_THEME) && this.getPreferredColorScheme() === ColorScheme.HIGH_CONTRAST) {
|
|
this.applyPreferredColorTheme(ColorScheme.HIGH_CONTRAST);
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.FILE_ICON_THEME)) {
|
|
this.restoreFileIconTheme();
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.PRODUCT_ICON_THEME)) {
|
|
this.restoreProductIconTheme();
|
|
}
|
|
if (this.currentColorTheme) {
|
|
let hasColorChanges = false;
|
|
if (e.affectsConfiguration(ThemeSettings.COLOR_CUSTOMIZATIONS)) {
|
|
this.currentColorTheme.setCustomColors(this.settings.colorCustomizations);
|
|
hasColorChanges = true;
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS)) {
|
|
this.currentColorTheme.setCustomTokenColors(this.settings.tokenColorCustomizations);
|
|
hasColorChanges = true;
|
|
}
|
|
if (e.affectsConfiguration(ThemeSettings.SEMANTIC_TOKEN_COLOR_CUSTOMIZATIONS)) {
|
|
this.currentColorTheme.setCustomSemanticTokenColors(this.settings.semanticTokenColorCustomizations);
|
|
hasColorChanges = true;
|
|
}
|
|
if (hasColorChanges) {
|
|
this.updateDynamicCSSRules(this.currentColorTheme);
|
|
this.onColorThemeChange.fire(this.currentColorTheme);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private installRegistryListeners(): Promise<any> {
|
|
|
|
let prevColorId: string | undefined = undefined;
|
|
|
|
// update settings schema setting for theme specific settings
|
|
this.colorThemeRegistry.onDidChange(async event => {
|
|
updateColorThemeConfigurationSchemas(event.themes);
|
|
if (await this.restoreColorTheme()) { // checks if theme from settings exists and is set
|
|
// restore theme
|
|
if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) {
|
|
await this.setColorTheme(prevColorId, 'auto');
|
|
prevColorId = undefined;
|
|
} else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
|
|
await this.reloadCurrentColorTheme();
|
|
}
|
|
} else if (event.removed.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
|
|
// current theme is no longer available
|
|
prevColorId = this.currentColorTheme.id;
|
|
await this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto');
|
|
}
|
|
});
|
|
|
|
let prevFileIconId: string | undefined = undefined;
|
|
this.fileIconThemeRegistry.onDidChange(async event => {
|
|
updateFileIconThemeConfigurationSchemas(event.themes);
|
|
if (await this.restoreFileIconTheme()) { // checks if theme from settings exists and is set
|
|
// restore theme
|
|
if (this.currentFileIconTheme.id === DEFAULT_FILE_ICON_THEME_ID && !types.isUndefined(prevFileIconId) && this.fileIconThemeRegistry.findThemeById(prevFileIconId)) {
|
|
await this.setFileIconTheme(prevFileIconId, 'auto');
|
|
prevFileIconId = undefined;
|
|
} else if (event.added.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
|
|
await this.reloadCurrentFileIconTheme();
|
|
}
|
|
} else if (event.removed.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
|
|
// current theme is no longer available
|
|
prevFileIconId = this.currentFileIconTheme.id;
|
|
await this.setFileIconTheme(DEFAULT_FILE_ICON_THEME_ID, 'auto');
|
|
}
|
|
|
|
});
|
|
|
|
let prevProductIconId: string | undefined = undefined;
|
|
this.productIconThemeRegistry.onDidChange(async event => {
|
|
updateProductIconThemeConfigurationSchemas(event.themes);
|
|
if (await this.restoreProductIconTheme()) { // checks if theme from settings exists and is set
|
|
// restore theme
|
|
if (this.currentProductIconTheme.id === DEFAULT_PRODUCT_ICON_THEME_ID && !types.isUndefined(prevProductIconId) && this.productIconThemeRegistry.findThemeById(prevProductIconId)) {
|
|
await this.setProductIconTheme(prevProductIconId, 'auto');
|
|
prevProductIconId = undefined;
|
|
} else if (event.added.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
|
|
await this.reloadCurrentProductIconTheme();
|
|
}
|
|
} else if (event.removed.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
|
|
// current theme is no longer available
|
|
prevProductIconId = this.currentProductIconTheme.id;
|
|
await this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto');
|
|
}
|
|
});
|
|
|
|
return Promise.all([this.getColorThemes(), this.getFileIconThemes(), this.getProductIconThemes()]).then(([ct, fit, pit]) => {
|
|
updateColorThemeConfigurationSchemas(ct);
|
|
updateFileIconThemeConfigurationSchemas(fit);
|
|
updateProductIconThemeConfigurationSchemas(pit);
|
|
});
|
|
}
|
|
|
|
|
|
// preferred scheme handling
|
|
|
|
private installPreferredSchemeListener() {
|
|
this.hostColorService.onDidChangeColorScheme(() => this.handlePreferredSchemeUpdated());
|
|
}
|
|
|
|
private async handlePreferredSchemeUpdated() {
|
|
const scheme = this.getPreferredColorScheme();
|
|
const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL);
|
|
if (scheme !== prevScheme) {
|
|
this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL, StorageTarget.MACHINE);
|
|
if (scheme) {
|
|
if (!prevScheme) {
|
|
// remember the theme before scheme switching
|
|
this.themeSettingIdBeforeSchemeSwitch = this.settings.colorTheme;
|
|
}
|
|
return this.applyPreferredColorTheme(scheme);
|
|
} else if (prevScheme && this.themeSettingIdBeforeSchemeSwitch) {
|
|
// reapply the theme before scheme switching
|
|
const theme = this.colorThemeRegistry.findThemeBySettingsId(this.themeSettingIdBeforeSchemeSwitch, undefined);
|
|
if (theme) {
|
|
this.setColorTheme(theme.id, 'auto');
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private getPreferredColorScheme(): ColorScheme | undefined {
|
|
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) {
|
|
return ColorScheme.HIGH_CONTRAST;
|
|
}
|
|
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
|
|
return this.hostColorService.dark ? ColorScheme.DARK : ColorScheme.LIGHT;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private async applyPreferredColorTheme(type: ColorScheme): Promise<IWorkbenchColorTheme | null> {
|
|
const settingId = type === ColorScheme.DARK ? ThemeSettings.PREFERRED_DARK_THEME : type === ColorScheme.LIGHT ? ThemeSettings.PREFERRED_LIGHT_THEME : ThemeSettings.PREFERRED_HC_THEME;
|
|
const themeSettingId = this.configurationService.getValue(settingId);
|
|
if (themeSettingId && typeof themeSettingId === 'string') {
|
|
const theme = this.colorThemeRegistry.findThemeBySettingsId(themeSettingId, undefined);
|
|
if (theme) {
|
|
const configurationTarget = this.settings.findAutoConfigurationTarget(settingId);
|
|
return this.setColorTheme(theme.id, configurationTarget);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getColorTheme(): IWorkbenchColorTheme {
|
|
return this.currentColorTheme;
|
|
}
|
|
|
|
public async getColorThemes(): Promise<IWorkbenchColorTheme[]> {
|
|
return this.colorThemeRegistry.getThemes();
|
|
}
|
|
|
|
public async getMarketplaceColorThemes(publisher: string, name: string, version: string): Promise<IWorkbenchColorTheme[]> {
|
|
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
|
|
if (extensionLocation) {
|
|
try {
|
|
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
|
|
return this.colorThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
|
|
} catch (e) {
|
|
this.logService.error('Problem loading themes from marketplace', e);
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
public get onDidColorThemeChange(): Event<IWorkbenchColorTheme> {
|
|
return this.onColorThemeChange.event;
|
|
}
|
|
|
|
public setColorTheme(themeIdOrTheme: string | undefined | IWorkbenchColorTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchColorTheme | null> {
|
|
return this.colorThemeSequencer.queue(async () => {
|
|
if (!themeIdOrTheme) {
|
|
return null;
|
|
}
|
|
const themeId = types.isString(themeIdOrTheme) ? validateThemeId(themeIdOrTheme) : themeIdOrTheme.id;
|
|
if (this.currentColorTheme.isLoaded && themeId === this.currentColorTheme.id) {
|
|
if (settingsTarget !== 'preview') {
|
|
this.currentColorTheme.toStorage(this.storageService);
|
|
}
|
|
return this.settings.setColorTheme(this.currentColorTheme, settingsTarget);
|
|
}
|
|
|
|
let themeData = this.colorThemeRegistry.findThemeById(themeId);
|
|
if (!themeData) {
|
|
if (themeIdOrTheme instanceof ColorThemeData) {
|
|
themeData = themeIdOrTheme;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
try {
|
|
await themeData.ensureLoaded(this.extensionResourceLoaderService);
|
|
themeData.setCustomizations(this.settings);
|
|
return this.applyTheme(themeData, settingsTarget);
|
|
} catch (error) {
|
|
throw new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location?.toString(), error.message));
|
|
}
|
|
});
|
|
}
|
|
|
|
private reloadCurrentColorTheme() {
|
|
return this.colorThemeSequencer.queue(async () => {
|
|
try {
|
|
const theme = this.colorThemeRegistry.findThemeBySettingsId(this.currentColorTheme.settingsId) || this.currentColorTheme;
|
|
await theme.reload(this.extensionResourceLoaderService);
|
|
theme.setCustomizations(this.settings);
|
|
await this.applyTheme(theme, undefined, false);
|
|
} catch (error) {
|
|
this.logService.info('Unable to reload {0}: {1}', this.currentColorTheme.location?.toString());
|
|
}
|
|
});
|
|
}
|
|
|
|
public async restoreColorTheme(): Promise<boolean> {
|
|
return this.colorThemeSequencer.queue(async () => {
|
|
const settingId = this.settings.colorTheme;
|
|
const theme = this.colorThemeRegistry.findThemeBySettingsId(settingId);
|
|
if (theme) {
|
|
if (settingId !== this.currentColorTheme.settingsId) {
|
|
await this.setColorTheme(theme.id, undefined);
|
|
} else if (theme !== this.currentColorTheme) {
|
|
theme.setCustomizations(this.settings);
|
|
await this.applyTheme(theme, undefined, true);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private updateDynamicCSSRules(themeData: IColorTheme) {
|
|
const cssRules = new Set<string>();
|
|
const ruleCollector = {
|
|
addRule: (rule: string) => {
|
|
if (!cssRules.has(rule)) {
|
|
cssRules.add(rule);
|
|
}
|
|
}
|
|
};
|
|
ruleCollector.addRule(`.monaco-workbench { forced-color-adjust: none; }`);
|
|
themingRegistry.getThemingParticipants().forEach(p => p(themeData, ruleCollector, this.environmentService));
|
|
|
|
const colorVariables: string[] = [];
|
|
for (const item of getColorRegistry().getColors()) {
|
|
const color = themeData.getColor(item.id, true);
|
|
if (color) {
|
|
colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
|
|
}
|
|
}
|
|
ruleCollector.addRule(`.monaco-workbench { ${colorVariables.join('\n')} }`);
|
|
|
|
_applyRules([...cssRules].join('\n'), colorThemeRulesClassName);
|
|
}
|
|
|
|
private applyTheme(newTheme: ColorThemeData, settingsTarget: ThemeSettingTarget, silent = false): Promise<IWorkbenchColorTheme | null> {
|
|
this.updateDynamicCSSRules(newTheme);
|
|
|
|
if (this.currentColorTheme.id) {
|
|
this.container.classList.remove(...this.currentColorTheme.classNames);
|
|
} else {
|
|
this.container.classList.remove(VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME);
|
|
}
|
|
this.container.classList.add(...newTheme.classNames);
|
|
|
|
this.currentColorTheme.clearCaches();
|
|
this.currentColorTheme = newTheme;
|
|
if (!this.colorThemingParticipantChangeListener) {
|
|
this.colorThemingParticipantChangeListener = themingRegistry.onThemingParticipantAdded(_ => this.updateDynamicCSSRules(this.currentColorTheme));
|
|
}
|
|
|
|
this.colorThemeWatcher.update(newTheme);
|
|
|
|
this.sendTelemetry(newTheme.id, newTheme.extensionData, 'color');
|
|
|
|
if (silent) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
this.onColorThemeChange.fire(this.currentColorTheme);
|
|
|
|
// remember theme data for a quick restore
|
|
if (newTheme.isLoaded && settingsTarget !== 'preview') {
|
|
newTheme.toStorage(this.storageService);
|
|
}
|
|
|
|
return this.settings.setColorTheme(this.currentColorTheme, settingsTarget);
|
|
}
|
|
|
|
|
|
private themeExtensionsActivated = new Map<string, boolean>();
|
|
private sendTelemetry(themeId: string, themeData: ExtensionData | undefined, themeType: string) {
|
|
if (themeData) {
|
|
const key = themeType + themeData.extensionId;
|
|
if (!this.themeExtensionsActivated.get(key)) {
|
|
type ActivatePluginClassification = {
|
|
id: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
|
name: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
|
isBuiltin: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
|
publisherDisplayName: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
|
themeId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
|
};
|
|
type ActivatePluginEvent = {
|
|
id: string;
|
|
name: string;
|
|
isBuiltin: boolean;
|
|
publisherDisplayName: string;
|
|
themeId: string;
|
|
};
|
|
this.telemetryService.publicLog2<ActivatePluginEvent, ActivatePluginClassification>('activatePlugin', {
|
|
id: themeData.extensionId,
|
|
name: themeData.extensionName,
|
|
isBuiltin: themeData.extensionIsBuiltin,
|
|
publisherDisplayName: themeData.extensionPublisher,
|
|
themeId: themeId
|
|
});
|
|
this.themeExtensionsActivated.set(key, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async getFileIconThemes(): Promise<IWorkbenchFileIconTheme[]> {
|
|
return this.fileIconThemeRegistry.getThemes();
|
|
}
|
|
|
|
public getFileIconTheme() {
|
|
return this.currentFileIconTheme;
|
|
}
|
|
|
|
public get onDidFileIconThemeChange(): Event<IWorkbenchFileIconTheme> {
|
|
return this.onFileIconThemeChange.event;
|
|
}
|
|
|
|
public async setFileIconTheme(iconThemeOrId: string | undefined | IWorkbenchFileIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchFileIconTheme> {
|
|
return this.fileIconThemeSequencer.queue(async () => {
|
|
if (iconThemeOrId === undefined) {
|
|
iconThemeOrId = '';
|
|
}
|
|
const themeId = types.isString(iconThemeOrId) ? iconThemeOrId : iconThemeOrId.id;
|
|
if (themeId !== this.currentFileIconTheme.id || !this.currentFileIconTheme.isLoaded) {
|
|
|
|
let newThemeData = this.fileIconThemeRegistry.findThemeById(themeId);
|
|
if (!newThemeData && iconThemeOrId instanceof FileIconThemeData) {
|
|
newThemeData = iconThemeOrId;
|
|
}
|
|
if (!newThemeData) {
|
|
newThemeData = FileIconThemeData.noIconTheme;
|
|
}
|
|
await newThemeData.ensureLoaded(this.extensionResourceLoaderService);
|
|
|
|
this.applyAndSetFileIconTheme(newThemeData); // updates this.currentFileIconTheme
|
|
}
|
|
|
|
const themeData = this.currentFileIconTheme;
|
|
|
|
// remember theme data for a quick restore
|
|
if (themeData.isLoaded && settingsTarget !== 'preview' && (!themeData.location || !getRemoteAuthority(themeData.location))) {
|
|
themeData.toStorage(this.storageService);
|
|
}
|
|
await this.settings.setFileIconTheme(this.currentFileIconTheme, settingsTarget);
|
|
|
|
return themeData;
|
|
});
|
|
}
|
|
|
|
public async getMarketplaceFileIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchFileIconTheme[]> {
|
|
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
|
|
if (extensionLocation) {
|
|
try {
|
|
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
|
|
return this.fileIconThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
|
|
} catch (e) {
|
|
this.logService.error('Problem loading themes from marketplace', e);
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private async reloadCurrentFileIconTheme() {
|
|
return this.fileIconThemeSequencer.queue(async () => {
|
|
await this.currentFileIconTheme.reload(this.extensionResourceLoaderService);
|
|
this.applyAndSetFileIconTheme(this.currentFileIconTheme);
|
|
});
|
|
}
|
|
|
|
public async restoreFileIconTheme(): Promise<boolean> {
|
|
return this.fileIconThemeSequencer.queue(async () => {
|
|
const settingId = this.settings.fileIconTheme;
|
|
const theme = this.fileIconThemeRegistry.findThemeBySettingsId(settingId);
|
|
if (theme) {
|
|
if (settingId !== this.currentFileIconTheme.settingsId) {
|
|
await this.setFileIconTheme(theme.id, undefined);
|
|
} else if (theme !== this.currentFileIconTheme) {
|
|
this.applyAndSetFileIconTheme(theme, true);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private applyAndSetFileIconTheme(iconThemeData: FileIconThemeData, silent = false): void {
|
|
this.currentFileIconTheme = iconThemeData;
|
|
|
|
_applyRules(iconThemeData.styleSheetContent!, fileIconThemeRulesClassName);
|
|
|
|
if (iconThemeData.id) {
|
|
this.container.classList.add(fileIconsEnabledClass);
|
|
} else {
|
|
this.container.classList.remove(fileIconsEnabledClass);
|
|
}
|
|
|
|
this.fileIconThemeWatcher.update(iconThemeData);
|
|
|
|
if (iconThemeData.id) {
|
|
this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'fileIcon');
|
|
}
|
|
|
|
if (!silent) {
|
|
this.onFileIconThemeChange.fire(this.currentFileIconTheme);
|
|
}
|
|
}
|
|
|
|
public async getProductIconThemes(): Promise<IWorkbenchProductIconTheme[]> {
|
|
return this.productIconThemeRegistry.getThemes();
|
|
}
|
|
|
|
public getProductIconTheme() {
|
|
return this.currentProductIconTheme;
|
|
}
|
|
|
|
public get onDidProductIconThemeChange(): Event<IWorkbenchProductIconTheme> {
|
|
return this.onProductIconThemeChange.event;
|
|
}
|
|
|
|
public async setProductIconTheme(iconThemeOrId: string | undefined | IWorkbenchProductIconTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchProductIconTheme> {
|
|
return this.productIconThemeSequencer.queue(async () => {
|
|
if (iconThemeOrId === undefined) {
|
|
iconThemeOrId = '';
|
|
}
|
|
const themeId = types.isString(iconThemeOrId) ? iconThemeOrId : iconThemeOrId.id;
|
|
if (themeId !== this.currentProductIconTheme.id || !this.currentProductIconTheme.isLoaded) {
|
|
let newThemeData = this.productIconThemeRegistry.findThemeById(themeId);
|
|
if (!newThemeData && iconThemeOrId instanceof ProductIconThemeData) {
|
|
newThemeData = iconThemeOrId;
|
|
}
|
|
if (!newThemeData) {
|
|
newThemeData = ProductIconThemeData.defaultTheme;
|
|
}
|
|
await newThemeData.ensureLoaded(this.extensionResourceLoaderService, this.logService);
|
|
|
|
this.applyAndSetProductIconTheme(newThemeData); // updates this.currentProductIconTheme
|
|
}
|
|
const themeData = this.currentProductIconTheme;
|
|
|
|
// remember theme data for a quick restore
|
|
if (themeData.isLoaded && settingsTarget !== 'preview' && (!themeData.location || !getRemoteAuthority(themeData.location))) {
|
|
themeData.toStorage(this.storageService);
|
|
}
|
|
await this.settings.setProductIconTheme(this.currentProductIconTheme, settingsTarget);
|
|
|
|
return themeData;
|
|
});
|
|
}
|
|
|
|
public async getMarketplaceProductIconThemes(publisher: string, name: string, version: string): Promise<IWorkbenchProductIconTheme[]> {
|
|
const extensionLocation = this.extensionResourceLoaderService.getExtensionGalleryResourceURL({ publisher, name, version }, 'extension');
|
|
if (extensionLocation) {
|
|
try {
|
|
const manifestContent = await this.extensionResourceLoaderService.readExtensionResource(resources.joinPath(extensionLocation, 'package.json'));
|
|
return this.productIconThemeRegistry.getMarketplaceThemes(JSON.parse(manifestContent), extensionLocation, ExtensionData.fromName(publisher, name));
|
|
} catch (e) {
|
|
this.logService.error('Problem loading themes from marketplace', e);
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private async reloadCurrentProductIconTheme() {
|
|
return this.productIconThemeSequencer.queue(async () => {
|
|
await this.currentProductIconTheme.reload(this.extensionResourceLoaderService, this.logService);
|
|
this.applyAndSetProductIconTheme(this.currentProductIconTheme);
|
|
});
|
|
}
|
|
|
|
public async restoreProductIconTheme(): Promise<boolean> {
|
|
return this.productIconThemeSequencer.queue(async () => {
|
|
const settingId = this.settings.productIconTheme;
|
|
const theme = this.productIconThemeRegistry.findThemeBySettingsId(settingId);
|
|
if (theme) {
|
|
if (settingId !== this.currentProductIconTheme.settingsId) {
|
|
await this.setProductIconTheme(theme.id, undefined);
|
|
} else if (theme !== this.currentProductIconTheme) {
|
|
this.applyAndSetProductIconTheme(theme, true);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private applyAndSetProductIconTheme(iconThemeData: ProductIconThemeData, silent = false): void {
|
|
|
|
this.currentProductIconTheme = iconThemeData;
|
|
|
|
_applyRules(iconThemeData.styleSheetContent!, productIconThemeRulesClassName);
|
|
|
|
this.productIconThemeWatcher.update(iconThemeData);
|
|
|
|
if (iconThemeData.id) {
|
|
this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'productIcon');
|
|
}
|
|
if (!silent) {
|
|
this.onProductIconThemeChange.fire(this.currentProductIconTheme);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ThemeFileWatcher {
|
|
|
|
private watchedLocation: URI | undefined;
|
|
private watcherDisposable: IDisposable | undefined;
|
|
private fileChangeListener: IDisposable | undefined;
|
|
|
|
constructor(private fileService: IFileService, private environmentService: IWorkbenchEnvironmentService, private onUpdate: () => void) {
|
|
}
|
|
|
|
update(theme: { location?: URI, watch?: boolean; }) {
|
|
if (!resources.isEqual(theme.location, this.watchedLocation)) {
|
|
this.dispose();
|
|
if (theme.location && (theme.watch || this.environmentService.isExtensionDevelopment)) {
|
|
this.watchedLocation = theme.location;
|
|
this.watcherDisposable = this.fileService.watch(theme.location);
|
|
this.fileService.onDidFilesChange(e => {
|
|
if (this.watchedLocation && e.contains(this.watchedLocation, FileChangeType.UPDATED)) {
|
|
this.onUpdate();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
dispose() {
|
|
this.watcherDisposable = dispose(this.watcherDisposable);
|
|
this.fileChangeListener = dispose(this.fileChangeListener);
|
|
this.watchedLocation = undefined;
|
|
}
|
|
}
|
|
|
|
function _applyRules(styleSheetContent: string, rulesClassName: string) {
|
|
const themeStyles = document.head.getElementsByClassName(rulesClassName);
|
|
if (themeStyles.length === 0) {
|
|
const elStyle = document.createElement('style');
|
|
elStyle.type = 'text/css';
|
|
elStyle.className = rulesClassName;
|
|
elStyle.textContent = styleSheetContent;
|
|
document.head.appendChild(elStyle);
|
|
} else {
|
|
(<HTMLStyleElement>themeStyles[0]).textContent = styleSheetContent;
|
|
}
|
|
}
|
|
|
|
registerColorThemeSchemas();
|
|
registerFileIconThemeSchemas();
|
|
registerProductIconThemeSchemas();
|
|
|
|
registerSingleton(IWorkbenchThemeService, WorkbenchThemeService);
|