mirror of
https://github.com/coder/code-server.git
synced 2026-05-08 21:37:27 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService';
|
||||
|
||||
export class BrowserHostColorSchemeService extends Disposable implements IHostColorSchemeService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidSchemeChangeEvent = this._register(new Emitter<void>());
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener(() => {
|
||||
this._onDidSchemeChangeEvent.fire();
|
||||
});
|
||||
window.matchMedia('(forced-colors: active)').addListener(() => {
|
||||
this._onDidSchemeChangeEvent.fire();
|
||||
});
|
||||
}
|
||||
|
||||
get onDidChangeColorScheme(): Event<void> {
|
||||
return this._onDidSchemeChangeEvent.event;
|
||||
}
|
||||
|
||||
get dark(): boolean {
|
||||
if (window.matchMedia(`(prefers-color-scheme: light)`).matches) {
|
||||
return false;
|
||||
} else if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
|
||||
return true;
|
||||
}
|
||||
return this.environmentService.configuration.colorScheme.dark;
|
||||
}
|
||||
|
||||
get highContrast(): boolean {
|
||||
if (window.matchMedia(`(forced-colors: active)`).matches) {
|
||||
return true;
|
||||
}
|
||||
return this.environmentService.configuration.colorScheme.highContrast;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IHostColorSchemeService, BrowserHostColorSchemeService, true);
|
||||
@@ -0,0 +1,381 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as Paths from 'vs/base/common/path';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as Json from 'vs/base/common/json';
|
||||
import { ExtensionData, IThemeExtensionPoint, IWorkbenchFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { asCSSUrl } from 'vs/base/browser/dom';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class FileIconThemeData implements IWorkbenchFileIconTheme {
|
||||
|
||||
static readonly STORAGE_KEY = 'iconThemeData';
|
||||
|
||||
id: string;
|
||||
label: string;
|
||||
settingsId: string | null;
|
||||
description?: string;
|
||||
hasFileIcons: boolean;
|
||||
hasFolderIcons: boolean;
|
||||
hidesExplorerArrows: boolean;
|
||||
isLoaded: boolean;
|
||||
location?: URI;
|
||||
extensionData?: ExtensionData;
|
||||
watch?: boolean;
|
||||
|
||||
styleSheetContent?: string;
|
||||
|
||||
private constructor(id: string, label: string, settingsId: string | null) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.settingsId = settingsId;
|
||||
this.isLoaded = false;
|
||||
this.hasFileIcons = false;
|
||||
this.hasFolderIcons = false;
|
||||
this.hidesExplorerArrows = false;
|
||||
}
|
||||
|
||||
public ensureLoaded(fileService: IFileService): Promise<string | undefined> {
|
||||
return !this.isLoaded ? this.load(fileService) : Promise.resolve(this.styleSheetContent);
|
||||
}
|
||||
|
||||
public reload(fileService: IFileService): Promise<string | undefined> {
|
||||
return this.load(fileService);
|
||||
}
|
||||
|
||||
private load(fileService: IFileService): Promise<string | undefined> {
|
||||
if (!this.location) {
|
||||
return Promise.resolve(this.styleSheetContent);
|
||||
}
|
||||
return _loadIconThemeDocument(fileService, this.location).then(iconThemeDocument => {
|
||||
const result = _processIconThemeDocument(this.id, this.location!, iconThemeDocument);
|
||||
this.styleSheetContent = result.content;
|
||||
this.hasFileIcons = result.hasFileIcons;
|
||||
this.hasFolderIcons = result.hasFolderIcons;
|
||||
this.hidesExplorerArrows = result.hidesExplorerArrows;
|
||||
this.isLoaded = true;
|
||||
return this.styleSheetContent;
|
||||
});
|
||||
}
|
||||
|
||||
static fromExtensionTheme(iconTheme: IThemeExtensionPoint, iconThemeLocation: URI, extensionData: ExtensionData): FileIconThemeData {
|
||||
const id = extensionData.extensionId + '-' + iconTheme.id;
|
||||
const label = iconTheme.label || Paths.basename(iconTheme.path);
|
||||
const settingsId = iconTheme.id;
|
||||
|
||||
const themeData = new FileIconThemeData(id, label, settingsId);
|
||||
|
||||
themeData.description = iconTheme.description;
|
||||
themeData.location = iconThemeLocation;
|
||||
themeData.extensionData = extensionData;
|
||||
themeData.watch = iconTheme._watch;
|
||||
themeData.isLoaded = false;
|
||||
return themeData;
|
||||
}
|
||||
|
||||
private static _noIconTheme: FileIconThemeData | null = null;
|
||||
|
||||
static get noIconTheme(): FileIconThemeData {
|
||||
let themeData = FileIconThemeData._noIconTheme;
|
||||
if (!themeData) {
|
||||
themeData = FileIconThemeData._noIconTheme = new FileIconThemeData('', '', null);
|
||||
themeData.hasFileIcons = false;
|
||||
themeData.hasFolderIcons = false;
|
||||
themeData.hidesExplorerArrows = false;
|
||||
themeData.isLoaded = true;
|
||||
themeData.extensionData = undefined;
|
||||
themeData.watch = false;
|
||||
}
|
||||
return themeData;
|
||||
}
|
||||
|
||||
static createUnloadedTheme(id: string): FileIconThemeData {
|
||||
const themeData = new FileIconThemeData(id, '', '__' + id);
|
||||
themeData.isLoaded = false;
|
||||
themeData.hasFileIcons = false;
|
||||
themeData.hasFolderIcons = false;
|
||||
themeData.hidesExplorerArrows = false;
|
||||
themeData.extensionData = undefined;
|
||||
themeData.watch = false;
|
||||
return themeData;
|
||||
}
|
||||
|
||||
|
||||
static fromStorageData(storageService: IStorageService): FileIconThemeData | undefined {
|
||||
const input = storageService.get(FileIconThemeData.STORAGE_KEY, StorageScope.GLOBAL);
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
let data = JSON.parse(input);
|
||||
const theme = new FileIconThemeData('', '', null);
|
||||
for (let key in data) {
|
||||
switch (key) {
|
||||
case 'id':
|
||||
case 'label':
|
||||
case 'description':
|
||||
case 'settingsId':
|
||||
case 'styleSheetContent':
|
||||
case 'hasFileIcons':
|
||||
case 'hidesExplorerArrows':
|
||||
case 'hasFolderIcons':
|
||||
case 'watch':
|
||||
(theme as any)[key] = data[key];
|
||||
break;
|
||||
case 'location':
|
||||
// ignore, no longer restore
|
||||
break;
|
||||
case 'extensionData':
|
||||
theme.extensionData = ExtensionData.fromJSONObject(data.extensionData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
toStorage(storageService: IStorageService) {
|
||||
const data = JSON.stringify({
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
description: this.description,
|
||||
settingsId: this.settingsId,
|
||||
styleSheetContent: this.styleSheetContent,
|
||||
hasFileIcons: this.hasFileIcons,
|
||||
hasFolderIcons: this.hasFolderIcons,
|
||||
hidesExplorerArrows: this.hidesExplorerArrows,
|
||||
extensionData: ExtensionData.toJSONObject(this.extensionData),
|
||||
watch: this.watch
|
||||
});
|
||||
storageService.store(FileIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
interface IconDefinition {
|
||||
iconPath: string;
|
||||
fontColor: string;
|
||||
fontCharacter: string;
|
||||
fontSize: string;
|
||||
fontId: string;
|
||||
}
|
||||
|
||||
interface FontDefinition {
|
||||
id: string;
|
||||
weight: string;
|
||||
style: string;
|
||||
size: string;
|
||||
src: { path: string; format: string; }[];
|
||||
}
|
||||
|
||||
interface IconsAssociation {
|
||||
folder?: string;
|
||||
file?: string;
|
||||
folderExpanded?: string;
|
||||
rootFolder?: string;
|
||||
rootFolderExpanded?: string;
|
||||
folderNames?: { [folderName: string]: string; };
|
||||
folderNamesExpanded?: { [folderName: string]: string; };
|
||||
fileExtensions?: { [extension: string]: string; };
|
||||
fileNames?: { [fileName: string]: string; };
|
||||
languageIds?: { [languageId: string]: string; };
|
||||
}
|
||||
|
||||
interface IconThemeDocument extends IconsAssociation {
|
||||
iconDefinitions: { [key: string]: IconDefinition };
|
||||
fonts: FontDefinition[];
|
||||
light?: IconsAssociation;
|
||||
highContrast?: IconsAssociation;
|
||||
hidesExplorerArrows?: boolean;
|
||||
}
|
||||
|
||||
function _loadIconThemeDocument(fileService: IFileService, location: URI): Promise<IconThemeDocument> {
|
||||
return fileService.readFile(location).then((content) => {
|
||||
let errors: Json.ParseError[] = [];
|
||||
let contentValue = Json.parse(content.value.toString(), errors);
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing file icons file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', '))));
|
||||
} else if (Json.getNodeType(contentValue) !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize('error.invalidformat', "Invalid format for file icons theme file: Object expected.")));
|
||||
}
|
||||
return Promise.resolve(contentValue);
|
||||
});
|
||||
}
|
||||
|
||||
function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, iconThemeDocument: IconThemeDocument): { content: string; hasFileIcons: boolean; hasFolderIcons: boolean; hidesExplorerArrows: boolean; } {
|
||||
|
||||
const result = { content: '', hasFileIcons: false, hasFolderIcons: false, hidesExplorerArrows: !!iconThemeDocument.hidesExplorerArrows };
|
||||
|
||||
if (!iconThemeDocument.iconDefinitions) {
|
||||
return result;
|
||||
}
|
||||
let selectorByDefinitionId: { [def: string]: string[] } = {};
|
||||
|
||||
const iconThemeDocumentLocationDirname = resources.dirname(iconThemeDocumentLocation);
|
||||
function resolvePath(path: string) {
|
||||
return resources.joinPath(iconThemeDocumentLocationDirname, path);
|
||||
}
|
||||
|
||||
function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: string) {
|
||||
function addSelector(selector: string, defId: string) {
|
||||
if (defId) {
|
||||
let list = selectorByDefinitionId[defId];
|
||||
if (!list) {
|
||||
list = selectorByDefinitionId[defId] = [];
|
||||
}
|
||||
list.push(selector);
|
||||
}
|
||||
}
|
||||
if (associations) {
|
||||
let qualifier = '.show-file-icons';
|
||||
if (baseThemeClassName) {
|
||||
qualifier = baseThemeClassName + ' ' + qualifier;
|
||||
}
|
||||
|
||||
const expanded = '.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents';
|
||||
|
||||
if (associations.folder) {
|
||||
addSelector(`${qualifier} .folder-icon::before`, associations.folder);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
if (associations.folderExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
let rootFolder = associations.rootFolder || associations.folder;
|
||||
let rootFolderExpanded = associations.rootFolderExpanded || associations.folderExpanded;
|
||||
|
||||
if (rootFolder) {
|
||||
addSelector(`${qualifier} .rootfolder-icon::before`, rootFolder);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
if (rootFolderExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
if (associations.file) {
|
||||
addSelector(`${qualifier} .file-icon::before`, associations.file);
|
||||
result.hasFileIcons = true;
|
||||
}
|
||||
|
||||
let folderNames = associations.folderNames;
|
||||
if (folderNames) {
|
||||
for (let folderName in folderNames) {
|
||||
addSelector(`${qualifier} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNames[folderName]);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
}
|
||||
let folderNamesExpanded = associations.folderNamesExpanded;
|
||||
if (folderNamesExpanded) {
|
||||
for (let folderName in folderNamesExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNamesExpanded[folderName]);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
}
|
||||
|
||||
let languageIds = associations.languageIds;
|
||||
if (languageIds) {
|
||||
if (!languageIds.jsonc && languageIds.json) {
|
||||
languageIds.jsonc = languageIds.json;
|
||||
}
|
||||
for (let languageId in languageIds) {
|
||||
addSelector(`${qualifier} .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId]);
|
||||
result.hasFileIcons = true;
|
||||
}
|
||||
}
|
||||
let fileExtensions = associations.fileExtensions;
|
||||
if (fileExtensions) {
|
||||
for (let fileExtension in fileExtensions) {
|
||||
let selectors: string[] = [];
|
||||
let segments = fileExtension.toLowerCase().split('.');
|
||||
if (segments.length) {
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`);
|
||||
}
|
||||
selectors.push('.ext-file-icon'); // extra segment to increase file-ext score
|
||||
}
|
||||
addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[fileExtension]);
|
||||
result.hasFileIcons = true;
|
||||
}
|
||||
}
|
||||
let fileNames = associations.fileNames;
|
||||
if (fileNames) {
|
||||
for (let fileName in fileNames) {
|
||||
let selectors: string[] = [];
|
||||
fileName = fileName.toLowerCase();
|
||||
selectors.push(`.${escapeCSS(fileName)}-name-file-icon`);
|
||||
let segments = fileName.split('.');
|
||||
if (segments.length) {
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`);
|
||||
}
|
||||
selectors.push('.ext-file-icon'); // extra segment to increase file-ext score
|
||||
}
|
||||
addSelector(`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[fileName]);
|
||||
result.hasFileIcons = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collectSelectors(iconThemeDocument);
|
||||
collectSelectors(iconThemeDocument.light, '.vs');
|
||||
collectSelectors(iconThemeDocument.highContrast, '.hc-black');
|
||||
|
||||
if (!result.hasFileIcons && !result.hasFolderIcons) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let cssRules: string[] = [];
|
||||
|
||||
let fonts = iconThemeDocument.fonts;
|
||||
if (Array.isArray(fonts)) {
|
||||
fonts.forEach(font => {
|
||||
let src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', ');
|
||||
cssRules.push(`@font-face { src: ${src}; font-family: '${font.id}'; font-weight: ${font.weight}; font-style: ${font.style}; }`);
|
||||
});
|
||||
cssRules.push(`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: '${fonts[0].id}'; font-size: ${fonts[0].size || '150%'}}`);
|
||||
}
|
||||
|
||||
for (let defId in selectorByDefinitionId) {
|
||||
let selectors = selectorByDefinitionId[defId];
|
||||
let definition = iconThemeDocument.iconDefinitions[defId];
|
||||
if (definition) {
|
||||
if (definition.iconPath) {
|
||||
cssRules.push(`${selectors.join(', ')} { content: ' '; background-image: ${asCSSUrl(resolvePath(definition.iconPath))}; }`);
|
||||
}
|
||||
if (definition.fontCharacter || definition.fontColor) {
|
||||
let body = '';
|
||||
if (definition.fontColor) {
|
||||
body += ` color: ${definition.fontColor};`;
|
||||
}
|
||||
if (definition.fontCharacter) {
|
||||
body += ` content: '${definition.fontCharacter}';`;
|
||||
}
|
||||
if (definition.fontSize) {
|
||||
body += ` font-size: ${definition.fontSize};`;
|
||||
}
|
||||
if (definition.fontId) {
|
||||
body += ` font-family: ${definition.fontId};`;
|
||||
}
|
||||
cssRules.push(`${selectors.join(', ')} { ${body} }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.content = cssRules.join('\n');
|
||||
return result;
|
||||
}
|
||||
function escapeCSS(str: string) {
|
||||
return window.CSS.escape(str);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as Paths from 'vs/base/common/path';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as Json from 'vs/base/common/json';
|
||||
import { ExtensionData, IThemeExtensionPoint, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { asCSSUrl } from 'vs/base/browser/dom';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE } from 'vs/workbench/services/themes/common/themeConfiguration';
|
||||
import { fontIdRegex, fontWeightRegex, fontStyleRegex } from 'vs/workbench/services/themes/common/productIconThemeSchema';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export const DEFAULT_PRODUCT_ICON_THEME_ID = ''; // TODO
|
||||
|
||||
export class ProductIconThemeData implements IWorkbenchProductIconTheme {
|
||||
|
||||
static readonly STORAGE_KEY = 'productIconThemeData';
|
||||
|
||||
id: string;
|
||||
label: string;
|
||||
settingsId: string;
|
||||
description?: string;
|
||||
isLoaded: boolean;
|
||||
location?: URI;
|
||||
extensionData?: ExtensionData;
|
||||
watch?: boolean;
|
||||
|
||||
styleSheetContent?: string;
|
||||
|
||||
private constructor(id: string, label: string, settingsId: string) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.settingsId = settingsId;
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
public ensureLoaded(fileService: IFileService, logService: ILogService): Promise<string | undefined> {
|
||||
return !this.isLoaded ? this.load(fileService, logService) : Promise.resolve(this.styleSheetContent);
|
||||
}
|
||||
|
||||
public reload(fileService: IFileService, logService: ILogService): Promise<string | undefined> {
|
||||
return this.load(fileService, logService);
|
||||
}
|
||||
|
||||
private load(fileService: IFileService, logService: ILogService): Promise<string | undefined> {
|
||||
const location = this.location;
|
||||
if (!location) {
|
||||
return Promise.resolve(this.styleSheetContent);
|
||||
}
|
||||
return _loadProductIconThemeDocument(fileService, location).then(iconThemeDocument => {
|
||||
const result = _processIconThemeDocument(this.id, location, iconThemeDocument);
|
||||
this.styleSheetContent = result.content;
|
||||
this.isLoaded = true;
|
||||
if (result.warnings.length) {
|
||||
logService.error(nls.localize('error.parseicondefs', "Problems processing product icons definitions in {0}:\n{1}", location.toString(), result.warnings.join('\n')));
|
||||
}
|
||||
return this.styleSheetContent;
|
||||
});
|
||||
}
|
||||
|
||||
static fromExtensionTheme(iconTheme: IThemeExtensionPoint, iconThemeLocation: URI, extensionData: ExtensionData): ProductIconThemeData {
|
||||
const id = extensionData.extensionId + '-' + iconTheme.id;
|
||||
const label = iconTheme.label || Paths.basename(iconTheme.path);
|
||||
const settingsId = iconTheme.id;
|
||||
|
||||
const themeData = new ProductIconThemeData(id, label, settingsId);
|
||||
|
||||
themeData.description = iconTheme.description;
|
||||
themeData.location = iconThemeLocation;
|
||||
themeData.extensionData = extensionData;
|
||||
themeData.watch = iconTheme._watch;
|
||||
themeData.isLoaded = false;
|
||||
return themeData;
|
||||
}
|
||||
|
||||
static createUnloadedTheme(id: string): ProductIconThemeData {
|
||||
const themeData = new ProductIconThemeData(id, '', '__' + id);
|
||||
themeData.isLoaded = false;
|
||||
themeData.extensionData = undefined;
|
||||
themeData.watch = false;
|
||||
return themeData;
|
||||
}
|
||||
|
||||
private static _defaultProductIconTheme: ProductIconThemeData | null = null;
|
||||
|
||||
static get defaultTheme(): ProductIconThemeData {
|
||||
let themeData = ProductIconThemeData._defaultProductIconTheme;
|
||||
if (!themeData) {
|
||||
themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE);
|
||||
themeData.isLoaded = true;
|
||||
themeData.extensionData = undefined;
|
||||
themeData.watch = false;
|
||||
}
|
||||
return themeData;
|
||||
}
|
||||
|
||||
static fromStorageData(storageService: IStorageService): ProductIconThemeData | undefined {
|
||||
const input = storageService.get(ProductIconThemeData.STORAGE_KEY, StorageScope.GLOBAL);
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
let data = JSON.parse(input);
|
||||
const theme = new ProductIconThemeData('', '', '');
|
||||
for (let key in data) {
|
||||
switch (key) {
|
||||
case 'id':
|
||||
case 'label':
|
||||
case 'description':
|
||||
case 'settingsId':
|
||||
case 'styleSheetContent':
|
||||
case 'watch':
|
||||
(theme as any)[key] = data[key];
|
||||
break;
|
||||
case 'location':
|
||||
// ignore, no longer restore
|
||||
break;
|
||||
case 'extensionData':
|
||||
theme.extensionData = ExtensionData.fromJSONObject(data.extensionData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return theme;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
toStorage(storageService: IStorageService) {
|
||||
const data = JSON.stringify({
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
description: this.description,
|
||||
settingsId: this.settingsId,
|
||||
styleSheetContent: this.styleSheetContent,
|
||||
watch: this.watch,
|
||||
extensionData: ExtensionData.toJSONObject(this.extensionData),
|
||||
});
|
||||
storageService.store(ProductIconThemeData.STORAGE_KEY, data, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
interface IconDefinition {
|
||||
fontCharacter: string;
|
||||
fontId: string;
|
||||
}
|
||||
|
||||
interface FontDefinition {
|
||||
id: string;
|
||||
weight: string;
|
||||
style: string;
|
||||
size: string;
|
||||
src: { path: string; format: string; }[];
|
||||
}
|
||||
|
||||
interface ProductIconThemeDocument {
|
||||
iconDefinitions: { [key: string]: IconDefinition };
|
||||
fonts: FontDefinition[];
|
||||
}
|
||||
|
||||
function _loadProductIconThemeDocument(fileService: IFileService, location: URI): Promise<ProductIconThemeDocument> {
|
||||
return fileService.readFile(location).then((content) => {
|
||||
let errors: Json.ParseError[] = [];
|
||||
let contentValue = Json.parse(content.value.toString(), errors);
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing product icons file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', '))));
|
||||
} else if (Json.getNodeType(contentValue) !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize('error.invalidformat', "Invalid format for product icons theme file: Object expected.")));
|
||||
} else if (!contentValue.iconDefinitions || !Array.isArray(contentValue.fonts) || !contentValue.fonts.length) {
|
||||
return Promise.reject(new Error(nls.localize('error.missingProperties', "Invalid format for product icons theme file: Must contain iconDefinitions and fonts.")));
|
||||
}
|
||||
return Promise.resolve(contentValue);
|
||||
});
|
||||
}
|
||||
|
||||
function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, iconThemeDocument: ProductIconThemeDocument): { content: string; warnings: string[] } {
|
||||
|
||||
const warnings: string[] = [];
|
||||
const result = { content: '', warnings };
|
||||
|
||||
if (!iconThemeDocument.iconDefinitions || !Array.isArray(iconThemeDocument.fonts) || !iconThemeDocument.fonts.length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const iconThemeDocumentLocationDirname = resources.dirname(iconThemeDocumentLocation);
|
||||
function resolvePath(path: string) {
|
||||
return resources.joinPath(iconThemeDocumentLocationDirname, path);
|
||||
}
|
||||
|
||||
const cssRules: string[] = [];
|
||||
|
||||
const fonts = iconThemeDocument.fonts;
|
||||
const fontIdMapping: { [id: string]: string } = {};
|
||||
for (const font of fonts) {
|
||||
const src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', ');
|
||||
if (isString(font.id) && font.id.match(fontIdRegex)) {
|
||||
const fontId = `pi-` + font.id;
|
||||
fontIdMapping[font.id] = fontId;
|
||||
|
||||
let fontWeight = '';
|
||||
if (isString(font.weight) && font.weight.match(fontWeightRegex)) {
|
||||
fontWeight = `font-weight: ${font.weight};`;
|
||||
} else {
|
||||
warnings.push(nls.localize('error.fontWeight', 'Invalid font weight in font \'{0}\'. Ignoring setting.', font.id));
|
||||
}
|
||||
|
||||
let fontStyle = '';
|
||||
if (isString(font.style) && font.style.match(fontStyleRegex)) {
|
||||
fontStyle = `font-style: ${font.style};`;
|
||||
} else {
|
||||
warnings.push(nls.localize('error.fontStyle', 'Invalid font style in font \'{0}\'. Ignoring setting.', font.id));
|
||||
}
|
||||
|
||||
cssRules.push(`@font-face { src: ${src}; font-family: '${fontId}';${fontWeight}${fontStyle} }`);
|
||||
} else {
|
||||
warnings.push(nls.localize('error.fontId', 'Missing or invalid font id \'{0}\'. Skipping font definition.', font.id));
|
||||
}
|
||||
}
|
||||
|
||||
const primaryFontId = fonts.length > 0 ? fontIdMapping[fonts[0].id] : '';
|
||||
|
||||
const iconDefinitions = iconThemeDocument.iconDefinitions;
|
||||
const iconRegistry = getIconRegistry();
|
||||
|
||||
|
||||
for (let iconContribution of iconRegistry.getIcons()) {
|
||||
const iconId = iconContribution.id;
|
||||
|
||||
let definition = iconDefinitions[iconId];
|
||||
|
||||
// look if an inherited icon has a definition
|
||||
while (!definition && ThemeIcon.isThemeIcon(iconContribution.defaults)) {
|
||||
const ic = iconRegistry.getIcon(iconContribution.defaults.id);
|
||||
if (ic) {
|
||||
definition = iconDefinitions[ic.id];
|
||||
iconContribution = ic;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (definition) {
|
||||
if (isString(definition.fontCharacter)) {
|
||||
const fontId = definition.fontId !== undefined ? fontIdMapping[definition.fontId] : primaryFontId;
|
||||
if (fontId) {
|
||||
cssRules.push(`.codicon-${iconId}:before { content: '${definition.fontCharacter}' !important; font-family: ${fontId} !important; }`);
|
||||
} else {
|
||||
warnings.push(nls.localize('error.icon.fontId', 'Skipping icon definition \'{0}\'. Unknown font.', iconId));
|
||||
}
|
||||
} else {
|
||||
warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\'. Unknown fontCharacter.', iconId));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.content = cssRules.join('\n');
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,716 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IStorageService, StorageScope } 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 { CodiconStyles } from 'vs/base/browser/ui/codicons/codiconStyles';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
// implementation
|
||||
|
||||
const DEFAULT_COLOR_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json';
|
||||
|
||||
const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme';
|
||||
|
||||
const defaultThemeExtensionId = 'vscode-theme-defaults';
|
||||
const oldDefaultThemeExtensionId = 'vscode-theme-colorful-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`;
|
||||
case `vs ${oldDefaultThemeExtensionId}-themes-light_plus-tmTheme`: return `vs ${defaultThemeExtensionId}-themes-light_plus-json`;
|
||||
case `vs-dark ${oldDefaultThemeExtensionId}-themes-dark_plus-tmTheme`: return `vs-dark ${defaultThemeExtensionId}-themes-dark_plus-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 fileIconThemeRegistry: ThemeRegistry<FileIconThemeData>;
|
||||
private currentFileIconTheme: FileIconThemeData;
|
||||
private readonly onFileIconThemeChange: Emitter<IWorkbenchFileIconTheme>;
|
||||
private readonly fileIconThemeWatcher: ThemeFileWatcher;
|
||||
|
||||
private readonly productIconThemeRegistry: ThemeRegistry<ProductIconThemeData>;
|
||||
private currentProductIconTheme: ProductIconThemeData;
|
||||
private readonly onProductIconThemeChange: Emitter<IWorkbenchProductIconTheme>;
|
||||
private readonly productIconThemeWatcher: ThemeFileWatcher;
|
||||
|
||||
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 private readonly fileService: IFileService,
|
||||
@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,
|
||||
@IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService,
|
||||
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService
|
||||
) {
|
||||
// roam persisted color theme colors. Don't enable for icons as they contain references to fonts and images.
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: ColorThemeData.STORAGE_KEY, version: 1 });
|
||||
|
||||
this.container = layoutService.container;
|
||||
this.settings = new ThemeConfiguration(configurationService);
|
||||
|
||||
this.colorThemeRegistry = new ThemeRegistry(extensionService, 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.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this));
|
||||
this.fileIconThemeRegistry = new ThemeRegistry(extensionService, fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme);
|
||||
this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>();
|
||||
this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme('');
|
||||
|
||||
this.productIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentProductIconTheme.bind(this));
|
||||
this.productIconThemeRegistry = new ThemeRegistry(extensionService, productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme, true);
|
||||
this.onProductIconThemeChange = new Emitter<IWorkbenchProductIconTheme>();
|
||||
this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme('');
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
this.initialize().then(undefined, errors.onUnexpectedError).then(_ => {
|
||||
this.installConfigurationListener();
|
||||
this.installPreferredSchemeListener();
|
||||
this.installRegistryListeners();
|
||||
});
|
||||
|
||||
const codiconStyleSheet = createStyleSheet();
|
||||
codiconStyleSheet.id = 'codiconStyles';
|
||||
|
||||
function updateAll() {
|
||||
codiconStyleSheet.textContent = CodiconStyles.getCSS();
|
||||
}
|
||||
|
||||
const delayer = new RunOnceScheduler(updateAll, 0);
|
||||
CodiconStyles.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 = await this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
||||
if (devThemes.length) {
|
||||
return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
||||
}
|
||||
const theme = await this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, DEFAULT_COLOR_THEME_ID);
|
||||
|
||||
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);
|
||||
if (preferredColorScheme && theme?.type !== preferredColorScheme) {
|
||||
return this.applyPreferredColorTheme(preferredColorScheme);
|
||||
}
|
||||
}
|
||||
return this.setColorTheme(theme && theme.id, undefined);
|
||||
};
|
||||
|
||||
const initializeFileIconTheme = async () => {
|
||||
const devThemes = await this.fileIconThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
||||
if (devThemes.length) {
|
||||
return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
||||
}
|
||||
const theme = await this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme);
|
||||
return this.setFileIconTheme(theme ? theme.id : DEFAULT_FILE_ICON_THEME_ID, undefined);
|
||||
};
|
||||
|
||||
const initializeProductIconTheme = async () => {
|
||||
const devThemes = await this.productIconThemeRegistry.findThemeByExtensionLocation(extDevLoc);
|
||||
if (devThemes.length) {
|
||||
return this.setProductIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
|
||||
}
|
||||
const theme = await 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) || e.affectsConfiguration(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL)) {
|
||||
this.currentColorTheme.setCustomSemanticTokenColors(this.settings.semanticTokenColorCustomizations, this.settings.experimentalSemanticTokenColorCustomizations);
|
||||
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)) {
|
||||
// restore theme
|
||||
this.setColorTheme(prevColorId, 'auto');
|
||||
prevColorId = undefined;
|
||||
} else if (event.added.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
|
||||
this.reloadCurrentColorTheme();
|
||||
}
|
||||
} else if (event.removed.some(t => t.settingsId === this.currentColorTheme.settingsId)) {
|
||||
// current theme is no longer available
|
||||
prevColorId = this.currentColorTheme.id;
|
||||
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) && await this.fileIconThemeRegistry.findThemeById(prevFileIconId)) {
|
||||
this.setFileIconTheme(prevFileIconId, 'auto');
|
||||
prevFileIconId = undefined;
|
||||
} else if (event.added.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
|
||||
this.reloadCurrentFileIconTheme();
|
||||
}
|
||||
} else if (event.removed.some(t => t.settingsId === this.currentFileIconTheme.settingsId)) {
|
||||
// current theme is no longer available
|
||||
prevFileIconId = this.currentFileIconTheme.id;
|
||||
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) && await this.productIconThemeRegistry.findThemeById(prevProductIconId)) {
|
||||
this.setProductIconTheme(prevProductIconId, 'auto');
|
||||
prevProductIconId = undefined;
|
||||
} else if (event.added.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
|
||||
this.reloadCurrentProductIconTheme();
|
||||
}
|
||||
} else if (event.removed.some(t => t.settingsId === this.currentProductIconTheme.settingsId)) {
|
||||
// current theme is no longer available
|
||||
prevProductIconId = this.currentProductIconTheme.id;
|
||||
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);
|
||||
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 = await this.colorThemeRegistry.findThemeBySettingsId(this.themeSettingIdBeforeSchemeSwitch, undefined);
|
||||
if (theme) {
|
||||
this.setColorTheme(theme.id, 'auto');
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getPreferredColorScheme(): ColorScheme | undefined {
|
||||
if (this.configurationService.getValue<boolean>(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) {
|
||||
return ColorScheme.HIGH_CONTRAST;
|
||||
}
|
||||
if (this.configurationService.getValue<boolean>(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<string>(settingId);
|
||||
if (themeSettingId) {
|
||||
const theme = await 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 getColorThemes(): Promise<IWorkbenchColorTheme[]> {
|
||||
return this.colorThemeRegistry.getThemes();
|
||||
}
|
||||
|
||||
public get onDidColorThemeChange(): Event<IWorkbenchColorTheme> {
|
||||
return this.onColorThemeChange.event;
|
||||
}
|
||||
|
||||
public setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchColorTheme | null> {
|
||||
if (!themeId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (themeId === this.currentColorTheme.id && this.currentColorTheme.isLoaded) {
|
||||
return this.settings.setColorTheme(this.currentColorTheme, settingsTarget);
|
||||
}
|
||||
|
||||
themeId = validateThemeId(themeId); // migrate theme ids
|
||||
|
||||
return this.colorThemeRegistry.findThemeById(themeId, DEFAULT_COLOR_THEME_ID).then(themeData => {
|
||||
if (!themeData) {
|
||||
return null;
|
||||
}
|
||||
return themeData.ensureLoaded(this.extensionResourceLoaderService).then(_ => {
|
||||
themeData.setCustomizations(this.settings);
|
||||
return this.applyTheme(themeData, settingsTarget);
|
||||
}, error => {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location!.toString(), error.message)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async reloadCurrentColorTheme() {
|
||||
await this.currentColorTheme.reload(this.extensionResourceLoaderService);
|
||||
this.currentColorTheme.setCustomizations(this.settings);
|
||||
this.applyTheme(this.currentColorTheme, undefined, false);
|
||||
}
|
||||
|
||||
public async restoreColorTheme(): Promise<boolean> {
|
||||
const settingId = this.settings.colorTheme;
|
||||
const theme = await this.colorThemeRegistry.findThemeBySettingsId(settingId);
|
||||
if (theme) {
|
||||
if (settingId !== this.currentColorTheme.settingsId) {
|
||||
await this.setColorTheme(theme.id, undefined);
|
||||
}
|
||||
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));
|
||||
_applyRules([...cssRules].join('\n'), colorThemeRulesClassName);
|
||||
}
|
||||
|
||||
private applyTheme(newTheme: ColorThemeData, settingsTarget: ConfigurationTarget | undefined | 'auto', 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) {
|
||||
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 getFileIconThemes(): Promise<IWorkbenchFileIconTheme[]> {
|
||||
return this.fileIconThemeRegistry.getThemes();
|
||||
}
|
||||
|
||||
public getFileIconTheme() {
|
||||
return this.currentFileIconTheme;
|
||||
}
|
||||
|
||||
public get onDidFileIconThemeChange(): Event<IWorkbenchFileIconTheme> {
|
||||
return this.onFileIconThemeChange.event;
|
||||
}
|
||||
|
||||
|
||||
public async setFileIconTheme(iconTheme: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchFileIconTheme> {
|
||||
iconTheme = iconTheme || '';
|
||||
if (iconTheme === this.currentFileIconTheme.id && this.currentFileIconTheme.isLoaded) {
|
||||
await this.settings.setFileIconTheme(this.currentFileIconTheme, settingsTarget);
|
||||
return this.currentFileIconTheme;
|
||||
}
|
||||
|
||||
const newThemeData = (await this.fileIconThemeRegistry.findThemeById(iconTheme)) || FileIconThemeData.noIconTheme;
|
||||
await newThemeData.ensureLoaded(this.fileService);
|
||||
|
||||
this.applyAndSetFileIconTheme(newThemeData);
|
||||
|
||||
// remember theme data for a quick restore
|
||||
if (newThemeData.isLoaded && (!newThemeData.location || !getRemoteAuthority(newThemeData.location))) {
|
||||
newThemeData.toStorage(this.storageService);
|
||||
}
|
||||
await this.settings.setFileIconTheme(this.currentFileIconTheme, settingsTarget);
|
||||
|
||||
return newThemeData;
|
||||
}
|
||||
|
||||
private async reloadCurrentFileIconTheme() {
|
||||
await this.currentFileIconTheme.reload(this.fileService);
|
||||
this.applyAndSetFileIconTheme(this.currentFileIconTheme);
|
||||
}
|
||||
|
||||
public async restoreFileIconTheme(): Promise<boolean> {
|
||||
const settingId = this.settings.fileIconTheme;
|
||||
const theme = await this.fileIconThemeRegistry.findThemeBySettingsId(settingId);
|
||||
if (theme) {
|
||||
if (settingId !== this.currentFileIconTheme.settingsId) {
|
||||
await this.setFileIconTheme(theme.id, undefined);
|
||||
}
|
||||
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 getProductIconThemes(): Promise<IWorkbenchProductIconTheme[]> {
|
||||
return this.productIconThemeRegistry.getThemes();
|
||||
}
|
||||
|
||||
public getProductIconTheme() {
|
||||
return this.currentProductIconTheme;
|
||||
}
|
||||
|
||||
public get onDidProductIconThemeChange(): Event<IWorkbenchProductIconTheme> {
|
||||
return this.onProductIconThemeChange.event;
|
||||
}
|
||||
|
||||
public async setProductIconTheme(iconTheme: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchProductIconTheme> {
|
||||
iconTheme = iconTheme || '';
|
||||
if (iconTheme === this.currentProductIconTheme.id && this.currentProductIconTheme.isLoaded) {
|
||||
await this.settings.setProductIconTheme(this.currentProductIconTheme, settingsTarget);
|
||||
return this.currentProductIconTheme;
|
||||
}
|
||||
|
||||
const newThemeData = await this.productIconThemeRegistry.findThemeById(iconTheme) || ProductIconThemeData.defaultTheme;
|
||||
await newThemeData.ensureLoaded(this.fileService, this.logService);
|
||||
|
||||
this.applyAndSetProductIconTheme(newThemeData);
|
||||
|
||||
// remember theme data for a quick restore
|
||||
if (newThemeData.isLoaded && (!newThemeData.location || !getRemoteAuthority(newThemeData.location))) {
|
||||
newThemeData.toStorage(this.storageService);
|
||||
}
|
||||
await this.settings.setProductIconTheme(this.currentProductIconTheme, settingsTarget);
|
||||
|
||||
return newThemeData;
|
||||
}
|
||||
|
||||
private async reloadCurrentProductIconTheme() {
|
||||
await this.currentProductIconTheme.reload(this.fileService, this.logService);
|
||||
this.applyAndSetProductIconTheme(this.currentProductIconTheme);
|
||||
}
|
||||
|
||||
public async restoreProductIconTheme(): Promise<boolean> {
|
||||
const settingId = this.settings.productIconTheme;
|
||||
const theme = await this.productIconThemeRegistry.findThemeBySettingsId(settingId);
|
||||
if (theme) {
|
||||
if (settingId !== this.currentProductIconTheme.settingsId) {
|
||||
await this.setProductIconTheme(theme.id, undefined);
|
||||
}
|
||||
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 inExtensionDevelopment: boolean;
|
||||
private watchedLocation: URI | undefined;
|
||||
private watcherDisposable: IDisposable | undefined;
|
||||
private fileChangeListener: IDisposable | undefined;
|
||||
|
||||
constructor(private fileService: IFileService, environmentService: IWorkbenchEnvironmentService, private onUpdate: () => void) {
|
||||
this.inExtensionDevelopment = !!environmentService.extensionDevelopmentLocationURI;
|
||||
}
|
||||
|
||||
update(theme: { location?: URI, watch?: boolean; }) {
|
||||
if (!resources.isEqual(theme.location, this.watchedLocation)) {
|
||||
this.dispose();
|
||||
if (theme.location && (theme.watch || this.inExtensionDevelopment)) {
|
||||
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);
|
||||
@@ -0,0 +1,135 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
interface IColorExtensionPoint {
|
||||
id: string;
|
||||
description: string;
|
||||
defaults: { light: string, dark: string, highContrast: string };
|
||||
}
|
||||
|
||||
const colorRegistry: IColorRegistry = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution);
|
||||
|
||||
const colorReferenceSchema = colorRegistry.getColorReferenceSchema();
|
||||
const colorIdPattern = '^\\w+[.\\w+]*$';
|
||||
|
||||
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IColorExtensionPoint[]>({
|
||||
extensionPoint: 'colors',
|
||||
jsonSchema: {
|
||||
description: nls.localize('contributes.color', 'Contributes extension defined themable colors'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.color.id', 'The identifier of the themable color'),
|
||||
pattern: colorIdPattern,
|
||||
patternErrorMessage: nls.localize('contributes.color.id.format', 'Identifiers must only contain letters, digits and dots and can not start with a dot'),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.color.description', 'The description of the themable color'),
|
||||
},
|
||||
defaults: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
description: nls.localize('contributes.defaults.light', 'The default color for light themes. Either a color value in hex (#RRGGBB[AA]) or the identifier of a themable color which provides the default.'),
|
||||
type: 'string',
|
||||
anyOf: [
|
||||
colorReferenceSchema,
|
||||
{ type: 'string', format: 'color-hex' }
|
||||
]
|
||||
},
|
||||
dark: {
|
||||
description: nls.localize('contributes.defaults.dark', 'The default color for dark themes. Either a color value in hex (#RRGGBB[AA]) or the identifier of a themable color which provides the default.'),
|
||||
type: 'string',
|
||||
anyOf: [
|
||||
colorReferenceSchema,
|
||||
{ type: 'string', format: 'color-hex' }
|
||||
]
|
||||
},
|
||||
highContrast: {
|
||||
description: nls.localize('contributes.defaults.highContrast', 'The default color for high contrast themes. Either a color value in hex (#RRGGBB[AA]) or the identifier of a themable color which provides the default.'),
|
||||
type: 'string',
|
||||
anyOf: [
|
||||
colorReferenceSchema,
|
||||
{ type: 'string', format: 'color-hex' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export class ColorExtensionPoint {
|
||||
|
||||
constructor() {
|
||||
configurationExtPoint.setHandler((extensions, delta) => {
|
||||
for (const extension of delta.added) {
|
||||
const extensionValue = <IColorExtensionPoint[]>extension.value;
|
||||
const collector = extension.collector;
|
||||
|
||||
if (!extensionValue || !Array.isArray(extensionValue)) {
|
||||
collector.error(nls.localize('invalid.colorConfiguration', "'configuration.colors' must be a array"));
|
||||
return;
|
||||
}
|
||||
let parseColorValue = (s: string, name: string) => {
|
||||
if (s.length > 0) {
|
||||
if (s[0] === '#') {
|
||||
return Color.Format.CSS.parseHex(s);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
collector.error(nls.localize('invalid.default.colorType', "{0} must be either a color value in hex (#RRGGBB[AA] or #RGB[A]) or the identifier of a themable color which provides the default.", name));
|
||||
return Color.red;
|
||||
};
|
||||
|
||||
for (const colorContribution of extensionValue) {
|
||||
if (typeof colorContribution.id !== 'string' || colorContribution.id.length === 0) {
|
||||
collector.error(nls.localize('invalid.id', "'configuration.colors.id' must be defined and can not be empty"));
|
||||
return;
|
||||
}
|
||||
if (!colorContribution.id.match(colorIdPattern)) {
|
||||
collector.error(nls.localize('invalid.id.format', "'configuration.colors.id' must only contain letters, digits and dots and can not start with a dot"));
|
||||
return;
|
||||
}
|
||||
if (typeof colorContribution.description !== 'string' || colorContribution.id.length === 0) {
|
||||
collector.error(nls.localize('invalid.description', "'configuration.colors.description' must be defined and can not be empty"));
|
||||
return;
|
||||
}
|
||||
let defaults = colorContribution.defaults;
|
||||
if (!defaults || typeof defaults !== 'object' || typeof defaults.light !== 'string' || typeof defaults.dark !== 'string' || typeof defaults.highContrast !== 'string') {
|
||||
collector.error(nls.localize('invalid.defaults', "'configuration.colors.defaults' must be defined and must contain 'light', 'dark' and 'highContrast'"));
|
||||
return;
|
||||
}
|
||||
colorRegistry.registerColor(colorContribution.id, {
|
||||
light: parseColorValue(defaults.light, 'configuration.colors.defaults.light'),
|
||||
dark: parseColorValue(defaults.dark, 'configuration.colors.defaults.dark'),
|
||||
hc: parseColorValue(defaults.highContrast, 'configuration.colors.defaults.highContrast')
|
||||
}, colorContribution.description);
|
||||
}
|
||||
}
|
||||
for (const extension of delta.removed) {
|
||||
const extensionValue = <IColorExtensionPoint[]>extension.value;
|
||||
for (const colorContribution of extensionValue) {
|
||||
colorRegistry.deregisterColor(colorContribution.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,919 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import * as Json from 'vs/base/common/json';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ExtensionData, ITokenColorCustomizations, ITextMateThemingRule, IWorkbenchColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations, ISemanticTokenRules, ISemanticTokenColorizationSetting, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { convertSettings } from 'vs/workbench/services/themes/common/themeCompatibility';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ITokenStyle, getThemeTypeSelector } from 'vs/platform/theme/common/themeService';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { parse as parsePList } from 'vs/workbench/services/themes/common/plistParser';
|
||||
import { TokenStyle, SemanticTokenRule, ProbeScope, getTokenClassificationRegistry, TokenStyleValue, TokenStyleData, parseClassifierString } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { MatcherWithPriority, Matcher, createMatchers } from 'vs/workbench/services/themes/common/textMateScopeMatcher';
|
||||
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ThemeConfiguration } from 'vs/workbench/services/themes/common/themeConfiguration';
|
||||
import { ColorScheme } from 'vs/platform/theme/common/theme';
|
||||
|
||||
let colorRegistry = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution);
|
||||
|
||||
let tokenClassificationRegistry = getTokenClassificationRegistry();
|
||||
|
||||
const tokenGroupToScopesMap = {
|
||||
comments: ['comment', 'punctuation.definition.comment'],
|
||||
strings: ['string', 'meta.embedded.assembly'],
|
||||
keywords: ['keyword - keyword.operator', 'keyword.control', 'storage', 'storage.type'],
|
||||
numbers: ['constant.numeric'],
|
||||
types: ['entity.name.type', 'entity.name.class', 'support.type', 'support.class'],
|
||||
functions: ['entity.name.function', 'support.function'],
|
||||
variables: ['variable', 'entity.name.variable']
|
||||
};
|
||||
|
||||
|
||||
export type TokenStyleDefinition = SemanticTokenRule | ProbeScope[] | TokenStyleValue;
|
||||
export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDefinition | undefined };
|
||||
|
||||
export type TextMateThemingRuleDefinitions = { [P in keyof TokenStyleData]?: ITextMateThemingRule | undefined; } & { scope?: ProbeScope; };
|
||||
|
||||
export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
|
||||
static readonly STORAGE_KEY = 'colorThemeData';
|
||||
|
||||
id: string;
|
||||
label: string;
|
||||
settingsId: string;
|
||||
description?: string;
|
||||
isLoaded: boolean;
|
||||
location?: URI; // only set for extension from the registry, not for themes restored from the storage
|
||||
watch?: boolean;
|
||||
extensionData?: ExtensionData;
|
||||
|
||||
private themeSemanticHighlighting: boolean | undefined;
|
||||
private customSemanticHighlighting: boolean | undefined;
|
||||
private customSemanticHighlightingDeprecated: boolean | undefined;
|
||||
|
||||
private themeTokenColors: ITextMateThemingRule[] = [];
|
||||
private customTokenColors: ITextMateThemingRule[] = [];
|
||||
private colorMap: IColorMap = {};
|
||||
private customColorMap: IColorMap = {};
|
||||
|
||||
private semanticTokenRules: SemanticTokenRule[] = [];
|
||||
private customSemanticTokenRules: SemanticTokenRule[] = [];
|
||||
|
||||
private themeTokenScopeMatchers: Matcher<ProbeScope>[] | undefined;
|
||||
private customTokenScopeMatchers: Matcher<ProbeScope>[] | undefined;
|
||||
|
||||
private textMateThemingRules: ITextMateThemingRule[] | undefined = undefined; // created on demand
|
||||
private tokenColorIndex: TokenColorIndex | undefined = undefined; // created on demand
|
||||
|
||||
private constructor(id: string, label: string, settingsId: string) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.settingsId = settingsId;
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
get semanticHighlighting(): boolean {
|
||||
if (this.customSemanticHighlighting !== undefined) {
|
||||
return this.customSemanticHighlighting;
|
||||
}
|
||||
if (this.customSemanticHighlightingDeprecated !== undefined) {
|
||||
return this.customSemanticHighlightingDeprecated;
|
||||
}
|
||||
return !!this.themeSemanticHighlighting;
|
||||
}
|
||||
|
||||
get tokenColors(): ITextMateThemingRule[] {
|
||||
if (!this.textMateThemingRules) {
|
||||
const result: ITextMateThemingRule[] = [];
|
||||
|
||||
// the default rule (scope empty) is always the first rule. Ignore all other default rules.
|
||||
const foreground = this.getColor(editorForeground) || this.getDefault(editorForeground)!;
|
||||
const background = this.getColor(editorBackground) || this.getDefault(editorBackground)!;
|
||||
result.push({
|
||||
settings: {
|
||||
foreground: normalizeColor(foreground),
|
||||
background: normalizeColor(background)
|
||||
}
|
||||
});
|
||||
|
||||
let hasDefaultTokens = false;
|
||||
|
||||
function addRule(rule: ITextMateThemingRule) {
|
||||
if (rule.scope && rule.settings) {
|
||||
if (rule.scope === 'token.info-token') {
|
||||
hasDefaultTokens = true;
|
||||
}
|
||||
result.push({ scope: rule.scope, settings: { foreground: normalizeColor(rule.settings.foreground), background: normalizeColor(rule.settings.background), fontStyle: rule.settings.fontStyle } });
|
||||
}
|
||||
}
|
||||
|
||||
this.themeTokenColors.forEach(addRule);
|
||||
// Add the custom colors after the theme colors
|
||||
// so that they will override them
|
||||
this.customTokenColors.forEach(addRule);
|
||||
|
||||
if (!hasDefaultTokens) {
|
||||
defaultThemeColors[this.type].forEach(addRule);
|
||||
}
|
||||
this.textMateThemingRules = result;
|
||||
}
|
||||
return this.textMateThemingRules;
|
||||
}
|
||||
|
||||
public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {
|
||||
let color: Color | undefined = this.customColorMap[colorId];
|
||||
if (color) {
|
||||
return color;
|
||||
}
|
||||
color = this.colorMap[colorId];
|
||||
if (useDefault !== false && types.isUndefined(color)) {
|
||||
color = this.getDefault(colorId);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
private getTokenStyle(type: string, modifiers: string[], language: string, useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined {
|
||||
let result: any = {
|
||||
foreground: undefined,
|
||||
bold: undefined,
|
||||
underline: undefined,
|
||||
italic: undefined
|
||||
};
|
||||
let score = {
|
||||
foreground: -1,
|
||||
bold: -1,
|
||||
underline: -1,
|
||||
italic: -1
|
||||
};
|
||||
|
||||
function _processStyle(matchScore: number, style: TokenStyle, definition: TokenStyleDefinition) {
|
||||
if (style.foreground && score.foreground <= matchScore) {
|
||||
score.foreground = matchScore;
|
||||
result.foreground = style.foreground;
|
||||
definitions.foreground = definition;
|
||||
}
|
||||
for (let p of ['bold', 'underline', 'italic']) {
|
||||
const property = p as keyof TokenStyle;
|
||||
const info = style[property];
|
||||
if (info !== undefined) {
|
||||
if (score[property] <= matchScore) {
|
||||
score[property] = matchScore;
|
||||
result[property] = info;
|
||||
definitions[property] = definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function _processSemanticTokenRule(rule: SemanticTokenRule) {
|
||||
const matchScore = rule.selector.match(type, modifiers, language);
|
||||
if (matchScore >= 0) {
|
||||
_processStyle(matchScore, rule.style, rule);
|
||||
}
|
||||
}
|
||||
|
||||
this.semanticTokenRules.forEach(_processSemanticTokenRule);
|
||||
this.customSemanticTokenRules.forEach(_processSemanticTokenRule);
|
||||
|
||||
let hasUndefinedStyleProperty = false;
|
||||
for (let k in score) {
|
||||
const key = k as keyof TokenStyle;
|
||||
if (score[key] === -1) {
|
||||
hasUndefinedStyleProperty = true;
|
||||
} else {
|
||||
score[key] = Number.MAX_VALUE; // set it to the max, so it won't be replaced by a default
|
||||
}
|
||||
}
|
||||
if (hasUndefinedStyleProperty) {
|
||||
for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) {
|
||||
const matchScore = rule.selector.match(type, modifiers, language);
|
||||
if (matchScore >= 0) {
|
||||
let style: TokenStyle | undefined;
|
||||
if (rule.defaults.scopesToProbe) {
|
||||
style = this.resolveScopes(rule.defaults.scopesToProbe);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, rule.defaults.scopesToProbe);
|
||||
}
|
||||
}
|
||||
if (!style && useDefault !== false) {
|
||||
const tokenStyleValue = rule.defaults[this.type];
|
||||
style = this.resolveTokenStyleValue(tokenStyleValue);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, tokenStyleValue!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return TokenStyle.fromData(result);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenStyleValue Resolve a tokenStyleValue in the context of a theme
|
||||
*/
|
||||
public resolveTokenStyleValue(tokenStyleValue: TokenStyleValue | undefined): TokenStyle | undefined {
|
||||
if (tokenStyleValue === undefined) {
|
||||
return undefined;
|
||||
} else if (typeof tokenStyleValue === 'string') {
|
||||
const { type, modifiers, language } = parseClassifierString(tokenStyleValue, '');
|
||||
return this.getTokenStyle(type, modifiers, language);
|
||||
} else if (typeof tokenStyleValue === 'object') {
|
||||
return tokenStyleValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getTokenColorIndex(): TokenColorIndex {
|
||||
// collect all colors that tokens can have
|
||||
if (!this.tokenColorIndex) {
|
||||
const index = new TokenColorIndex();
|
||||
this.tokenColors.forEach(rule => {
|
||||
index.add(rule.settings.foreground);
|
||||
index.add(rule.settings.background);
|
||||
});
|
||||
|
||||
this.semanticTokenRules.forEach(r => index.add(r.style.foreground));
|
||||
tokenClassificationRegistry.getTokenStylingDefaultRules().forEach(r => {
|
||||
const defaultColor = r.defaults[this.type];
|
||||
if (defaultColor && typeof defaultColor === 'object') {
|
||||
index.add(defaultColor.foreground);
|
||||
}
|
||||
});
|
||||
this.customSemanticTokenRules.forEach(r => index.add(r.style.foreground));
|
||||
|
||||
this.tokenColorIndex = index;
|
||||
}
|
||||
return this.tokenColorIndex;
|
||||
}
|
||||
|
||||
public get tokenColorMap(): string[] {
|
||||
return this.getTokenColorIndex().asArray();
|
||||
}
|
||||
|
||||
public getTokenStyleMetadata(typeWithLanguage: string, modifiers: string[], defaultLanguage: string, useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined {
|
||||
const { type, language } = parseClassifierString(typeWithLanguage, defaultLanguage);
|
||||
let style = this.getTokenStyle(type, modifiers, language, useDefault, definitions);
|
||||
if (!style) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
foreground: this.getTokenColorIndex().get(style.foreground),
|
||||
bold: style.bold,
|
||||
underline: style.underline,
|
||||
italic: style.italic
|
||||
};
|
||||
}
|
||||
|
||||
public getTokenStylingRuleScope(rule: SemanticTokenRule): 'setting' | 'theme' | undefined {
|
||||
if (this.customSemanticTokenRules.indexOf(rule) !== -1) {
|
||||
return 'setting';
|
||||
}
|
||||
if (this.semanticTokenRules.indexOf(rule) !== -1) {
|
||||
return 'theme';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getDefault(colorId: ColorIdentifier): Color | undefined {
|
||||
return colorRegistry.resolveDefaultColor(colorId, this);
|
||||
}
|
||||
|
||||
|
||||
public resolveScopes(scopes: ProbeScope[], definitions?: TextMateThemingRuleDefinitions): TokenStyle | undefined {
|
||||
|
||||
if (!this.themeTokenScopeMatchers) {
|
||||
this.themeTokenScopeMatchers = this.themeTokenColors.map(getScopeMatcher);
|
||||
}
|
||||
if (!this.customTokenScopeMatchers) {
|
||||
this.customTokenScopeMatchers = this.customTokenColors.map(getScopeMatcher);
|
||||
}
|
||||
|
||||
for (let scope of scopes) {
|
||||
let foreground: string | undefined = undefined;
|
||||
let fontStyle: string | undefined = undefined;
|
||||
let foregroundScore = -1;
|
||||
let fontStyleScore = -1;
|
||||
let fontStyleThemingRule: ITextMateThemingRule | undefined = undefined;
|
||||
let foregroundThemingRule: ITextMateThemingRule | undefined = undefined;
|
||||
|
||||
function findTokenStyleForScopeInScopes(scopeMatchers: Matcher<ProbeScope>[], themingRules: ITextMateThemingRule[]) {
|
||||
for (let i = 0; i < scopeMatchers.length; i++) {
|
||||
const score = scopeMatchers[i](scope);
|
||||
if (score >= 0) {
|
||||
const themingRule = themingRules[i];
|
||||
const settings = themingRules[i].settings;
|
||||
if (score >= foregroundScore && settings.foreground) {
|
||||
foreground = settings.foreground;
|
||||
foregroundScore = score;
|
||||
foregroundThemingRule = themingRule;
|
||||
}
|
||||
if (score >= fontStyleScore && types.isString(settings.fontStyle)) {
|
||||
fontStyle = settings.fontStyle;
|
||||
fontStyleScore = score;
|
||||
fontStyleThemingRule = themingRule;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findTokenStyleForScopeInScopes(this.themeTokenScopeMatchers, this.themeTokenColors);
|
||||
findTokenStyleForScopeInScopes(this.customTokenScopeMatchers, this.customTokenColors);
|
||||
if (foreground !== undefined || fontStyle !== undefined) {
|
||||
if (definitions) {
|
||||
definitions.foreground = foregroundThemingRule;
|
||||
definitions.bold = definitions.italic = definitions.underline = fontStyleThemingRule;
|
||||
definitions.scope = scope;
|
||||
}
|
||||
|
||||
return TokenStyle.fromSettings(foreground, fontStyle);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public defines(colorId: ColorIdentifier): boolean {
|
||||
return this.customColorMap.hasOwnProperty(colorId) || this.colorMap.hasOwnProperty(colorId);
|
||||
}
|
||||
|
||||
public setCustomizations(settings: ThemeConfiguration) {
|
||||
this.setCustomColors(settings.colorCustomizations);
|
||||
this.setCustomTokenColors(settings.tokenColorCustomizations);
|
||||
this.setCustomSemanticTokenColors(settings.semanticTokenColorCustomizations, settings.experimentalSemanticTokenColorCustomizations);
|
||||
}
|
||||
|
||||
public setCustomColors(colors: IColorCustomizations) {
|
||||
this.customColorMap = {};
|
||||
this.overwriteCustomColors(colors);
|
||||
|
||||
const themeSpecificColors = colors[`[${this.settingsId}]`] as IColorCustomizations;
|
||||
if (types.isObject(themeSpecificColors)) {
|
||||
this.overwriteCustomColors(themeSpecificColors);
|
||||
}
|
||||
|
||||
this.tokenColorIndex = undefined;
|
||||
this.textMateThemingRules = undefined;
|
||||
this.customTokenScopeMatchers = undefined;
|
||||
}
|
||||
|
||||
private overwriteCustomColors(colors: IColorCustomizations) {
|
||||
for (let id in colors) {
|
||||
let colorVal = colors[id];
|
||||
if (typeof colorVal === 'string') {
|
||||
this.customColorMap[id] = Color.fromHex(colorVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setCustomTokenColors(customTokenColors: ITokenColorCustomizations) {
|
||||
this.customTokenColors = [];
|
||||
this.customSemanticHighlightingDeprecated = undefined;
|
||||
|
||||
// first add the non-theme specific settings
|
||||
this.addCustomTokenColors(customTokenColors);
|
||||
|
||||
// append theme specific settings. Last rules will win.
|
||||
const themeSpecificTokenColors = customTokenColors[`[${this.settingsId}]`] as ITokenColorCustomizations;
|
||||
if (types.isObject(themeSpecificTokenColors)) {
|
||||
this.addCustomTokenColors(themeSpecificTokenColors);
|
||||
}
|
||||
|
||||
this.tokenColorIndex = undefined;
|
||||
this.textMateThemingRules = undefined;
|
||||
this.customTokenScopeMatchers = undefined;
|
||||
}
|
||||
|
||||
public setCustomSemanticTokenColors(semanticTokenColors: ISemanticTokenColorCustomizations | undefined, experimental?: IExperimentalSemanticTokenColorCustomizations) {
|
||||
this.customSemanticTokenRules = [];
|
||||
this.customSemanticHighlighting = undefined;
|
||||
|
||||
if (experimental) { // apply deprecated settings first
|
||||
this.readSemanticTokenRules(experimental);
|
||||
const themeSpecificColors = experimental[`[${this.settingsId}]`] as IExperimentalSemanticTokenColorCustomizations;
|
||||
if (types.isObject(themeSpecificColors)) {
|
||||
this.readSemanticTokenRules(themeSpecificColors);
|
||||
}
|
||||
}
|
||||
if (semanticTokenColors) {
|
||||
this.customSemanticHighlighting = semanticTokenColors.enabled;
|
||||
if (semanticTokenColors.rules) {
|
||||
this.readSemanticTokenRules(semanticTokenColors.rules);
|
||||
}
|
||||
const themeSpecificColors = semanticTokenColors[`[${this.settingsId}]`] as ISemanticTokenColorCustomizations;
|
||||
if (types.isObject(themeSpecificColors)) {
|
||||
if (themeSpecificColors.enabled !== undefined) {
|
||||
this.customSemanticHighlighting = themeSpecificColors.enabled;
|
||||
}
|
||||
if (themeSpecificColors.rules) {
|
||||
this.readSemanticTokenRules(themeSpecificColors.rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tokenColorIndex = undefined;
|
||||
this.textMateThemingRules = undefined;
|
||||
}
|
||||
|
||||
|
||||
private readSemanticTokenRules(tokenStylingRuleSection: ISemanticTokenRules) {
|
||||
for (let key in tokenStylingRuleSection) {
|
||||
if (key[0] !== '[') { // still do this test until experimental settings are gone
|
||||
try {
|
||||
const rule = readSemanticTokenRule(key, tokenStylingRuleSection[key]);
|
||||
if (rule) {
|
||||
this.customSemanticTokenRules.push(rule);
|
||||
}
|
||||
} catch (e) {
|
||||
// invalid selector, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addCustomTokenColors(customTokenColors: ITokenColorCustomizations) {
|
||||
// Put the general customizations such as comments, strings, etc. first so that
|
||||
// they can be overridden by specific customizations like "string.interpolated"
|
||||
for (let tokenGroup in tokenGroupToScopesMap) {
|
||||
const group = <keyof typeof tokenGroupToScopesMap>tokenGroup; // TS doesn't type 'tokenGroup' properly
|
||||
let value = customTokenColors[group];
|
||||
if (value) {
|
||||
let settings = typeof value === 'string' ? { foreground: value } : value;
|
||||
let scopes = tokenGroupToScopesMap[group];
|
||||
for (let scope of scopes) {
|
||||
this.customTokenColors.push({ scope, settings });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific customizations
|
||||
if (Array.isArray(customTokenColors.textMateRules)) {
|
||||
for (let rule of customTokenColors.textMateRules) {
|
||||
if (rule.scope && rule.settings) {
|
||||
this.customTokenColors.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (customTokenColors.semanticHighlighting !== undefined) {
|
||||
this.customSemanticHighlightingDeprecated = customTokenColors.semanticHighlighting;
|
||||
}
|
||||
}
|
||||
|
||||
public ensureLoaded(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
|
||||
return !this.isLoaded ? this.load(extensionResourceLoaderService) : Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public reload(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
|
||||
return this.load(extensionResourceLoaderService);
|
||||
}
|
||||
|
||||
private load(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
|
||||
if (!this.location) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
this.themeTokenColors = [];
|
||||
this.clearCaches();
|
||||
|
||||
const result = {
|
||||
colors: {},
|
||||
textMateRules: [],
|
||||
semanticTokenRules: [],
|
||||
semanticHighlighting: false
|
||||
};
|
||||
return _loadColorTheme(extensionResourceLoaderService, this.location, result).then(_ => {
|
||||
this.isLoaded = true;
|
||||
this.semanticTokenRules = result.semanticTokenRules;
|
||||
this.colorMap = result.colors;
|
||||
this.themeTokenColors = result.textMateRules;
|
||||
this.themeSemanticHighlighting = result.semanticHighlighting;
|
||||
});
|
||||
}
|
||||
|
||||
public clearCaches() {
|
||||
this.tokenColorIndex = undefined;
|
||||
this.textMateThemingRules = undefined;
|
||||
this.themeTokenScopeMatchers = undefined;
|
||||
this.customTokenScopeMatchers = undefined;
|
||||
}
|
||||
|
||||
toStorage(storageService: IStorageService) {
|
||||
let colorMapData: { [key: string]: string } = {};
|
||||
for (let key in this.colorMap) {
|
||||
colorMapData[key] = Color.Format.CSS.formatHexA(this.colorMap[key], true);
|
||||
}
|
||||
// no need to persist custom colors, they will be taken from the settings
|
||||
const value = JSON.stringify({
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
settingsId: this.settingsId,
|
||||
themeTokenColors: this.themeTokenColors.map(tc => ({ settings: tc.settings, scope: tc.scope })), // don't pesist names
|
||||
semanticTokenRules: this.semanticTokenRules.map(SemanticTokenRule.toJSONObject),
|
||||
extensionData: ExtensionData.toJSONObject(this.extensionData),
|
||||
themeSemanticHighlighting: this.themeSemanticHighlighting,
|
||||
colorMap: colorMapData,
|
||||
watch: this.watch
|
||||
});
|
||||
|
||||
storageService.store(ColorThemeData.STORAGE_KEY, value, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
get baseTheme(): string {
|
||||
return this.classNames[0];
|
||||
}
|
||||
|
||||
get classNames(): string[] {
|
||||
return this.id.split(' ');
|
||||
}
|
||||
|
||||
get type(): ColorScheme {
|
||||
switch (this.baseTheme) {
|
||||
case VS_LIGHT_THEME: return ColorScheme.LIGHT;
|
||||
case VS_HC_THEME: return ColorScheme.HIGH_CONTRAST;
|
||||
default: return ColorScheme.DARK;
|
||||
}
|
||||
}
|
||||
|
||||
// constructors
|
||||
|
||||
static createUnloadedThemeForThemeType(themeType: ColorScheme, colorMap?: { [id: string]: string }): ColorThemeData {
|
||||
return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType), colorMap);
|
||||
}
|
||||
|
||||
static createUnloadedTheme(id: string, colorMap?: { [id: string]: string }): ColorThemeData {
|
||||
let themeData = new ColorThemeData(id, '', '__' + id);
|
||||
themeData.isLoaded = false;
|
||||
themeData.themeTokenColors = [];
|
||||
themeData.watch = false;
|
||||
if (colorMap) {
|
||||
for (let id in colorMap) {
|
||||
themeData.colorMap[id] = Color.fromHex(colorMap[id]);
|
||||
}
|
||||
}
|
||||
return themeData;
|
||||
}
|
||||
|
||||
static createLoadedEmptyTheme(id: string, settingsId: string): ColorThemeData {
|
||||
let themeData = new ColorThemeData(id, '', settingsId);
|
||||
themeData.isLoaded = true;
|
||||
themeData.themeTokenColors = [];
|
||||
themeData.watch = false;
|
||||
return themeData;
|
||||
}
|
||||
|
||||
static fromStorageData(storageService: IStorageService): ColorThemeData | undefined {
|
||||
const input = storageService.get(ColorThemeData.STORAGE_KEY, StorageScope.GLOBAL);
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
let data = JSON.parse(input);
|
||||
let theme = new ColorThemeData('', '', '');
|
||||
for (let key in data) {
|
||||
switch (key) {
|
||||
case 'colorMap':
|
||||
let colorMapData = data[key];
|
||||
for (let id in colorMapData) {
|
||||
theme.colorMap[id] = Color.fromHex(colorMapData[id]);
|
||||
}
|
||||
break;
|
||||
case 'themeTokenColors':
|
||||
case 'id': case 'label': case 'settingsId': case 'watch': case 'themeSemanticHighlighting':
|
||||
(theme as any)[key] = data[key];
|
||||
break;
|
||||
case 'semanticTokenRules':
|
||||
const rulesData = data[key];
|
||||
if (Array.isArray(rulesData)) {
|
||||
for (let d of rulesData) {
|
||||
const rule = SemanticTokenRule.fromJSONObject(tokenClassificationRegistry, d);
|
||||
if (rule) {
|
||||
theme.semanticTokenRules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'location':
|
||||
// ignore, no longer restore
|
||||
break;
|
||||
case 'extensionData':
|
||||
theme.extensionData = ExtensionData.fromJSONObject(data.extensionData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!theme.id || !theme.settingsId) {
|
||||
return undefined;
|
||||
}
|
||||
return theme;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static fromExtensionTheme(theme: IThemeExtensionPoint, colorThemeLocation: URI, extensionData: ExtensionData): ColorThemeData {
|
||||
const baseTheme: string = theme['uiTheme'] || 'vs-dark';
|
||||
const themeSelector = toCSSSelector(extensionData.extensionId, theme.path);
|
||||
const id = `${baseTheme} ${themeSelector}`;
|
||||
const label = theme.label || basename(theme.path);
|
||||
const settingsId = theme.id || label;
|
||||
const themeData = new ColorThemeData(id, label, settingsId);
|
||||
themeData.description = theme.description;
|
||||
themeData.watch = theme._watch === true;
|
||||
themeData.location = colorThemeLocation;
|
||||
themeData.extensionData = extensionData;
|
||||
themeData.isLoaded = false;
|
||||
return themeData;
|
||||
}
|
||||
}
|
||||
|
||||
function toCSSSelector(extensionId: string, path: string) {
|
||||
if (path.startsWith('./')) {
|
||||
path = path.substr(2);
|
||||
}
|
||||
let str = `${extensionId}-${path}`;
|
||||
|
||||
//remove all characters that are not allowed in css
|
||||
str = str.replace(/[^_\-a-zA-Z0-9]/g, '-');
|
||||
if (str.charAt(0).match(/[0-9\-]/)) {
|
||||
str = '_' + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, semanticTokenRules: SemanticTokenRule[], semanticHighlighting: boolean }): Promise<any> {
|
||||
if (resources.extname(themeLocation) === '.json') {
|
||||
const content = await extensionResourceLoaderService.readExtensionResource(themeLocation);
|
||||
let errors: Json.ParseError[] = [];
|
||||
let contentValue = Json.parse(content, errors);
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', '))));
|
||||
} else if (Json.getNodeType(contentValue) !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize('error.invalidformat', "Invalid format for JSON theme file: Object expected.")));
|
||||
}
|
||||
if (contentValue.include) {
|
||||
await _loadColorTheme(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), result);
|
||||
}
|
||||
if (Array.isArray(contentValue.settings)) {
|
||||
convertSettings(contentValue.settings, result);
|
||||
return null;
|
||||
}
|
||||
result.semanticHighlighting = result.semanticHighlighting || contentValue.semanticHighlighting;
|
||||
let colors = contentValue.colors;
|
||||
if (colors) {
|
||||
if (typeof colors !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themeLocation.toString())));
|
||||
}
|
||||
// new JSON color themes format
|
||||
for (let colorId in colors) {
|
||||
let colorHex = colors[colorId];
|
||||
if (typeof colorHex === 'string') { // ignore colors tht are null
|
||||
result.colors[colorId] = Color.fromHex(colors[colorId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let tokenColors = contentValue.tokenColors;
|
||||
if (tokenColors) {
|
||||
if (Array.isArray(tokenColors)) {
|
||||
result.textMateRules.push(...tokenColors);
|
||||
} else if (typeof tokenColors === 'string') {
|
||||
await _loadSyntaxTokens(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), tokenColors), result);
|
||||
} else {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString())));
|
||||
}
|
||||
}
|
||||
let semanticTokenColors = contentValue.semanticTokenColors;
|
||||
if (semanticTokenColors && typeof semanticTokenColors === 'object') {
|
||||
for (let key in semanticTokenColors) {
|
||||
try {
|
||||
const rule = readSemanticTokenRule(key, semanticTokenColors[key]);
|
||||
if (rule) {
|
||||
result.semanticTokenRules.push(rule);
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.semanticTokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'semanticTokenColors' contains a invalid selector", themeLocation.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return _loadSyntaxTokens(extensionResourceLoaderService, themeLocation, result);
|
||||
}
|
||||
}
|
||||
|
||||
function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap }): Promise<any> {
|
||||
return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => {
|
||||
try {
|
||||
let contentValue = parsePList(content);
|
||||
let settings: ITextMateThemingRule[] = contentValue.settings;
|
||||
if (!Array.isArray(settings)) {
|
||||
return Promise.reject(new Error(nls.localize('error.plist.invalidformat', "Problem parsing tmTheme file: {0}. 'settings' is not array.")));
|
||||
}
|
||||
convertSettings(settings, result);
|
||||
return Promise.resolve(null);
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparse', "Problems parsing tmTheme file: {0}", e.message)));
|
||||
}
|
||||
}, error => {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotload', "Problems loading tmTheme file {0}: {1}", themeLocation.toString(), error.message)));
|
||||
});
|
||||
}
|
||||
|
||||
let defaultThemeColors: { [baseTheme: string]: ITextMateThemingRule[] } = {
|
||||
'light': [
|
||||
{ scope: 'token.info-token', settings: { foreground: '#316bcd' } },
|
||||
{ scope: 'token.warn-token', settings: { foreground: '#cd9731' } },
|
||||
{ scope: 'token.error-token', settings: { foreground: '#cd3131' } },
|
||||
{ scope: 'token.debug-token', settings: { foreground: '#800080' } }
|
||||
],
|
||||
'dark': [
|
||||
{ scope: 'token.info-token', settings: { foreground: '#6796e6' } },
|
||||
{ scope: 'token.warn-token', settings: { foreground: '#cd9731' } },
|
||||
{ scope: 'token.error-token', settings: { foreground: '#f44747' } },
|
||||
{ scope: 'token.debug-token', settings: { foreground: '#b267e6' } }
|
||||
],
|
||||
'hc': [
|
||||
{ scope: 'token.info-token', settings: { foreground: '#6796e6' } },
|
||||
{ scope: 'token.warn-token', settings: { foreground: '#008000' } },
|
||||
{ scope: 'token.error-token', settings: { foreground: '#FF0000' } },
|
||||
{ scope: 'token.debug-token', settings: { foreground: '#b267e6' } }
|
||||
],
|
||||
};
|
||||
|
||||
const noMatch = (_scope: ProbeScope) => -1;
|
||||
|
||||
function nameMatcher(identifers: string[], scope: ProbeScope): number {
|
||||
function findInIdents(s: string, lastIndent: number): number {
|
||||
for (let i = lastIndent - 1; i >= 0; i--) {
|
||||
if (scopesAreMatching(s, identifers[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (scope.length < identifers.length) {
|
||||
return -1;
|
||||
}
|
||||
let lastScopeIndex = scope.length - 1;
|
||||
let lastIdentifierIndex = findInIdents(scope[lastScopeIndex--], identifers.length);
|
||||
if (lastIdentifierIndex >= 0) {
|
||||
const score = (lastIdentifierIndex + 1) * 0x10000 + identifers[lastIdentifierIndex].length;
|
||||
while (lastScopeIndex >= 0) {
|
||||
lastIdentifierIndex = findInIdents(scope[lastScopeIndex--], lastIdentifierIndex);
|
||||
if (lastIdentifierIndex === -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
function scopesAreMatching(thisScopeName: string, scopeName: string): boolean {
|
||||
if (!thisScopeName) {
|
||||
return false;
|
||||
}
|
||||
if (thisScopeName === scopeName) {
|
||||
return true;
|
||||
}
|
||||
const len = scopeName.length;
|
||||
return thisScopeName.length > len && thisScopeName.substr(0, len) === scopeName && thisScopeName[len] === '.';
|
||||
}
|
||||
|
||||
function getScopeMatcher(rule: ITextMateThemingRule): Matcher<ProbeScope> {
|
||||
const ruleScope = rule.scope;
|
||||
if (!ruleScope || !rule.settings) {
|
||||
return noMatch;
|
||||
}
|
||||
const matchers: MatcherWithPriority<ProbeScope>[] = [];
|
||||
if (Array.isArray(ruleScope)) {
|
||||
for (let rs of ruleScope) {
|
||||
createMatchers(rs, nameMatcher, matchers);
|
||||
}
|
||||
} else {
|
||||
createMatchers(ruleScope, nameMatcher, matchers);
|
||||
}
|
||||
|
||||
if (matchers.length === 0) {
|
||||
return noMatch;
|
||||
}
|
||||
return (scope: ProbeScope) => {
|
||||
let max = matchers[0].matcher(scope);
|
||||
for (let i = 1; i < matchers.length; i++) {
|
||||
max = Math.max(max, matchers[i].matcher(scope));
|
||||
}
|
||||
return max;
|
||||
};
|
||||
}
|
||||
|
||||
function readSemanticTokenRule(selectorString: string, settings: ISemanticTokenColorizationSetting | string | boolean | undefined): SemanticTokenRule | undefined {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString);
|
||||
let style: TokenStyle | undefined;
|
||||
if (typeof settings === 'string') {
|
||||
style = TokenStyle.fromSettings(settings, undefined);
|
||||
} else if (isSemanticTokenColorizationSetting(settings)) {
|
||||
style = TokenStyle.fromSettings(settings.foreground, settings.fontStyle, settings.bold, settings.underline, settings.italic);
|
||||
}
|
||||
if (style) {
|
||||
return { selector, style };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isSemanticTokenColorizationSetting(style: any): style is ISemanticTokenColorizationSetting {
|
||||
return style && (types.isString(style.foreground) || types.isString(style.fontStyle) || types.isBoolean(style.italic)
|
||||
|| types.isBoolean(style.underline) || types.isBoolean(style.bold));
|
||||
}
|
||||
|
||||
|
||||
class TokenColorIndex {
|
||||
|
||||
private _lastColorId: number;
|
||||
private _id2color: string[];
|
||||
private _color2id: { [color: string]: number; };
|
||||
|
||||
constructor() {
|
||||
this._lastColorId = 0;
|
||||
this._id2color = [];
|
||||
this._color2id = Object.create(null);
|
||||
}
|
||||
|
||||
public add(color: string | Color | undefined): number {
|
||||
color = normalizeColor(color);
|
||||
if (color === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let value = this._color2id[color];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
value = ++this._lastColorId;
|
||||
this._color2id[color] = value;
|
||||
this._id2color[value] = color;
|
||||
return value;
|
||||
}
|
||||
|
||||
public get(color: string | Color | undefined): number {
|
||||
color = normalizeColor(color);
|
||||
if (color === undefined) {
|
||||
return 0;
|
||||
}
|
||||
let value = this._color2id[color];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
console.log(`Color ${color} not in index.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public asArray(): string[] {
|
||||
return this._id2color.slice(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function normalizeColor(color: string | Color | undefined | null): string | undefined {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof color !== 'string') {
|
||||
color = Color.Format.CSS.formatHexA(color, true);
|
||||
}
|
||||
const len = color.length;
|
||||
if (color.charCodeAt(0) !== CharCode.Hash || (len !== 4 && len !== 5 && len !== 7 && len !== 9)) {
|
||||
return undefined;
|
||||
}
|
||||
let result = [CharCode.Hash];
|
||||
|
||||
for (let i = 1; i < len; i++) {
|
||||
const upper = hexUpper(color.charCodeAt(i));
|
||||
if (!upper) {
|
||||
return undefined;
|
||||
}
|
||||
result.push(upper);
|
||||
if (len === 4 || len === 5) {
|
||||
result.push(upper);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 9 && result[7] === CharCode.F && result[8] === CharCode.F) {
|
||||
result.length = 7;
|
||||
}
|
||||
return String.fromCharCode(...result);
|
||||
}
|
||||
|
||||
function hexUpper(charCode: CharCode): number {
|
||||
if (charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9 || charCode >= CharCode.A && charCode <= CharCode.F) {
|
||||
return charCode;
|
||||
} else if (charCode >= CharCode.a && charCode <= CharCode.f) {
|
||||
return charCode - CharCode.a + CharCode.A;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
|
||||
let textMateScopes = [
|
||||
'comment',
|
||||
'comment.block',
|
||||
'comment.block.documentation',
|
||||
'comment.line',
|
||||
'constant',
|
||||
'constant.character',
|
||||
'constant.character.escape',
|
||||
'constant.numeric',
|
||||
'constant.numeric.integer',
|
||||
'constant.numeric.float',
|
||||
'constant.numeric.hex',
|
||||
'constant.numeric.octal',
|
||||
'constant.other',
|
||||
'constant.regexp',
|
||||
'constant.rgb-value',
|
||||
'emphasis',
|
||||
'entity',
|
||||
'entity.name',
|
||||
'entity.name.class',
|
||||
'entity.name.function',
|
||||
'entity.name.method',
|
||||
'entity.name.section',
|
||||
'entity.name.selector',
|
||||
'entity.name.tag',
|
||||
'entity.name.type',
|
||||
'entity.other',
|
||||
'entity.other.attribute-name',
|
||||
'entity.other.inherited-class',
|
||||
'invalid',
|
||||
'invalid.deprecated',
|
||||
'invalid.illegal',
|
||||
'keyword',
|
||||
'keyword.control',
|
||||
'keyword.operator',
|
||||
'keyword.operator.new',
|
||||
'keyword.operator.assignment',
|
||||
'keyword.operator.arithmetic',
|
||||
'keyword.operator.logical',
|
||||
'keyword.other',
|
||||
'markup',
|
||||
'markup.bold',
|
||||
'markup.changed',
|
||||
'markup.deleted',
|
||||
'markup.heading',
|
||||
'markup.inline.raw',
|
||||
'markup.inserted',
|
||||
'markup.italic',
|
||||
'markup.list',
|
||||
'markup.list.numbered',
|
||||
'markup.list.unnumbered',
|
||||
'markup.other',
|
||||
'markup.quote',
|
||||
'markup.raw',
|
||||
'markup.underline',
|
||||
'markup.underline.link',
|
||||
'meta',
|
||||
'meta.block',
|
||||
'meta.cast',
|
||||
'meta.class',
|
||||
'meta.function',
|
||||
'meta.function-call',
|
||||
'meta.preprocessor',
|
||||
'meta.return-type',
|
||||
'meta.selector',
|
||||
'meta.tag',
|
||||
'meta.type.annotation',
|
||||
'meta.type',
|
||||
'punctuation.definition.string.begin',
|
||||
'punctuation.definition.string.end',
|
||||
'punctuation.separator',
|
||||
'punctuation.separator.continuation',
|
||||
'punctuation.terminator',
|
||||
'storage',
|
||||
'storage.modifier',
|
||||
'storage.type',
|
||||
'string',
|
||||
'string.interpolated',
|
||||
'string.other',
|
||||
'string.quoted',
|
||||
'string.quoted.double',
|
||||
'string.quoted.other',
|
||||
'string.quoted.single',
|
||||
'string.quoted.triple',
|
||||
'string.regexp',
|
||||
'string.unquoted',
|
||||
'strong',
|
||||
'support',
|
||||
'support.class',
|
||||
'support.constant',
|
||||
'support.function',
|
||||
'support.other',
|
||||
'support.type',
|
||||
'support.type.property-name',
|
||||
'support.variable',
|
||||
'variable',
|
||||
'variable.language',
|
||||
'variable.name',
|
||||
'variable.other',
|
||||
'variable.other.readwrite',
|
||||
'variable.parameter'
|
||||
];
|
||||
|
||||
export const textmateColorsSchemaId = 'vscode://schemas/textmate-colors';
|
||||
export const textmateColorSettingsSchemaId = `${textmateColorsSchemaId}#definitions/settings`;
|
||||
export const textmateColorGroupSchemaId = `${textmateColorsSchemaId}#definitions/colorGroup`;
|
||||
|
||||
const textmateColorSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
definitions: {
|
||||
colorGroup: {
|
||||
default: '#FF0000',
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
format: 'color-hex'
|
||||
},
|
||||
{
|
||||
$ref: '#definitions/settings'
|
||||
}
|
||||
]
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.token.settings', 'Colors and styles for the token.'),
|
||||
properties: {
|
||||
foreground: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.token.foreground', 'Foreground color for the token.'),
|
||||
format: 'color-hex',
|
||||
default: '#ff0000'
|
||||
},
|
||||
background: {
|
||||
type: 'string',
|
||||
deprecationMessage: nls.localize('schema.token.background.warning', 'Token background colors are currently not supported.')
|
||||
},
|
||||
fontStyle: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.token.fontStyle', 'Font style of the rule: \'italic\', \'bold\' or \'underline\' or a combination. The empty string unsets inherited settings.'),
|
||||
pattern: '^(\\s*\\b(italic|bold|underline))*\\s*$',
|
||||
patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' or a combination or the empty string.'),
|
||||
defaultSnippets: [{ label: nls.localize('schema.token.fontStyle.none', 'None (clear inherited style)'), bodyText: '""' }, { body: 'italic' }, { body: 'bold' }, { body: 'underline' }, { body: 'italic bold' }, { body: 'italic underline' }, { body: 'bold underline' }, { body: 'italic bold underline' }]
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
defaultSnippets: [{ body: { foreground: '${1:#FF0000}', fontStyle: '${2:bold}' } }]
|
||||
}
|
||||
},
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { scope: '${1:keyword.operator}', settings: { foreground: '${2:#FF0000}' } } }],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.properties.name', 'Description of the rule.')
|
||||
},
|
||||
scope: {
|
||||
description: nls.localize('schema.properties.scope', 'Scope selector against which this rule matches.'),
|
||||
anyOf: [
|
||||
{
|
||||
enum: textMateScopes
|
||||
},
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
enum: textMateScopes
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
settings: {
|
||||
$ref: '#definitions/settings'
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'settings', 'scope'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
};
|
||||
|
||||
export const colorThemeSchemaId = 'vscode://schemas/color-theme';
|
||||
|
||||
const colorThemeSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
properties: {
|
||||
colors: {
|
||||
description: nls.localize('schema.workbenchColors', 'Colors in the workbench'),
|
||||
$ref: workbenchColorsSchemaId,
|
||||
additionalProperties: false
|
||||
},
|
||||
tokenColors: {
|
||||
anyOf: [{
|
||||
type: 'string',
|
||||
description: nls.localize('schema.tokenColors.path', 'Path to a tmTheme file (relative to the current file).')
|
||||
},
|
||||
{
|
||||
description: nls.localize('schema.colors', 'Colors for syntax highlighting'),
|
||||
$ref: textmateColorsSchemaId
|
||||
}
|
||||
]
|
||||
},
|
||||
semanticHighlighting: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('schema.supportsSemanticHighlighting', 'Whether semantic highlighting should be enabled for this theme.')
|
||||
},
|
||||
semanticTokenColors: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.semanticTokenColors', 'Colors for semantic tokens'),
|
||||
$ref: tokenStylingSchemaId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function registerColorThemeSchemas() {
|
||||
let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
schemaRegistry.registerSchema(colorThemeSchemaId, colorThemeSchema);
|
||||
schemaRegistry.registerSchema(textmateColorsSchemaId, textmateColorSchema);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex } from 'vs/workbench/services/themes/common/productIconThemeSchema';
|
||||
|
||||
const schemaId = 'vscode://schemas/icon-theme';
|
||||
const schema: IJSONSchema = {
|
||||
type: 'object',
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
definitions: {
|
||||
folderExpanded: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folderExpanded', 'The folder icon for expanded folders. The expanded folder icon is optional. If not set, the icon defined for folder will be shown.')
|
||||
},
|
||||
folder: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folder', 'The folder icon for collapsed folders, and if folderExpanded is not set, also for expanded folders.')
|
||||
|
||||
},
|
||||
file: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.file', 'The default file icon, shown for all files that don\'t match any extension, filename or language id.')
|
||||
|
||||
},
|
||||
folderNames: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.folderNames', 'Associates folder names to icons. The object key is the folder name, not including any path segments. No patterns or wildcards are allowed. Folder name matching is case insensitive.'),
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folderName', 'The ID of the icon definition for the association.')
|
||||
}
|
||||
},
|
||||
folderNamesExpanded: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.folderNamesExpanded', 'Associates folder names to icons for expanded folders. The object key is the folder name, not including any path segments. No patterns or wildcards are allowed. Folder name matching is case insensitive.'),
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.folderNameExpanded', 'The ID of the icon definition for the association.')
|
||||
}
|
||||
},
|
||||
fileExtensions: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.fileExtensions', 'Associates file extensions to icons. The object key is the file extension name. The extension name is the last segment of a file name after the last dot (not including the dot). Extensions are compared case insensitive.'),
|
||||
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.fileExtension', 'The ID of the icon definition for the association.')
|
||||
}
|
||||
},
|
||||
fileNames: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.fileNames', 'Associates file names to icons. The object key is the full file name, but not including any path segments. File name can include dots and a possible file extension. No patterns or wildcards are allowed. File name matching is case insensitive.'),
|
||||
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.fileName', 'The ID of the icon definition for the association.')
|
||||
}
|
||||
},
|
||||
languageIds: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.languageIds', 'Associates languages to icons. The object key is the language id as defined in the language contribution point.'),
|
||||
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.languageId', 'The ID of the icon definition for the association.')
|
||||
}
|
||||
},
|
||||
associations: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folderExpanded: {
|
||||
$ref: '#/definitions/folderExpanded'
|
||||
},
|
||||
folder: {
|
||||
$ref: '#/definitions/folder'
|
||||
},
|
||||
file: {
|
||||
$ref: '#/definitions/file'
|
||||
},
|
||||
folderNames: {
|
||||
$ref: '#/definitions/folderNames'
|
||||
},
|
||||
folderNamesExpanded: {
|
||||
$ref: '#/definitions/folderNamesExpanded'
|
||||
},
|
||||
fileExtensions: {
|
||||
$ref: '#/definitions/fileExtensions'
|
||||
},
|
||||
fileNames: {
|
||||
$ref: '#/definitions/fileNames'
|
||||
},
|
||||
languageIds: {
|
||||
$ref: '#/definitions/languageIds'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
fonts: {
|
||||
type: 'array',
|
||||
description: nls.localize('schema.fonts', 'Fonts that are used in the icon definitions.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.id', 'The ID of the font.'),
|
||||
pattern: fontIdRegex,
|
||||
patternErrorMessage: nls.localize('schema.id.formatError', 'The ID must only contain letter, numbers, underscore and minus.')
|
||||
},
|
||||
src: {
|
||||
type: 'array',
|
||||
description: nls.localize('schema.src', 'The location of the font.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-path', 'The font path, relative to the current file icon theme file.'),
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-format', 'The format of the font.'),
|
||||
enum: ['woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', 'svg']
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'path',
|
||||
'format'
|
||||
]
|
||||
}
|
||||
},
|
||||
weight: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-weight', 'The weight of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight for valid values.'),
|
||||
pattern: fontWeightRegex
|
||||
},
|
||||
style: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-style', 'The style of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style for valid values.'),
|
||||
pattern: fontStyleRegex
|
||||
},
|
||||
size: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-size', 'The default size of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-size for valid values.'),
|
||||
pattern: fontSizeRegex
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'src'
|
||||
]
|
||||
}
|
||||
},
|
||||
iconDefinitions: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.iconDefinitions', 'Description of all icons that can be used when associating files to icons.'),
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.iconDefinition', 'An icon definition. The object key is the ID of the definition.'),
|
||||
properties: {
|
||||
iconPath: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.iconPath', 'When using a SVG or PNG: The path to the image. The path is relative to the icon set file.')
|
||||
},
|
||||
fontCharacter: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.')
|
||||
},
|
||||
fontColor: {
|
||||
type: 'string',
|
||||
format: 'color-hex',
|
||||
description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.')
|
||||
},
|
||||
fontSize: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.fontSize', 'When using a font: The font size in percentage to the text font. If not set, defaults to the size in the font definition.'),
|
||||
pattern: fontSizeRegex
|
||||
},
|
||||
fontId: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.fontId', 'When using a font: The id of the font. If not set, defaults to the first font definition.')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
folderExpanded: {
|
||||
$ref: '#/definitions/folderExpanded'
|
||||
},
|
||||
folder: {
|
||||
$ref: '#/definitions/folder'
|
||||
},
|
||||
file: {
|
||||
$ref: '#/definitions/file'
|
||||
},
|
||||
folderNames: {
|
||||
$ref: '#/definitions/folderNames'
|
||||
},
|
||||
fileExtensions: {
|
||||
$ref: '#/definitions/fileExtensions'
|
||||
},
|
||||
fileNames: {
|
||||
$ref: '#/definitions/fileNames'
|
||||
},
|
||||
languageIds: {
|
||||
$ref: '#/definitions/languageIds'
|
||||
},
|
||||
light: {
|
||||
$ref: '#/definitions/associations',
|
||||
description: nls.localize('schema.light', 'Optional associations for file icons in light color themes.')
|
||||
},
|
||||
highContrast: {
|
||||
$ref: '#/definitions/associations',
|
||||
description: nls.localize('schema.highContrast', 'Optional associations for file icons in high contrast color themes.')
|
||||
},
|
||||
hidesExplorerArrows: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('schema.hidesExplorerArrows', 'Configures whether the file explorer\'s arrows should be hidden when this theme is active.')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function registerFileIconThemeSchemas() {
|
||||
let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
schemaRegistry.registerSchema(schemaId, schema);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IHostColorSchemeService = createDecorator<IHostColorSchemeService>('hostColorSchemeService');
|
||||
|
||||
export interface IHostColorSchemeService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly dark: boolean;
|
||||
readonly highContrast: boolean;
|
||||
readonly onDidChangeColorScheme: Event<void>;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const enum ChCode {
|
||||
BOM = 65279,
|
||||
|
||||
SPACE = 32,
|
||||
TAB = 9,
|
||||
CARRIAGE_RETURN = 13,
|
||||
LINE_FEED = 10,
|
||||
|
||||
SLASH = 47,
|
||||
|
||||
LESS_THAN = 60,
|
||||
QUESTION_MARK = 63,
|
||||
EXCLAMATION_MARK = 33,
|
||||
}
|
||||
|
||||
const enum State {
|
||||
ROOT_STATE = 0,
|
||||
DICT_STATE = 1,
|
||||
ARR_STATE = 2
|
||||
}
|
||||
|
||||
export function parseWithLocation(content: string, filename: string, locationKeyName: string): any {
|
||||
return _parse(content, filename, locationKeyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A very fast plist parser
|
||||
*/
|
||||
export function parse(content: string): any {
|
||||
return _parse(content, null, null);
|
||||
}
|
||||
|
||||
function _parse(content: string, filename: string | null, locationKeyName: string | null): any {
|
||||
const len = content.length;
|
||||
|
||||
let pos = 0;
|
||||
let line = 1;
|
||||
let char = 0;
|
||||
|
||||
// Skip UTF8 BOM
|
||||
if (len > 0 && content.charCodeAt(0) === ChCode.BOM) {
|
||||
pos = 1;
|
||||
}
|
||||
|
||||
function advancePosBy(by: number): void {
|
||||
if (locationKeyName === null) {
|
||||
pos = pos + by;
|
||||
} else {
|
||||
while (by > 0) {
|
||||
let chCode = content.charCodeAt(pos);
|
||||
if (chCode === ChCode.LINE_FEED) {
|
||||
pos++; line++; char = 0;
|
||||
} else {
|
||||
pos++; char++;
|
||||
}
|
||||
by--;
|
||||
}
|
||||
}
|
||||
}
|
||||
function advancePosTo(to: number): void {
|
||||
if (locationKeyName === null) {
|
||||
pos = to;
|
||||
} else {
|
||||
advancePosBy(to - pos);
|
||||
}
|
||||
}
|
||||
|
||||
function skipWhitespace(): void {
|
||||
while (pos < len) {
|
||||
let chCode = content.charCodeAt(pos);
|
||||
if (chCode !== ChCode.SPACE && chCode !== ChCode.TAB && chCode !== ChCode.CARRIAGE_RETURN && chCode !== ChCode.LINE_FEED) {
|
||||
break;
|
||||
}
|
||||
advancePosBy(1);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceIfStartsWith(str: string): boolean {
|
||||
if (content.substr(pos, str.length) === str) {
|
||||
advancePosBy(str.length);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function advanceUntil(str: string): void {
|
||||
let nextOccurence = content.indexOf(str, pos);
|
||||
if (nextOccurence !== -1) {
|
||||
advancePosTo(nextOccurence + str.length);
|
||||
} else {
|
||||
// EOF
|
||||
advancePosTo(len);
|
||||
}
|
||||
}
|
||||
|
||||
function captureUntil(str: string): string {
|
||||
let nextOccurence = content.indexOf(str, pos);
|
||||
if (nextOccurence !== -1) {
|
||||
let r = content.substring(pos, nextOccurence);
|
||||
advancePosTo(nextOccurence + str.length);
|
||||
return r;
|
||||
} else {
|
||||
// EOF
|
||||
let r = content.substr(pos);
|
||||
advancePosTo(len);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
let state = State.ROOT_STATE;
|
||||
|
||||
let cur: any = null;
|
||||
let stateStack: State[] = [];
|
||||
let objStack: any[] = [];
|
||||
let curKey: string | null = null;
|
||||
|
||||
function pushState(newState: State, newCur: any): void {
|
||||
stateStack.push(state);
|
||||
objStack.push(cur);
|
||||
state = newState;
|
||||
cur = newCur;
|
||||
}
|
||||
|
||||
function popState(): void {
|
||||
if (stateStack.length === 0) {
|
||||
return fail('illegal state stack');
|
||||
}
|
||||
state = stateStack.pop()!;
|
||||
cur = objStack.pop();
|
||||
}
|
||||
|
||||
function fail(msg: string): void {
|
||||
throw new Error('Near offset ' + pos + ': ' + msg + ' ~~~' + content.substr(pos, 50) + '~~~');
|
||||
}
|
||||
|
||||
const dictState = {
|
||||
enterDict: function () {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
let newDict: { [key: string]: any } = {};
|
||||
if (locationKeyName !== null) {
|
||||
newDict[locationKeyName] = {
|
||||
filename: filename,
|
||||
line: line,
|
||||
char: char
|
||||
};
|
||||
}
|
||||
cur[curKey] = newDict;
|
||||
curKey = null;
|
||||
pushState(State.DICT_STATE, newDict);
|
||||
},
|
||||
enterArray: function () {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
let newArr: any[] = [];
|
||||
cur[curKey] = newArr;
|
||||
curKey = null;
|
||||
pushState(State.ARR_STATE, newArr);
|
||||
}
|
||||
};
|
||||
|
||||
const arrState = {
|
||||
enterDict: function () {
|
||||
let newDict: { [key: string]: any } = {};
|
||||
if (locationKeyName !== null) {
|
||||
newDict[locationKeyName] = {
|
||||
filename: filename,
|
||||
line: line,
|
||||
char: char
|
||||
};
|
||||
}
|
||||
cur.push(newDict);
|
||||
pushState(State.DICT_STATE, newDict);
|
||||
},
|
||||
enterArray: function () {
|
||||
let newArr: any[] = [];
|
||||
cur.push(newArr);
|
||||
pushState(State.ARR_STATE, newArr);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function enterDict() {
|
||||
if (state === State.DICT_STATE) {
|
||||
dictState.enterDict();
|
||||
} else if (state === State.ARR_STATE) {
|
||||
arrState.enterDict();
|
||||
} else { // ROOT_STATE
|
||||
cur = {};
|
||||
if (locationKeyName !== null) {
|
||||
cur[locationKeyName] = {
|
||||
filename: filename,
|
||||
line: line,
|
||||
char: char
|
||||
};
|
||||
}
|
||||
pushState(State.DICT_STATE, cur);
|
||||
}
|
||||
}
|
||||
function leaveDict() {
|
||||
if (state === State.DICT_STATE) {
|
||||
popState();
|
||||
} else if (state === State.ARR_STATE) {
|
||||
return fail('unexpected </dict>');
|
||||
} else { // ROOT_STATE
|
||||
return fail('unexpected </dict>');
|
||||
}
|
||||
}
|
||||
function enterArray() {
|
||||
if (state === State.DICT_STATE) {
|
||||
dictState.enterArray();
|
||||
} else if (state === State.ARR_STATE) {
|
||||
arrState.enterArray();
|
||||
} else { // ROOT_STATE
|
||||
cur = [];
|
||||
pushState(State.ARR_STATE, cur);
|
||||
}
|
||||
}
|
||||
function leaveArray() {
|
||||
if (state === State.DICT_STATE) {
|
||||
return fail('unexpected </array>');
|
||||
} else if (state === State.ARR_STATE) {
|
||||
popState();
|
||||
} else { // ROOT_STATE
|
||||
return fail('unexpected </array>');
|
||||
}
|
||||
}
|
||||
function acceptKey(val: string) {
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey !== null) {
|
||||
return fail('too many <key>');
|
||||
}
|
||||
curKey = val;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
return fail('unexpected <key>');
|
||||
} else { // ROOT_STATE
|
||||
return fail('unexpected <key>');
|
||||
}
|
||||
}
|
||||
function acceptString(val: string) {
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
function acceptReal(val: number) {
|
||||
if (isNaN(val)) {
|
||||
return fail('cannot parse float');
|
||||
}
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
function acceptInteger(val: number) {
|
||||
if (isNaN(val)) {
|
||||
return fail('cannot parse integer');
|
||||
}
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
function acceptDate(val: Date) {
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
function acceptData(val: string) {
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
function acceptBool(val: boolean) {
|
||||
if (state === State.DICT_STATE) {
|
||||
if (curKey === null) {
|
||||
return fail('missing <key>');
|
||||
}
|
||||
cur[curKey] = val;
|
||||
curKey = null;
|
||||
} else if (state === State.ARR_STATE) {
|
||||
cur.push(val);
|
||||
} else { // ROOT_STATE
|
||||
cur = val;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeVal(str: string): string {
|
||||
return str.replace(/&#([0-9]+);/g, function (_: string, m0: string) {
|
||||
return (<any>String).fromCodePoint(parseInt(m0, 10));
|
||||
}).replace(/&#x([0-9a-f]+);/g, function (_: string, m0: string) {
|
||||
return (<any>String).fromCodePoint(parseInt(m0, 16));
|
||||
}).replace(/&|<|>|"|'/g, function (_: string) {
|
||||
switch (_) {
|
||||
case '&': return '&';
|
||||
case '<': return '<';
|
||||
case '>': return '>';
|
||||
case '"': return '"';
|
||||
case ''': return '\'';
|
||||
}
|
||||
return _;
|
||||
});
|
||||
}
|
||||
|
||||
interface IParsedTag {
|
||||
name: string;
|
||||
isClosed: boolean;
|
||||
}
|
||||
|
||||
function parseOpenTag(): IParsedTag {
|
||||
let r = captureUntil('>');
|
||||
let isClosed = false;
|
||||
if (r.charCodeAt(r.length - 1) === ChCode.SLASH) {
|
||||
isClosed = true;
|
||||
r = r.substring(0, r.length - 1);
|
||||
}
|
||||
|
||||
return {
|
||||
name: r.trim(),
|
||||
isClosed: isClosed
|
||||
};
|
||||
}
|
||||
|
||||
function parseTagValue(tag: IParsedTag): string {
|
||||
if (tag.isClosed) {
|
||||
return '';
|
||||
}
|
||||
let val = captureUntil('</');
|
||||
advanceUntil('>');
|
||||
return escapeVal(val);
|
||||
}
|
||||
|
||||
while (pos < len) {
|
||||
skipWhitespace();
|
||||
if (pos >= len) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chCode = content.charCodeAt(pos);
|
||||
advancePosBy(1);
|
||||
if (chCode !== ChCode.LESS_THAN) {
|
||||
return fail('expected <');
|
||||
}
|
||||
|
||||
if (pos >= len) {
|
||||
return fail('unexpected end of input');
|
||||
}
|
||||
|
||||
const peekChCode = content.charCodeAt(pos);
|
||||
|
||||
if (peekChCode === ChCode.QUESTION_MARK) {
|
||||
advancePosBy(1);
|
||||
advanceUntil('?>');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peekChCode === ChCode.EXCLAMATION_MARK) {
|
||||
advancePosBy(1);
|
||||
|
||||
if (advanceIfStartsWith('--')) {
|
||||
advanceUntil('-->');
|
||||
continue;
|
||||
}
|
||||
|
||||
advanceUntil('>');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peekChCode === ChCode.SLASH) {
|
||||
advancePosBy(1);
|
||||
skipWhitespace();
|
||||
|
||||
if (advanceIfStartsWith('plist')) {
|
||||
advanceUntil('>');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (advanceIfStartsWith('dict')) {
|
||||
advanceUntil('>');
|
||||
leaveDict();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (advanceIfStartsWith('array')) {
|
||||
advanceUntil('>');
|
||||
leaveArray();
|
||||
continue;
|
||||
}
|
||||
|
||||
return fail('unexpected closed tag');
|
||||
}
|
||||
|
||||
let tag = parseOpenTag();
|
||||
|
||||
switch (tag.name) {
|
||||
case 'dict':
|
||||
enterDict();
|
||||
if (tag.isClosed) {
|
||||
leaveDict();
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'array':
|
||||
enterArray();
|
||||
if (tag.isClosed) {
|
||||
leaveArray();
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'key':
|
||||
acceptKey(parseTagValue(tag));
|
||||
continue;
|
||||
|
||||
case 'string':
|
||||
acceptString(parseTagValue(tag));
|
||||
continue;
|
||||
|
||||
case 'real':
|
||||
acceptReal(parseFloat(parseTagValue(tag)));
|
||||
continue;
|
||||
|
||||
case 'integer':
|
||||
acceptInteger(parseInt(parseTagValue(tag), 10));
|
||||
continue;
|
||||
|
||||
case 'date':
|
||||
acceptDate(new Date(parseTagValue(tag)));
|
||||
continue;
|
||||
|
||||
case 'data':
|
||||
acceptData(parseTagValue(tag));
|
||||
continue;
|
||||
|
||||
case 'true':
|
||||
parseTagValue(tag);
|
||||
acceptBool(true);
|
||||
continue;
|
||||
|
||||
case 'false':
|
||||
parseTagValue(tag);
|
||||
acceptBool(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^plist/.test(tag.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return fail('unexpected opened tag ' + tag.name);
|
||||
}
|
||||
|
||||
return cur;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { iconsSchemaId } from 'vs/platform/theme/common/iconRegistry';
|
||||
|
||||
export const fontIdRegex = '^([\\w-_]+)$';
|
||||
export const fontStyleRegex = '^(normal|italic|(oblique[ \\w\\s-]+))$';
|
||||
export const fontWeightRegex = '^(normal|bold|lighter|bolder|(\\d{0-1000}))$';
|
||||
export const fontSizeRegex = '^([\\w .%-_]+)$';
|
||||
|
||||
const schemaId = 'vscode://schemas/product-icon-theme';
|
||||
const schema: IJSONSchema = {
|
||||
type: 'object',
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
properties: {
|
||||
fonts: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.id', 'The ID of the font.'),
|
||||
pattern: fontIdRegex,
|
||||
patternErrorMessage: nls.localize('schema.id.formatError', 'The ID must only contain letters, numbers, underscore and minus.')
|
||||
},
|
||||
src: {
|
||||
type: 'array',
|
||||
description: nls.localize('schema.src', 'The location of the font.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-path', 'The font path, relative to the current product icon theme file.'),
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-format', 'The format of the font.'),
|
||||
enum: ['woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', 'svg']
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'path',
|
||||
'format'
|
||||
]
|
||||
}
|
||||
},
|
||||
weight: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-weight', 'The weight of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight for valid values.'),
|
||||
anyOf: [
|
||||
{ enum: ['normal', 'bold', 'lighter', 'bolder'] },
|
||||
{ type: 'string', pattern: fontWeightRegex }
|
||||
]
|
||||
},
|
||||
style: {
|
||||
type: 'string',
|
||||
description: nls.localize('schema.font-style', 'The style of the font. See https://developer.mozilla.org/en-US/docs/Web/CSS/font-style for valid values.'),
|
||||
anyOf: [
|
||||
{ enum: ['normal', 'italic', 'oblique'] },
|
||||
{ type: 'string', pattern: fontStyleRegex }
|
||||
]
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'id',
|
||||
'src'
|
||||
]
|
||||
}
|
||||
},
|
||||
iconDefinitions: {
|
||||
description: nls.localize('schema.iconDefinitions', 'Association of icon name to a font character.'),
|
||||
$ref: iconsSchemaId,
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function registerProductIconThemeSchemas() {
|
||||
let schemaRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
schemaRegistry.registerSchema(schemaId, schema);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export interface MatcherWithPriority<T> {
|
||||
matcher: Matcher<T>;
|
||||
priority: -1 | 0 | 1;
|
||||
}
|
||||
|
||||
export interface Matcher<T> {
|
||||
(matcherInput: T): number;
|
||||
}
|
||||
|
||||
export function createMatchers<T>(selector: string, matchesName: (names: string[], matcherInput: T) => number, results: MatcherWithPriority<T>[]): void {
|
||||
const tokenizer = newTokenizer(selector);
|
||||
let token = tokenizer.next();
|
||||
while (token !== null) {
|
||||
let priority: -1 | 0 | 1 = 0;
|
||||
if (token.length === 2 && token.charAt(1) === ':') {
|
||||
switch (token.charAt(0)) {
|
||||
case 'R': priority = 1; break;
|
||||
case 'L': priority = -1; break;
|
||||
default:
|
||||
console.log(`Unknown priority ${token} in scope selector`);
|
||||
}
|
||||
token = tokenizer.next();
|
||||
}
|
||||
let matcher = parseConjunction();
|
||||
if (matcher) {
|
||||
results.push({ matcher, priority });
|
||||
}
|
||||
if (token !== ',') {
|
||||
break;
|
||||
}
|
||||
token = tokenizer.next();
|
||||
}
|
||||
|
||||
function parseOperand(): Matcher<T> | null {
|
||||
if (token === '-') {
|
||||
token = tokenizer.next();
|
||||
const expressionToNegate = parseOperand();
|
||||
if (!expressionToNegate) {
|
||||
return null;
|
||||
}
|
||||
return matcherInput => {
|
||||
const score = expressionToNegate(matcherInput);
|
||||
return score < 0 ? 0 : -1;
|
||||
};
|
||||
}
|
||||
if (token === '(') {
|
||||
token = tokenizer.next();
|
||||
const expressionInParents = parseInnerExpression();
|
||||
if (token === ')') {
|
||||
token = tokenizer.next();
|
||||
}
|
||||
return expressionInParents;
|
||||
}
|
||||
if (isIdentifier(token)) {
|
||||
const identifiers: string[] = [];
|
||||
do {
|
||||
identifiers.push(token);
|
||||
token = tokenizer.next();
|
||||
} while (isIdentifier(token));
|
||||
return matcherInput => matchesName(identifiers, matcherInput);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function parseConjunction(): Matcher<T> | null {
|
||||
let matcher = parseOperand();
|
||||
if (!matcher) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchers: Matcher<T>[] = [];
|
||||
while (matcher) {
|
||||
matchers.push(matcher);
|
||||
matcher = parseOperand();
|
||||
}
|
||||
return matcherInput => { // and
|
||||
let min = matchers[0](matcherInput);
|
||||
for (let i = 1; min >= 0 && i < matchers.length; i++) {
|
||||
min = Math.min(min, matchers[i](matcherInput));
|
||||
}
|
||||
return min;
|
||||
};
|
||||
}
|
||||
function parseInnerExpression(): Matcher<T> | null {
|
||||
let matcher = parseConjunction();
|
||||
if (!matcher) {
|
||||
return null;
|
||||
}
|
||||
const matchers: Matcher<T>[] = [];
|
||||
while (matcher) {
|
||||
matchers.push(matcher);
|
||||
if (token === '|' || token === ',') {
|
||||
do {
|
||||
token = tokenizer.next();
|
||||
} while (token === '|' || token === ','); // ignore subsequent commas
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
matcher = parseConjunction();
|
||||
}
|
||||
return matcherInput => { // or
|
||||
let max = matchers[0](matcherInput);
|
||||
for (let i = 1; i < matchers.length; i++) {
|
||||
max = Math.max(max, matchers[i](matcherInput));
|
||||
}
|
||||
return max;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isIdentifier(token: string | null): token is string {
|
||||
return !!token && !!token.match(/[\w\.:]+/);
|
||||
}
|
||||
|
||||
function newTokenizer(input: string): { next: () => string | null } {
|
||||
let regex = /([LR]:|[\w\.:][\w\.:\-]*|[\,\|\-\(\)])/g;
|
||||
let match = regex.exec(input);
|
||||
return {
|
||||
next: () => {
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const res = match[0];
|
||||
match = regex.exec(input);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextMateThemingRule, IColorMap } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
import * as editorColorRegistry from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
const settingToColorIdMapping: { [settingId: string]: string[] } = {};
|
||||
function addSettingMapping(settingId: string, colorId: string) {
|
||||
let colorIds = settingToColorIdMapping[settingId];
|
||||
if (!colorIds) {
|
||||
settingToColorIdMapping[settingId] = colorIds = [];
|
||||
}
|
||||
colorIds.push(colorId);
|
||||
}
|
||||
|
||||
export function convertSettings(oldSettings: ITextMateThemingRule[], result: { textMateRules: ITextMateThemingRule[], colors: IColorMap }): void {
|
||||
for (let rule of oldSettings) {
|
||||
result.textMateRules.push(rule);
|
||||
if (!rule.scope) {
|
||||
let settings = rule.settings;
|
||||
if (!settings) {
|
||||
rule.settings = {};
|
||||
} else {
|
||||
for (const settingKey in settings) {
|
||||
const key = <keyof typeof settings>settingKey;
|
||||
let mappings = settingToColorIdMapping[key];
|
||||
if (mappings) {
|
||||
let colorHex = settings[key];
|
||||
if (typeof colorHex === 'string') {
|
||||
let color = Color.fromHex(colorHex);
|
||||
for (let colorId of mappings) {
|
||||
result.colors[colorId] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key !== 'foreground' && key !== 'background' && key !== 'fontStyle') {
|
||||
delete settings[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSettingMapping('background', colorRegistry.editorBackground);
|
||||
addSettingMapping('foreground', colorRegistry.editorForeground);
|
||||
addSettingMapping('selection', colorRegistry.editorSelectionBackground);
|
||||
addSettingMapping('inactiveSelection', colorRegistry.editorInactiveSelection);
|
||||
addSettingMapping('selectionHighlightColor', colorRegistry.editorSelectionHighlight);
|
||||
addSettingMapping('findMatchHighlight', colorRegistry.editorFindMatchHighlight);
|
||||
addSettingMapping('currentFindMatchHighlight', colorRegistry.editorFindMatch);
|
||||
addSettingMapping('hoverHighlight', colorRegistry.editorHoverHighlight);
|
||||
addSettingMapping('wordHighlight', 'editor.wordHighlightBackground'); // inlined to avoid editor/contrib dependenies
|
||||
addSettingMapping('wordHighlightStrong', 'editor.wordHighlightStrongBackground');
|
||||
addSettingMapping('findRangeHighlight', colorRegistry.editorFindRangeHighlight);
|
||||
addSettingMapping('findMatchHighlight', 'peekViewResult.matchHighlightBackground');
|
||||
addSettingMapping('referenceHighlight', 'peekViewEditor.matchHighlightBackground');
|
||||
addSettingMapping('lineHighlight', editorColorRegistry.editorLineHighlight);
|
||||
addSettingMapping('rangeHighlight', editorColorRegistry.editorRangeHighlight);
|
||||
addSettingMapping('caret', editorColorRegistry.editorCursorForeground);
|
||||
addSettingMapping('invisibles', editorColorRegistry.editorWhitespaces);
|
||||
addSettingMapping('guide', editorColorRegistry.editorIndentGuides);
|
||||
addSettingMapping('activeGuide', editorColorRegistry.editorActiveIndentGuides);
|
||||
|
||||
const ansiColorMap = ['ansiBlack', 'ansiRed', 'ansiGreen', 'ansiYellow', 'ansiBlue', 'ansiMagenta', 'ansiCyan', 'ansiWhite',
|
||||
'ansiBrightBlack', 'ansiBrightRed', 'ansiBrightGreen', 'ansiBrightYellow', 'ansiBrightBlue', 'ansiBrightMagenta', 'ansiBrightCyan', 'ansiBrightWhite'
|
||||
];
|
||||
|
||||
for (const color of ansiColorMap) {
|
||||
addSettingMapping(color, 'terminal.' + color);
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { textmateColorsSchemaId, textmateColorGroupSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
|
||||
import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IWorkbenchProductIconTheme, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform';
|
||||
|
||||
const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+';
|
||||
const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+';
|
||||
const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast';
|
||||
|
||||
const DEFAULT_FILE_ICON_THEME_SETTING_VALUE = 'vs-seti';
|
||||
|
||||
export const DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE = 'Default';
|
||||
|
||||
// Configuration: Themes
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
|
||||
const colorThemeSettingEnum: string[] = [];
|
||||
const colorThemeSettingEnumDescriptions: string[] = [];
|
||||
|
||||
const colorThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'string',
|
||||
description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."),
|
||||
default: isWeb ? DEFAULT_THEME_LIGHT_SETTING_VALUE : DEFAULT_THEME_DARK_SETTING_VALUE,
|
||||
enum: colorThemeSettingEnum,
|
||||
enumDescriptions: colorThemeSettingEnumDescriptions,
|
||||
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
|
||||
};
|
||||
const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'string', //
|
||||
markdownDescription: nls.localize({ key: 'preferredDarkColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme for dark OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME),
|
||||
default: DEFAULT_THEME_DARK_SETTING_VALUE,
|
||||
enum: colorThemeSettingEnum,
|
||||
enumDescriptions: colorThemeSettingEnumDescriptions,
|
||||
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
|
||||
};
|
||||
const preferredLightThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize({ key: 'preferredLightColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme for light OS appearance when `#{0}#` is enabled.', ThemeSettings.DETECT_COLOR_SCHEME),
|
||||
default: DEFAULT_THEME_LIGHT_SETTING_VALUE,
|
||||
enum: colorThemeSettingEnum,
|
||||
enumDescriptions: colorThemeSettingEnumDescriptions,
|
||||
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
|
||||
};
|
||||
const preferredHCThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize({ key: 'preferredHCColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme used in high contrast mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC),
|
||||
default: DEFAULT_THEME_HC_SETTING_VALUE,
|
||||
enum: colorThemeSettingEnum,
|
||||
enumDescriptions: colorThemeSettingEnumDescriptions,
|
||||
included: isWindows || isMacintosh,
|
||||
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
|
||||
};
|
||||
const detectColorSchemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'boolean',
|
||||
description: nls.localize('detectColorScheme', 'If set, automatically switch to the preferred color theme based on the OS appearance.'),
|
||||
default: false
|
||||
};
|
||||
|
||||
const colorCustomizationsSchema: IConfigurationPropertySchema = {
|
||||
type: 'object',
|
||||
description: nls.localize('workbenchColors', "Overrides colors from the currently selected color theme."),
|
||||
allOf: [{ $ref: workbenchColorsSchemaId }],
|
||||
default: {},
|
||||
defaultSnippets: [{
|
||||
body: {
|
||||
}
|
||||
}]
|
||||
};
|
||||
const fileIconThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: ['string', 'null'],
|
||||
default: DEFAULT_FILE_ICON_THEME_SETTING_VALUE,
|
||||
description: nls.localize('iconTheme', "Specifies the file icon theme used in the workbench or 'null' to not show any file icons."),
|
||||
enum: [null],
|
||||
enumDescriptions: [nls.localize('noIconThemeDesc', 'No file icons')],
|
||||
errorMessage: nls.localize('iconThemeError', "File icon theme is unknown or not installed.")
|
||||
};
|
||||
const productIconThemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: ['string', 'null'],
|
||||
default: DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE,
|
||||
description: nls.localize('productIconTheme', "Specifies the product icon theme used."),
|
||||
enum: [DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE],
|
||||
enumDescriptions: [nls.localize('defaultProductIconThemeDesc', 'Default')],
|
||||
errorMessage: nls.localize('productIconThemeError', "Product icon theme is unknown or not installed.")
|
||||
};
|
||||
|
||||
const detectHCSchemeSettingSchema: IConfigurationPropertySchema = {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme."),
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
};
|
||||
|
||||
const themeSettingsConfiguration: IConfigurationNode = {
|
||||
id: 'workbench',
|
||||
order: 7.1,
|
||||
type: 'object',
|
||||
properties: {
|
||||
[ThemeSettings.COLOR_THEME]: colorThemeSettingSchema,
|
||||
[ThemeSettings.PREFERRED_DARK_THEME]: preferredDarkThemeSettingSchema,
|
||||
[ThemeSettings.PREFERRED_LIGHT_THEME]: preferredLightThemeSettingSchema,
|
||||
[ThemeSettings.PREFERRED_HC_THEME]: preferredHCThemeSettingSchema,
|
||||
[ThemeSettings.FILE_ICON_THEME]: fileIconThemeSettingSchema,
|
||||
[ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema,
|
||||
[ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema
|
||||
}
|
||||
};
|
||||
configurationRegistry.registerConfiguration(themeSettingsConfiguration);
|
||||
|
||||
const themeSettingsWindowConfiguration: IConfigurationNode = {
|
||||
id: 'window',
|
||||
order: 8.1,
|
||||
type: 'object',
|
||||
properties: {
|
||||
[ThemeSettings.DETECT_HC]: detectHCSchemeSettingSchema,
|
||||
[ThemeSettings.DETECT_COLOR_SCHEME]: detectColorSchemeSettingSchema,
|
||||
}
|
||||
};
|
||||
configurationRegistry.registerConfiguration(themeSettingsWindowConfiguration);
|
||||
|
||||
function tokenGroupSettings(description: string): IJSONSchema {
|
||||
return {
|
||||
description,
|
||||
$ref: textmateColorGroupSchemaId
|
||||
};
|
||||
}
|
||||
|
||||
const tokenColorSchema: IJSONSchema = {
|
||||
properties: {
|
||||
comments: tokenGroupSettings(nls.localize('editorColors.comments', "Sets the colors and styles for comments")),
|
||||
strings: tokenGroupSettings(nls.localize('editorColors.strings', "Sets the colors and styles for strings literals.")),
|
||||
keywords: tokenGroupSettings(nls.localize('editorColors.keywords', "Sets the colors and styles for keywords.")),
|
||||
numbers: tokenGroupSettings(nls.localize('editorColors.numbers', "Sets the colors and styles for number literals.")),
|
||||
types: tokenGroupSettings(nls.localize('editorColors.types', "Sets the colors and styles for type declarations and references.")),
|
||||
functions: tokenGroupSettings(nls.localize('editorColors.functions', "Sets the colors and styles for functions declarations and references.")),
|
||||
variables: tokenGroupSettings(nls.localize('editorColors.variables', "Sets the colors and styles for variables declarations and references.")),
|
||||
textMateRules: {
|
||||
description: nls.localize('editorColors.textMateRules', 'Sets colors and styles using textmate theming rules (advanced).'),
|
||||
$ref: textmateColorsSchemaId
|
||||
},
|
||||
semanticHighlighting: {
|
||||
description: nls.localize('editorColors.semanticHighlighting', 'Whether semantic highlighting should be enabled for this theme.'),
|
||||
deprecationMessage: nls.localize('editorColors.semanticHighlighting.deprecationMessage', 'Use `enabled` in `editor.semanticTokenColorCustomizations` setting instead.'),
|
||||
markdownDeprecationMessage: nls.localize('editorColors.semanticHighlighting.deprecationMessageMarkdown', 'Use `enabled` in `#editor.semanticTokenColorCustomizations#` setting instead.'),
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tokenColorCustomizationSchema: IConfigurationPropertySchema = {
|
||||
description: nls.localize('editorColors', "Overrides editor syntax colors and font style from the currently selected color theme."),
|
||||
default: {},
|
||||
allOf: [tokenColorSchema]
|
||||
};
|
||||
|
||||
const semanticTokenColorSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('editorColors.semanticHighlighting.enabled', 'Whether semantic highlighting is enabled or disabled for this theme'),
|
||||
suggestSortText: '0_enabled'
|
||||
},
|
||||
rules: {
|
||||
$ref: tokenStylingSchemaId,
|
||||
description: nls.localize('editorColors.semanticHighlighting.rules', 'Semantic token styling rules for this theme.'),
|
||||
suggestSortText: '0_rules'
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const semanticTokenColorCustomizationSchema: IConfigurationPropertySchema = {
|
||||
description: nls.localize('semanticTokenColors', "Overrides editor semantic token color and styles from the currently selected color theme."),
|
||||
default: {},
|
||||
allOf: [{ ...semanticTokenColorSchema, patternProperties: { '^\\[': {} } }]
|
||||
};
|
||||
|
||||
const experimentalTokenStylingCustomizationSchema: IConfigurationPropertySchema = {
|
||||
deprecationMessage: nls.localize('editorColors.experimentalTokenStyling.deprecationMessage', 'Use `editor.semanticTokenColorCustomizations` instead.'),
|
||||
markdownDeprecationMessage: nls.localize('editorColors.experimentalTokenStyling.deprecationMessageMarkdown', 'Use `#editor.semanticTokenColorCustomizations#` instead.'),
|
||||
default: {},
|
||||
allOf: [{ $ref: tokenStylingSchemaId }],
|
||||
};
|
||||
const tokenColorCustomizationConfiguration: IConfigurationNode = {
|
||||
id: 'editor',
|
||||
order: 7.2,
|
||||
type: 'object',
|
||||
properties: {
|
||||
[ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS]: tokenColorCustomizationSchema,
|
||||
[ThemeSettings.SEMANTIC_TOKEN_COLOR_CUSTOMIZATIONS]: semanticTokenColorCustomizationSchema,
|
||||
[ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL]: experimentalTokenStylingCustomizationSchema
|
||||
}
|
||||
};
|
||||
|
||||
configurationRegistry.registerConfiguration(tokenColorCustomizationConfiguration);
|
||||
|
||||
export function updateColorThemeConfigurationSchemas(themes: IWorkbenchColorTheme[]) {
|
||||
// updates enum for the 'workbench.colorTheme` setting
|
||||
colorThemeSettingEnum.splice(0, colorThemeSettingEnum.length, ...themes.map(t => t.settingsId));
|
||||
colorThemeSettingEnumDescriptions.splice(0, colorThemeSettingEnumDescriptions.length, ...themes.map(t => t.description || ''));
|
||||
|
||||
const themeSpecificWorkbenchColors: IJSONSchema = { properties: {} };
|
||||
const themeSpecificTokenColors: IJSONSchema = { properties: {} };
|
||||
const themeSpecificSemanticTokenColors: IJSONSchema = { properties: {} };
|
||||
const experimentalThemeSpecificSemanticTokenColors: IJSONSchema = { properties: {} };
|
||||
|
||||
const workbenchColors = { $ref: workbenchColorsSchemaId, additionalProperties: false };
|
||||
const tokenColors = { properties: tokenColorSchema.properties, additionalProperties: false };
|
||||
for (let t of themes) {
|
||||
// add theme specific color customization ("[Abyss]":{ ... })
|
||||
const themeId = `[${t.settingsId}]`;
|
||||
themeSpecificWorkbenchColors.properties![themeId] = workbenchColors;
|
||||
themeSpecificTokenColors.properties![themeId] = tokenColors;
|
||||
themeSpecificSemanticTokenColors.properties![themeId] = semanticTokenColorSchema;
|
||||
experimentalThemeSpecificSemanticTokenColors.properties![themeId] = { $ref: tokenStylingSchemaId, additionalProperties: false };
|
||||
}
|
||||
|
||||
colorCustomizationsSchema.allOf![1] = themeSpecificWorkbenchColors;
|
||||
tokenColorCustomizationSchema.allOf![1] = themeSpecificTokenColors;
|
||||
semanticTokenColorCustomizationSchema.allOf![1] = themeSpecificSemanticTokenColors;
|
||||
experimentalTokenStylingCustomizationSchema.allOf![1] = experimentalThemeSpecificSemanticTokenColors;
|
||||
|
||||
configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration, tokenColorCustomizationConfiguration);
|
||||
}
|
||||
|
||||
export function updateFileIconThemeConfigurationSchemas(themes: IWorkbenchFileIconTheme[]) {
|
||||
fileIconThemeSettingSchema.enum!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.settingsId));
|
||||
fileIconThemeSettingSchema.enumDescriptions!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.description || ''));
|
||||
|
||||
configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration);
|
||||
}
|
||||
|
||||
export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchProductIconTheme[]) {
|
||||
productIconThemeSettingSchema.enum!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.settingsId));
|
||||
productIconThemeSettingSchema.enumDescriptions!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.description || ''));
|
||||
|
||||
configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration);
|
||||
}
|
||||
|
||||
|
||||
export class ThemeConfiguration {
|
||||
constructor(private configurationService: IConfigurationService) {
|
||||
}
|
||||
|
||||
public get colorTheme(): string {
|
||||
return this.configurationService.getValue<string>(ThemeSettings.COLOR_THEME);
|
||||
}
|
||||
|
||||
public get fileIconTheme(): string | null {
|
||||
return this.configurationService.getValue<string | null>(ThemeSettings.FILE_ICON_THEME);
|
||||
}
|
||||
|
||||
public get productIconTheme(): string {
|
||||
return this.configurationService.getValue<string>(ThemeSettings.PRODUCT_ICON_THEME);
|
||||
}
|
||||
|
||||
public get colorCustomizations(): IColorCustomizations {
|
||||
return this.configurationService.getValue<IColorCustomizations>(ThemeSettings.COLOR_CUSTOMIZATIONS) || {};
|
||||
}
|
||||
|
||||
public get tokenColorCustomizations(): ITokenColorCustomizations {
|
||||
return this.configurationService.getValue<ITokenColorCustomizations>(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS) || {};
|
||||
}
|
||||
|
||||
public get semanticTokenColorCustomizations(): ISemanticTokenColorCustomizations | undefined {
|
||||
return this.configurationService.getValue<ISemanticTokenColorCustomizations>(ThemeSettings.SEMANTIC_TOKEN_COLOR_CUSTOMIZATIONS);
|
||||
}
|
||||
|
||||
public get experimentalSemanticTokenColorCustomizations(): IExperimentalSemanticTokenColorCustomizations | undefined {
|
||||
return this.configurationService.getValue<IExperimentalSemanticTokenColorCustomizations>(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL);
|
||||
}
|
||||
|
||||
public async setColorTheme(theme: IWorkbenchColorTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchColorTheme> {
|
||||
await this.writeConfiguration(ThemeSettings.COLOR_THEME, theme.settingsId, settingsTarget);
|
||||
return theme;
|
||||
}
|
||||
|
||||
public async setFileIconTheme(theme: IWorkbenchFileIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchFileIconTheme> {
|
||||
await this.writeConfiguration(ThemeSettings.FILE_ICON_THEME, theme.settingsId, settingsTarget);
|
||||
return theme;
|
||||
}
|
||||
|
||||
public async setProductIconTheme(theme: IWorkbenchProductIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchProductIconTheme> {
|
||||
await this.writeConfiguration(ThemeSettings.PRODUCT_ICON_THEME, theme.settingsId, settingsTarget);
|
||||
return theme;
|
||||
}
|
||||
|
||||
public findAutoConfigurationTarget(key: string) {
|
||||
let settings = this.configurationService.inspect(key);
|
||||
if (!types.isUndefined(settings.workspaceFolderValue)) {
|
||||
return ConfigurationTarget.WORKSPACE_FOLDER;
|
||||
} else if (!types.isUndefined(settings.workspaceValue)) {
|
||||
return ConfigurationTarget.WORKSPACE;
|
||||
} else if (!types.isUndefined(settings.userRemote)) {
|
||||
return ConfigurationTarget.USER_REMOTE;
|
||||
}
|
||||
return ConfigurationTarget.USER;
|
||||
}
|
||||
|
||||
private async writeConfiguration(key: string, value: any, settingsTarget: ConfigurationTarget | 'auto' | undefined): Promise<void> {
|
||||
if (settingsTarget === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let settings = this.configurationService.inspect(key);
|
||||
if (settingsTarget === 'auto') {
|
||||
settingsTarget = this.findAutoConfigurationTarget(key);
|
||||
}
|
||||
|
||||
if (settingsTarget === ConfigurationTarget.USER) {
|
||||
if (value === settings.userValue) {
|
||||
return Promise.resolve(undefined); // nothing to do
|
||||
} else if (value === settings.defaultValue) {
|
||||
if (types.isUndefined(settings.userValue)) {
|
||||
return Promise.resolve(undefined); // nothing to do
|
||||
}
|
||||
value = undefined; // remove configuration from user settings
|
||||
}
|
||||
} else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER || settingsTarget === ConfigurationTarget.USER_REMOTE) {
|
||||
if (value === settings.value) {
|
||||
return Promise.resolve(undefined); // nothing to do
|
||||
}
|
||||
}
|
||||
return this.configurationService.updateValue(key, value, settingsTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as resources from 'vs/base/common/resources';
|
||||
import { ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
import { IExtensionService, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export function registerColorThemeExtensionPoint() {
|
||||
return ExtensionsRegistry.registerExtensionPoint<IThemeExtensionPoint[]>({
|
||||
extensionPoint: 'themes',
|
||||
jsonSchema: {
|
||||
description: nls.localize('vscode.extension.contributes.themes', 'Contributes textmate color themes.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { label: '${1:label}', id: '${2:id}', uiTheme: VS_DARK_THEME, path: './themes/${3:id}.tmTheme.' } }],
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('vscode.extension.contributes.themes.id', 'Id of the color theme as used in the user settings.'),
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
description: nls.localize('vscode.extension.contributes.themes.label', 'Label of the color theme as shown in the UI.'),
|
||||
type: 'string'
|
||||
},
|
||||
uiTheme: {
|
||||
description: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: \'vs\' is the light color theme, \'vs-dark\' is the dark color theme. \'hc-black\' is the dark high contrast theme.'),
|
||||
enum: [VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME]
|
||||
},
|
||||
path: {
|
||||
description: nls.localize('vscode.extension.contributes.themes.path', 'Path of the tmTheme file. The path is relative to the extension folder and is typically \'./colorthemes/awesome-color-theme.json\'.'),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['path', 'uiTheme']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerFileIconThemeExtensionPoint() {
|
||||
return ExtensionsRegistry.registerExtensionPoint<IThemeExtensionPoint[]>({
|
||||
extensionPoint: 'iconThemes',
|
||||
jsonSchema: {
|
||||
description: nls.localize('vscode.extension.contributes.iconThemes', 'Contributes file icon themes.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { id: '${1:id}', label: '${2:label}', path: './fileicons/${3:id}-icon-theme.json' } }],
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('vscode.extension.contributes.iconThemes.id', 'Id of the file icon theme as used in the user settings.'),
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
description: nls.localize('vscode.extension.contributes.iconThemes.label', 'Label of the file icon theme as shown in the UI.'),
|
||||
type: 'string'
|
||||
},
|
||||
path: {
|
||||
description: nls.localize('vscode.extension.contributes.iconThemes.path', 'Path of the file icon theme definition file. The path is relative to the extension folder and is typically \'./fileicons/awesome-icon-theme.json\'.'),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['path', 'id']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function registerProductIconThemeExtensionPoint() {
|
||||
return ExtensionsRegistry.registerExtensionPoint<IThemeExtensionPoint[]>({
|
||||
extensionPoint: 'productIconThemes',
|
||||
jsonSchema: {
|
||||
description: nls.localize('vscode.extension.contributes.productIconThemes', 'Contributes product icon themes.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { id: '${1:id}', label: '${2:label}', path: './producticons/${3:id}-product-icon-theme.json' } }],
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('vscode.extension.contributes.productIconThemes.id', 'Id of the product icon theme as used in the user settings.'),
|
||||
type: 'string'
|
||||
},
|
||||
label: {
|
||||
description: nls.localize('vscode.extension.contributes.productIconThemes.label', 'Label of the product icon theme as shown in the UI.'),
|
||||
type: 'string'
|
||||
},
|
||||
path: {
|
||||
description: nls.localize('vscode.extension.contributes.productIconThemes.path', 'Path of the product icon theme definition file. The path is relative to the extension folder and is typically \'./producticons/awesome-product-icon-theme.json\'.'),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['path', 'id']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface ThemeChangeEvent<T> {
|
||||
themes: T[];
|
||||
added: T[];
|
||||
removed: T[];
|
||||
}
|
||||
|
||||
export interface IThemeData {
|
||||
id: string;
|
||||
settingsId: string | null;
|
||||
location?: URI;
|
||||
}
|
||||
|
||||
export class ThemeRegistry<T extends IThemeData> {
|
||||
|
||||
private extensionThemes: T[];
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<ThemeChangeEvent<T>>();
|
||||
public readonly onDidChange: Event<ThemeChangeEvent<T>> = this.onDidChangeEmitter.event;
|
||||
|
||||
constructor(
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
private readonly themesExtPoint: IExtensionPoint<IThemeExtensionPoint[]>,
|
||||
private create: (theme: IThemeExtensionPoint, themeLocation: URI, extensionData: ExtensionData) => T,
|
||||
private idRequired = false,
|
||||
private builtInTheme: T | undefined = undefined,
|
||||
private isProposedApi = false
|
||||
) {
|
||||
this.extensionThemes = [];
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.themesExtPoint.setHandler((extensions, delta) => {
|
||||
const previousIds: { [key: string]: T } = {};
|
||||
|
||||
const added: T[] = [];
|
||||
for (const theme of this.extensionThemes) {
|
||||
previousIds[theme.id] = theme;
|
||||
}
|
||||
this.extensionThemes.length = 0;
|
||||
for (let ext of extensions) {
|
||||
if (this.isProposedApi) {
|
||||
checkProposedApiEnabled(ext.description);
|
||||
}
|
||||
let extensionData: ExtensionData = {
|
||||
extensionId: ext.description.identifier.value,
|
||||
extensionPublisher: ext.description.publisher,
|
||||
extensionName: ext.description.name,
|
||||
extensionIsBuiltin: ext.description.isBuiltin
|
||||
};
|
||||
this.onThemes(extensionData, ext.description.extensionLocation, ext.value, ext.collector);
|
||||
}
|
||||
for (const theme of this.extensionThemes) {
|
||||
if (!previousIds[theme.id]) {
|
||||
added.push(theme);
|
||||
} else {
|
||||
delete previousIds[theme.id];
|
||||
}
|
||||
}
|
||||
const removed = Object.values(previousIds);
|
||||
this.onDidChangeEmitter.fire({ themes: this.extensionThemes, added, removed });
|
||||
});
|
||||
}
|
||||
|
||||
private onThemes(extensionData: ExtensionData, extensionLocation: URI, themes: IThemeExtensionPoint[], collector: ExtensionMessageCollector): void {
|
||||
if (!Array.isArray(themes)) {
|
||||
collector.error(nls.localize(
|
||||
'reqarray',
|
||||
"Extension point `{0}` must be an array.",
|
||||
this.themesExtPoint.name
|
||||
));
|
||||
return;
|
||||
}
|
||||
themes.forEach(theme => {
|
||||
if (!theme.path || !types.isString(theme.path)) {
|
||||
collector.error(nls.localize(
|
||||
'reqpath',
|
||||
"Expected string in `contributes.{0}.path`. Provided value: {1}",
|
||||
this.themesExtPoint.name,
|
||||
String(theme.path)
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (this.idRequired && (!theme.id || !types.isString(theme.id))) {
|
||||
collector.error(nls.localize(
|
||||
'reqid',
|
||||
"Expected string in `contributes.{0}.id`. Provided value: {1}",
|
||||
this.themesExtPoint.name,
|
||||
String(theme.id)
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
const themeLocation = resources.joinPath(extensionLocation, theme.path);
|
||||
if (!resources.isEqualOrParent(themeLocation, extensionLocation)) {
|
||||
collector.warn(nls.localize('invalid.path.1', "Expected `contributes.{0}.path` ({1}) to be included inside extension's folder ({2}). This might make the extension non-portable.", this.themesExtPoint.name, themeLocation.path, extensionLocation.path));
|
||||
}
|
||||
|
||||
let themeData = this.create(theme, themeLocation, extensionData);
|
||||
this.extensionThemes.push(themeData);
|
||||
});
|
||||
}
|
||||
|
||||
public async findThemeById(themeId: string, defaultId?: string): Promise<T | undefined> {
|
||||
if (this.builtInTheme && this.builtInTheme.id === themeId) {
|
||||
return this.builtInTheme;
|
||||
}
|
||||
const allThemes = await this.getThemes();
|
||||
let defaultTheme: T | undefined = undefined;
|
||||
for (let t of allThemes) {
|
||||
if (t.id === themeId) {
|
||||
return t;
|
||||
}
|
||||
if (t.id === defaultId) {
|
||||
defaultTheme = t;
|
||||
}
|
||||
}
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
public async findThemeBySettingsId(settingsId: string | null, defaultId?: string): Promise<T | undefined> {
|
||||
if (this.builtInTheme && this.builtInTheme.settingsId === settingsId) {
|
||||
return this.builtInTheme;
|
||||
}
|
||||
const allThemes = await this.getThemes();
|
||||
let defaultTheme: T | undefined = undefined;
|
||||
for (let t of allThemes) {
|
||||
if (t.settingsId === settingsId) {
|
||||
return t;
|
||||
}
|
||||
if (t.id === defaultId) {
|
||||
defaultTheme = t;
|
||||
}
|
||||
}
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
public findThemeByExtensionLocation(extLocation: URI | undefined): Promise<T[]> {
|
||||
if (extLocation) {
|
||||
return this.getThemes().then(allThemes => {
|
||||
return allThemes.filter(t => t.location && resources.isEqualOrParent(t.location, extLocation));
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
|
||||
}
|
||||
|
||||
public getThemes(): Promise<T[]> {
|
||||
return this.extensionService.whenInstalledExtensionsRegistered().then(_ => {
|
||||
return this.extensionThemes;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
|
||||
interface ITokenTypeExtensionPoint {
|
||||
id: string;
|
||||
description: string;
|
||||
superType?: string;
|
||||
}
|
||||
|
||||
interface ITokenModifierExtensionPoint {
|
||||
id: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ITokenStyleDefaultExtensionPoint {
|
||||
language?: string;
|
||||
scopes: { [selector: string]: string[] };
|
||||
}
|
||||
|
||||
const tokenClassificationRegistry: ITokenClassificationRegistry = getTokenClassificationRegistry();
|
||||
|
||||
const tokenTypeExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenTypeExtensionPoint[]>({
|
||||
extensionPoint: 'semanticTokenTypes',
|
||||
jsonSchema: {
|
||||
description: nls.localize('contributes.semanticTokenTypes', 'Contributes semantic token types.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.semanticTokenTypes.id', 'The identifier of the semantic token type'),
|
||||
pattern: typeAndModifierIdPattern,
|
||||
patternErrorMessage: nls.localize('contributes.semanticTokenTypes.id.format', 'Identifiers should be in the form letterOrDigit[_-letterOrDigit]*'),
|
||||
},
|
||||
superType: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.semanticTokenTypes.superType', 'The super type of the semantic token type'),
|
||||
pattern: typeAndModifierIdPattern,
|
||||
patternErrorMessage: nls.localize('contributes.semanticTokenTypes.superType.format', 'Super types should be in the form letterOrDigit[_-letterOrDigit]*'),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.color.description', 'The description of the semantic token type'),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tokenModifierExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenModifierExtensionPoint[]>({
|
||||
extensionPoint: 'semanticTokenModifiers',
|
||||
jsonSchema: {
|
||||
description: nls.localize('contributes.semanticTokenModifiers', 'Contributes semantic token modifiers.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.semanticTokenModifiers.id', 'The identifier of the semantic token modifier'),
|
||||
pattern: typeAndModifierIdPattern,
|
||||
patternErrorMessage: nls.localize('contributes.semanticTokenModifiers.id.format', 'Identifiers should be in the form letterOrDigit[_-letterOrDigit]*')
|
||||
},
|
||||
description: {
|
||||
description: nls.localize('contributes.semanticTokenModifiers.description', 'The description of the semantic token modifier')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tokenStyleDefaultsExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenStyleDefaultExtensionPoint[]>({
|
||||
extensionPoint: 'semanticTokenScopes',
|
||||
jsonSchema: {
|
||||
description: nls.localize('contributes.semanticTokenScopes', 'Contributes semantic token scope maps.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
language: {
|
||||
description: nls.localize('contributes.semanticTokenScopes.languages', 'Lists the languge for which the defaults are.'),
|
||||
type: 'string'
|
||||
},
|
||||
scopes: {
|
||||
description: nls.localize('contributes.semanticTokenScopes.scopes', 'Maps a semantic token (described by semantic token selector) to one or more textMate scopes used to represent that token.'),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export class TokenClassificationExtensionPoints {
|
||||
|
||||
constructor() {
|
||||
function validateTypeOrModifier(contribution: ITokenTypeExtensionPoint | ITokenModifierExtensionPoint, extensionPoint: string, collector: ExtensionMessageCollector): boolean {
|
||||
if (typeof contribution.id !== 'string' || contribution.id.length === 0) {
|
||||
collector.error(nls.localize('invalid.id', "'configuration.{0}.id' must be defined and can not be empty", extensionPoint));
|
||||
return false;
|
||||
}
|
||||
if (!contribution.id.match(typeAndModifierIdPattern)) {
|
||||
collector.error(nls.localize('invalid.id.format', "'configuration.{0}.id' must follow the pattern letterOrDigit[-_letterOrDigit]*", extensionPoint));
|
||||
return false;
|
||||
}
|
||||
const superType = (contribution as ITokenTypeExtensionPoint).superType;
|
||||
if (superType && !superType.match(typeAndModifierIdPattern)) {
|
||||
collector.error(nls.localize('invalid.superType.format', "'configuration.{0}.superType' must follow the pattern letterOrDigit[-_letterOrDigit]*", extensionPoint));
|
||||
return false;
|
||||
}
|
||||
if (typeof contribution.description !== 'string' || contribution.id.length === 0) {
|
||||
collector.error(nls.localize('invalid.description', "'configuration.{0}.description' must be defined and can not be empty", extensionPoint));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
tokenTypeExtPoint.setHandler((extensions, delta) => {
|
||||
for (const extension of delta.added) {
|
||||
const extensionValue = <ITokenTypeExtensionPoint[]>extension.value;
|
||||
const collector = extension.collector;
|
||||
|
||||
if (!extensionValue || !Array.isArray(extensionValue)) {
|
||||
collector.error(nls.localize('invalid.semanticTokenTypeConfiguration', "'configuration.semanticTokenType' must be an array"));
|
||||
return;
|
||||
}
|
||||
for (const contribution of extensionValue) {
|
||||
if (validateTypeOrModifier(contribution, 'semanticTokenType', collector)) {
|
||||
tokenClassificationRegistry.registerTokenType(contribution.id, contribution.description, contribution.superType);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const extension of delta.removed) {
|
||||
const extensionValue = <ITokenTypeExtensionPoint[]>extension.value;
|
||||
for (const contribution of extensionValue) {
|
||||
tokenClassificationRegistry.deregisterTokenType(contribution.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
tokenModifierExtPoint.setHandler((extensions, delta) => {
|
||||
for (const extension of delta.added) {
|
||||
const extensionValue = <ITokenModifierExtensionPoint[]>extension.value;
|
||||
const collector = extension.collector;
|
||||
|
||||
if (!extensionValue || !Array.isArray(extensionValue)) {
|
||||
collector.error(nls.localize('invalid.semanticTokenModifierConfiguration', "'configuration.semanticTokenModifier' must be an array"));
|
||||
return;
|
||||
}
|
||||
for (const contribution of extensionValue) {
|
||||
if (validateTypeOrModifier(contribution, 'semanticTokenModifier', collector)) {
|
||||
tokenClassificationRegistry.registerTokenModifier(contribution.id, contribution.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const extension of delta.removed) {
|
||||
const extensionValue = <ITokenModifierExtensionPoint[]>extension.value;
|
||||
for (const contribution of extensionValue) {
|
||||
tokenClassificationRegistry.deregisterTokenModifier(contribution.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
tokenStyleDefaultsExtPoint.setHandler((extensions, delta) => {
|
||||
for (const extension of delta.added) {
|
||||
const extensionValue = <ITokenStyleDefaultExtensionPoint[]>extension.value;
|
||||
const collector = extension.collector;
|
||||
|
||||
if (!extensionValue || !Array.isArray(extensionValue)) {
|
||||
collector.error(nls.localize('invalid.semanticTokenScopes.configuration', "'configuration.semanticTokenScopes' must be an array"));
|
||||
return;
|
||||
}
|
||||
for (const contribution of extensionValue) {
|
||||
if (contribution.language && typeof contribution.language !== 'string') {
|
||||
collector.error(nls.localize('invalid.semanticTokenScopes.language', "'configuration.semanticTokenScopes.language' must be a string"));
|
||||
continue;
|
||||
}
|
||||
if (!contribution.scopes || typeof contribution.scopes !== 'object') {
|
||||
collector.error(nls.localize('invalid.semanticTokenScopes.scopes', "'configuration.semanticTokenScopes.scopes' must be defined as an object"));
|
||||
continue;
|
||||
}
|
||||
for (let selectorString in contribution.scopes) {
|
||||
const tmScopes = contribution.scopes[selectorString];
|
||||
if (!Array.isArray(tmScopes) || tmScopes.some(l => typeof l !== 'string')) {
|
||||
collector.error(nls.localize('invalid.semanticTokenScopes.scopes.value', "'configuration.semanticTokenScopes.scopes' values must be an array of strings"));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language);
|
||||
tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) });
|
||||
} catch (e) {
|
||||
collector.error(nls.localize('invalid.semanticTokenScopes.scopes.selector', "configuration.semanticTokenScopes.scopes': Problems parsing selector {0}.", selectorString));
|
||||
// invalid selector, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const extension of delta.removed) {
|
||||
const extensionValue = <ITokenStyleDefaultExtensionPoint[]>extension.value;
|
||||
for (const contribution of extensionValue) {
|
||||
for (let selectorString in contribution.scopes) {
|
||||
const tmScopes = contribution.scopes[selectorString];
|
||||
try {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language);
|
||||
tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) });
|
||||
} catch (e) {
|
||||
// invalid selector, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { IColorTheme, IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService';
|
||||
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { isBoolean, isString } from 'vs/base/common/types';
|
||||
|
||||
export const IWorkbenchThemeService = createDecorator<IWorkbenchThemeService>('themeService');
|
||||
|
||||
export const VS_LIGHT_THEME = 'vs';
|
||||
export const VS_DARK_THEME = 'vs-dark';
|
||||
export const VS_HC_THEME = 'hc-black';
|
||||
|
||||
export const HC_THEME_ID = 'Default High Contrast';
|
||||
|
||||
export enum ThemeSettings {
|
||||
COLOR_THEME = 'workbench.colorTheme',
|
||||
FILE_ICON_THEME = 'workbench.iconTheme',
|
||||
PRODUCT_ICON_THEME = 'workbench.productIconTheme',
|
||||
COLOR_CUSTOMIZATIONS = 'workbench.colorCustomizations',
|
||||
TOKEN_COLOR_CUSTOMIZATIONS = 'editor.tokenColorCustomizations',
|
||||
SEMANTIC_TOKEN_COLOR_CUSTOMIZATIONS = 'editor.semanticTokenColorCustomizations',
|
||||
TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL = 'editor.tokenColorCustomizationsExperimental',
|
||||
|
||||
PREFERRED_DARK_THEME = 'workbench.preferredDarkColorTheme',
|
||||
PREFERRED_LIGHT_THEME = 'workbench.preferredLightColorTheme',
|
||||
PREFERRED_HC_THEME = 'workbench.preferredHighContrastColorTheme',
|
||||
DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme',
|
||||
DETECT_HC = 'window.autoDetectHighContrast'
|
||||
}
|
||||
|
||||
export interface IWorkbenchTheme {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly extensionData?: ExtensionData;
|
||||
readonly description?: string;
|
||||
readonly settingsId: string | null;
|
||||
}
|
||||
|
||||
export interface IWorkbenchColorTheme extends IWorkbenchTheme, IColorTheme {
|
||||
readonly settingsId: string;
|
||||
readonly tokenColors: ITextMateThemingRule[];
|
||||
}
|
||||
|
||||
export interface IColorMap {
|
||||
[id: string]: Color;
|
||||
}
|
||||
|
||||
export interface IWorkbenchFileIconTheme extends IWorkbenchTheme, IFileIconTheme {
|
||||
}
|
||||
|
||||
export interface IWorkbenchProductIconTheme extends IWorkbenchTheme {
|
||||
readonly settingsId: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IWorkbenchThemeService extends IThemeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchColorTheme | null>;
|
||||
getColorTheme(): IWorkbenchColorTheme;
|
||||
getColorThemes(): Promise<IWorkbenchColorTheme[]>;
|
||||
onDidColorThemeChange: Event<IWorkbenchColorTheme>;
|
||||
restoreColorTheme(): void;
|
||||
|
||||
setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchFileIconTheme>;
|
||||
getFileIconTheme(): IWorkbenchFileIconTheme;
|
||||
getFileIconThemes(): Promise<IWorkbenchFileIconTheme[]>;
|
||||
onDidFileIconThemeChange: Event<IWorkbenchFileIconTheme>;
|
||||
|
||||
setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchProductIconTheme>;
|
||||
getProductIconTheme(): IWorkbenchProductIconTheme;
|
||||
getProductIconThemes(): Promise<IWorkbenchProductIconTheme[]>;
|
||||
onDidProductIconThemeChange: Event<IWorkbenchProductIconTheme>;
|
||||
}
|
||||
|
||||
export interface IColorCustomizations {
|
||||
[colorIdOrThemeSettingsId: string]: string | IColorCustomizations;
|
||||
}
|
||||
|
||||
export interface ITokenColorCustomizations {
|
||||
[groupIdOrThemeSettingsId: string]: string | ITokenColorizationSetting | ITokenColorCustomizations | undefined | ITextMateThemingRule[] | boolean;
|
||||
comments?: string | ITokenColorizationSetting;
|
||||
strings?: string | ITokenColorizationSetting;
|
||||
numbers?: string | ITokenColorizationSetting;
|
||||
keywords?: string | ITokenColorizationSetting;
|
||||
types?: string | ITokenColorizationSetting;
|
||||
functions?: string | ITokenColorizationSetting;
|
||||
variables?: string | ITokenColorizationSetting;
|
||||
textMateRules?: ITextMateThemingRule[];
|
||||
semanticHighlighting?: boolean; // deprecated, use ISemanticTokenColorCustomizations.enabled instead
|
||||
}
|
||||
|
||||
export interface ISemanticTokenColorCustomizations {
|
||||
enabled?: boolean;
|
||||
rules?: ISemanticTokenRules;
|
||||
[styleRuleOrThemeSettingsId: string]: ISemanticTokenRules | ISemanticTokenColorCustomizations | boolean | undefined;
|
||||
}
|
||||
|
||||
export interface IExperimentalSemanticTokenColorCustomizations {
|
||||
[styleRuleOrThemeSettingsId: string]: ISemanticTokenRules | IExperimentalSemanticTokenColorCustomizations | undefined;
|
||||
}
|
||||
|
||||
export interface ISemanticTokenRules {
|
||||
[selector: string]: string | ISemanticTokenColorizationSetting | undefined;
|
||||
}
|
||||
|
||||
export interface ITextMateThemingRule {
|
||||
name?: string;
|
||||
scope?: string | string[];
|
||||
settings: ITokenColorizationSetting;
|
||||
}
|
||||
|
||||
export interface ITokenColorizationSetting {
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
fontStyle?: string; /* [italic|underline|bold] */
|
||||
}
|
||||
|
||||
export interface ISemanticTokenColorizationSetting {
|
||||
foreground?: string;
|
||||
fontStyle?: string; /* [italic|underline|bold] */
|
||||
bold?: boolean;
|
||||
underline?: boolean;
|
||||
italic?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtensionData {
|
||||
extensionId: string;
|
||||
extensionPublisher: string;
|
||||
extensionName: string;
|
||||
extensionIsBuiltin: boolean;
|
||||
}
|
||||
|
||||
export namespace ExtensionData {
|
||||
export function toJSONObject(d: ExtensionData | undefined): any {
|
||||
return d && { _extensionId: d.extensionId, _extensionIsBuiltin: d.extensionIsBuiltin, _extensionName: d.extensionName, _extensionPublisher: d.extensionPublisher };
|
||||
}
|
||||
export function fromJSONObject(o: any): ExtensionData | undefined {
|
||||
if (o && isString(o._extensionId) && isBoolean(o._extensionIsBuiltin) && isString(o._extensionName) && isString(o._extensionPublisher)) {
|
||||
return { extensionId: o._extensionId, extensionIsBuiltin: o._extensionIsBuiltin, extensionName: o._extensionName, extensionPublisher: o._extensionPublisher };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IThemeExtensionPoint {
|
||||
id: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
uiTheme?: typeof VS_LIGHT_THEME | typeof VS_DARK_THEME | typeof VS_HC_THEME;
|
||||
_watch: boolean; // unsupported options to watch location
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IHostColorSchemeService } from 'vs/workbench/services/themes/common/hostColorSchemeService';
|
||||
|
||||
export class NativeHostColorSchemeService extends Disposable implements IHostColorSchemeService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@INativeHostService private readonly nativeHostService: INativeHostService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Color Scheme
|
||||
this._register(this.nativeHostService.onDidChangeColorScheme(({ highContrast, dark }) => {
|
||||
this.dark = dark;
|
||||
this.highContrast = highContrast;
|
||||
this._onDidChangeColorScheme.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
private readonly _onDidChangeColorScheme = this._register(new Emitter<void>());
|
||||
readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event;
|
||||
|
||||
public dark: boolean = this.environmentService.configuration.colorScheme.dark;
|
||||
public highContrast: boolean = this.environmentService.configuration.colorScheme.highContrast;
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IHostColorSchemeService, NativeHostColorSchemeService, true);
|
||||
@@ -0,0 +1,445 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData';
|
||||
import * as assert from 'assert';
|
||||
import { ITokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { TokenStyle, getTokenClassificationRegistry } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { ExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService';
|
||||
import { ITokenStyle } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
const undefinedStyle = { bold: undefined, underline: undefined, italic: undefined };
|
||||
const unsetStyle = { bold: false, underline: false, italic: false };
|
||||
|
||||
function ts(foreground: string | undefined, styleFlags: { bold?: boolean; underline?: boolean; italic?: boolean } | undefined): TokenStyle {
|
||||
const foregroundColor = isString(foreground) ? Color.fromHex(foreground) : undefined;
|
||||
return new TokenStyle(foregroundColor, styleFlags && styleFlags.bold, styleFlags && styleFlags.underline, styleFlags && styleFlags.italic);
|
||||
}
|
||||
|
||||
function tokenStyleAsString(ts: TokenStyle | undefined | null) {
|
||||
if (!ts) {
|
||||
return 'tokenstyle-undefined';
|
||||
}
|
||||
let str = ts.foreground ? ts.foreground.toString() : 'no-foreground';
|
||||
if (ts.bold !== undefined) {
|
||||
str += ts.bold ? '+B' : '-B';
|
||||
}
|
||||
if (ts.underline !== undefined) {
|
||||
str += ts.underline ? '+U' : '-U';
|
||||
}
|
||||
if (ts.italic !== undefined) {
|
||||
str += ts.italic ? '+I' : '-I';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function assertTokenStyle(actual: TokenStyle | undefined | null, expected: TokenStyle | undefined | null, message?: string) {
|
||||
assert.equal(tokenStyleAsString(actual), tokenStyleAsString(expected), message);
|
||||
}
|
||||
|
||||
function assertTokenStyleMetaData(colorIndex: string[], actual: ITokenStyle | undefined, expected: TokenStyle | undefined | null, message = '') {
|
||||
if (expected === undefined || expected === null || actual === undefined) {
|
||||
assert.equal(actual, expected, message);
|
||||
return;
|
||||
}
|
||||
assert.strictEqual(actual.bold, expected.bold, 'bold ' + message);
|
||||
assert.strictEqual(actual.italic, expected.italic, 'italic ' + message);
|
||||
assert.strictEqual(actual.underline, expected.underline, 'underline ' + message);
|
||||
|
||||
const actualForegroundIndex = actual.foreground;
|
||||
if (actualForegroundIndex && expected.foreground) {
|
||||
assert.equal(colorIndex[actualForegroundIndex], Color.Format.CSS.formatHexA(expected.foreground, true).toUpperCase(), 'foreground ' + message);
|
||||
} else {
|
||||
assert.equal(actualForegroundIndex, expected.foreground || 0, 'foreground ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function assertTokenStyles(themeData: ColorThemeData, expected: { [qualifiedClassifier: string]: TokenStyle }, language = 'typescript') {
|
||||
const colorIndex = themeData.tokenColorMap;
|
||||
|
||||
for (let qualifiedClassifier in expected) {
|
||||
const [type, ...modifiers] = qualifiedClassifier.split('.');
|
||||
|
||||
const expectedTokenStyle = expected[qualifiedClassifier];
|
||||
|
||||
const tokenStyleMetaData = themeData.getTokenStyleMetadata(type, modifiers, language);
|
||||
assertTokenStyleMetaData(colorIndex, tokenStyleMetaData, expectedTokenStyle, qualifiedClassifier);
|
||||
}
|
||||
}
|
||||
|
||||
suite('Themes - TokenStyleResolving', () => {
|
||||
|
||||
|
||||
const fileService = new FileService(new NullLogService());
|
||||
const extensionResourceLoaderService = new ExtensionResourceLoaderService(fileService);
|
||||
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
|
||||
|
||||
test('color defaults - monokai', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-monokai/themes/monokai-color-theme.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#88846f', undefinedStyle),
|
||||
'variable': ts('#F8F8F2', unsetStyle),
|
||||
'type': ts('#A6E22E', { bold: false, underline: true, italic: false }),
|
||||
'function': ts('#A6E22E', unsetStyle),
|
||||
'string': ts('#E6DB74', undefinedStyle),
|
||||
'number': ts('#AE81FF', undefinedStyle),
|
||||
'keyword': ts('#F92672', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('color defaults - dark+', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-defaults/themes/dark_plus.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#6A9955', undefinedStyle),
|
||||
'variable': ts('#9CDCFE', undefinedStyle),
|
||||
'type': ts('#4EC9B0', undefinedStyle),
|
||||
'function': ts('#DCDCAA', undefinedStyle),
|
||||
'string': ts('#CE9178', undefinedStyle),
|
||||
'number': ts('#B5CEA8', undefinedStyle),
|
||||
'keyword': ts('#C586C0', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('color defaults - light vs', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-defaults/themes/light_vs.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#008000', undefinedStyle),
|
||||
'variable': ts(undefined, undefinedStyle),
|
||||
'type': ts(undefined, undefinedStyle),
|
||||
'function': ts(undefined, undefinedStyle),
|
||||
'string': ts('#a31515', undefinedStyle),
|
||||
'number': ts('#098658', undefinedStyle),
|
||||
'keyword': ts('#0000ff', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('color defaults - hc', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-defaults/themes/hc_black.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#7ca668', undefinedStyle),
|
||||
'variable': ts('#9CDCFE', undefinedStyle),
|
||||
'type': ts('#4EC9B0', undefinedStyle),
|
||||
'function': ts('#DCDCAA', undefinedStyle),
|
||||
'string': ts('#ce9178', undefinedStyle),
|
||||
'number': ts('#b5cea8', undefinedStyle),
|
||||
'keyword': ts('#C586C0', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('color defaults - kimbie dark', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#a57a4c', undefinedStyle),
|
||||
'variable': ts('#dc3958', undefinedStyle),
|
||||
'type': ts('#f06431', undefinedStyle),
|
||||
'function': ts('#8ab1b0', undefinedStyle),
|
||||
'string': ts('#889b4a', undefinedStyle),
|
||||
'number': ts('#f79a32', undefinedStyle),
|
||||
'keyword': ts('#98676a', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('color defaults - abyss', async () => {
|
||||
const themeData = ColorThemeData.createUnloadedTheme('foo');
|
||||
const themeLocation = getPathFromAmdModule(require, '../../../../../../../extensions/theme-abyss/themes/abyss-color-theme.json');
|
||||
themeData.location = URI.file(themeLocation);
|
||||
await themeData.ensureLoaded(extensionResourceLoaderService);
|
||||
|
||||
assert.equal(themeData.isLoaded, true);
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'comment': ts('#384887', undefinedStyle),
|
||||
'variable': ts(undefined, unsetStyle),
|
||||
'type': ts('#ffeebb', { underline: true, bold: false, italic: false }),
|
||||
'function': ts('#ddbb88', unsetStyle),
|
||||
'string': ts('#22aa44', undefinedStyle),
|
||||
'number': ts('#f280d0', undefinedStyle),
|
||||
'keyword': ts('#225588', undefinedStyle)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('resolveScopes', async () => {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
|
||||
const customTokenColors: ITokenColorCustomizations = {
|
||||
textMateRules: [
|
||||
{
|
||||
scope: 'variable',
|
||||
settings: {
|
||||
fontStyle: '',
|
||||
foreground: '#F8F8F2'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'keyword.operator',
|
||||
settings: {
|
||||
fontStyle: 'italic bold underline',
|
||||
foreground: '#F92672'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'storage',
|
||||
settings: {
|
||||
fontStyle: 'italic',
|
||||
foreground: '#F92672'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: ['storage.type', 'meta.structure.dictionary.json string.quoted.double.json'],
|
||||
settings: {
|
||||
foreground: '#66D9EF'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'entity.name.type, entity.name.class, entity.name.namespace, entity.name.scope-resolution',
|
||||
settings: {
|
||||
fontStyle: 'underline',
|
||||
foreground: '#A6E22E'
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
themeData.setCustomTokenColors(customTokenColors);
|
||||
|
||||
let tokenStyle;
|
||||
let defaultTokenStyle = undefined;
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['variable']]);
|
||||
assertTokenStyle(tokenStyle, ts('#F8F8F2', unsetStyle), 'variable');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['keyword.operator']]);
|
||||
assertTokenStyle(tokenStyle, ts('#F92672', { italic: true, bold: true, underline: true }), 'keyword');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['keyword']]);
|
||||
assertTokenStyle(tokenStyle, defaultTokenStyle, 'keyword');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['keyword.operator']]);
|
||||
assertTokenStyle(tokenStyle, ts('#F92672', { italic: true, bold: true, underline: true }), 'keyword.operator');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['keyword.operators']]);
|
||||
assertTokenStyle(tokenStyle, defaultTokenStyle, 'keyword.operators');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['storage']]);
|
||||
assertTokenStyle(tokenStyle, ts('#F92672', { italic: true, bold: false, underline: false }), 'storage');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['storage.type']]);
|
||||
assertTokenStyle(tokenStyle, ts('#66D9EF', { italic: true, bold: false, underline: false }), 'storage.type');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['entity.name.class']]);
|
||||
assertTokenStyle(tokenStyle, ts('#A6E22E', { italic: false, bold: false, underline: true }), 'entity.name.class');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['meta.structure.dictionary.json', 'string.quoted.double.json']]);
|
||||
assertTokenStyle(tokenStyle, ts('#66D9EF', undefined), 'json property');
|
||||
|
||||
tokenStyle = themeData.resolveScopes([['keyword'], ['storage.type'], ['entity.name.class']]);
|
||||
assertTokenStyle(tokenStyle, ts('#66D9EF', { italic: true, bold: false, underline: false }), 'storage.type');
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('resolveScopes - match most specific', async () => {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
|
||||
const customTokenColors: ITokenColorCustomizations = {
|
||||
textMateRules: [
|
||||
{
|
||||
scope: 'entity.name.type',
|
||||
settings: {
|
||||
fontStyle: 'underline',
|
||||
foreground: '#A6E22E'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'entity.name.type.class',
|
||||
settings: {
|
||||
foreground: '#FF00FF'
|
||||
}
|
||||
},
|
||||
{
|
||||
scope: 'entity.name',
|
||||
settings: {
|
||||
foreground: '#FFFFFF'
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
themeData.setCustomTokenColors(customTokenColors);
|
||||
|
||||
const tokenStyle = themeData.resolveScopes([['entity.name.type.class']]);
|
||||
assertTokenStyle(tokenStyle, ts('#FF00FF', { italic: false, bold: false, underline: true }), 'entity.name.type.class');
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('rule matching', async () => {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomSemanticTokenColors({
|
||||
enabled: true,
|
||||
rules: {
|
||||
'type': '#ff0000',
|
||||
'class': { foreground: '#0000ff', italic: true },
|
||||
'*.static': { bold: true },
|
||||
'*.declaration': { italic: true },
|
||||
'*.async.static': { italic: true, underline: true },
|
||||
'*.async': { foreground: '#000fff', underline: true }
|
||||
}
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, {
|
||||
'type': ts('#ff0000', undefinedStyle),
|
||||
'type.static': ts('#ff0000', { bold: true }),
|
||||
'type.static.declaration': ts('#ff0000', { bold: true, italic: true }),
|
||||
'class': ts('#0000ff', { italic: true }),
|
||||
'class.static.declaration': ts('#0000ff', { bold: true, italic: true, }),
|
||||
'class.declaration': ts('#0000ff', { italic: true }),
|
||||
'class.declaration.async': ts('#000fff', { underline: true, italic: true }),
|
||||
'class.declaration.async.static': ts('#000fff', { italic: true, underline: true, bold: true }),
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('super type', async () => {
|
||||
const registry = getTokenClassificationRegistry();
|
||||
|
||||
registry.registerTokenType('myTestInterface', 'A type just for testing', 'interface');
|
||||
registry.registerTokenType('myTestSubInterface', 'A type just for testing', 'myTestInterface');
|
||||
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomSemanticTokenColors({
|
||||
enabled: true,
|
||||
rules: {
|
||||
'interface': '#ff0000',
|
||||
'myTestInterface': { italic: true },
|
||||
'interface.static': { bold: true }
|
||||
}
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, { 'myTestSubInterface': ts('#ff0000', { italic: true }) });
|
||||
assertTokenStyles(themeData, { 'myTestSubInterface.static': ts('#ff0000', { italic: true, bold: true }) });
|
||||
|
||||
themeData.setCustomSemanticTokenColors({
|
||||
enabled: true,
|
||||
rules: {
|
||||
'interface': '#ff0000',
|
||||
'myTestInterface': { foreground: '#ff00ff', italic: true }
|
||||
}
|
||||
});
|
||||
assertTokenStyles(themeData, { 'myTestSubInterface': ts('#ff00ff', { italic: true }) });
|
||||
} finally {
|
||||
registry.deregisterTokenType('myTestInterface');
|
||||
registry.deregisterTokenType('myTestSubInterface');
|
||||
}
|
||||
});
|
||||
|
||||
test('language', async () => {
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomSemanticTokenColors({
|
||||
enabled: true,
|
||||
rules: {
|
||||
'interface': '#fff000',
|
||||
'interface:java': '#ff0000',
|
||||
'interface.static': { bold: true },
|
||||
'interface.static:typescript': { italic: true }
|
||||
}
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, { 'interface': ts('#ff0000', undefined) }, 'java');
|
||||
assertTokenStyles(themeData, { 'interface': ts('#fff000', undefined) }, 'typescript');
|
||||
assertTokenStyles(themeData, { 'interface.static': ts('#ff0000', { bold: true }) }, 'java');
|
||||
assertTokenStyles(themeData, { 'interface.static': ts('#fff000', { bold: true, italic: true }) }, 'typescript');
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
test('language - scope resolving', async () => {
|
||||
const registry = getTokenClassificationRegistry();
|
||||
|
||||
const numberOfDefaultRules = registry.getTokenStylingDefaultRules().length;
|
||||
|
||||
registry.registerTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1'), { scopesToProbe: [['entity.name.type.ts1']] });
|
||||
registry.registerTokenStyleDefault(registry.parseTokenSelector('type:javascript1'), { scopesToProbe: [['entity.name.type.js1']] });
|
||||
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomTokenColors({
|
||||
textMateRules: [
|
||||
{
|
||||
scope: 'entity.name.type',
|
||||
settings: { foreground: '#aa0000' }
|
||||
},
|
||||
{
|
||||
scope: 'entity.name.type.ts1',
|
||||
settings: { foreground: '#bb0000' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript1');
|
||||
assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript1');
|
||||
|
||||
} finally {
|
||||
registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1'));
|
||||
registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type:javascript1'));
|
||||
|
||||
assert.equal(registry.getTokenStylingDefaultRules().length, numberOfDefaultRules);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user