language specific scopes

This commit is contained in:
Martin Aeschlimann 2020-03-29 18:03:08 +02:00
parent 3ddb3d0bd6
commit 3c938c800a
8 changed files with 177 additions and 177 deletions

View file

@ -90,6 +90,30 @@
"path": "./syntaxes/Regular Expressions (JavaScript).tmLanguage"
}
],
"semanticTokenScopes": [
{
"language": "javascript",
"scopes": {
"property": ["variable.other.property.js"],
"property.readonly": ["variable.other.constant.property.js"],
"variable": ["variable.other.readwrite.js"],
"variable.readonly": ["variable.other.constant.object.js"],
"function": ["entity.name.function.js"],
"namespace": ["entity.name.type.module.js"]
}
},
{
"language": "javascriptreact",
"scopes": {
"property": ["variable.other.property.jsx"],
"property.readonly": ["variable.other.constant.property.jsx"],
"variable": ["variable.other.readwrite.jsx"],
"variable.readonly": ["variable.other.constant.object.jsx"],
"function": ["entity.name.function.jsx"],
"namespace": ["entity.name.type.module.jsx"]
}
}
],
"snippets": [
{
"language": "javascript",

View file

@ -79,6 +79,30 @@
}
}
],
"semanticTokenScopes": [
{
"language": "typescript",
"scopes": {
"property": ["variable.other.property.ts"],
"property.readonly": ["variable.other.constant.property.ts"],
"variable": ["variable.other.readwrite.ts"],
"variable.readonly": ["variable.other.constant.object.ts"],
"function": ["entity.name.function.ts"],
"namespace": ["entity.name.type.module.ts"]
}
},
{
"language": "typescriptreact",
"scopes": {
"property": ["variable.other.property.tsx"],
"property.readonly": ["variable.other.constant.property.tsx"],
"variable": ["variable.other.readwrite.tsx"],
"variable.readonly": ["variable.other.constant.object.tsx"],
"function": ["entity.name.function.tsx"],
"namespace": ["entity.name.type.module.tsx"]
}
}
],
"snippets": [
{
"language": "typescript",

View file

@ -1,68 +1,59 @@
{
"name": "vscode-colorize-tests",
"description": "Colorize tests for VS Code",
"version": "0.0.1",
"publisher": "vscode",
"license": "MIT",
"private": true,
"activationEvents": [
"onLanguage:json"
],
"main": "./out/colorizerTestMain",
"enableProposedApi": true,
"engines": {
"vscode": "*"
},
"scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json"
},
"dependencies": {
"jsonc-parser": "2.2.1"
},
"devDependencies": {
"@types/node": "^12.11.7",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"vscode": "1.1.5"
},
"contributes": {
"semanticTokenTypes": [
{
"id": "testToken",
"description": "A test token"
}
],
"semanticTokenModifiers": [
{
"id": "testModifier",
"description": "A test modifier"
}
],
"semanticTokenStyleDefaults": [
{
"selector": "testToken",
"scope": [ "entity.name.function.special" ]
},
{
"selector": "*.testModifier",
"light": {
"fontStyle": "bold"
},
"dark": {
"fontStyle": "bold"
},
"highContrast": {
"fontStyle": "bold"
}
}
],
"productIconThemes": [
{
"id": "Test Product Icons",
"label": "The Test Product Icon Theme",
"path": "./producticons/test-product-icon-theme.json",
"_watch": true
}
]
}
"name": "vscode-colorize-tests",
"description": "Colorize tests for VS Code",
"version": "0.0.1",
"publisher": "vscode",
"license": "MIT",
"private": true,
"activationEvents": [
"onLanguage:json"
],
"main": "./out/colorizerTestMain",
"enableProposedApi": true,
"engines": {
"vscode": "*"
},
"scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json"
},
"dependencies": {
"jsonc-parser": "2.2.1"
},
"devDependencies": {
"@types/node": "^12.11.7",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"vscode": "1.1.5"
},
"contributes": {
"semanticTokenTypes": [
{
"id": "testToken",
"description": "A test token"
}
],
"semanticTokenModifiers": [
{
"id": "testModifier",
"description": "A test modifier"
}
],
"semanticTokenScopes": [
{
"scopes": {
"testToken": [
"entity.name.function.special"
]
}
}
],
"productIconThemes": [
{
"id": "Test Product Icons",
"label": "The Test Product Icon Theme",
"path": "./producticons/test-product-icon-theme.json",
"_watch": true
}
]
}
}

View file

@ -28,7 +28,7 @@ export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$';
export interface TokenSelector {
match(type: string, modifiers: string[], language: string): number;
readonly selectorString: string;
readonly id: string;
}
export interface TokenTypeOrModifierContribution {
@ -155,7 +155,7 @@ export namespace TokenStylingRule {
}
export function toJSONObject(rule: TokenStylingRule): any {
return {
_selector: rule.selector.selectorString,
_selector: rule.selector.id,
_style: TokenStyle.toJSONObject(rule.style)
};
}
@ -164,7 +164,7 @@ export namespace TokenStylingRule {
return true;
}
return r1 !== undefined && r2 !== undefined
&& r1.selector && r2.selector && r1.selector.selectorString === r2.selector.selectorString
&& r1.selector && r2.selector && r1.selector.id === r2.selector.id
&& TokenStyle.equals(r1.style, r2.style);
}
export function is(r: any): r is TokenStylingRule {
@ -203,10 +203,11 @@ export interface ITokenClassificationRegistry {
/**
* 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): TokenSelector;
parseTokenSelector(selectorString: string, language?: string): TokenSelector;
/**
* Register a TokenStyle default to the registry.
@ -335,13 +336,13 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage);
}
public parseTokenSelector(selectorString: string): TokenSelector {
const selector = parseClassifierString(selectorString);
public parseTokenSelector(selectorString: string, language?: string): TokenSelector {
const selector = parseClassifierString(selectorString, language);
if (!selector.type) {
return {
match: () => -1,
selectorString
id: '$invalid'
};
}
@ -370,7 +371,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
}
return score + selector.modifiers.length * 100;
},
selectorString
id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${selector.language !== undefined ? ':' + selector.language : ''}`
};
}
@ -379,8 +380,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
}
public deregisterTokenStyleDefault(selector: TokenSelector): void {
const selectorString = selector.selectorString;
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.selectorString !== selectorString);
const selectorString = selector.id;
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.id !== selectorString);
}
public deregisterTokenType(id: string): void {
@ -442,9 +443,11 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
const CHAR_LANGUAGE = TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR.charCodeAt(0);
const CHAR_MODIFIER = CLASSIFIER_MODIFIER_SEPARATOR.charCodeAt(0);
export function parseClassifierString(s: string): { type: string, modifiers: string[], language: string | undefined; } {
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 = undefined;
let language: string | undefined = defaultLanguage;
const modifiers = [];
for (let i = k - 1; i >= 0; i--) {

View file

@ -551,9 +551,9 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
} else if (TokenStylingRule.is(definition)) {
const scope = theme.getTokenStylingRuleScope(definition);
if (scope === 'setting') {
return `User settings: ${definition.selector.selectorString} - ${this._renderStyleProperty(definition.style, property)}`;
return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`;
} else if (scope === 'theme') {
return `Color theme: ${definition.selector.selectorString} - ${this._renderStyleProperty(definition.style, property)}`;
return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`;
}
return '';
} else {

View file

@ -212,8 +212,8 @@ export class ColorThemeData implements IWorkbenchColorTheme {
if (tokenStyleValue === undefined) {
return undefined;
} else if (typeof tokenStyleValue === 'string') {
const { type, modifiers, language } = parseClassifierString(tokenStyleValue);
return this.getTokenStyle(type, modifiers, language || '');
const { type, modifiers, language } = parseClassifierString(tokenStyleValue, '');
return this.getTokenStyle(type, modifiers, language);
} else if (typeof tokenStyleValue === 'object') {
return tokenStyleValue;
}
@ -248,8 +248,8 @@ export class ColorThemeData implements IWorkbenchColorTheme {
}
public getTokenStyleMetadata(typeWithLanguage: string, modifiers: string[], defaultLanguage: string, useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined {
const { type, language } = parseClassifierString(typeWithLanguage);
let style = this.getTokenStyle(type, modifiers, language || defaultLanguage, useDefault, definitions);
const { type, language } = parseClassifierString(typeWithLanguage, defaultLanguage);
let style = this.getTokenStyle(type, modifiers, language, useDefault, definitions);
if (!style) {
return undefined;
}

View file

@ -5,8 +5,7 @@
import * as nls from 'vs/nls';
import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern, TokenStyleDefaults, TokenStyle, fontStylePattern, selectorPattern } from 'vs/platform/theme/common/tokenClassificationRegistry';
import { textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern } from 'vs/platform/theme/common/tokenClassificationRegistry';
interface ITokenTypeExtensionPoint {
id: string;
@ -20,24 +19,10 @@ interface ITokenModifierExtensionPoint {
}
interface ITokenStyleDefaultExtensionPoint {
selector: string;
scope?: string[];
light?: {
foreground?: string;
fontStyle?: string;
};
dark?: {
foreground?: string;
fontStyle?: string;
};
highContrast?: {
foreground?: string;
fontStyle?: string;
};
language?: string;
scopes: { [selector: string]: string[] };
}
const colorPattern = '^#([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$';
const tokenClassificationRegistry: ITokenClassificationRegistry = getTokenClassificationRegistry();
const tokenTypeExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenTypeExtensionPoint[]>({
@ -86,37 +71,26 @@ const tokenModifierExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenMo
});
const tokenStyleDefaultsExtPoint = ExtensionsRegistry.registerExtensionPoint<ITokenStyleDefaultExtensionPoint[]>({
extensionPoint: 'semanticTokenStyleDefaults',
extensionPoint: 'semanticTokenScopes',
jsonSchema: {
description: nls.localize('contributes.semanticTokenStyleDefaults', 'Contributes semantic token style defaults.'),
description: nls.localize('contributes.semanticTokenScopes', 'Contributes semantic token scope maps.'),
type: 'array',
items: {
type: 'object',
properties: {
selector: {
type: 'string',
description: nls.localize('contributes.semanticTokenStyleDefaults.selector', 'The selector matching token types and modifiers.'),
pattern: selectorPattern,
patternErrorMessage: nls.localize('contributes.semanticTokenStyleDefaults.selector.format', 'Selectors should be in the form (type|*)(.modifier)*(:language)?'),
language: {
description: nls.localize('contributes.semanticTokenScopes.languages', 'Lists the languge for which the defaults are.'),
type: 'string'
},
scope: {
type: 'array',
description: nls.localize('contributes.semanticTokenStyleDefaults.scope', 'A TextMate scope against the current color theme is matched to find the style for the given selector'),
items: {
type: 'string'
scopes: {
description: nls.localize('contributes.semanticTokenScopes.scopes', 'Maps a semantic token (described by semantic token selector) to one or more textMate scopes used to represent that token.'),
type: 'object',
additionalProperties: {
type: 'array',
items: {
type: 'string'
}
}
},
light: {
description: nls.localize('contributes.semanticTokenStyleDefaults.light', 'The default style used for light themes'),
$ref: textmateColorSettingsSchemaId
},
dark: {
description: nls.localize('contributes.semanticTokenStyleDefaults.dark', 'The default style used for dark themes'),
$ref: textmateColorSettingsSchemaId
},
highContrast: {
description: nls.localize('contributes.semanticTokenStyleDefaults.hc', 'The default style used for high contrast themes'),
$ref: textmateColorSettingsSchemaId
}
}
}
@ -147,24 +121,6 @@ export class TokenClassificationExtensionPoints {
}
return true;
}
function validateStyle(style: { foreground?: string; fontStyle?: string; } | undefined, extensionPoint: string, collector: ExtensionMessageCollector): TokenStyle | undefined {
if (!style) {
return undefined;
}
if (style.foreground) {
if (typeof style.foreground !== 'string' || !style.foreground.match(colorPattern)) {
collector.error(nls.localize('invalid.color', "'configuration.{0}.foreground' must follow the pattern #RRGGBB[AA]", extensionPoint));
return undefined;
}
}
if (style.fontStyle) {
if (typeof style.fontStyle !== 'string' || !style.fontStyle.match(fontStylePattern)) {
collector.error(nls.localize('invalid.fontStyle', "'configuration.{0}.fontStyle' must be one or a combination of \'italic\', \'bold\' or \'underline\' or the empty string", extensionPoint));
return undefined;
}
}
return TokenStyle.fromSettings(style.foreground, style.fontStyle);
}
tokenTypeExtPoint.setHandler((extensions, delta) => {
for (const extension of delta.added) {
@ -216,49 +172,45 @@ export class TokenClassificationExtensionPoints {
const collector = extension.collector;
if (!extensionValue || !Array.isArray(extensionValue)) {
collector.error(nls.localize('invalid.semanticTokenStyleDefaultConfiguration', "'configuration.semanticTokenStyleDefaults' must be an array"));
collector.error(nls.localize('invalid.semanticTokenScopes.configuration', "'configuration.semanticTokenScopes' must be an array"));
return;
}
for (const contribution of extensionValue) {
if (typeof contribution.selector !== 'string' || contribution.selector.length === 0) {
collector.error(nls.localize('invalid.selector', "'configuration.semanticTokenStyleDefaults.selector' must be defined and can not be empty"));
if (contribution.language && typeof contribution.language !== 'string') {
collector.error(nls.localize('invalid.semanticTokenScopes.language', "'configuration.semanticTokenScopes.language' must be a string"));
continue;
}
if (!contribution.selector.match(selectorPattern)) {
collector.error(nls.localize('invalid.selector.format', "'configuration.semanticTokenStyleDefaults.selector' must be in the form (type|*)(.modifier)*(:language)?"));
if (!contribution.scopes || typeof contribution.scopes !== 'object') {
collector.error(nls.localize('invalid.semanticTokenScopes.scopes', "'configuration.semanticTokenScopes.scopes' must be defined as an object"));
continue;
}
const tokenStyleDefault: TokenStyleDefaults = {};
if (contribution.scope) {
if ((!Array.isArray(contribution.scope) || contribution.scope.some(s => typeof s !== 'string'))) {
collector.error(nls.localize('invalid.scope', "If defined, 'configuration.semanticTokenStyleDefaults.scope' must be an array of strings"));
for (let selectorString in contribution.scopes) {
const tmScopes = contribution.scopes[selectorString];
if (!Array.isArray(tmScopes) || tmScopes.some(l => typeof l !== 'string')) {
collector.error(nls.localize('invalid.semanticTokenScopes.scopes.value', "'configuration.semanticTokenScopes.scopes' values must be an array of strings"));
continue;
}
tokenStyleDefault.scopesToProbe = [contribution.scope];
}
tokenStyleDefault.light = validateStyle(contribution.light, 'semanticTokenStyleDefaults.light', collector);
tokenStyleDefault.dark = validateStyle(contribution.dark, 'semanticTokenStyleDefaults.dark', collector);
tokenStyleDefault.hc = validateStyle(contribution.highContrast, 'semanticTokenStyleDefaults.highContrast', collector);
try {
const selector = tokenClassificationRegistry.parseTokenSelector(contribution.selector);
tokenClassificationRegistry.registerTokenStyleDefault(selector, tokenStyleDefault);
} catch (e) {
collector.error(nls.localize('invalid.selector.parsing', "configuration.semanticTokenStyleDefaults.selector': Problems parsing {0}.", contribution.selector));
// invalid selector, ignore
try {
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language);
tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) });
} catch (e) {
collector.error(nls.localize('invalid.semanticTokenScopes.scopes.selector', "configuration.semanticTokenScopes.scopes': Problems parsing selector {0}.", selectorString));
// invalid selector, ignore
}
}
}
}
for (const extension of delta.removed) {
const extensionValue = <ITokenStyleDefaultExtensionPoint[]>extension.value;
for (const contribution of extensionValue) {
try {
const selector = tokenClassificationRegistry.parseTokenSelector(contribution.selector);
tokenClassificationRegistry.deregisterTokenStyleDefault(selector);
} catch (e) {
// invalid selector, ignore
for (let selectorString in contribution.scopes) {
const tmScopes = contribution.scopes[selectorString];
try {
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString, contribution.language);
tokenClassificationRegistry.registerTokenStyleDefault(selector, { scopesToProbe: tmScopes.map(s => s.split(' ')) });
} catch (e) {
// invalid selector, ignore
}
}
}
}

View file

@ -398,8 +398,11 @@ suite('Themes - TokenStyleResolving', () => {
test('language - scope resolving', async () => {
const registry = getTokenClassificationRegistry();
registry.registerTokenStyleDefault(registry.parseTokenSelector('type:typescript'), { scopesToProbe: [['entity.name.type.ts']] });
const numberOfDefaultRules = registry.getTokenStylingDefaultRules().length;
registry.registerTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1'), { scopesToProbe: [['entity.name.type.ts1']] });
registry.registerTokenStyleDefault(registry.parseTokenSelector('type:javascript1'), { scopesToProbe: [['entity.name.type.js1']] });
try {
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
@ -411,17 +414,20 @@ suite('Themes - TokenStyleResolving', () => {
settings: { foreground: '#aa0000' }
},
{
scope: 'entity.name.type.ts',
scope: 'entity.name.type.ts1',
settings: { foreground: '#bb0000' }
}
]
});
assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript');
assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript');
assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript1');
assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript1');
} finally {
registry.deregisterTokenType('type/typescript');
registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type', 'typescript1'));
registry.deregisterTokenStyleDefault(registry.parseTokenSelector('type:javascript1'));
assert.equal(registry.getTokenStylingDefaultRules().length, numberOfDefaultRules);
}
});
});