This revision was automatically updated to reflect the committed changes.
jvikstrom marked 10 inline comments as done.
Closed by commit rL369893: [clangd] Added a colorizer to the vscode extension. 
(authored by jvikstrom, committed by ).
Herald added a project: LLVM.
Herald added a subscriber: llvm-commits.

Changed prior to commit:
  https://reviews.llvm.org/D66219?vs=217099&id=217115#toc

Repository:
  rL LLVM

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D66219/new/

https://reviews.llvm.org/D66219

Files:
  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

Index: clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts
===================================================================
--- clang-tools-extra/trunk/clangd/clients/clangd-vscode/src/extension.ts
+++ clang-tools-extra/trunk/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 @@
 
   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(
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
@@ -34,6 +34,13 @@
   // 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 @@
   // 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 @@
   }
 
   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 @@
     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 @@
   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.
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,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 @@
         {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 @@
       {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 @@
     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) ]));
+  });
 });
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to