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 --- a/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts +++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts @@ -47,6 +47,8 @@ // The TextMate scope lookup table. A token with scope index i has the scopes // on index i in the lookup table. scopeLookupTable: string[][]; + // The rules for the scopes given the current theme. + scopeRules: ScopeRules; fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) { // Extend the ClientCapabilities type and add semantic highlighting // capability to the object. @@ -58,6 +60,18 @@ }; } + async loadCurrentTheme() { + const name = + vscode.workspace.getConfiguration('workbench').get('colorTheme'); + if (typeof name != 'string') { + console.warn('The current theme name is not a string, is:' + + (typeof name) + ', value: ', + name); + return; + } + this.scopeRules = await loadTheme(name, this.scopeLookupTable); + } + initialize(capabilities: vscodelc.ServerCapabilities, documentSelector: vscodelc.DocumentSelector|undefined) { // The semantic highlighting capability information is in the capabilities @@ -68,6 +82,7 @@ if (!serverCapabilities.semanticHighlighting) return; this.scopeLookupTable = serverCapabilities.semanticHighlighting.scopes; + this.loadCurrentTheme(); } handleNotification(params: SemanticHighlightingParams) {} @@ -101,8 +116,55 @@ foreground: string; } +export class ScopeRules { + // The TextMate scopes that should be mapped to a color. + private scopeLookupTable: string[][]; + // Contains the current best matching scope for the scope at the corresponding + // index. + private scopeRules: TokenColorRule[]; + // Each list of scopes can only have one rule matching. The actual scope that + // matches for each index should be as close to zero as possible. + private currentScopeRuleIdx: number[]; + constructor(scopeLookupTable: string[][]) { + this.scopeLookupTable = scopeLookupTable; + this.currentScopeRuleIdx = + this.scopeLookupTable.map(() => Number.POSITIVE_INFINITY); + this.scopeRules = + this.scopeLookupTable.map(() => ({scope : '', foreground : '#000'})); + } + + // Associates rule to the scopeLookupTable if possible. For a rule to be + // associated to an array of scopes it must be a prefix of any of the scopes. + // Be the earliest match in the array of scopes and be the most specific + // match. + addRule(rule: TokenColorRule) { + this.scopeLookupTable.forEach((scopes, i) => { + const lookupIdx = scopes.findIndex( + (scope) => scope.substr(0, rule.scope.length) == rule.scope); + if (lookupIdx == -1) + // Could not find a match for this rule. + return; + if (lookupIdx > this.currentScopeRuleIdx[i]) + // There is already a rule for this scope that has a higher precedence + // than this match. + return; + if (rule.scope.length > this.scopeRules[i].scope.length || + lookupIdx < this.currentScopeRuleIdx[i]) { + // This rule is more specific than the last rule or has a higher + // precedence. + this.scopeRules[i] = rule; + this.currentScopeRuleIdx[i] = lookupIdx; + } + }); + } + + // Get the rule at idx. + getRule(idx: number) { return this.scopeRules[idx]; } +} + // Get all token color rules provided by the theme. -function loadTheme(themeName: string): Promise { +async function loadTheme(themeName: string, + scopes: string[][]): Promise { const extension = vscode.extensions.all.find((extension: vscode.Extension) => { const contribs = extension.packageJSON.contributes; @@ -118,7 +180,11 @@ const themeInfo = extension.packageJSON.contributes.themes.find( (theme: any) => theme.id === themeName || theme.label === themeName); - return parseThemeFile(path.join(extension.extensionPath, themeInfo.path)); + const tokenScopeRules = + await parseThemeFile(path.join(extension.extensionPath, themeInfo.path)); + const scopeRules = new ScopeRules(scopes); + tokenScopeRules.forEach((rule) => scopeRules.addRule(rule)); + return scopeRules; } /** 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 --- 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 @@ -1,13 +1,13 @@ import * as assert from 'assert'; import * as path from 'path'; -import * as TM from '../src/semantic-highlighting'; +import * as SM from '../src/semantic-highlighting'; suite('SemanticHighlighting Tests', () => { test('Parses arrays of textmate themes.', async () => { const themePath = path.join(__dirname, '../../test/assets/includeTheme.jsonc'); - const scopeColorRules = await TM.parseThemeFile(themePath); + const scopeColorRules = await SM.parseThemeFile(themePath); const getScopeRule = (scope: string) => scopeColorRules.find((v) => v.scope === scope); assert.equal(scopeColorRules.length, 3); @@ -33,6 +33,21 @@ ] ]; testCases.forEach((testCase, i) => assert.deepEqual( - TM.decodeTokens(testCase), expected[i])); + SM.decodeTokens(testCase), expected[i])); + }); + test('ScopeRules overrides for more specific themes', () => { + const scopes = [ [ 'a.b', 'a.b' ], [ 'a', 'a.b', 'c.b.a' ], [ 'c.b.a' ] ]; + const rules = [ + {scope : 'c.b', foreground : '1'}, + {scope : 'a', foreground : '2'}, + {scope : 'a.b', foreground : '3'}, + {scope : 'a', foreground : '4'}, + {scope : 'c.b.a', foreground : '5'}, + ]; + const tm = new SM.ScopeRules(scopes); + rules.forEach((r) => tm.addRule(r)); + assert.deepEqual(tm.getRule(0), rules[2]); + assert.deepEqual(tm.getRule(1), rules[1]); + assert.deepEqual(tm.getRule(2), rules[4]); }); });