[clang-tools-extra] r369893 - [clangd] Added a colorizer to the vscode extension.

Johan Vikstrom via cfe-commits cfe-commits at lists.llvm.org
Mon Aug 26 04:36:11 PDT 2019


Author: jvikstrom
Date: Mon Aug 26 04:36:11 2019
New Revision: 369893

URL: http://llvm.org/viewvc/llvm-project?rev=369893&view=rev
Log:
[clangd] Added a colorizer to the vscode extension.

Summary:
Adds the main colorizer component. It colorizes every time clangd sends a publishSemanticHighlighting notification.
Every time it colorizes it does a full recolorization (removes all decorations from the editor and applies new ones). The reason for this is that all ranges for the same scope share a TextEditorDecorationType. This is due to TextEditorDecorationTypes being very expensive to create. The prototype used one DecorationType per range but that ran into very big performance problems (it took >100 ms to apply 600 lines of highlightings which froze the editor).

This version does not share the problem of being extremly slow, but there is probably potential to optimize it even more.

No document/texteditor lifecycle management code in this CL, that will come in the next one.

Reviewers: hokein, ilya-biryukov

Subscribers: MaskRay, jkorous, arphaman, kadircet, cfe-commits

Tags: #clang

Differential Revision: https://reviews.llvm.org/D66219

Modified:
    clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts
    clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
    clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts

Modified: clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts?rev=369893&r1=369892&r2=369893&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts (original)
+++ clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts Mon Aug 26 04:36:11 2019
@@ -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
 
   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(

Modified: clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts?rev=369893&r1=369892&r2=369893&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts (original)
+++ clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/semantic-highlighting.ts Mon Aug 26 04:36:11 2019
@@ -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
   // 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
   }
 
   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
     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: str
   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.

Modified: clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts
URL: http://llvm.org/viewvc/llvm-project/clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts?rev=369893&r1=369892&r2=369893&view=diff
==============================================================================
--- clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts (original)
+++ clang-tools-extra/trunk/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts Mon Aug 26 04:36:11 2019
@@ -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) ]));
+  });
 });




More information about the cfe-commits mailing list