Introduce Theme
This commit is contained in:
Родитель
a33377e93d
Коммит
146168e6b5
292
release/main.js
292
release/main.js
|
@ -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, '../../');
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче