jvikstrom created this revision.
jvikstrom added reviewers: hokein, ilya-biryukov.
Herald added subscribers: cfe-commits, kadircet, arphaman, jkorous, MaskRay.
Herald added a project: clang.

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.

The Colorizer uses a FileColorizer interface to make it possible to mock out 
all the code that applies colorizations to a specific editor so that we can 
test it.

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


Repository:
  rG LLVM Github Monorepo

https://reviews.llvm.org/D66219

Files:
  clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
  
clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts

Index: clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts
===================================================================
--- clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts
+++ clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts
@@ -1,5 +1,6 @@
 import * as assert from 'assert';
 import * as path from 'path';
+import * as vscode from 'vscode';
 
 import * as SM from '../src/semantic-highlighting';
 
@@ -57,4 +58,73 @@
     assert.deepEqual(tm.getBestThemeRule('variable.other.parameter.cpp').scope,
                      'variable.other.parameter');
   });
+  test('Colorizer groups decorations correctly', () => {
+    const colorizations: {uri: string, decorations: any[]}[] = [];
+    // Mock of a colorizer that saves the parameters in the colorizations array.
+    class MockFileColorizer {
+      public colorize(uri: string, decorationRangePairs: any[]) {
+        colorizations.push({uri : uri, decorations : decorationRangePairs});
+      }
+      public dispose() {}
+    }
+    // Helper for creating a vscode Range.
+    const createRange = (line: number, startCharacter: number,
+                         length: number) =>
+        new vscode.Range(new vscode.Position(line, startCharacter),
+                         new vscode.Position(line, startCharacter + length));
+    const scopeTable = [
+      [ 'variable' ], [ 'entity.type.function' ],
+      [ 'entity.type.function.method' ]
+    ];
+    const rules = [
+      {scope : 'variable', foreground : '1'},
+      {scope : 'entity.type', foreground : '2'},
+    ];
+    const tm = new SM.ThemeRuleMatcher(rules);
+    const colorizer =
+        new SM.Colorizer<MockFileColorizer>(MockFileColorizer, scopeTable);
+    // No colorization if themeRuleMatcher has not been set.
+    colorizer.setFileLines('a', []);
+    assert.deepEqual(colorizations, []);
+    colorizer.updateThemeRuleMatcher(tm);
+    assert.deepEqual(colorizations, [ {decorations : [], uri : 'a'} ]);
+    // Groups decorations into the scopes used.
+    let line = [
+      {character : 1, length : 2, scopeIndex : 1},
+      {character : 5, length : 2, scopeIndex : 1},
+      {character : 10, length : 2, scopeIndex : 2}
+    ];
+    colorizer.setFileLines(
+        'a', [ {line : 1, tokens : line}, {line : 2, tokens : line} ]);
+    assert.equal(colorizations[1].uri, 'a');
+    assert.equal(colorizations[1].decorations.length, 2);
+    // Can't test the actual decorations as vscode does not seem to have an api
+    // for getting the actual decoration objects.
+    assert.deepEqual(colorizations[1].decorations[0].ranges, [
+      createRange(1, 1, 2), createRange(1, 5, 2), createRange(2, 1, 2),
+      createRange(2, 5, 2)
+    ]);
+    assert.deepEqual(colorizations[1].decorations[1].ranges,
+                     [ createRange(1, 10, 2), createRange(2, 10, 2) ]);
+    // Keeps state separate between files.
+    colorizer.setFileLines('b', [
+      {line : 1, tokens : [ {character : 1, length : 1, scopeIndex : 0} ]}
+    ]);
+    assert.equal(colorizations[2].uri, 'b');
+    assert.equal(colorizations[2].decorations.length, 1);
+    assert.deepEqual(colorizations[2].decorations[0].ranges,
+                     [ createRange(1, 1, 1) ]);
+    // Does full colorizations.
+    colorizer.setFileLines('a', [
+      {line : 1, tokens : [ {character : 2, length : 1, scopeIndex : 0} ]}
+    ]);
+    assert.equal(colorizations[3].uri, 'a');
+    assert.equal(colorizations[3].decorations.length, 3);
+    assert.deepEqual(colorizations[3].decorations[0].ranges,
+                     [ createRange(1, 2, 1) ]);
+    assert.deepEqual(colorizations[3].decorations[1].ranges,
+                     [ createRange(2, 1, 2), createRange(2, 5, 2) ]);
+    assert.deepEqual(colorizations[3].decorations[2].ranges,
+                     [ createRange(2, 10, 2) ]);
+  });
 });
Index: clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
===================================================================
--- clang-tools-extra/clangd/clients/clangd-vscode/src/semantic-highlighting.ts
+++ clang-tools-extra/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.
+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.
@@ -49,6 +56,8 @@
   scopeLookupTable: string[][];
   // The rules for the current theme.
   themeRuleMatcher: ThemeRuleMatcher;
+  // The object that colorizes the highlightings clangd sends.
+  colorizer: Colorizer<FileColorizer>;
   fillClientCapabilities(capabilities: vscodelc.ClientCapabilities) {
     // Extend the ClientCapabilities type and add semantic highlighting
     // capability to the object.
@@ -64,6 +73,7 @@
     this.themeRuleMatcher = new ThemeRuleMatcher(
         await loadTheme(vscode.workspace.getConfiguration('workbench')
                             .get<string>('colorTheme')));
+    this.colorizer.updateThemeRuleMatcher(this.themeRuleMatcher);
   }
 
   initialize(capabilities: vscodelc.ServerCapabilities,
@@ -76,10 +86,19 @@
     if (!serverCapabilities.semanticHighlighting)
       return;
     this.scopeLookupTable = serverCapabilities.semanticHighlighting.scopes;
+    // Important that colorizer is created before the theme is loading as
+    // otherwise it could try to update the themeRuleMatcher without the
+    // colorizer being created.
+    this.colorizer =
+        new Colorizer<FileColorizer>(FileColorizer, 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.colorizer.setFileLines(params.textDocument.uri, lines);
+  }
 }
 
 // Converts a string of base64 encoded tokens into the corresponding array of
@@ -101,6 +120,136 @@
   return retTokens;
 }
 
+// A collection of ranges where all ranges should be decorated with the same
+// decoration.
+interface DecorationRangePair {
+  // The decoration to apply to the ranges.
+  decoration: vscode.TextEditorDecorationType;
+  // The ranges that should be decorated.
+  ranges: vscode.Range[];
+}
+// A simple interface of a class that applies decorations to make the Colorizer
+// to be testable.
+interface IFileColorizer {
+  // Function that is called whenever new decorations should be applied. Must be
+  // called with all decorations that should be visible in the TextEditor
+  // responsible for uriString currently.
+  colorize: (uriString: string,
+             decorationRangePairs: DecorationRangePair[]) => void;
+  // Called when any old decorations should be removed.
+  dispose: () => void;
+}
+// The main class responsible for colorization of highlightings that clangd
+// sends.
+export class Colorizer<T> {
+  private files: Map<string, Map<number, SemanticHighlightingLine>> = new Map();
+  private colorizers: Map<string, IFileColorizer> = new Map();
+  private themeRuleMatcher: ThemeRuleMatcher;
+  private scopeLookupTable: string[][];
+  private ColorizerType: {new(): IFileColorizer;};
+  constructor(ColorizerType: {new(): IFileColorizer;},
+              scopeLookupTable: string[][]) {
+    this.ColorizerType = ColorizerType;
+    this.scopeLookupTable = scopeLookupTable;
+  }
+  /// Update the themeRuleMatcher that is used when colorizing. Also triggers a
+  /// recolorization for all current colorizers.
+  public updateThemeRuleMatcher(themeRuleMatcher: ThemeRuleMatcher) {
+    this.themeRuleMatcher = themeRuleMatcher;
+    Array.from(this.colorizers.keys()).forEach((uri) => this.colorize(uri));
+  }
+
+  // Called when clangd sends an incremental update of highlightings.
+  public setFileLines(uriString: string, tokens: SemanticHighlightingLine[]) {
+    // Patch in the new highlightings to the highlightings cache. If this is the
+    // first time the file should be highlighted a new colorizer and a file
+    // container are created.
+    if (!this.files.has(uriString)) {
+      this.files.set(uriString, new Map());
+      this.colorizers.set(uriString, new this.ColorizerType());
+    }
+
+    const fileHighlightings = this.files.get(uriString);
+    tokens.forEach((line) => fileHighlightings.set(line.line, line));
+    this.colorize(uriString);
+  }
+
+  // Applies all highlightings to the file with uri uriString.
+  private colorize(uriString: string) {
+    if (!this.colorizers.has(uriString)) {
+      this.colorizers.set(uriString, new this.ColorizerType());
+    }
+
+    // Can't colorize if there is no matcher. When a matcher is set a
+    // colorization will be triggered. So it's ok to simply return here.
+    if (!this.themeRuleMatcher) {
+      return;
+    }
+    // This must always do a full re-colorization 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.
+    // FIXME: It might be faster to cache the TextEditorDecorationTypes and only
+    // update the ranges for them.
+    const lines: SemanticHighlightingLine[] = [];
+    Array.from(this.files.get(uriString).values())
+        .forEach((line) => lines.push(line));
+    // Maps scopeIndexes -> the DecorationRangePair used for the scope.
+    const decorations: Map<number, DecorationRangePair> = new Map();
+    lines.forEach((line) => {
+      line.tokens.forEach((token) => {
+        if (!decorations.has(token.scopeIndex)) {
+          const options: vscode.DecorationRenderOptions = {
+            color : this.themeRuleMatcher
+                        .getBestThemeRule(
+                            this.scopeLookupTable[token.scopeIndex][0])
+                        .foreground,
+            // If the rangeBehavior is set to Open in any direction the
+            // colorization becomes weird in certain cases.
+            rangeBehavior : vscode.DecorationRangeBehavior.ClosedClosed,
+          };
+          decorations.set(token.scopeIndex, {
+            decoration : vscode.window.createTextEditorDecorationType(options),
+            ranges : []
+          });
+        }
+        decorations.get(token.scopeIndex)
+            .ranges.push(new vscode.Range(
+                new vscode.Position(line.line, token.character),
+                new vscode.Position(line.line,
+                                    token.character + token.length)));
+      });
+    });
+
+    this.colorizers.get(uriString).colorize(uriString,
+                                            Array.from(decorations.values()));
+  }
+}
+
+// Implementation of IFileColorizer to colorize text editors.
+class FileColorizer implements IFileColorizer {
+  // The decoration datatypes used last time a colorization was triggered.
+  private oldDecorations: DecorationRangePair[] = [];
+  // Apply decorations to the textEditor with uri.
+  public colorize(uri: string, decorationRangePairs: DecorationRangePair[]) {
+    vscode.window.visibleTextEditors.forEach((e) => {
+      if (e.document.uri.toString() !== uri) {
+        return;
+      }
+      decorationRangePairs.forEach(
+          (dp) => { e.setDecorations(dp.decoration, dp.ranges); });
+    });
+    // Clear the old decoration after the new ones have already been applied as
+    // otherwise there might be flicker.
+    this.dispose();
+    this.oldDecorations = decorationRangePairs;
+  }
+  public dispose() {
+    this.oldDecorations.forEach((decorations) =>
+                                    decorations.decoration.dispose());
+  }
+}
+
 // A rule for how to color TextMate scopes.
 interface TokenColorRule {
   // A TextMate scope that specifies the context of the token, e.g.
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to