diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/package.json b/clang-tools-extra/clangd/clients/clangd-vscode/package.json --- a/clang-tools-extra/clangd/clients/clangd-vscode/package.json +++ b/clang-tools-extra/clangd/clients/clangd-vscode/package.json @@ -36,14 +36,15 @@ "test": "node ./node_modules/vscode/bin/test" }, "dependencies": { + "jsonc-parser": "^2.1.0", "vscode-languageclient": "^5.3.0-next.6", "vscode-languageserver": "^5.3.0-next.6" }, "devDependencies": { "@types/mocha": "^2.2.32", "@types/node": "^6.0.40", - "mocha": "^5.2.0", "clang-format": "1.2.4", + "mocha": "^5.2.0", "typescript": "^2.0.3", "vscode": "^1.1.0" }, diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts b/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts @@ -0,0 +1,97 @@ +import * as fs from 'fs'; +import * as jsonc from "jsonc-parser"; +import * as path from 'path'; +import * as vscode from 'vscode'; + +interface TokenColorRule { + scope: string, textColor: string, +} + +// Gets a TextMate theme and all its included themes by its name. +async function loadTheme(themeName: string): Promise { + const extension = + vscode.extensions.all.find((extension: vscode.Extension) => { + const contribs = extension.packageJSON.contributes; + if (!contribs || !contribs.themes) + return false; + return contribs.themes.some((theme: any) => theme.id === themeName || + theme.label === themeName); + }); + + if (!extension) { + return Promise.reject('Could not find a theme with name: ' + themeName); + } + + const extensionInfo = extension.packageJSON.contributes.themes.find( + (theme: any) => theme.id === themeName || theme.label === themeName); + return parseThemeFile(path.join(extension.extensionPath, extensionInfo.path)); +} + +/** + * Recursively parse the TM grammar at fullPath. If there are multiple TM scopes + * of the same name in the include chain only the earliest entry of the scope is + * saved. + * @param fullPath The absolute path to the theme. + * @param scopeSet A set containing the name of the scopes that have already + * been set. + */ +export async function parseThemeFile(fullPath: string, scopeSet?: Set): + Promise { + if (!scopeSet) + scopeSet = new Set(); + // FIXME: Add support for themes written as .thTheme. + if (path.extname(fullPath) === '.tmTheme') + return []; + // If there is an error opening a file, the TM files that were correctly found + // and parsed further up the chain should be returned. Otherwise there will be + // no highlightings at all. + try { + const contents = await readFileText(fullPath); + const parsed = jsonc.parse(contents); + const rules: TokenColorRule[] = []; + // To make sure it does not crash if tokenColors is undefined. + if (!parsed.tokenColors) + parsed.tokenColors = []; + parsed.tokenColors.forEach((rule: any) => { + if (!rule.scope || !rule.settings || !rule.settings.foreground) + return; + const textColor = rule.settings.foreground; + // Scopes that were found further up the TM chain should not be + // overwritten. + const addColor = (scope: string) => { + if (scopeSet.has(scope)) + return; + rules.push({scope, textColor}); + scopeSet.add(scope); + }; + if (rule.scope instanceof Array) { + return rule.scope.forEach((s: string) => addColor(s)); + } + addColor(rule.scope); + }); + + if (parsed.include) + // Get all includes and merge into a flat list of parsed json. + return [ + ...(await parseThemeFile( + path.join(path.dirname(fullPath), parsed.include), scopeSet)), + ...rules + ]; + return rules; + } catch (err) { + console.warn('Could not open file: ' + fullPath + ', error: ', err); + } + + return []; +} + +function readFileText(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err) { + return reject(err); + } + resolve(data); + }); + }); +} diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/firstIncludedTheme.jsonc b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/firstIncludedTheme.jsonc new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/firstIncludedTheme.jsonc @@ -0,0 +1,18 @@ +{ + // Some comment + "include": "secondIncludedTheme.jsonc", + "tokenColors": [ + { + "scope": "a", + "settings": { + "foreground": "#fff" + } + }, + { + "scope": ["a", "b"], + "settings": { + "foreground": "#000" + } + } + ] +} diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/secondIncludedTheme.jsonc b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/secondIncludedTheme.jsonc new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/secondIncludedTheme.jsonc @@ -0,0 +1,11 @@ +{ + "include": "tmThemeInclude.tmTheme", + "tokenColors": [ + { + "scope": "a", + "settings": { + "foreground": "#ff0000" + } + } + ] +} diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/testTheme.jsonc b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/testTheme.jsonc new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/testTheme.jsonc @@ -0,0 +1,16 @@ +{ + // Some comment + "include": "firstIncludedTheme.jsonc", + "name": "TestTheme", + "type": "dark", + "colors": { + "dropdown.background": "#fff" + }, + "tokenColors": [ + { + "settings": { + "foreground": "#fff" + } + } + ] +} diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/tmThemeInclude.tmTheme b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/tmThemeInclude.tmTheme new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/assets/tmThemeInclude.tmTheme @@ -0,0 +1,5 @@ + + + + + diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts b/clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts @@ -0,0 +1,17 @@ +/** The module 'assert' provides assertion methods from node */ +import * as assert from 'assert'; +import * as path from 'path'; + +import * as TM from '../src/semantic-highlighting'; + +suite('TextMate Tests', () => { + test('Parses arrays of textmate themes.', async () => { + const themePath = path.join(__dirname, '../../test/assets/testTheme.jsonc'); + const scopeColorRules = await TM.parseThemeFile(themePath); + const getScopeRule = (scope: string) => + scopeColorRules.find((v) => v.scope === scope); + assert.equal(scopeColorRules.length, 2); + assert.deepEqual(getScopeRule('a'), {scope : 'a', textColor : '#fff'}); + assert.deepEqual(getScopeRule('b'), {scope : 'b', textColor : '#000'}); + }); +}); \ No newline at end of file