vscode/src/vs/platform/theme/common/tokenClassificationRegistry.ts
2021-11-15 13:05:02 +01:00

596 lines
23 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls';
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import * as platform from 'vs/platform/registry/common/platform';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
export const TOKEN_TYPE_WILDCARD = '*';
export const TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR = ':';
export const CLASSIFIER_MODIFIER_SEPARATOR = '.';
// qualified string [type|*](.modifier)*(/language)!
export type TokenClassificationString = string;
export const idPattern = '\\w+[-_\\w+]*';
export const typeAndModifierIdPattern = `^${idPattern}$`;
export const selectorPattern = `^(${idPattern}|\\*)(\\${CLASSIFIER_MODIFIER_SEPARATOR}${idPattern})*(${TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR}${idPattern})?$`;
export const fontStylePattern = '^(\\s*(italic|bold|underline))*\\s*$';
export interface TokenSelector {
match(type: string, modifiers: string[], language: string): number;
readonly id: string;
}
export interface TokenTypeOrModifierContribution {
readonly num: number;
readonly id: string;
readonly superType?: string;
readonly description: string;
readonly deprecationMessage?: string;
}
export interface TokenStyleData {
foreground?: Color;
bold?: boolean;
underline?: boolean;
italic?: boolean;
}
export class TokenStyle implements Readonly<TokenStyleData> {
constructor(
public readonly foreground?: Color,
public readonly bold?: boolean,
public readonly underline?: boolean,
public readonly italic?: boolean,
) {
}
}
export namespace TokenStyle {
export function toJSONObject(style: TokenStyle): any {
return {
_foreground: style.foreground === undefined ? null : Color.Format.CSS.formatHexA(style.foreground, true),
_bold: style.bold === undefined ? null : style.bold,
_underline: style.underline === undefined ? null : style.underline,
_italic: style.italic === undefined ? null : style.italic,
};
}
export function fromJSONObject(obj: any): TokenStyle | undefined {
if (obj) {
const boolOrUndef = (b: any) => (typeof b === 'boolean') ? b : undefined;
const colorOrUndef = (s: any) => (typeof s === 'string') ? Color.fromHex(s) : undefined;
return new TokenStyle(colorOrUndef(obj._foreground), boolOrUndef(obj._bold), boolOrUndef(obj._underline), boolOrUndef(obj._italic));
}
return undefined;
}
export function equals(s1: any, s2: any): boolean {
if (s1 === s2) {
return true;
}
return s1 !== undefined && s2 !== undefined
&& (s1.foreground instanceof Color ? s1.foreground.equals(s2.foreground) : s2.foreground === undefined)
&& s1.bold === s2.bold
&& s1.underline === s2.underline
&& s1.italic === s2.italic;
}
export function is(s: any): s is TokenStyle {
return s instanceof TokenStyle;
}
export function fromData(data: { foreground?: Color, bold?: boolean, underline?: boolean, italic?: boolean }): TokenStyle {
return new TokenStyle(data.foreground, data.bold, data.underline, data.italic);
}
export function fromSettings(foreground: string | undefined, fontStyle: string | undefined, bold?: boolean, underline?: boolean, italic?: boolean): TokenStyle {
let foregroundColor = undefined;
if (foreground !== undefined) {
foregroundColor = Color.fromHex(foreground);
}
if (fontStyle !== undefined) {
bold = italic = underline = false;
const expression = /italic|bold|underline/g;
let match;
while ((match = expression.exec(fontStyle))) {
switch (match[0]) {
case 'bold': bold = true; break;
case 'italic': italic = true; break;
case 'underline': underline = true; break;
}
}
}
return new TokenStyle(foregroundColor, bold, underline, italic);
}
}
export type ProbeScope = string[];
export interface TokenStyleFunction {
(theme: IColorTheme): TokenStyle | undefined;
}
export interface TokenStyleDefaults {
scopesToProbe?: ProbeScope[];
light?: TokenStyleValue;
dark?: TokenStyleValue;
hc?: TokenStyleValue;
}
export interface SemanticTokenDefaultRule {
selector: TokenSelector;
defaults: TokenStyleDefaults;
}
export interface SemanticTokenRule {
style: TokenStyle;
selector: TokenSelector;
}
export namespace SemanticTokenRule {
export function fromJSONObject(registry: ITokenClassificationRegistry, o: any): SemanticTokenRule | undefined {
if (o && typeof o._selector === 'string' && o._style) {
const style = TokenStyle.fromJSONObject(o._style);
if (style) {
try {
return { selector: registry.parseTokenSelector(o._selector), style };
} catch (_ignore) {
}
}
}
return undefined;
}
export function toJSONObject(rule: SemanticTokenRule): any {
return {
_selector: rule.selector.id,
_style: TokenStyle.toJSONObject(rule.style)
};
}
export function equals(r1: SemanticTokenRule | undefined, r2: SemanticTokenRule | undefined) {
if (r1 === r2) {
return true;
}
return r1 !== undefined && r2 !== undefined
&& r1.selector && r2.selector && r1.selector.id === r2.selector.id
&& TokenStyle.equals(r1.style, r2.style);
}
export function is(r: any): r is SemanticTokenRule {
return r && r.selector && typeof r.selector.id === 'string' && TokenStyle.is(r.style);
}
}
/**
* 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<void>;
/**
* Register a token type to the registry.
* @param id The TokenType id as used in theme description files
* @param description the description
*/
registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void;
/**
* Register a token modifier to the registry.
* @param id The TokenModifier id as used in theme description files
* @param description the description
*/
registerTokenModifier(id: string, description: string): void;
/**
* Parses a token selector from a selector string.
* @param selectorString selector string in the form (*|type)(.modifier)*
* @param language language to which the selector applies or undefined if the selector is for all languafe
* @returns the parsesd selector
* @throws an error if the string is not a valid selector
*/
parseTokenSelector(selectorString: string, language?: string): TokenSelector;
/**
* Register a TokenStyle default to the registry.
* @param selector The rule selector
* @param defaults The default values
*/
registerTokenStyleDefault(selector: TokenSelector, defaults: TokenStyleDefaults): void;
/**
* Deregister a TokenStyle default to the registry.
* @param selector The rule selector
*/
deregisterTokenStyleDefault(selector: TokenSelector): 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[];
/**
* The styling rules to used when a schema does not define any styling rules.
*/
getTokenStylingDefaultRules(): SemanticTokenDefaultRule[];
/**
* JSON schema for an object to assign styling to token classifications
*/
getTokenStylingSchema(): IJSONSchema;
}
class TokenClassificationRegistry implements ITokenClassificationRegistry {
private readonly _onDidChangeSchema = new Emitter<void>();
readonly onDidChangeSchema: Event<void> = this._onDidChangeSchema.event;
private currentTypeNumber = 0;
private currentModifierBit = 1;
private tokenTypeById: { [key: string]: TokenTypeOrModifierContribution };
private tokenModifierById: { [key: string]: TokenTypeOrModifierContribution };
private tokenStylingDefaultRules: SemanticTokenDefaultRule[] = [];
private typeHierarchy: { [id: string]: string[] };
private tokenStylingSchema: IJSONSchema & { properties: IJSONSchemaMap, patternProperties: IJSONSchemaMap } = {
type: 'object',
properties: {},
patternProperties: {
[selectorPattern]: getStylingSchemeEntry()
},
//errorMessage: nls.localize('schema.token.errors', 'Valid token selectors have the form (*|tokenType)(.tokenModifier)*(:tokenLanguage)?.'),
additionalProperties: false,
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', 'Sets the all font styles of the rule: \'italic\', \'bold\' or \'underline\' or a combination. All styles that are not listed are unset. The empty string unsets all styles.'),
pattern: fontStylePattern,
patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' 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 underline' }, { body: 'bold underline' }, { body: 'italic bold underline' }]
},
bold: {
type: 'boolean',
description: nls.localize('schema.token.bold', 'Sets or unsets the font style to bold. Note, the presence of \'fontStyle\' overrides this setting.'),
},
italic: {
type: 'boolean',
description: nls.localize('schema.token.italic', 'Sets or unsets the font style to italic. Note, the presence of \'fontStyle\' overrides this setting.'),
},
underline: {
type: 'boolean',
description: nls.localize('schema.token.underline', 'Sets or unsets the font style to underline. Note, the presence of \'fontStyle\' overrides this setting.'),
}
},
defaultSnippets: [{ body: { foreground: '${1:#FF0000}', fontStyle: '${2:bold}' } }]
}
}
};
constructor() {
this.tokenTypeById = Object.create(null);
this.tokenModifierById = Object.create(null);
this.typeHierarchy = Object.create(null);
}
public registerTokenType(id: string, description: string, superType?: string, deprecationMessage?: string): void {
if (!id.match(typeAndModifierIdPattern)) {
throw new Error('Invalid token type id.');
}
if (superType && !superType.match(typeAndModifierIdPattern)) {
throw new Error('Invalid token super type id.');
}
const num = this.currentTypeNumber++;
let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, superType, description, deprecationMessage };
this.tokenTypeById[id] = tokenStyleContribution;
const stylingSchemeEntry = getStylingSchemeEntry(description, deprecationMessage);
this.tokenStylingSchema.properties[id] = stylingSchemeEntry;
this.typeHierarchy = Object.create(null);
}
public registerTokenModifier(id: string, description: string, deprecationMessage?: string): void {
if (!id.match(typeAndModifierIdPattern)) {
throw new Error('Invalid token modifier id.');
}
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 parseTokenSelector(selectorString: string, language?: string): TokenSelector {
const selector = parseClassifierString(selectorString, language);
if (!selector.type) {
return {
match: () => -1,
id: '$invalid'
};
}
return {
match: (type: string, modifiers: string[], language: string) => {
let score = 0;
if (selector.language !== undefined) {
if (selector.language !== language) {
return -1;
}
score += 10;
}
if (selector.type !== TOKEN_TYPE_WILDCARD) {
const hierarchy = this.getTypeHierarchy(type);
const level = hierarchy.indexOf(selector.type);
if (level === -1) {
return -1;
}
score += (100 - level);
}
// all selector modifiers must be present
for (const selectorModifier of selector.modifiers) {
if (modifiers.indexOf(selectorModifier) === -1) {
return -1;
}
}
return score + selector.modifiers.length * 100;
},
id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${selector.language !== undefined ? ':' + selector.language : ''}`
};
}
public registerTokenStyleDefault(selector: TokenSelector, defaults: TokenStyleDefaults): void {
this.tokenStylingDefaultRules.push({ selector, defaults });
}
public deregisterTokenStyleDefault(selector: TokenSelector): void {
const selectorString = selector.id;
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.id !== selectorString);
}
public deregisterTokenType(id: string): void {
delete this.tokenTypeById[id];
delete this.tokenStylingSchema.properties[id];
this.typeHierarchy = Object.create(null);
}
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 getTokenStylingSchema(): IJSONSchema {
return this.tokenStylingSchema;
}
public getTokenStylingDefaultRules(): SemanticTokenDefaultRule[] {
return this.tokenStylingDefaultRules;
}
private getTypeHierarchy(typeId: string): string[] {
let hierarchy = this.typeHierarchy[typeId];
if (!hierarchy) {
this.typeHierarchy[typeId] = hierarchy = [typeId];
let type = this.tokenTypeById[typeId];
while (type && type.superType) {
hierarchy.push(type.superType);
type = this.tokenTypeById[type.superType];
}
}
return hierarchy;
}
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');
}
}
const CHAR_LANGUAGE = TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR.charCodeAt(0);
const CHAR_MODIFIER = CLASSIFIER_MODIFIER_SEPARATOR.charCodeAt(0);
export function parseClassifierString(s: string, defaultLanguage: string): { type: string, modifiers: string[], language: string; };
export function parseClassifierString(s: string, defaultLanguage?: string): { type: string, modifiers: string[], language: string | undefined; };
export function parseClassifierString(s: string, defaultLanguage: string | undefined): { type: string, modifiers: string[], language: string | undefined; } {
let k = s.length;
let language: string | undefined = defaultLanguage;
const modifiers = [];
for (let i = k - 1; i >= 0; i--) {
const ch = s.charCodeAt(i);
if (ch === CHAR_LANGUAGE || ch === CHAR_MODIFIER) {
const segment = s.substring(i + 1, k);
k = i;
if (ch === CHAR_LANGUAGE) {
language = segment;
} else {
modifiers.push(segment);
}
}
}
const type = s.substring(0, k);
return { type, modifiers, language };
}
let tokenClassificationRegistry = createDefaultTokenClassificationRegistry();
platform.Registry.add(Extensions.TokenClassificationContribution, tokenClassificationRegistry);
function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry {
const registry = new TokenClassificationRegistry();
function registerTokenType(id: string, description: string, scopesToProbe: ProbeScope[] = [], superType?: string, deprecationMessage?: string): string {
registry.registerTokenType(id, description, superType, deprecationMessage);
if (scopesToProbe) {
registerTokenStyleDefault(id, scopesToProbe);
}
return id;
}
function registerTokenStyleDefault(selectorString: string, scopesToProbe: ProbeScope[]) {
try {
const selector = registry.parseTokenSelector(selectorString);
registry.registerTokenStyleDefault(selector, { scopesToProbe });
} catch (e) {
console.log(e);
}
}
// default token types
registerTokenType('comment', nls.localize('comment', "Style for comments."), [['comment']]);
registerTokenType('string', nls.localize('string', "Style for strings."), [['string']]);
registerTokenType('keyword', nls.localize('keyword', "Style for keywords."), [['keyword.control']]);
registerTokenType('number', nls.localize('number', "Style for numbers."), [['constant.numeric']]);
registerTokenType('regexp', nls.localize('regexp', "Style for expressions."), [['constant.regexp']]);
registerTokenType('operator', nls.localize('operator', "Style for operators."), [['keyword.operator']]);
registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]);
registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['support.type']]);
registerTokenType('struct', nls.localize('struct', "Style for structs."), [['entity.name.type.struct']]);
registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.type.class'], ['support.class']]);
registerTokenType('interface', nls.localize('interface', "Style for interfaces."), [['entity.name.type.interface']]);
registerTokenType('enum', nls.localize('enum', "Style for enums."), [['entity.name.type.enum']]);
registerTokenType('typeParameter', nls.localize('typeParameter', "Style for type parameters."), [['entity.name.type.parameter']]);
registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]);
registerTokenType('member', nls.localize('member', "Style for member functions"), [], 'method', 'Deprecated use `method` instead');
registerTokenType('method', nls.localize('method', "Style for method (member functions)"), [['entity.name.function.member'], ['support.function']]);
registerTokenType('macro', nls.localize('macro', "Style for macros."), [['entity.name.function.preprocessor']]);
registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable.other.readwrite'], ['entity.name.variable']]);
registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), [['variable.parameter']]);
registerTokenType('property', nls.localize('property', "Style for properties."), [['variable.other.property']]);
registerTokenType('enumMember', nls.localize('enumMember', "Style for enum members."), [['variable.other.enummember']]);
registerTokenType('event', nls.localize('event', "Style for events."), [['variable.other.event']]);
registerTokenType('decorator', nls.localize('decorator', "Style for decorators & annotations."), [['entity.name.decorator'], ['entity.name.function']]);
registerTokenType('label', nls.localize('labels', "Style for labels. "), undefined);
// default token modifiers
registry.registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined);
registry.registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined);
registry.registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined);
registry.registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined);
registry.registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined);
registry.registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined);
registry.registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined);
registry.registerTokenModifier('readonly', nls.localize('readonly', "Style to use for symbols that are readonly."), undefined);
registerTokenStyleDefault('variable.readonly', [['variable.other.constant']]);
registerTokenStyleDefault('property.readonly', [['variable.other.constant.property']]);
registerTokenStyleDefault('type.defaultLibrary', [['support.type']]);
registerTokenStyleDefault('class.defaultLibrary', [['support.class']]);
registerTokenStyleDefault('interface.defaultLibrary', [['support.class']]);
registerTokenStyleDefault('variable.defaultLibrary', [['support.variable'], ['support.other.variable']]);
registerTokenStyleDefault('variable.defaultLibrary.readonly', [['support.constant']]);
registerTokenStyleDefault('property.defaultLibrary', [['support.variable.property']]);
registerTokenStyleDefault('property.defaultLibrary.readonly', [['support.constant.property']]);
registerTokenStyleDefault('function.defaultLibrary', [['support.function']]);
registerTokenStyleDefault('member.defaultLibrary', [['support.function']]);
return registry;
}
export function getTokenClassificationRegistry(): ITokenClassificationRegistry {
return tokenClassificationRegistry;
}
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<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
schemaRegistry.registerSchema(tokenStylingSchemaId, tokenClassificationRegistry.getTokenStylingSchema());
const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(tokenStylingSchemaId), 200);
tokenClassificationRegistry.onDidChangeSchema(() => {
if (!delayer.isScheduled()) {
delayer.schedule();
}
});