summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts12
-rw-r--r--clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts118
-rw-r--r--clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts109
3 files changed, 230 insertions, 9 deletions
diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts b/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
index a082e100e43..dc187ee48d4 100644
--- a/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
+++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import * as vscodelc from 'vscode-languageclient';
+import * as semanticHighlighting from './semantic-highlighting';
/**
* Method to get workspace configuration option
@@ -108,6 +109,17 @@ export function activate(context: vscode.ExtensionContext) {
const clangdClient = new ClangdLanguageClient('Clang Language Server',
serverOptions, clientOptions);
+ const semanticHighlightingFeature =
+ new semanticHighlighting.SemanticHighlightingFeature();
+ clangdClient.registerFeature(semanticHighlightingFeature);
+ // The notification handler must be registered after the client is ready or
+ // the client will crash.
+ clangdClient.onReady().then(
+ () => clangdClient.onNotification(
+ semanticHighlighting.NotificationType,
+ semanticHighlightingFeature.handleNotification.bind(
+ semanticHighlightingFeature)));
+
console.log('Clang Language Server is now active!');
context.subscriptions.push(clangdClient.start());
context.subscriptions.push(vscode.commands.registerCommand(
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
index a403db2c75b..21bce9cac45 100644
--- a/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
+++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
@@ -34,6 +34,13 @@ interface SemanticHighlightingToken {
// The TextMate scope index to the clangd scope lookup table.
scopeIndex: number;
}
+// A line of decoded highlightings from the data clangd sent.
+export interface SemanticHighlightingLine {
+ // The zero-based line position in the text document.
+ line: number;
+ // All SemanticHighlightingTokens on the line.
+ tokens: SemanticHighlightingToken[];
+}
// Language server push notification providing the semantic highlighting
// information for a text document.
@@ -47,8 +54,8 @@ export class SemanticHighlightingFeature implements vscodelc.StaticFeature {
// 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;
+ // The object that applies the highlightings clangd sends.
+ highlighter: Highlighter;
fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {
// Extend the ClientCapabilities type and add semantic highlighting
// capability to the object.
@@ -61,9 +68,10 @@ export class SemanticHighlightingFeature implements vscodelc.StaticFeature {
}
async loadCurrentTheme() {
- this.themeRuleMatcher = new ThemeRuleMatcher(
+ const themeRuleMatcher = new ThemeRuleMatcher(
await loadTheme(vscode.workspace.getConfiguration('workbench')
.get<string>('colorTheme')));
+ this.highlighter.initialize(themeRuleMatcher);
}
initialize(capabilities: vscodelc.ServerCapabilities,
@@ -76,10 +84,18 @@ export class SemanticHighlightingFeature implements vscodelc.StaticFeature {
if (!serverCapabilities.semanticHighlighting)
return;
this.scopeLookupTable = serverCapabilities.semanticHighlighting.scopes;
+ // Important that highlighter is created before the theme is loading as
+ // otherwise it could try to update the themeRuleMatcher without the
+ // highlighter being created.
+ this.highlighter = new Highlighter(this.scopeLookupTable);
this.loadCurrentTheme();
}
- handleNotification(params: SemanticHighlightingParams) {}
+ handleNotification(params: SemanticHighlightingParams) {
+ const lines: SemanticHighlightingLine[] = params.lines.map(
+ (line) => ({line : line.line, tokens : decodeTokens(line.tokens)}));
+ this.highlighter.highlight(params.textDocument.uri, lines);
+ }
}
// Converts a string of base64 encoded tokens into the corresponding array of
@@ -101,6 +117,100 @@ export function decodeTokens(tokens: string): SemanticHighlightingToken[] {
return retTokens;
}
+// The main class responsible for processing of highlightings that clangd
+// sends.
+export class Highlighter {
+ // Maps uris with currently open TextDocuments to the current highlightings.
+ private files: Map<string, Map<number, SemanticHighlightingLine>> = new Map();
+ // DecorationTypes for the current theme that are used when highlighting. A
+ // SemanticHighlightingToken with scopeIndex i should have the decoration at
+ // index i in this list.
+ private decorationTypes: vscode.TextEditorDecorationType[] = [];
+ // The clangd TextMate scope lookup table.
+ private scopeLookupTable: string[][];
+ constructor(scopeLookupTable: string[][]) {
+ this.scopeLookupTable = scopeLookupTable;
+ }
+ // This function must be called at least once or no highlightings will be
+ // done. Sets the theme that is used when highlighting. Also triggers a
+ // recolorization for all current highlighters. Should be called whenever the
+ // theme changes and has been loaded. Should also be called when the first
+ // theme is loaded.
+ public initialize(themeRuleMatcher: ThemeRuleMatcher) {
+ this.decorationTypes.forEach((t) => t.dispose());
+ this.decorationTypes = this.scopeLookupTable.map((scopes) => {
+ const options: vscode.DecorationRenderOptions = {
+ // If there exists no rule for this scope the matcher returns an empty
+ // color. That's ok because vscode does not do anything when applying
+ // empty decorations.
+ color : themeRuleMatcher.getBestThemeRule(scopes[0]).foreground,
+ // If the rangeBehavior is set to Open in any direction the
+ // highlighting becomes weird in certain cases.
+ rangeBehavior : vscode.DecorationRangeBehavior.ClosedClosed,
+ };
+ return vscode.window.createTextEditorDecorationType(options);
+ });
+ this.getVisibleTextEditorUris().forEach((fileUri) => {
+ // A TextEditor might not be a cpp file. So we must check we have
+ // highlightings for the file before applying them.
+ if (this.files.has(fileUri))
+ this.applyHighlights(fileUri);
+ })
+ }
+
+ // Adds incremental highlightings to the current highlightings for the file
+ // with fileUri. Also applies the highlightings to any associated
+ // TextEditor(s).
+ public highlight(fileUri: string,
+ highlightingLines: SemanticHighlightingLine[]) {
+ if (!this.files.has(fileUri)) {
+ this.files.set(fileUri, new Map());
+ }
+ const fileHighlightings = this.files.get(fileUri);
+ highlightingLines.forEach((line) => fileHighlightings.set(line.line, line));
+ this.applyHighlights(fileUri);
+ }
+
+ // Gets the uris as strings for the currently visible text editors.
+ protected getVisibleTextEditorUris(): string[] {
+ return vscode.window.visibleTextEditors.map((e) =>
+ e.document.uri.toString());
+ }
+
+ // Returns the ranges that should be used when decorating. Index i in the
+ // range array has the decoration type at index i of this.decorationTypes.
+ protected getDecorationRanges(fileUri: string): vscode.Range[][] {
+ const lines: SemanticHighlightingLine[] =
+ Array.from(this.files.get(fileUri).values());
+ const decorations: vscode.Range[][] = this.decorationTypes.map(() => []);
+ lines.forEach((line) => {
+ line.tokens.forEach((token) => {
+ decorations[token.scopeIndex].push(new vscode.Range(
+ new vscode.Position(line.line, token.character),
+ new vscode.Position(line.line, token.character + token.length)));
+ });
+ });
+ return decorations;
+ }
+
+ // Applies all the highlightings currently stored for a file with fileUri.
+ protected applyHighlights(fileUri: string) {
+ if (!this.decorationTypes.length)
+ // Can't apply any decorations when there is no theme loaded.
+ return;
+ // This must always do a full re-highlighting due to the fact that
+ // TextEditorDecorationType are very expensive to create (which makes
+ // incremental updates infeasible). For this reason one
+ // TextEditorDecorationType is used per scope.
+ const ranges = this.getDecorationRanges(fileUri);
+ vscode.window.visibleTextEditors.forEach((e) => {
+ if (e.document.uri.toString() !== fileUri)
+ return;
+ this.decorationTypes.forEach((d, i) => e.setDecorations(d, ranges[i]));
+ });
+ }
+}
+
// A rule for how to color TextMate scopes.
interface TokenColorRule {
// A TextMate scope that specifies the context of the token, e.g.
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
index a8b39306eb4..eab555d4587 100644
--- 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,15 @@
import * as assert from 'assert';
import * as path from 'path';
+import * as vscode from 'vscode';
-import * as SM from '../src/semantic-highlighting';
+import * as semanticHighlighting 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 SM.parseThemeFile(themePath);
+ const scopeColorRules =
+ await semanticHighlighting.parseThemeFile(themePath);
const getScopeRule = (scope: string) =>
scopeColorRules.find((v) => v.scope === scope);
assert.equal(scopeColorRules.length, 3);
@@ -32,8 +34,9 @@ suite('SemanticHighlighting Tests', () => {
{character : 10, scopeIndex : 0, length : 1}
]
];
- testCases.forEach((testCase, i) => assert.deepEqual(
- SM.decodeTokens(testCase), expected[i]));
+ testCases.forEach(
+ (testCase, i) => assert.deepEqual(
+ semanticHighlighting.decodeTokens(testCase), expected[i]));
});
test('ScopeRules overrides for more specific themes', () => {
const rules = [
@@ -44,7 +47,7 @@ suite('SemanticHighlighting Tests', () => {
{scope : 'storage', foreground : '5'},
{scope : 'variable.other.parameter', foreground : '6'},
];
- const tm = new SM.ThemeRuleMatcher(rules);
+ const tm = new semanticHighlighting.ThemeRuleMatcher(rules);
assert.deepEqual(tm.getBestThemeRule('variable.other.cpp').scope,
'variable.other');
assert.deepEqual(tm.getBestThemeRule('storage.static').scope,
@@ -57,4 +60,100 @@ suite('SemanticHighlighting Tests', () => {
assert.deepEqual(tm.getBestThemeRule('variable.other.parameter.cpp').scope,
'variable.other.parameter');
});
+ test('Colorizer groups decorations correctly', async () => {
+ const scopeTable = [
+ [ 'variable' ], [ 'entity.type.function' ],
+ [ 'entity.type.function.method' ]
+ ];
+ // Create the scope source ranges the highlightings should be highlighted
+ // at. Assumes the scopes used are the ones in the "scopeTable" variable.
+ const createHighlightingScopeRanges =
+ (highlightingLines:
+ semanticHighlighting.SemanticHighlightingLine[]) => {
+ // Initialize the scope ranges list to the correct size. Otherwise
+ // scopes that don't have any highlightings are missed.
+ let scopeRanges: vscode.Range[][] = scopeTable.map(() => []);
+ highlightingLines.forEach((line) => {
+ line.tokens.forEach((token) => {
+ scopeRanges[token.scopeIndex].push(new vscode.Range(
+ new vscode.Position(line.line, token.character),
+ new vscode.Position(line.line,
+ token.character + token.length)));
+ });
+ });
+ return scopeRanges;
+ };
+
+ class MockHighlighter extends semanticHighlighting.Highlighter {
+ applicationUriHistory: string[] = [];
+ // Override to make the highlighting calls accessible to the test. Also
+ // makes the test not depend on visible text editors.
+ applyHighlights(fileUri: string) {
+ this.applicationUriHistory.push(fileUri);
+ }
+ // Override to make it accessible from the test.
+ getDecorationRanges(fileUri: string) {
+ return super.getDecorationRanges(fileUri);
+ }
+ // Override to make tests not depend on visible text editors.
+ getVisibleTextEditorUris() { return [ 'file1', 'file2' ]; }
+ }
+ const highlighter = new MockHighlighter(scopeTable);
+ const tm = new semanticHighlighting.ThemeRuleMatcher([
+ {scope : 'variable', foreground : '1'},
+ {scope : 'entity.type', foreground : '2'},
+ ]);
+ // Recolorizes when initialized.
+ highlighter.highlight('file1', []);
+ assert.deepEqual(highlighter.applicationUriHistory, [ 'file1' ]);
+ highlighter.initialize(tm);
+ assert.deepEqual(highlighter.applicationUriHistory, [ 'file1', 'file1' ]);
+ // Groups decorations into the scopes used.
+ let highlightingsInLine: semanticHighlighting.SemanticHighlightingLine[] = [
+ {
+ line : 1,
+ tokens : [
+ {character : 1, length : 2, scopeIndex : 1},
+ {character : 10, length : 2, scopeIndex : 2},
+ ]
+ },
+ {
+ line : 2,
+ tokens : [
+ {character : 3, length : 2, scopeIndex : 1},
+ {character : 6, length : 2, scopeIndex : 1},
+ {character : 8, length : 2, scopeIndex : 2},
+ ]
+ },
+ ];
+
+ highlighter.highlight('file1', highlightingsInLine);
+ assert.deepEqual(highlighter.applicationUriHistory,
+ [ 'file1', 'file1', 'file1' ]);
+ assert.deepEqual(highlighter.getDecorationRanges('file1'),
+ createHighlightingScopeRanges(highlightingsInLine));
+ // Keeps state separate between files.
+ const highlightingsInLine1:
+ semanticHighlighting.SemanticHighlightingLine = {
+ line : 1,
+ tokens : [
+ {character : 2, length : 1, scopeIndex : 0},
+ ]
+ };
+ highlighter.highlight('file2', [ highlightingsInLine1 ]);
+ assert.deepEqual(highlighter.applicationUriHistory,
+ [ 'file1', 'file1', 'file1', 'file2' ]);
+ assert.deepEqual(highlighter.getDecorationRanges('file2'),
+ createHighlightingScopeRanges([ highlightingsInLine1 ]));
+ // Does full colorizations.
+ highlighter.highlight('file1', [ highlightingsInLine1 ]);
+ assert.deepEqual(highlighter.applicationUriHistory,
+ [ 'file1', 'file1', 'file1', 'file2', 'file1' ]);
+ // After the incremental update to line 1, the old highlightings at line 1
+ // will no longer exist in the array.
+ assert.deepEqual(
+ highlighter.getDecorationRanges('file1'),
+ createHighlightingScopeRanges(
+ [ highlightingsInLine1, ...highlightingsInLine.slice(1) ]));
+ });
});
OpenPOWER on IntegriCloud