Index: clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts =================================================================== --- clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts +++ clang-tools-extra/trunk/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 current theme. + themeRuleMatcher: ThemeRuleMatcher; fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) { // Extend the ClientCapabilities type and add semantic highlighting // capability to the object. @@ -58,6 +60,12 @@ }; } + async loadCurrentTheme() { + this.themeRuleMatcher = new ThemeRuleMatcher( + await loadTheme(vscode.workspace.getConfiguration('workbench') + .get('colorTheme'))); + } + initialize(capabilities: vscodelc.ServerCapabilities, documentSelector: vscodelc.DocumentSelector|undefined) { // The semantic highlighting capability information is in the capabilities @@ -68,6 +76,7 @@ if (!serverCapabilities.semanticHighlighting) return; this.scopeLookupTable = serverCapabilities.semanticHighlighting.scopes; + this.loadCurrentTheme(); } handleNotification(params: SemanticHighlightingParams) {} @@ -101,6 +110,39 @@ foreground: string; } +export class ThemeRuleMatcher { + // The rules for the theme. + private themeRules: TokenColorRule[]; + // A cache for the getBestThemeRule function. + private bestRuleCache: Map = new Map(); + constructor(rules: TokenColorRule[]) { this.themeRules = rules; } + // Returns the best rule for a scope. + getBestThemeRule(scope: string): TokenColorRule { + if (this.bestRuleCache.has(scope)) + return this.bestRuleCache.get(scope); + let bestRule: TokenColorRule = {scope : '', foreground : ''}; + this.themeRules.forEach((rule) => { + // The best rule for a scope is the rule that is the longest prefix of the + // scope (unless a perfect match exists in which case the perfect match is + // the best). If a rule is not a prefix and we tried to match with longest + // common prefix instead variables would be highlighted as `less` + // variables when using Light+ (as variable.other would be matched against + // variable.other.less in this case). Doing common prefix matching also + // means we could match variable.cpp to variable.css if variable.css + // occurs before variable in themeRules. + // FIXME: This is not defined in the TextMate standard (it is explicitly + // undefined, https://macromates.com/manual/en/scope_selectors). Might + // want to rank some other way. + if (scope.startsWith(rule.scope) && + rule.scope.length > bestRule.scope.length) + // This rule matches and is more specific than the old rule. + bestRule = rule; + }); + this.bestRuleCache.set(scope, bestRule); + return bestRule; + } +} + // Get all token color rules provided by the theme. function loadTheme(themeName: string): Promise { const extension = Index: clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts =================================================================== --- clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts +++ clang-tools-extra/trunk/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,28 @@ ] ]; testCases.forEach((testCase, i) => assert.deepEqual( - TM.decodeTokens(testCase), expected[i])); + SM.decodeTokens(testCase), expected[i])); + }); + test('ScopeRules overrides for more specific themes', () => { + const rules = [ + {scope : 'variable.other.css', foreground : '1'}, + {scope : 'variable.other', foreground : '2'}, + {scope : 'storage', foreground : '3'}, + {scope : 'storage.static', foreground : '4'}, + {scope : 'storage', foreground : '5'}, + {scope : 'variable.other.parameter', foreground : '6'}, + ]; + const tm = new SM.ThemeRuleMatcher(rules); + assert.deepEqual(tm.getBestThemeRule('variable.other.cpp').scope, + 'variable.other'); + assert.deepEqual(tm.getBestThemeRule('storage.static').scope, + 'storage.static'); + assert.deepEqual( + tm.getBestThemeRule('storage'), + rules[2]); // Match the first element if there are duplicates. + assert.deepEqual(tm.getBestThemeRule('variable.other.parameter').scope, + 'variable.other.parameter'); + assert.deepEqual(tm.getBestThemeRule('variable.other.parameter.cpp').scope, + 'variable.other.parameter'); }); });