diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index f0a326bb34d..1df39d31c90 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -252,6 +252,9 @@ ], "colors": { + "editor.background": "#000c18", + "editor.foreground": "#6688cc", + // Base // "foreground": "", "focusBorder": "#596F99", @@ -295,8 +298,6 @@ "scrollbarSlider.hoverBackground": "#3B3F5188", // Editor - "editor.background": "#000c18", - // "editor.foreground": "#6688cc", "editorWidget.background": "#262641", "editorCursor.foreground": "#ddbb88", "editorWhitespace.foreground": "#103050", diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 613ec87b2de..61cf4274a0d 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -14,6 +14,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Registry } from 'vs/platform/registry/common/platform'; import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ThemingExtensions, ICssStyleCollector, IIconTheme, IThemingRegistry } from 'vs/platform/theme/common/themeService'; +import { TokenStyle, TokenClassification, ProbeScope } from 'vs/platform/theme/common/tokenClassificationRegistry'; const VS_THEME_NAME = 'vs'; const VS_DARK_THEME_NAME = 'vs-dark'; @@ -23,6 +24,7 @@ const colorRegistry = Registry.as(Extensions.ColorContribution); const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution); class StandaloneTheme implements IStandaloneTheme { + public readonly id: string; public readonly themeName: string; @@ -128,6 +130,14 @@ class StandaloneTheme implements IStandaloneTheme { } return this._tokenTheme; } + + getTokenStyle(classification: TokenClassification, useDefault?: boolean | undefined): TokenStyle | undefined { + return undefined; + } + + resolveScopes(scopes: ProbeScope[]): TokenStyle | undefined { + return undefined; + } } function isBuiltinTheme(themeName: string): themeName is BuiltinTheme { diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index b35c23cc03a..3cb4e474b1d 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -54,7 +54,11 @@ suite('TokenizationSupport2Adapter', () => { defines: (color: ColorIdentifier): boolean => { throw new Error('Not implemented'); - } + }, + + getTokenStyle: () => undefined, + resolveScopes: () => undefined + }; } public getIconTheme(): IIconTheme { diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 89c695d2dee..4c3093b823d 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -103,7 +103,7 @@ class ColorRegistry implements IColorRegistry { public registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency = false, deprecationMessage?: string): ColorIdentifier { let colorContribution: ColorContribution = { id, description, defaults, needsTransparency, deprecationMessage }; this.colorsById[id] = colorContribution; - let propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '#ff0000' }] }; + let propertySchema: IJSONSchema = { type: 'string', description, format: 'color-hex', defaultSnippets: [{ body: '${1:#ff0000}' }] }; if (deprecationMessage) { propertySchema.deprecationMessage = deprecationMessage; } diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 42e93300a85..8117fd486a3 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -10,6 +10,7 @@ import * as platform from 'vs/platform/registry/common/platform'; import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TokenStyle, TokenClassification, ProbeScope } from 'vs/platform/theme/common/tokenClassificationRegistry'; export const IThemeService = createDecorator('themeService'); @@ -59,6 +60,10 @@ export interface ITheme { * default color will be used. */ defines(color: ColorIdentifier): boolean; + + getTokenStyle(classification: TokenClassification, useDefault?: boolean): TokenStyle | undefined; + + resolveScopes(scopes: ProbeScope[]): TokenStyle | undefined; } export interface IIconTheme { diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts new file mode 100644 index 00000000000..2ec489ae46f --- /dev/null +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -0,0 +1,478 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/platform/registry/common/platform'; +import { Color } from 'vs/base/common/color'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import * as nls from 'vs/nls'; +import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; + +// ------ API types + +export const TOKEN_TYPE_WILDCARD = '*'; +export const TOKEN_TYPE_WILDCARD_NUM = -1; + +// qualified string [type|*](.modifier)* +export type TokenClassificationString = string; + +export interface TokenClassification { + type: number; + modifiers: number; +} + +export interface TokenTypeOrModifierContribution { + readonly num: number; + readonly id: string; + readonly description: string; + readonly deprecationMessage: string | undefined; +} + + +export interface TokenStyleData { + foreground?: Color; + bold?: boolean; + underline?: boolean; + italic?: boolean; +} + +export class TokenStyle implements Readonly { + constructor( + public readonly foreground?: Color, + public readonly bold?: boolean, + public readonly underline?: boolean, + public readonly italic?: boolean, + ) { + } +} + +export namespace TokenStyle { + export function fromData(data: { foreground?: Color, bold?: boolean, underline?: boolean, italic?: boolean }) { + return new TokenStyle(data.foreground, data.bold, data.underline, data.italic); + } +} + +export type ProbeScope = string[]; + +export interface TokenStyleFunction { + (theme: ITheme): TokenStyle | undefined; +} + +export interface TokenStyleDefaults { + scopesToProbe: ProbeScope[]; + light: TokenStyleValue | null; + dark: TokenStyleValue | null; + hc: TokenStyleValue | null; +} + +export interface TokenStylingDefaultRule { + classification: TokenClassification; + matchScore: number; + defaults: TokenStyleDefaults; +} + +export interface TokenStylingRule { + classification: TokenClassification; + matchScore: number; + value: TokenStyle; +} + +/** + * A TokenStyle Value is either a token style literal, or a TokenClassificationString + */ +export type TokenStyleValue = TokenStyle | TokenClassificationString; + +// TokenStyle registry +export const Extensions = { + TokenClassificationContribution: 'base.contributions.tokenClassification' +}; + +export interface ITokenClassificationRegistry { + + readonly onDidChangeSchema: Event; + + /** + * Register a token type to the registry. + * @param id The TokenType id as used in theme description files + * @description the description + */ + registerTokenType(id: string, description: string): void; + + /** + * Register a token modifier to the registry. + * @param id The TokenModifier id as used in theme description files + * @description the description + */ + registerTokenModifier(id: string, description: string): void; + + getTokenClassificationFromString(str: TokenClassificationString): TokenClassification | undefined; + getTokenClassification(type: string, modifiers: string[]): TokenClassification | undefined; + + getTokenStylingRule(classification: TokenClassification, value: TokenStyle): TokenStylingRule; + + /** + * Register a TokenStyle default to the registry. + * @param selector The rule selector + * @param defaults The default values + */ + registerTokenStyleDefault(selector: TokenClassification, defaults: TokenStyleDefaults): void; + + /** + * Deregister a TokenType from the registry. + */ + deregisterTokenType(id: string): void; + + /** + * Deregister a TokenModifier from the registry. + */ + deregisterTokenModifier(id: string): void; + + /** + * Get all TokenType contributions + */ + getTokenTypes(): TokenTypeOrModifierContribution[]; + + /** + * Get all TokenModifier contributions + */ + getTokenModifiers(): TokenTypeOrModifierContribution[]; + + /** + * Resolves a token classification against the given rules and default rules from the registry. + */ + resolveTokenStyle(classification: TokenClassification, themingRules: TokenStylingRule[] | undefined, customThemingRules: TokenStylingRule[], theme: ITheme): TokenStyle | undefined; + + /** + * JSON schema for an object to assign styling to token classifications + */ + getTokenStylingSchema(): IJSONSchema; +} + +class TokenClassificationRegistry implements ITokenClassificationRegistry { + + private readonly _onDidChangeSchema = new Emitter(); + readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; + + private currentTypeNumber = 0; + private currentModifierBit = 1; + + private tokenTypeById: { [key: string]: TokenTypeOrModifierContribution }; + private tokenModifierById: { [key: string]: TokenTypeOrModifierContribution }; + + private tokenStylingDefaultRules: TokenStylingDefaultRule[] = []; + + private tokenStylingSchema: IJSONSchema & { properties: IJSONSchemaMap } = { + type: 'object', + properties: {}, + definitions: { + style: { + 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\', \'-italic\', \'-bold\' or \'-underline\'or a combination. The empty string unsets inherited settings.'), + pattern: '^(\\s*(-?italic|-?bold|-?underline))*\\s*$', + patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' to set a style or \'-italic\', \'-bold\' or \'-underline\' to unset or a combination. The empty string unsets all styles.'), + defaultSnippets: [{ label: nls.localize('schema.token.fontStyle.none', 'None (clear inherited style)'), bodyText: '""' }, { body: 'italic' }, { body: 'bold' }, { body: 'underline' }, { 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}' } }] + } + } + }; + + constructor() { + this.tokenTypeById = {}; + this.tokenModifierById = {}; + + this.tokenTypeById[TOKEN_TYPE_WILDCARD] = { num: TOKEN_TYPE_WILDCARD_NUM, id: TOKEN_TYPE_WILDCARD, description: '', deprecationMessage: undefined }; + } + + public registerTokenType(id: string, description: string, deprecationMessage?: string): void { + const num = this.currentTypeNumber++; + let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, description, deprecationMessage }; + this.tokenTypeById[id] = tokenStyleContribution; + + this.tokenStylingSchema.properties[id] = getStylingSchemeEntry(description, deprecationMessage); + } + + public registerTokenModifier(id: string, description: string, deprecationMessage?: string): void { + const num = this.currentModifierBit; + this.currentModifierBit = this.currentModifierBit * 2; + let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, description, deprecationMessage }; + this.tokenModifierById[id] = tokenStyleContribution; + + this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage); + } + + public getTokenClassification(type: string, modifiers: string[]): TokenClassification | undefined { + const tokenTypeDesc = this.tokenTypeById[type]; + if (!tokenTypeDesc) { + return undefined; + } + let allModifierBits = 0; + for (const modifier of modifiers) { + const tokenModifierDesc = this.tokenModifierById[modifier]; + if (tokenModifierDesc) { + allModifierBits |= tokenModifierDesc.num; + } + } + return { type: tokenTypeDesc.num, modifiers: allModifierBits }; + } + + public getTokenClassificationFromString(str: TokenClassificationString): TokenClassification | undefined { + const parts = str.split('.'); + const type = parts.shift(); + if (type) { + return this.getTokenClassification(type, parts); + } + return undefined; + } + + public getTokenStylingRule(classification: TokenClassification, value: TokenStyle): TokenStylingRule { + return { classification, matchScore: getTokenStylingScore(classification), value }; + } + + public registerTokenStyleDefault(classification: TokenClassification, defaults: TokenStyleDefaults): void { + this.tokenStylingDefaultRules.push({ classification, matchScore: getTokenStylingScore(classification), defaults }); + } + + public deregisterTokenType(id: string): void { + delete this.tokenTypeById[id]; + delete this.tokenStylingSchema.properties[id]; + } + + public deregisterTokenModifier(id: string): void { + delete this.tokenModifierById[id]; + delete this.tokenStylingSchema.properties[`*.${id}`]; + } + + public getTokenTypes(): TokenTypeOrModifierContribution[] { + return Object.keys(this.tokenTypeById).map(id => this.tokenTypeById[id]); + } + + public getTokenModifiers(): TokenTypeOrModifierContribution[] { + return Object.keys(this.tokenModifierById).map(id => this.tokenModifierById[id]); + } + + public resolveTokenStyle(classification: TokenClassification, themingRules: TokenStylingRule[] | undefined, customThemingRules: TokenStylingRule[], theme: ITheme): 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) { + if (style.foreground && score.foreground <= matchScore) { + score.foreground = matchScore; + result.foreground = style.foreground; + } + 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; + } + } + } + } + if (themingRules === undefined) { + for (const rule of this.tokenStylingDefaultRules) { + const matchScore = match(rule, classification); + if (matchScore >= 0) { + let style = theme.resolveScopes(rule.defaults.scopesToProbe); + if (!style) { + style = this.resolveTokenStyleValue(rule.defaults[theme.type], theme); + } + if (style) { + _processStyle(matchScore, style); + } + } + } + } else { + for (const rule of themingRules) { + const matchScore = match(rule, classification); + if (matchScore >= 0) { + _processStyle(matchScore, rule.value); + } + } + } + for (const rule of customThemingRules) { + const matchScore = match(rule, classification); + if (matchScore >= 0) { + _processStyle(matchScore, rule.value); + } + } + return TokenStyle.fromData(result); + } + + /** + * @param tokenStyleValue Resolve a tokenStyleValue in the context of a theme + */ + private resolveTokenStyleValue(tokenStyleValue: TokenStyleValue | null, theme: ITheme): TokenStyle | undefined { + if (tokenStyleValue === null) { + return undefined; + } else if (typeof tokenStyleValue === 'string') { + const classification = this.getTokenClassificationFromString(tokenStyleValue); + if (classification) { + return theme.getTokenStyle(classification); + } + } else if (typeof tokenStyleValue === 'object') { + return tokenStyleValue; + } + return undefined; + } + + public getTokenStylingSchema(): IJSONSchema { + return this.tokenStylingSchema; + } + + + public toString() { + let sorter = (a: string, b: string) => { + let cat1 = a.indexOf('.') === -1 ? 0 : 1; + let cat2 = b.indexOf('.') === -1 ? 0 : 1; + if (cat1 !== cat2) { + return cat1 - cat2; + } + return a.localeCompare(b); + }; + + return Object.keys(this.tokenTypeById).sort(sorter).map(k => `- \`${k}\`: ${this.tokenTypeById[k].description}`).join('\n'); + } + +} + +function match(themeSelector: TokenStylingRule | TokenStylingDefaultRule, classification: TokenClassification): number { + const selectorType = themeSelector.classification.type; + if (selectorType !== TOKEN_TYPE_WILDCARD_NUM && selectorType !== classification.type) { + return -1; + } + const selectorModifier = themeSelector.classification.modifiers; + if ((classification.modifiers & selectorModifier) !== selectorModifier) { + return -1; + } + return themeSelector.matchScore; +} + + +const tokenClassificationRegistry = new TokenClassificationRegistry(); +platform.Registry.add(Extensions.TokenClassificationContribution, tokenClassificationRegistry); + +export function registerTokenType(id: string, description: string, scopesToProbe: ProbeScope[] = [], extendsTC: string | null = null, deprecationMessage?: string): string { + tokenClassificationRegistry.registerTokenType(id, description, deprecationMessage); + + if (scopesToProbe || extendsTC) { + const classification = tokenClassificationRegistry.getTokenClassification(id, []); + tokenClassificationRegistry.registerTokenStyleDefault(classification!, { scopesToProbe, light: extendsTC, dark: extendsTC, hc: extendsTC }); + } + return id; +} + +export function registerTokenModifier(id: string, description: string, deprecationMessage?: string): string { + tokenClassificationRegistry.registerTokenModifier(id, description, deprecationMessage); + return id; +} + +export function getTokenClassificationRegistry(): ITokenClassificationRegistry { + return tokenClassificationRegistry; +} + +export const comments = registerTokenType('comments', nls.localize('comments', "Style for comments."), [['comment']]); +export const strings = registerTokenType('strings', nls.localize('strings', "Style for strings."), [['string']]); +export const keywords = registerTokenType('keywords', nls.localize('keywords', "Style for keywords."), [['keyword.control']]); +export const numbers = registerTokenType('numbers', nls.localize('numbers', "Style for numbers."), [['constant.numeric']]); +export const regexp = registerTokenType('regexp', nls.localize('regexp', "Style for expressions."), [['constant.regexp']]); +export const operators = registerTokenType('operators', nls.localize('operator', "Style for operators."), [['keyword.operator']]); + +export const namespaces = registerTokenType('namespaces', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]); + +export const types = registerTokenType('types', nls.localize('types', "Style for types."), [['entity.name.type'], ['entity.name.class'], ['support.type'], ['support.class']]); +export const structs = registerTokenType('structs', nls.localize('struct', "Style for structs."), [['storage.type.struct']], types); +export const classes = registerTokenType('classes', nls.localize('class', "Style for classes."), [['entity.name.class']], types); +export const interfaces = registerTokenType('interfaces', nls.localize('interface', "Style for interfaces."), undefined, types); +export const enums = registerTokenType('enums', nls.localize('enum', "Style for enums."), undefined, types); +export const parameterTypes = registerTokenType('parameterTypes', nls.localize('parameterType', "Style for parameter types."), undefined, types); + +export const functions = registerTokenType('functions', nls.localize('functions', "Style for functions"), [['entity.name.function'], ['support.function']]); +export const macros = registerTokenType('macros', nls.localize('macro', "Style for macros."), undefined, functions); + +export const variables = registerTokenType('variables', nls.localize('variables', "Style for variables."), [['variable'], ['entity.name.variable']]); +export const constants = registerTokenType('constants', nls.localize('constants', "Style for constants."), undefined, variables); +export const parameters = registerTokenType('parameters', nls.localize('parameters', "Style for parameters."), undefined, variables); +export const property = registerTokenType('properties', nls.localize('properties', "Style for properties."), undefined, variables); + +export const labels = registerTokenType('labels', nls.localize('labels', "Style for labels. "), undefined); + +export const m_declaration = registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined); +export const m_documentation = registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined); +export const m_member = registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); +export const m_static = registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined); +export const m_abstract = registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined); +export const m_deprecated = registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined); +export const m_modification = registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined); +export const m_async = registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined); + +function bitCount(u: number) { + // https://blogs.msdn.microsoft.com/jeuge/2005/06/08/bit-fiddling-3/ + const uCount = u - ((u >> 1) & 0o33333333333) - ((u >> 2) & 0o11111111111); + return ((uCount + (uCount >> 3)) & 0o30707070707) % 63; +} + +function getTokenStylingScore(classification: TokenClassification) { + return bitCount(classification.modifiers) + ((classification.type !== TOKEN_TYPE_WILDCARD_NUM) ? 1 : 0); +} + +function getStylingSchemeEntry(description: string, deprecationMessage?: string): IJSONSchema { + return { + description, + deprecationMessage, + defaultSnippets: [{ body: '${1:#ff0000}' }], + anyOf: [ + { + type: 'string', + format: 'color-hex' + }, + { + $ref: '#definitions/style' + } + ] + }; +} + +export const tokenStylingSchemaId = 'vscode://schemas/token-styling'; + +let schemaRegistry = platform.Registry.as(JSONExtensions.JSONContribution); +schemaRegistry.registerSchema(tokenStylingSchemaId, tokenClassificationRegistry.getTokenStylingSchema()); + +const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(tokenStylingSchemaId), 200); +tokenClassificationRegistry.onDidChangeSchema(() => { + if (!delayer.isScheduled()) { + delayer.schedule(); + } +}); diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index fd49d393a34..b9279dc39ee 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -6,6 +6,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService, ITheme, DARK, IIconTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { TokenStyle, TokenClassification, ProbeScope } from 'vs/platform/theme/common/tokenClassificationRegistry'; export class TestTheme implements ITheme { @@ -23,6 +24,14 @@ export class TestTheme implements ITheme { defines(color: string): boolean { throw new Error('Method not implemented.'); } + + getTokenStyle(classification: TokenClassification, useDefault?: boolean | undefined): TokenStyle | undefined { + throw new Error('Method not implemented.'); + } + + resolveScopes(scopes: ProbeScope[]): TokenStyle | undefined { + throw new Error('Method not implemented.'); + } } export class TestIconTheme implements IIconTheme { diff --git a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalColorRegistry.test.ts index bf413e442d4..10cad170f83 100644 --- a/src/vs/workbench/contrib/terminal/test/electron-browser/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/electron-browser/terminalColorRegistry.test.ts @@ -19,7 +19,10 @@ function getMockTheme(type: ThemeType): ITheme { label: '', type: type, getColor: (colorId: ColorIdentifier): Color | undefined => themingRegistry.resolveDefaultColor(colorId, theme), - defines: () => true + defines: () => true, + getTokenStyle: () => undefined, + resolveScopes: () => undefined + }; return theme; } @@ -99,4 +102,4 @@ suite('Workbench - TerminalColorRegistry', () => { '#e5e5e5' ], 'The dark terminal colors should be used when a dark theme is active'); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/services/extensionResourceLoader/electron-browser/extensionResourceLoaderService.ts b/src/vs/workbench/services/extensionResourceLoader/electron-browser/extensionResourceLoaderService.ts index 2acf10e0b53..744f2860788 100644 --- a/src/vs/workbench/services/extensionResourceLoader/electron-browser/extensionResourceLoaderService.ts +++ b/src/vs/workbench/services/extensionResourceLoader/electron-browser/extensionResourceLoaderService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; -class ExtensionResourceLoaderService implements IExtensionResourceLoaderService { +export class ExtensionResourceLoaderService implements IExtensionResourceLoaderService { _serviceBrand: undefined; diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index cb553b6fcc8..dc9222e4b02 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -22,7 +22,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars'; import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; -import { ITokenColorizationRule, IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ITextMateThemingRule, IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -257,7 +257,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex TokenizationRegistry.setColorMap(colorMap); } - private static equalsTokenRules(a: ITokenColorizationRule[] | null, b: ITokenColorizationRule[] | null): boolean { + private static equalsTokenRules(a: ITextMateThemingRule[] | null, b: ITextMateThemingRule[] | null): boolean { if (!b || !a || b.length !== a.length) { return false; } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 15488490b47..046764e60c3 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,7 +6,7 @@ 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, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } 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'; @@ -29,6 +29,7 @@ import * as resources from 'vs/base/common/resources'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { textmateColorsSchemaId, registerColorThemeSchemas, textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry'; +import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; 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'; @@ -92,6 +93,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return this.configurationService.getValue(CUSTOM_EDITOR_COLORS_SETTING) || {}; } + private get tokenStylesCustomizations(): IExperimentalTokenStyleCustomizations { + return this.configurationService.getValue(CUSTOM_EDITOR_TOKENSTYLES_SETTING) || {}; + } + constructor( @IExtensionService extensionService: IExtensionService, @IStorageService private readonly storageService: IStorageService, @@ -126,6 +131,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } themeData.setCustomColors(this.colorCustomizations); themeData.setCustomTokenColors(this.tokenColorCustomizations); + themeData.setCustomTokenStyleRules(this.tokenStylesCustomizations); this.updateDynamicCSSRules(themeData); this.applyTheme(themeData, undefined, true); @@ -154,18 +160,22 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const themeSpecificWorkbenchColors: IJSONSchema = { properties: {} }; const themeSpecificTokenColors: IJSONSchema = { properties: {} }; + const themeSpecificTokenStyling: IJSONSchema = { properties: {} }; const workbenchColors = { $ref: workbenchColorsSchemaId, additionalProperties: false }; const tokenColors = { properties: tokenColorSchema.properties, additionalProperties: false }; + const tokenStyling = { $ref: tokenStylingSchemaId, additionalProperties: false }; for (let t of event.themes) { // add theme specific color customization ("[Abyss]":{ ... }) const themeId = `[${t.settingsId}]`; themeSpecificWorkbenchColors.properties![themeId] = workbenchColors; themeSpecificTokenColors.properties![themeId] = tokenColors; + themeSpecificTokenStyling.properties![themeId] = tokenStyling; } colorCustomizationsSchema.allOf![1] = themeSpecificWorkbenchColors; tokenColorCustomizationSchema.allOf![1] = themeSpecificTokenColors; + experimentalTokenStylingCustomizationSchema.allOf![1] = themeSpecificTokenStyling; configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration, tokenColorCustomizationConfiguration); @@ -308,6 +318,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.currentColorTheme.setCustomTokenColors(this.tokenColorCustomizations); hasColorChanges = true; } + if (e.affectsConfiguration(CUSTOM_EDITOR_TOKENSTYLES_SETTING)) { + this.currentColorTheme.setCustomTokenStyleRules(this.tokenStylesCustomizations); + hasColorChanges = true; + } if (hasColorChanges) { this.updateDynamicCSSRules(this.currentColorTheme); this.onColorThemeChange.fire(this.currentColorTheme); @@ -698,12 +712,18 @@ const tokenColorCustomizationSchema: IConfigurationPropertySchema = { default: {}, allOf: [tokenColorSchema] }; +const experimentalTokenStylingCustomizationSchema: IConfigurationPropertySchema = { + description: nls.localize('editorColorsTokenStyles', "Overrides token color and styles from the currently selected color theme."), + default: {}, + allOf: [{ $ref: tokenStylingSchemaId }] +}; const tokenColorCustomizationConfiguration: IConfigurationNode = { id: 'editor', order: 7.2, type: 'object', properties: { - [CUSTOM_EDITOR_COLORS_SETTING]: tokenColorCustomizationSchema + [CUSTOM_EDITOR_COLORS_SETTING]: tokenColorCustomizationSchema, + [CUSTOM_EDITOR_TOKENSTYLES_SETTING]: experimentalTokenStylingCustomizationSchema } }; configurationRegistry.registerConfiguration(tokenColorCustomizationConfiguration); diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index df2006b67c8..4a1b760b4e2 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -6,22 +6,26 @@ 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, ITokenColorizationRule, IColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ExtensionData, ITokenColorCustomizations, ITextMateThemingRule, IColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations, IExperimentalTokenStyleCustomizations, ITokenColorizationSetting } 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 objects from 'vs/base/common/objects'; import * as resources from 'vs/base/common/resources'; -import { Extensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ThemeType } 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 { startsWith } from 'vs/base/common/strings'; +import { TokenStyle, TokenClassification, ProbeScope, TokenStylingRule, getTokenClassificationRegistry } 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'; -let colorRegistry = Registry.as(Extensions.ColorContribution); +let colorRegistry = Registry.as(ColorRegistryExtensions.ColorContribution); + +let tokenClassificationRegistry = getTokenClassificationRegistry(); const tokenGroupToScopesMap = { comments: ['comment', 'punctuation.definition.comment'], @@ -33,6 +37,7 @@ const tokenGroupToScopesMap = { variables: ['variable', 'entity.name.variable'] }; + export class ColorThemeData implements IColorTheme { id: string; @@ -44,11 +49,17 @@ export class ColorThemeData implements IColorTheme { watch?: boolean; extensionData?: ExtensionData; - private themeTokenColors: ITokenColorizationRule[] = []; - private customTokenColors: ITokenColorizationRule[] = []; + private themeTokenColors: ITextMateThemingRule[] = []; + private customTokenColors: ITextMateThemingRule[] = []; private colorMap: IColorMap = {}; private customColorMap: IColorMap = {}; + private tokenStylingRules: TokenStylingRule[] | undefined = undefined; + private customTokenStylingRules: TokenStylingRule[] = []; + + private themeTokenScopeMatchers: Matcher[] | undefined; + private customTokenScopeMatchers: Matcher[] | undefined; + private constructor(id: string, label: string, settingsId: string) { this.id = id; this.label = label; @@ -56,8 +67,8 @@ export class ColorThemeData implements IColorTheme { this.isLoaded = false; } - get tokenColors(): ITokenColorizationRule[] { - const result: ITokenColorizationRule[] = []; + get tokenColors(): ITextMateThemingRule[] { + 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)!; @@ -71,7 +82,7 @@ export class ColorThemeData implements IColorTheme { let hasDefaultTokens = false; - function addRule(rule: ITokenColorizationRule) { + function addRule(rule: ITextMateThemingRule) { if (rule.scope && rule.settings) { if (rule.scope === 'token.info-token') { hasDefaultTokens = true; @@ -103,10 +114,57 @@ export class ColorThemeData implements IColorTheme { return color; } + public getTokenStyle(tokenClassification: TokenClassification, useDefault?: boolean): TokenStyle | undefined { + // todo: cache results + return tokenClassificationRegistry.resolveTokenStyle(tokenClassification, this.tokenStylingRules, this.customTokenStylingRules, this); + } + public getDefault(colorId: ColorIdentifier): Color | undefined { return colorRegistry.resolveDefaultColor(colorId, this); } + public getDefaultTokenStyle(tokenClassification: TokenClassification): TokenStyle | undefined { + return tokenClassificationRegistry.resolveTokenStyle(tokenClassification, undefined, [], this); + } + + public resolveScopes(scopes: ProbeScope[]): 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; + + function findTokenStyleForScopeInScopes(scopeMatchers: Matcher[], tokenColors: ITextMateThemingRule[]) { + for (let i = 0; i < scopeMatchers.length; i++) { + const score = scopeMatchers[i](scope); + if (score >= 0) { + const settings = tokenColors[i].settings; + if (score >= foregroundScore && settings.foreground) { + foreground = settings.foreground; + } + if (score >= fontStyleScore && types.isString(settings.fontStyle)) { + fontStyle = settings.fontStyle; + } + } + } + } + findTokenStyleForScopeInScopes(this.themeTokenScopeMatchers, this.themeTokenColors); + findTokenStyleForScopeInScopes(this.customTokenScopeMatchers, this.customTokenColors); + if (foreground !== undefined || fontStyle !== undefined) { + return getTokenStyle(foreground, fontStyle); + } + } + return undefined; + } + public defines(colorId: ColorIdentifier): boolean { return this.customColorMap.hasOwnProperty(colorId) || this.colorMap.hasOwnProperty(colorId); } @@ -132,6 +190,7 @@ export class ColorThemeData implements IColorTheme { public setCustomTokenColors(customTokenColors: ITokenColorCustomizations) { this.customTokenColors = []; + this.customTokenScopeMatchers = undefined; // first add the non-theme specific settings this.addCustomTokenColors(customTokenColors); @@ -142,6 +201,16 @@ export class ColorThemeData implements IColorTheme { } } + public setCustomTokenStyleRules(tokenStylingRules: IExperimentalTokenStyleCustomizations) { + this.tokenStylingRules = []; + readCustomTokenStyleRules(tokenStylingRules, this.tokenStylingRules); + + const themeSpecificColors = tokenStylingRules[`[${this.settingsId}]`] as IExperimentalTokenStyleCustomizations; + if (types.isObject(themeSpecificColors)) { + readCustomTokenStyleRules(themeSpecificColors, this.tokenStylingRules); + } + } + 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" @@ -180,9 +249,18 @@ export class ColorThemeData implements IColorTheme { return Promise.resolve(undefined); } this.themeTokenColors = []; - this.colorMap = {}; - return _loadColorTheme(extensionResourceLoaderService, this.location, this.themeTokenColors, this.colorMap).then(_ => { + this.themeTokenScopeMatchers = undefined; + + const result = { + colors: {}, + textMateRules: [], + stylingRules: undefined + }; + return _loadColorTheme(extensionResourceLoaderService, this.location, result).then(_ => { this.isLoaded = true; + this.tokenStylingRules = result.stylingRules; + this.colorMap = result.colors; + this.themeTokenColors = result.textMateRules; }); } @@ -295,7 +373,7 @@ function toCSSSelector(extensionId: string, path: string) { return str; } -function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, resultRules: ITokenColorizationRule[], resultColors: IColorMap): Promise { +function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, stylingRules: TokenStylingRule[] | undefined }): Promise { if (resources.extname(themeLocation) === '.json') { return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => { let errors: Json.ParseError[] = []; @@ -307,11 +385,11 @@ function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoade } let includeCompletes: Promise = Promise.resolve(null); if (contentValue.include) { - includeCompletes = _loadColorTheme(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), resultRules, resultColors); + includeCompletes = _loadColorTheme(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), result); } return includeCompletes.then(_ => { if (Array.isArray(contentValue.settings)) { - convertSettings(contentValue.settings, resultRules, resultColors); + convertSettings(contentValue.settings, result); return null; } let colors = contentValue.colors; @@ -323,38 +401,42 @@ function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoade for (let colorId in colors) { let colorHex = colors[colorId]; if (typeof colorHex === 'string') { // ignore colors tht are null - resultColors[colorId] = Color.fromHex(colors[colorId]); + result.colors[colorId] = Color.fromHex(colors[colorId]); } } } let tokenColors = contentValue.tokenColors; if (tokenColors) { if (Array.isArray(tokenColors)) { - resultRules.push(...tokenColors); + result.textMateRules.push(...tokenColors); return null; } else if (typeof tokenColors === 'string') { - return _loadSyntaxTokens(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), tokenColors), resultRules, {}); + return _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 tokenStylingRules = contentValue.tokenStylingRules; + if (tokenStylingRules && typeof tokenStylingRules === 'object') { + result.stylingRules = readCustomTokenStyleRules(tokenStylingRules, result.stylingRules); + } return null; }); }); } else { - return _loadSyntaxTokens(extensionResourceLoaderService, themeLocation, resultRules, resultColors); + return _loadSyntaxTokens(extensionResourceLoaderService, themeLocation, result); } } -function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, resultRules: ITokenColorizationRule[], resultColors: IColorMap): Promise { +function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap }): Promise { return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => { try { let contentValue = parsePList(content); - let settings: ITokenColorizationRule[] = contentValue.settings; + 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, resultRules, resultColors); + 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))); @@ -364,7 +446,7 @@ function _loadSyntaxTokens(extensionResourceLoaderService: IExtensionResourceLoa }); } -let defaultThemeColors: { [baseTheme: string]: ITokenColorizationRule[] } = { +let defaultThemeColors: { [baseTheme: string]: ITextMateThemingRule[] } = { 'light': [ { scope: 'token.info-token', settings: { foreground: '#316bcd' } }, { scope: 'token.warn-token', settings: { foreground: '#cd9731' } }, @@ -384,3 +466,124 @@ let defaultThemeColors: { [baseTheme: string]: ITokenColorizationRule[] } = { { 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 + scope.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 { + const ruleScope = rule.scope; + if (!ruleScope || !rule.settings) { + return noMatch; + } + const matchers: MatcherWithPriority[] = []; + 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 getTokenStyle(foreground: string | undefined, fontStyle: string | undefined): TokenStyle { + let foregroundColor = undefined; + if (foreground !== undefined) { + foregroundColor = Color.fromHex(foreground); + } + let bold, underline, italic; + if (fontStyle !== undefined) { + fontStyle = fontStyle.trim(); + if (fontStyle.length === 0) { + bold = italic = underline = false; + } else { + const expression = /-?italic|-?bold|-?underline/g; + let match; + while ((match = expression.exec(fontStyle))) { + switch (match[0]) { + case 'bold': bold = true; break; + case '-bold': bold = false; break; + case 'italic': italic = true; break; + case '-italic': italic = false; break; + case 'underline': underline = true; break; + case '-underline': underline = false; break; + } + } + } + } + return new TokenStyle(foregroundColor, bold, underline, italic); + +} + +function readCustomTokenStyleRules(tokenStylingRuleSection: IExperimentalTokenStyleCustomizations, result: TokenStylingRule[] = []) { + for (let key in tokenStylingRuleSection) { + if (key[0] !== '[') { + const classification = tokenClassificationRegistry.getTokenClassificationFromString(key); + if (classification) { + const settings = tokenStylingRuleSection[key]; + let style: TokenStyle | undefined; + if (typeof settings === 'string') { + style = getTokenStyle(settings, undefined); + } else if (isTokenColorizationSetting(settings)) { + style = getTokenStyle(settings.foreground, settings.fontStyle); + } + if (style) { + result.push(tokenClassificationRegistry.getTokenStylingRule(classification, style)); + } + } + } + } + return result; +} + +function isTokenColorizationSetting(style: any): style is ITokenColorizationSetting { + return style && (style.foreground || style.fontStyle); +} diff --git a/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts b/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts new file mode 100644 index 00000000000..4d1c66adf6f --- /dev/null +++ b/src/vs/workbench/services/themes/common/textMateScopeMatcher.ts @@ -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 { + matcher: Matcher; + priority: -1 | 0 | 1; +} + +export interface Matcher { + (matcherInput: T): number; +} + +export function createMatchers(selector: string, matchesName: (names: string[], matcherInput: T) => number, results: MatcherWithPriority[]): 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 | 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 | null { + let matcher = parseOperand(); + if (!matcher) { + return null; + } + + const matchers: Matcher[] = []; + 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 | null { + let matcher = parseConjunction(); + if (!matcher) { + return null; + } + const matchers: Matcher[] = []; + 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; + } + }; +} diff --git a/src/vs/workbench/services/themes/common/themeCompatibility.ts b/src/vs/workbench/services/themes/common/themeCompatibility.ts index 6518ff4a4ac..7af65c955ad 100644 --- a/src/vs/workbench/services/themes/common/themeCompatibility.ts +++ b/src/vs/workbench/services/themes/common/themeCompatibility.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITokenColorizationRule, IColorMap } from 'vs/workbench/services/themes/common/workbenchThemeService'; +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'; @@ -18,9 +18,9 @@ function addSettingMapping(settingId: string, colorId: string) { colorIds.push(colorId); } -export function convertSettings(oldSettings: ITokenColorizationRule[], resultRules: ITokenColorizationRule[], resultColors: IColorMap): void { +export function convertSettings(oldSettings: ITextMateThemingRule[], result: { textMateRules: ITextMateThemingRule[], colors: IColorMap }): void { for (let rule of oldSettings) { - resultRules.push(rule); + result.textMateRules.push(rule); if (!rule.scope) { let settings = rule.settings; if (!settings) { @@ -34,7 +34,7 @@ export function convertSettings(oldSettings: ITokenColorizationRule[], resultRul if (typeof colorHex === 'string') { let color = Color.fromHex(colorHex); for (let colorId of mappings) { - resultColors[colorId] = color; + result.colors[colorId] = color; } } } diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 8684b54a804..9937ca11be9 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -22,6 +22,7 @@ export const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; export const ICON_THEME_SETTING = 'workbench.iconTheme'; export const CUSTOM_WORKBENCH_COLORS_SETTING = 'workbench.colorCustomizations'; export const CUSTOM_EDITOR_COLORS_SETTING = 'editor.tokenColorCustomizations'; +export const CUSTOM_EDITOR_TOKENSTYLES_SETTING = 'editor.tokenColorCustomizationsExperimental'; export interface IColorTheme extends ITheme { readonly id: string; @@ -30,7 +31,7 @@ export interface IColorTheme extends ITheme { readonly extensionData?: ExtensionData; readonly description?: string; readonly isLoaded: boolean; - readonly tokenColors: ITokenColorizationRule[]; + readonly tokenColors: ITextMateThemingRule[]; } export interface IColorMap { @@ -69,7 +70,7 @@ export interface IColorCustomizations { } export interface ITokenColorCustomizations { - [groupIdOrThemeSettingsId: string]: string | ITokenColorizationSetting | ITokenColorCustomizations | undefined | ITokenColorizationRule[]; + [groupIdOrThemeSettingsId: string]: string | ITokenColorizationSetting | ITokenColorCustomizations | undefined | ITextMateThemingRule[]; comments?: string | ITokenColorizationSetting; strings?: string | ITokenColorizationSetting; numbers?: string | ITokenColorizationSetting; @@ -77,10 +78,14 @@ export interface ITokenColorCustomizations { types?: string | ITokenColorizationSetting; functions?: string | ITokenColorizationSetting; variables?: string | ITokenColorizationSetting; - textMateRules?: ITokenColorizationRule[]; + textMateRules?: ITextMateThemingRule[]; } -export interface ITokenColorizationRule { +export interface IExperimentalTokenStyleCustomizations { + [styleRuleOrThemeSettingsId: string]: string | ITokenColorizationSetting | IExperimentalTokenStyleCustomizations | undefined; +} + +export interface ITextMateThemingRule { name?: string; scope?: string | string[]; settings: ITokenColorizationSetting; @@ -89,7 +94,7 @@ export interface ITokenColorizationRule { export interface ITokenColorizationSetting { foreground?: string; background?: string; - fontStyle?: string; // italic, underline, bold + fontStyle?: string; /* [italic|underline|bold] */ } export interface ExtensionData { @@ -106,4 +111,4 @@ export interface IThemeExtensionPoint { path: string; uiTheme?: typeof VS_LIGHT_THEME | typeof VS_DARK_THEME | typeof VS_HC_THEME; _watch: boolean; // unsupported options to watch location -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts new file mode 100644 index 00000000000..ab41c917cb8 --- /dev/null +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * 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, comments, variables, types, functions, keywords, numbers, strings, 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-browser/extensionResourceLoaderService'; + +let tokenClassificationRegistry = getTokenClassificationRegistry(); + +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 assertTokenStyles(themeData: ColorThemeData, expected: { [qualifiedClassifier: string]: TokenStyle }) { + for (let qualifiedClassifier in expected) { + const classification = tokenClassificationRegistry.getTokenClassificationFromString(qualifiedClassifier); + assert.ok(classification, 'Classification not found'); + + const tokenStyle = themeData.getTokenStyle(classification!); + assertTokenStyle(tokenStyle, expected[qualifiedClassifier], 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, { + [comments]: ts('#88846f', undefinedStyle), + [variables]: ts('#F8F8F2', unsetStyle), + [types]: ts('#A6E22E', { underline: true }), + [functions]: ts('#A6E22E', unsetStyle), + [strings]: ts('#E6DB74', undefinedStyle), + [numbers]: ts('#AE81FF', undefinedStyle), + [keywords]: 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, { + [comments]: ts('#6A9955', undefinedStyle), + [variables]: ts('#9CDCFE', undefinedStyle), + [types]: ts('#4EC9B0', undefinedStyle), + [functions]: ts('#DCDCAA', undefinedStyle), + [strings]: ts('#CE9178', undefinedStyle), + [numbers]: ts('#B5CEA8', undefinedStyle), + [keywords]: 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, { + [comments]: ts('#008000', undefinedStyle), + [variables]: ts(undefined, undefinedStyle), + [types]: ts(undefined, undefinedStyle), + [functions]: ts(undefined, undefinedStyle), + [strings]: ts('#a31515', undefinedStyle), + [numbers]: ts('#09885a', undefinedStyle), + [keywords]: 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, { + [comments]: ts('#7ca668', undefinedStyle), + [variables]: ts('#9CDCFE', undefinedStyle), + [types]: ts('#4EC9B0', undefinedStyle), + [functions]: ts('#DCDCAA', undefinedStyle), + [strings]: ts('#ce9178', undefinedStyle), + [numbers]: ts('#b5cea8', undefinedStyle), + [keywords]: 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, { + [comments]: ts('#a57a4c', undefinedStyle), + [variables]: ts('#dc3958', undefinedStyle), + [types]: ts('#f06431', undefinedStyle), + [functions]: ts('#8ab1b0', undefinedStyle), + [strings]: ts('#889b4a', undefinedStyle), + [numbers]: ts('#f79a32', undefinedStyle), + [keywords]: 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, { + [comments]: ts('#384887', undefinedStyle), + [variables]: ts(undefined, unsetStyle), + [types]: ts('#ffeebb', { underline: true }), + [functions]: ts('#ddbb88', unsetStyle), + [strings]: ts('#22aa44', undefinedStyle), + [numbers]: ts('#f280d0', undefinedStyle), + [keywords]: 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 }), 'storage'); + + tokenStyle = themeData.resolveScopes([['storage.type']]); + assertTokenStyle(tokenStyle, ts('#66D9EF', { italic: true }), 'storage.type'); + + tokenStyle = themeData.resolveScopes([['entity.name.class']]); + assertTokenStyle(tokenStyle, ts('#A6E22E', { 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 }), 'storage.type'); + + }); + + test('rule matching', async () => { + const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test'); + themeData.setCustomColors({ 'editor.foreground': '#000000' }); + themeData.setCustomTokenStyleRules({ + 'types': '#ff0000', + 'classes': { foreground: '#0000ff', fontStyle: 'italic' }, + '*.static': { fontStyle: 'bold' }, + '*.declaration': { fontStyle: 'italic' }, + '*.async.static': { fontStyle: 'italic underline' }, + '*.async': { foreground: '#000fff', fontStyle: '-italic underline' } + }); + + assertTokenStyles(themeData, { + 'types': ts('#ff0000', undefinedStyle), + 'types.static': ts('#ff0000', { bold: true }), + 'types.static.declaration': ts('#ff0000', { bold: true, italic: true }), + 'classes': ts('#0000ff', { italic: true }), + 'classes.static.declaration': ts('#0000ff', { bold: true, italic: true }), + 'classes.declaration': ts('#0000ff', { italic: true }), + 'classes.declaration.async': ts('#000fff', { underline: true, italic: false }), + 'classes.declaration.async.static': ts('#000fff', { italic: true, underline: true, bold: true }), + }); + + }); +});