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/TextMate.ts b/clang-tools-extra/clangd/clients/clangd-vscode/src/TextMate.ts new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/TextMate.ts @@ -0,0 +1,102 @@ +import * as fs from 'fs'; +import * as jsonc from "jsonc-parser"; +import * as path from 'path'; +import * as vscode from 'vscode'; + +export namespace SemanticHighlighting { +interface ScopeColorRule { + scope: string, textColor: string, +} + +async function getNamedThemeScopeColors(themeName: string): + Promise { + const contents = await getFullNamedTheme(themeName); + return themesToScopeColors(contents); +} + +/** + * Convert an array of TextMate theme contents into the an array of all scopes' + * colors. If there are duplicate entries of a scope only the latest occurence + * of the scope is kept. + * @param themeContents An entry in this array is either the theme's (or an + * included theme's) TextMate definition. + */ +export function themesToScopeColors(themeContents: any[]): ScopeColorRule[] { + const ruleMap: Map = new Map(); + themeContents.forEach((content) => { + if (!content.tokenColors) + return; + content.tokenColors.forEach((rule: any) => { + if (!rule.scope || !rule.settings || !rule.settings.foreground) + return; + const textColor = rule.settings.foreground; + if (rule.scope instanceof Array) { + rule.scope.forEach((s: string) => ruleMap.set(s, textColor)); + return; + } + ruleMap.set(rule.scope, textColor); + }); + }); + + return Array.from(ruleMap.entries()) + .map(([ scope, textColor ]) => ({scope, textColor})); +} + +// Gets a TextMate theme by its name and all its included themes. +async function getFullNamedTheme(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 recursiveGetTextMateGrammarPath( + path.join(extension.extensionPath, extensionInfo.path)); +} + +// TM grammars can include other TM grammars, this function recursively gets all +// of them. +async function recursiveGetTextMateGrammarPath(fullPath: string): + Promise { + // 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); + // FIXME: Add support for themes written as .thTheme. + const parsed = jsonc.parse(contents); + if (parsed.include) + // Get all includes and merge into a flat list of parsed json. + return [ + ...(await recursiveGetTextMateGrammarPath( + path.join(path.dirname(fullPath), parsed.include))), + parsed + ]; + return [ parsed ]; + } catch (err) { + console.warn('Could not open file: ' + fullPath + ', error: ', err); + } + + return []; +} + +function readFileText(path: string): Promise { + return new Promise((res, rej) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err) { + rej(err); + } + res(data); + }); + }); +} +} diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts b/clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts @@ -0,0 +1,27 @@ +/** The module 'assert' provides assertion methods from node */ +import * as assert from 'assert'; + +import {SemanticHighlighting} from '../src/TextMate'; + +suite('TextMate Tests', () => { + test('Parses arrays of textmate themes.', () => { + const themes = [ + {}, { + tokenColors : [ + {}, {scope : 'a'}, + {scope : [ 'b', 'c', 'd' ], settings : {background : '#000'}}, + {scope : 'a', settings : {foreground : '#000'}}, + {scope : [ 'a', 'b', 'c' ], settings : {foreground : '#fff'}} + ] + }, + {tokenColors : [ {scope : 'b', settings : {foreground : '#ff0000'}} ]} + ]; + const scopeColorRules = SemanticHighlighting.themesToScopeColors(themes); + const getScopeRule = (scope: string) => + scopeColorRules.find((v) => v.scope === scope); + assert.equal(scopeColorRules.length, 3); + assert.deepEqual(getScopeRule('a'), {scope : 'a', textColor : '#fff'}); + assert.deepEqual(getScopeRule('b'), {scope : 'b', textColor : '#ff0000'}); + assert.deepEqual(getScopeRule('c'), {scope : 'c', textColor : '#fff'}); + }); +}); \ No newline at end of file