This commit is contained in:
Alex Dima 2016-11-10 14:57:35 +01:00
Родитель a33377e93d
Коммит 146168e6b5
5 изменённых файлов: 1041 добавлений и 0 удалений

Просмотреть файл

@ -97,6 +97,298 @@ var RegexSource = (function () {
exports.RegexSource = RegexSource;
//# sourceMappingURL=utils.js.map
});
$load('./theme', function(require, module, exports) {
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
'use strict';
var ParsedThemeRule = (function () {
function ParsedThemeRule(scope, parentScopes, index, fontStyle, foreground, background) {
this.scope = scope;
this.parentScopes = parentScopes;
this.index = index;
this.fontStyle = fontStyle;
this.foreground = foreground;
this.background = background;
}
return ParsedThemeRule;
}());
exports.ParsedThemeRule = ParsedThemeRule;
/**
* Parse a raw theme into rules.
*/
function parseTheme(source) {
if (!source) {
return [];
}
if (!source.settings || !Array.isArray(source.settings)) {
return [];
}
var settings = source.settings;
var result = [], resultLen = 0;
for (var i = 0, len = settings.length; i < len; i++) {
var entry = settings[i];
if (!entry.settings) {
continue;
}
var scopes = void 0;
if (typeof entry.scope === 'string') {
scopes = entry.scope.split(',');
}
else if (Array.isArray(entry.scope)) {
scopes = entry.scope;
}
else {
scopes = [''];
}
var fontStyle = -1 /* NotSet */;
if (typeof entry.settings.fontStyle === 'string') {
fontStyle = 0 /* None */;
var segments = entry.settings.fontStyle.split(' ');
for (var j = 0, lenJ = segments.length; j < lenJ; j++) {
var segment = segments[j];
switch (segment) {
case 'italic':
fontStyle = fontStyle | 1 /* Italic */;
break;
case 'bold':
fontStyle = fontStyle | 2 /* Bold */;
break;
case 'underline':
fontStyle = fontStyle | 4 /* Underline */;
break;
}
}
}
var foreground = null;
if (typeof entry.settings.foreground === 'string') {
foreground = entry.settings.foreground;
}
var background = null;
if (typeof entry.settings.background === 'string') {
background = entry.settings.background;
}
for (var j = 0, lenJ = scopes.length; j < lenJ; j++) {
var _scope = scopes[j].trim();
var segments = _scope.split(' ');
var scope = segments[segments.length - 1];
var parentScopes = null;
if (segments.length > 1) {
parentScopes = segments.slice(0, segments.length - 1);
parentScopes.reverse();
}
result[resultLen++] = new ParsedThemeRule(scope, parentScopes, i, fontStyle, foreground, background);
}
}
return result;
}
exports.parseTheme = parseTheme;
/**
* Resolve rules (i.e. inheritance).
*/
function resolveParsedThemeRules(parsedThemeRules) {
// Sort rules lexicographically, and then by index if necessary
parsedThemeRules.sort(function (a, b) {
var r = strcmp(a.scope, b.scope);
if (r !== 0) {
return r;
}
r = strArrCmp(a.parentScopes, b.parentScopes);
if (r !== 0) {
return r;
}
return a.index - b.index;
});
var defaults;
if (parsedThemeRules.length >= 1 && parsedThemeRules[0].scope === '') {
var incomingDefaults = parsedThemeRules.shift();
var fontStyle = incomingDefaults.fontStyle;
var foreground = incomingDefaults.foreground;
var background = incomingDefaults.background;
if (fontStyle === -1 /* NotSet */) {
fontStyle = 0 /* None */;
}
if (foreground === null) {
foreground = '#000000';
}
if (background === null) {
background = '#ffffff';
}
defaults = new ParsedThemeRule('', null, incomingDefaults.index, fontStyle, foreground, background);
}
else {
defaults = new ParsedThemeRule('', null, -1, 0 /* None */, '#000000', '#ffffff');
}
var root = new ThemeTrieElement(new ThemeTrieElementRule(null, defaults.fontStyle, defaults.foreground, defaults.background), []);
for (var i = 0, len = parsedThemeRules.length; i < len; i++) {
root.insert(parsedThemeRules[i]);
}
return root;
}
exports.resolveParsedThemeRules = resolveParsedThemeRules;
var Theme = (function () {
function Theme(source) {
this._root = resolveParsedThemeRules(parseTheme(source));
this._cache = {};
}
Theme.prototype.match = function (scopeName) {
if (!this._cache.hasOwnProperty(scopeName)) {
this._cache[scopeName] = this._root.match(scopeName);
}
return this._cache[scopeName];
};
return Theme;
}());
exports.Theme = Theme;
function strcmp(a, b) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
exports.strcmp = strcmp;
function strArrCmp(a, b) {
if (a === null && b === null) {
return 0;
}
if (!a) {
return -1;
}
if (!b) {
return 1;
}
var len1 = a.length;
var len2 = b.length;
if (len1 === len2) {
for (var i = 0; i < len1; i++) {
var res = strcmp(a[i], b[i]);
if (res !== 0) {
return res;
}
}
return 0;
}
return len1 - len2;
}
exports.strArrCmp = strArrCmp;
var ThemeTrieElementRule = (function () {
function ThemeTrieElementRule(parentScopes, fontStyle, foreground, background) {
this.parentScopes = parentScopes;
this.fontStyle = fontStyle;
this.foreground = foreground;
this.background = background;
}
ThemeTrieElementRule.prototype.clone = function () {
return new ThemeTrieElementRule(this.parentScopes, this.fontStyle, this.foreground, this.background);
};
ThemeTrieElementRule.prototype.acceptOverwrite = function (fontStyle, foreground, background) {
if (fontStyle !== -1 /* NotSet */) {
this.fontStyle = fontStyle;
}
if (foreground !== null) {
this.foreground = foreground;
}
if (background !== null) {
this.background = background;
}
};
return ThemeTrieElementRule;
}());
exports.ThemeTrieElementRule = ThemeTrieElementRule;
var ThemeTrieElement = (function () {
function ThemeTrieElement(mainRule, rulesWithParentScopes, children) {
if (rulesWithParentScopes === void 0) { rulesWithParentScopes = []; }
if (children === void 0) { children = {}; }
this._mainRule = mainRule;
this._rulesWithParentScopes = rulesWithParentScopes;
this._children = children;
}
ThemeTrieElement.prototype.match = function (scope) {
if (scope === '') {
return [].concat(this._mainRule).concat(this._rulesWithParentScopes);
}
var dotIndex = scope.indexOf('.');
var head;
var tail;
if (dotIndex === -1) {
head = scope;
tail = '';
}
else {
head = scope.substring(0, dotIndex);
tail = scope.substring(dotIndex + 1);
}
if (this._children.hasOwnProperty(head)) {
return this._children[head].match(tail);
}
return [].concat(this._mainRule).concat(this._rulesWithParentScopes);
};
ThemeTrieElement.prototype.insert = function (rule) {
this._doInsert(rule.scope, rule.parentScopes, rule.fontStyle, rule.foreground, rule.background);
};
ThemeTrieElement.prototype._doInsert = function (scope, parentScopes, fontStyle, foreground, background) {
if (scope === '') {
this._doInsertHere(parentScopes, fontStyle, foreground, background);
return;
}
var dotIndex = scope.indexOf('.');
var head;
var tail;
if (dotIndex === -1) {
head = scope;
tail = '';
}
else {
head = scope.substring(0, dotIndex);
tail = scope.substring(dotIndex + 1);
}
var child;
if (this._children[head]) {
child = this._children[head];
}
else {
child = new ThemeTrieElement(this._mainRule.clone());
this._children[head] = child;
}
// TODO: In the case that this element has `parentScopes`, should we generate one insert for each parentScope ?
child._doInsert(tail, parentScopes, fontStyle, foreground, background);
};
ThemeTrieElement.prototype._doInsertHere = function (parentScopes, fontStyle, foreground, background) {
if (parentScopes === null) {
// Merge into the main rule
this._mainRule.acceptOverwrite(fontStyle, foreground, background);
return;
}
// Try to merge into existing rule
for (var i = 0, len = this._rulesWithParentScopes.length; i < len; i++) {
var rule = this._rulesWithParentScopes[i];
if (strArrCmp(rule.parentScopes, parentScopes) === 0) {
// bingo! => we get to merge this into an existing one
rule.acceptOverwrite(fontStyle, foreground, background);
return;
}
}
// Must add a new rule
// Inherit from main rule
if (fontStyle === -1 /* NotSet */) {
fontStyle = this._mainRule.fontStyle;
}
if (foreground === null) {
foreground = this._mainRule.foreground;
}
if (background === null) {
background = this._mainRule.background;
}
this._rulesWithParentScopes.push(new ThemeTrieElementRule(parentScopes, fontStyle, foreground, background));
};
return ThemeTrieElement;
}());
exports.ThemeTrieElement = ThemeTrieElement;
//# sourceMappingURL=theme.js.map
});
$load('./matcher', function(require, module, exports) {
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.

Просмотреть файл

@ -15,6 +15,7 @@ if (!fs.existsSync(RELEASE_FOLDER)) {
var sources = [
'utils.js',
'theme.js',
'matcher.js',
'debug.js',
'json.js',

Просмотреть файл

@ -9,6 +9,7 @@ import * as assert from 'assert';
import {Registry, IToken, IGrammar, IGrammarLocator, StackElement} from '../main';
import {createMatcher} from '../matcher';
import {parse as JSONparse} from '../json';
import './themes.test';
const REPO_ROOT = path.join(__dirname, '../../');

361
src/tests/themes.test.ts Normal file
Просмотреть файл

@ -0,0 +1,361 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
'use strict';
import * as fs from 'fs';
import * as path from 'path';
import * as assert from 'assert';
import { Registry, IToken, IGrammar, IGrammarLocator, StackElement } from '../main';
import { createMatcher } from '../matcher';
import { parse as JSONparse } from '../json';
import {
Theme, strcmp, strArrCmp, ThemeTrieElement, ThemeTrieElementRule,
parseTheme, resolveParsedThemeRules, ParsedThemeRule,
FontStyle
} from '../theme';
const THEMES_TEST_PATH = path.join(__dirname, '../../test-cases/themes');
// console.log(THEMES_TEST_PATH);
describe('Theme', () => {
let light_vs = JSON.parse(fs.readFileSync(path.join(THEMES_TEST_PATH, 'light_vs.json')).toString());
let light_vs_theme = new Theme(light_vs);
// console.log(light_vs_theme);
// console.log(light_vs);
// var light_vs
it('works', () => {
console.log('hello world!');
});
});
describe('Theme matching', () => {
it('can match', () => {
let theme = new Theme({
settings: [
{ settings: { foreground: '#F8F8F2', background: '#272822' } },
{ scope: 'source, something', settings: { background: '#100000' } },
{ scope: ['bar', 'baz'], settings: { background: '#200000' } },
{ scope: 'source.css selector bar', settings: { fontStyle: 'bold' } },
{ scope: 'constant', settings: { fontStyle: 'italic', foreground: '#300000' } },
{ scope: 'constant.numeric', settings: { foreground: '#400000' } },
{ scope: 'constant.numeric.hex', settings: { fontStyle: 'bold' } },
{ scope: 'constant.numeric.oct', settings: { fontStyle: 'bold italic underline' } },
{ scope: 'constant.numeric.dec', settings: { fontStyle: '', foreground: '#500000' } },
{ scope: 'storage.object.bar', settings: { fontStyle: '', foreground: '#600000' } },
]
});
function assertMatch(scopeName: string, expected: ThemeTrieElementRule[]): void {
let actual = theme.match(scopeName);
assert.deepEqual(actual, expected, 'when matching ' + scopeName);
}
function assertSimpleMatch(scopeName: string, fontStyle: FontStyle, foreground: string, background: string): void {
assertMatch(scopeName, [
new ThemeTrieElementRule(null, fontStyle, foreground, background)
]);
}
// matches defaults
assertSimpleMatch('', FontStyle.None, '#F8F8F2', '#272822');
assertSimpleMatch('bazz', FontStyle.None, '#F8F8F2', '#272822');
assertSimpleMatch('asdfg', FontStyle.None, '#F8F8F2', '#272822');
// matches source
assertSimpleMatch('source', FontStyle.None, '#F8F8F2', '#100000');
assertSimpleMatch('source.ts', FontStyle.None, '#F8F8F2', '#100000');
assertSimpleMatch('source.tss', FontStyle.None, '#F8F8F2', '#100000');
// matches something
assertSimpleMatch('something', FontStyle.None, '#F8F8F2', '#100000');
assertSimpleMatch('something.ts', FontStyle.None, '#F8F8F2', '#100000');
assertSimpleMatch('something.tss', FontStyle.None, '#F8F8F2', '#100000');
// matches baz
assertSimpleMatch('baz', FontStyle.None, '#F8F8F2', '#200000');
assertSimpleMatch('baz.ts', FontStyle.None, '#F8F8F2', '#200000');
assertSimpleMatch('baz.tss', FontStyle.None, '#F8F8F2', '#200000');
// matches constant
assertSimpleMatch('constant', FontStyle.Italic, '#300000', '#272822');
assertSimpleMatch('constant.string', FontStyle.Italic, '#300000', '#272822');
assertSimpleMatch('constant.hex', FontStyle.Italic, '#300000', '#272822');
// matches constant.numeric
assertSimpleMatch('constant.numeric', FontStyle.Italic, '#400000', '#272822');
assertSimpleMatch('constant.numeric.baz', FontStyle.Italic, '#400000', '#272822');
// matches constant.numeric.hex
assertSimpleMatch('constant.numeric.hex', FontStyle.Bold, '#400000', '#272822');
assertSimpleMatch('constant.numeric.hex.baz', FontStyle.Bold, '#400000', '#272822');
// matches constant.numeric.oct
assertSimpleMatch('constant.numeric.oct', FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, '#400000', '#272822');
assertSimpleMatch('constant.numeric.oct.baz', FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, '#400000', '#272822');
// matches constant.numeric.dec
assertSimpleMatch('constant.numeric.dec', FontStyle.None, '#500000', '#272822');
assertSimpleMatch('constant.numeric.dec.baz', FontStyle.None, '#500000', '#272822');
// matches storage.object.bar
assertSimpleMatch('storage.object.bar', FontStyle.None, '#600000', '#272822');
assertSimpleMatch('storage.object.bar.baz', FontStyle.None, '#600000', '#272822');
// does not match storage.object.bar
assertSimpleMatch('storage.object.bart', FontStyle.None, '#F8F8F2', '#272822');
assertSimpleMatch('storage.object', FontStyle.None, '#F8F8F2', '#272822');
assertSimpleMatch('storage', FontStyle.None, '#F8F8F2', '#272822');
assertMatch('bar', [
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#200000'),
new ThemeTrieElementRule(['selector', 'source.css'], FontStyle.Bold, '#F8F8F2', '#200000')
]);
});
});
describe('Theme parsing', () => {
it('can parse', () => {
let actual = parseTheme({
settings: [
{ settings: { foreground: '#F8F8F2', background: '#272822' } },
{ scope: 'source, something', settings: { background: '#100000' } },
{ scope: ['bar', 'baz'], settings: { background: '#010000' } },
{ scope: 'source.css selector bar', settings: { fontStyle: 'bold' } },
{ scope: 'constant', settings: { fontStyle: 'italic', foreground: '#ff0000' } },
{ scope: 'constant.numeric', settings: { foreground: '#00ff00' } },
{ scope: 'constant.numeric.hex', settings: { fontStyle: 'bold' } },
{ scope: 'constant.numeric.oct', settings: { fontStyle: 'bold italic underline' } },
{ scope: 'constant.numeric.dec', settings: { fontStyle: '', foreground: '#0000ff' } },
]
});
let expected = [
new ParsedThemeRule('', null, 0, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('source', null, 1, FontStyle.NotSet, null, '#100000'),
new ParsedThemeRule('something', null, 1, FontStyle.NotSet, null, '#100000'),
new ParsedThemeRule('bar', null, 2, FontStyle.NotSet, null, '#010000'),
new ParsedThemeRule('baz', null, 2, FontStyle.NotSet, null, '#010000'),
new ParsedThemeRule('bar', ['selector', 'source.css'], 3, FontStyle.Bold, null, null),
new ParsedThemeRule('constant', null, 4, FontStyle.Italic, '#ff0000', null),
new ParsedThemeRule('constant.numeric', null, 5, FontStyle.NotSet, '#00ff00', null),
new ParsedThemeRule('constant.numeric.hex', null, 6, FontStyle.Bold, null, null),
new ParsedThemeRule('constant.numeric.oct', null, 7, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, null, null),
new ParsedThemeRule('constant.numeric.dec', null, 8, FontStyle.None, '#0000ff', null),
];
assert.deepEqual(actual, expected);
});
});
describe('Theme resolving', () => {
it('strcmp works', () => {
let actual = ['bar', 'z', 'zu', 'a', 'ab', ''].sort(strcmp);
let expected = ['', 'a', 'ab', 'bar', 'z', 'zu'];
assert.deepEqual(actual, expected);
});
it('strArrCmp works', () => {
function assertStrArrCmp(testCase: string, a: string[], b: string[], expected: number): void {
assert.equal(strArrCmp(a, b), expected, testCase);
}
assertStrArrCmp('001', null, null, 0);
assertStrArrCmp('002', null, [], -1);
assertStrArrCmp('003', null, ['a'], -1);
assertStrArrCmp('004', [], null, 1);
assertStrArrCmp('005', ['a'], null, 1);
assertStrArrCmp('006', [], [], 0);
assertStrArrCmp('007', [], ['a'], -1);
assertStrArrCmp('008', ['a'], [], 1);
assertStrArrCmp('009', ['a'], ['a'], 0);
assertStrArrCmp('010', ['a', 'b'], ['a'], 1);
assertStrArrCmp('011', ['a'], ['a', 'b'], -1);
assertStrArrCmp('012', ['a', 'b'], ['a', 'b'], 0);
assertStrArrCmp('013', ['a', 'b'], ['a', 'c'], -1);
assertStrArrCmp('014', ['a', 'c'], ['a', 'b'], 1);
});
it('always has defaults', () => {
let actual = resolveParsedThemeRules([]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#000000', '#ffffff')
);
assert.deepEqual(actual, expected);
});
it('respects incoming defaults 1', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, null, null)
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#000000', '#ffffff')
);
assert.deepEqual(actual, expected);
});
it('respects incoming defaults 2', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.None, null, null)
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#000000', '#ffffff')
);
assert.deepEqual(actual, expected);
});
it('respects incoming defaults 3', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.Bold, null, null)
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.Bold, '#000000', '#ffffff')
);
assert.deepEqual(actual, expected);
});
it('respects incoming defaults 4', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#ff0000', null)
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#ff0000', '#ffffff')
);
assert.deepEqual(actual, expected);
});
it('respects incoming defaults 5', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, null, '#ff0000')
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#000000', '#ff0000')
);
assert.deepEqual(actual, expected);
});
it('can merge incoming defaults', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, null, '#ff0000'),
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#00ff00', null),
new ParsedThemeRule('', null, -1, FontStyle.Bold, null, null),
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.Bold, '#00ff00', '#ff0000'),
[],
{}
);
assert.deepEqual(actual, expected);
});
it('defaults are inherited', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('var', null, -1, FontStyle.NotSet, '#ff0000', null)
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#272822'), [],
{
'var': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.None, '#ff0000', '#272822'))
}
);
assert.deepEqual(actual, expected);
});
it('same rules get merged', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('var', null, 1, FontStyle.Bold, null, null),
new ParsedThemeRule('var', null, 0, FontStyle.NotSet, '#ff0000', null),
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#272822'), [],
{
'var': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#ff0000', '#272822'))
}
);
assert.deepEqual(actual, expected);
});
it('rules are inherited', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('var', null, -1, FontStyle.Bold, '#ff0000', null),
new ParsedThemeRule('var.identifier', null, -1, FontStyle.NotSet, '#00ff00', null),
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#272822'), [], {
'var': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#ff0000', '#272822'), [], {
'identifier': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#00ff00', '#272822'))
})
}
);
assert.deepEqual(actual, expected);
});
it('rules are inherited', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('var', null, -1, FontStyle.Bold, '#ff0000', null),
new ParsedThemeRule('var.identifier', null, -1, FontStyle.NotSet, '#00ff00', null),
new ParsedThemeRule('constant', null, 4, FontStyle.Italic, '#100000', null),
new ParsedThemeRule('constant.numeric', null, 5, FontStyle.NotSet, '#200000', null),
new ParsedThemeRule('constant.numeric.hex', null, 6, FontStyle.Bold, null, null),
new ParsedThemeRule('constant.numeric.oct', null, 7, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, null, null),
new ParsedThemeRule('constant.numeric.dec', null, 8, FontStyle.None, '#300000', null),
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#272822'), [], {
'var': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#ff0000', '#272822'), [], {
'identifier': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#00ff00', '#272822'))
}),
'constant': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Italic, '#100000', '#272822'), [], {
'numeric': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Italic, '#200000', '#272822'), [], {
'hex': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold, '#200000', '#272822')),
'oct': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline, '#200000', '#272822')),
'dec': new ThemeTrieElement(new ThemeTrieElementRule(null, FontStyle.None, '#300000', '#272822')),
})
})
}
);
assert.deepEqual(actual, expected);
});
it('rules with parent scopes', () => {
let actual = resolveParsedThemeRules([
new ParsedThemeRule('', null, -1, FontStyle.NotSet, '#F8F8F2', '#272822'),
new ParsedThemeRule('var', null, -1, FontStyle.Bold, '#100000', null),
new ParsedThemeRule('var.identifier', null, -1, FontStyle.NotSet, '#200000', null),
new ParsedThemeRule('var', ['source.css'], 1, FontStyle.Italic, '#300000', null),
new ParsedThemeRule('var', ['source.css'], 2, FontStyle.Underline, null, null),
]);
let expected = new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.None, '#F8F8F2', '#272822'), [], {
'var': new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.Bold, '#100000', '#272822'),
[new ThemeTrieElementRule(['source.css'], FontStyle.Underline, '#300000', '#272822')],
{
'identifier': new ThemeTrieElement(
new ThemeTrieElementRule(null, FontStyle.Bold, '#200000', '#272822'),
[/*new ThemeTrieElementRule(['source.css'], FontStyle.Underline, '#200000', '#272822')*/]
)
}
)
}
);
assert.deepEqual(actual, expected);
});
});

386
src/theme.ts Normal file
Просмотреть файл

@ -0,0 +1,386 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
'use strict';
export interface IRawThemeSetting {
readonly name?: string;
readonly scope?: string | string[];
readonly settings: {
readonly fontStyle?: string;
readonly foreground?: string;
readonly background?: string;
}
}
export interface IRawTheme {
readonly name?: string;
readonly settings: IRawThemeSetting[];
}
export const enum FontStyle {
NotSet = -1,
None = 0,
Italic = 1,
Bold = 2,
Underline = 4
}
export class ParsedThemeRule {
_parsedThemeRuleBrand: void;
readonly scope: string;
readonly parentScopes: string[];
readonly index: number;
/**
* -1 if not set. An or mask of `FontStyle` otherwise.
*/
readonly fontStyle: number;
readonly foreground: string;
readonly background: string;
constructor(
scope: string,
parentScopes: string[],
index: number,
fontStyle: number,
foreground: string,
background: string,
) {
this.scope = scope;
this.parentScopes = parentScopes;
this.index = index;
this.fontStyle = fontStyle;
this.foreground = foreground;
this.background = background;
}
}
/**
* Parse a raw theme into rules.
*/
export function parseTheme(source: IRawTheme): ParsedThemeRule[] {
if (!source) {
return [];
}
if (!source.settings || !Array.isArray(source.settings)) {
return [];
}
let settings = source.settings;
let result: ParsedThemeRule[] = [], resultLen = 0;
for (let i = 0, len = settings.length; i < len; i++) {
let entry = settings[i];
if (!entry.settings) {
continue;
}
let scopes: string[];
if (typeof entry.scope === 'string') {
scopes = entry.scope.split(',');
} else if (Array.isArray(entry.scope)) {
scopes = entry.scope;
} else {
scopes = [''];
}
let fontStyle: number = FontStyle.NotSet;
if (typeof entry.settings.fontStyle === 'string') {
fontStyle = FontStyle.None;
let segments = entry.settings.fontStyle.split(' ');
for (let j = 0, lenJ = segments.length; j < lenJ; j++) {
let segment = segments[j];
switch (segment) {
case 'italic':
fontStyle = fontStyle | FontStyle.Italic;
break;
case 'bold':
fontStyle = fontStyle | FontStyle.Bold;
break;
case 'underline':
fontStyle = fontStyle | FontStyle.Underline;
break;
}
}
}
let foreground: string = null;
if (typeof entry.settings.foreground === 'string') {
foreground = entry.settings.foreground;
}
let background: string = null;
if (typeof entry.settings.background === 'string') {
background = entry.settings.background;
}
for (let j = 0, lenJ = scopes.length; j < lenJ; j++) {
let _scope = scopes[j].trim();
let segments = _scope.split(' ');
let scope = segments[segments.length - 1];
let parentScopes: string[] = null;
if (segments.length > 1) {
parentScopes = segments.slice(0, segments.length - 1);
parentScopes.reverse();
}
result[resultLen++] = new ParsedThemeRule(
scope,
parentScopes,
i,
fontStyle,
foreground,
background
);
}
}
return result;
}
/**
* Resolve rules (i.e. inheritance).
*/
export function resolveParsedThemeRules(parsedThemeRules: ParsedThemeRule[]): ThemeTrieElement {
// Sort rules lexicographically, and then by index if necessary
parsedThemeRules.sort((a, b) => {
let r = strcmp(a.scope, b.scope);
if (r !== 0) {
return r;
}
r = strArrCmp(a.parentScopes, b.parentScopes);
if (r !== 0) {
return r;
}
return a.index - b.index;
});
let defaults: ParsedThemeRule;
if (parsedThemeRules.length >= 1 && parsedThemeRules[0].scope === '') {
let incomingDefaults = parsedThemeRules.shift();
let fontStyle = incomingDefaults.fontStyle;
let foreground = incomingDefaults.foreground;
let background = incomingDefaults.background;
if (fontStyle === FontStyle.NotSet) {
fontStyle = FontStyle.None;
}
if (foreground === null) {
foreground = '#000000';
}
if (background === null) {
background = '#ffffff';
}
defaults = new ParsedThemeRule('', null, incomingDefaults.index, fontStyle, foreground, background);
} else {
defaults = new ParsedThemeRule('', null, -1, FontStyle.None, '#000000', '#ffffff');
}
let root = new ThemeTrieElement(new ThemeTrieElementRule(null, defaults.fontStyle, defaults.foreground, defaults.background), []);
for (let i = 0, len = parsedThemeRules.length; i < len; i++) {
root.insert(parsedThemeRules[i]);
}
return root;
}
export class Theme {
private _root: ThemeTrieElement;
private _cache: { [scopeName: string]: ThemeTrieElementRule[]; }
constructor(source: IRawTheme) {
this._root = resolveParsedThemeRules(parseTheme(source));
this._cache = {};
}
public match(scopeName: string): ThemeTrieElementRule[] {
if (!this._cache.hasOwnProperty(scopeName)) {
this._cache[scopeName] = this._root.match(scopeName);
}
return this._cache[scopeName];
}
}
export function strcmp(a: string, b: string): number {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
export function strArrCmp(a: string[], b: string[]): number {
if (a === null && b === null) {
return 0;
}
if (!a) {
return -1;
}
if (!b) {
return 1;
}
let len1 = a.length;
let len2 = b.length;
if (len1 === len2) {
for (let i = 0; i < len1; i++) {
let res = strcmp(a[i], b[i]);
if (res !== 0) {
return res;
}
}
return 0;
}
return len1 - len2;
}
export class ThemeTrieElementRule {
_themeTrieElementRuleBrand: void;
parentScopes: string[];
fontStyle: number;
foreground: string;
background: string;
constructor(parentScopes: string[], fontStyle: number, foreground: string, background: string) {
this.parentScopes = parentScopes;
this.fontStyle = fontStyle;
this.foreground = foreground;
this.background = background;
}
public clone(): ThemeTrieElementRule {
return new ThemeTrieElementRule(this.parentScopes, this.fontStyle, this.foreground, this.background);
}
public acceptOverwrite(fontStyle: number, foreground: string, background: string): void {
if (fontStyle !== FontStyle.NotSet) {
this.fontStyle = fontStyle;
}
if (foreground !== null) {
this.foreground = foreground;
}
if (background !== null) {
this.background = background;
}
}
}
export interface ITrieChildrenMap {
[segment: string]: ThemeTrieElement;
}
export class ThemeTrieElement {
_themeTrieElementBrand: void;
private readonly _mainRule: ThemeTrieElementRule;
private readonly _rulesWithParentScopes: ThemeTrieElementRule[];
private readonly _children: ITrieChildrenMap;
constructor(
mainRule: ThemeTrieElementRule,
rulesWithParentScopes: ThemeTrieElementRule[] = [],
children: ITrieChildrenMap = {}
) {
this._mainRule = mainRule;
this._rulesWithParentScopes = rulesWithParentScopes;
this._children = children;
}
public match(scope: string): ThemeTrieElementRule[] {
if (scope === '') {
return [].concat(this._mainRule).concat(this._rulesWithParentScopes);
}
let dotIndex = scope.indexOf('.');
let head: string;
let tail: string;
if (dotIndex === -1) {
head = scope;
tail = '';
} else {
head = scope.substring(0, dotIndex);
tail = scope.substring(dotIndex + 1);
}
if (this._children.hasOwnProperty(head)) {
return this._children[head].match(tail);
}
return [].concat(this._mainRule).concat(this._rulesWithParentScopes);
}
public insert(rule: ParsedThemeRule): void {
this._doInsert(rule.scope, rule.parentScopes, rule.fontStyle, rule.foreground, rule.background);
}
private _doInsert(scope: string, parentScopes: string[], fontStyle: number, foreground: string, background: string): void {
if (scope === '') {
this._doInsertHere(parentScopes, fontStyle, foreground, background);
return;
}
let dotIndex = scope.indexOf('.');
let head: string;
let tail: string;
if (dotIndex === -1) {
head = scope;
tail = '';
} else {
head = scope.substring(0, dotIndex);
tail = scope.substring(dotIndex + 1);
}
let child: ThemeTrieElement;
if (this._children[head]) {
child = this._children[head];
} else {
child = new ThemeTrieElement(this._mainRule.clone());
this._children[head] = child;
}
// TODO: In the case that this element has `parentScopes`, should we generate one insert for each parentScope ?
child._doInsert(tail, parentScopes, fontStyle, foreground, background);
}
private _doInsertHere(parentScopes: string[], fontStyle: number, foreground: string, background: string): void {
if (parentScopes === null) {
// Merge into the main rule
this._mainRule.acceptOverwrite(fontStyle, foreground, background);
return;
}
// Try to merge into existing rule
for (let i = 0, len = this._rulesWithParentScopes.length; i < len; i++) {
let rule = this._rulesWithParentScopes[i];
if (strArrCmp(rule.parentScopes, parentScopes) === 0) {
// bingo! => we get to merge this into an existing one
rule.acceptOverwrite(fontStyle, foreground, background);
return;
}
}
// Must add a new rule
// Inherit from main rule
if (fontStyle === FontStyle.NotSet) {
fontStyle = this._mainRule.fontStyle;
}
if (foreground === null) {
foreground = this._mainRule.foreground;
}
if (background === null) {
background = this._mainRule.background;
}
this._rulesWithParentScopes.push(new ThemeTrieElementRule(parentScopes, fontStyle, foreground, background));
}
}