jvikstrom updated this revision to Diff 215886.
jvikstrom marked 4 inline comments as done.
jvikstrom added a comment.

Renamed colorizer to highlighter and added FIXME about highlightings below eof.


Repository:
  rG LLVM Github Monorepo

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

https://reviews.llvm.org/D66219

Files:
  clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
  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,72 @@
     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 extends SM.FileHighlighter {
+      public highlight(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.Highlighter(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 applies the highlightings clangd sends.
+  highlighter: Highlighter;
   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.highlighter.updateThemeRuleMatcher(this.themeRuleMatcher);
   }
 
   initialize(capabilities: vscodelc.ServerCapabilities,
@@ -76,10 +86,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(FileHighlighter, 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.setFileLines(params.textDocument.uri, lines);
+  }
 }
 
 // Converts a string of base64 encoded tokens into the corresponding array of
@@ -101,6 +119,134 @@
   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[];
+}
+
+// Applies highlightings to text editors.
+export class FileHighlighter {
+  // The decoration datatypes used last time highlight was called.
+  private oldDecorations: DecorationRangePair[] = [];
+  // Apply decorations to the textEditor with uri and remove the old ones.
+  public highlight(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());
+  }
+}
+
+// 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();
+  // Maps uris with currently open TextEditors to a FileHighlighter.
+  private highlighters: Map<string, FileHighlighter> = new Map();
+  // The matcher controling how tokens are highlighted.
+  private themeRuleMatcher: ThemeRuleMatcher;
+  // The clangd TextMate scope lookup table.
+  private scopeLookupTable: string[][];
+  // The FileHighlighter type that should be used when applying new
+  // highlightings.
+  private HighlighterType: {new(): FileHighlighter;};
+  constructor(HighlighterType: {new(): FileHighlighter;},
+              scopeLookupTable: string[][]) {
+    this.HighlighterType = HighlighterType;
+    this.scopeLookupTable = scopeLookupTable;
+  }
+  // Update the themeRuleMatcher that is used when highlighting. Also triggers a
+  // recolorization for all current highlighters.
+  public updateThemeRuleMatcher(themeRuleMatcher: ThemeRuleMatcher) {
+    this.themeRuleMatcher = themeRuleMatcher;
+    Array.from(this.highlighters.keys()).forEach((uri) => this.highlight(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 highlighter and a file
+    // container are created.
+    if (!this.files.has(uriString)) {
+      this.files.set(uriString, new Map());
+      this.highlighters.set(uriString, new this.HighlighterType());
+    }
+
+    const fileHighlightings = this.files.get(uriString);
+    // FIXME: If the number of lines in the file decreased the old highlightings
+    // outside the file will still exist in the file cache.
+    tokens.forEach((line) => fileHighlightings.set(line.line, line));
+    this.highlight(uriString);
+  }
+
+  // Applies all highlightings to the file with uri uriString.
+  private highlight(uriString: string) {
+    if (!this.highlighters.has(uriString)) {
+      this.highlighters.set(uriString, new this.HighlighterType());
+    }
+
+    // Can't highlight if there is no matcher. When a matcher is set a
+    // highlighting will be triggered. So it's ok to simply return here.
+    if (!this.themeRuleMatcher) {
+      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.
+    // 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
+            // highlighting 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.highlighters.get(uriString).highlight(
+        uriString, Array.from(decorations.values()));
+  }
+}
+
 // 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/clangd/clients/clangd-vscode/src/extension.ts
===================================================================
--- clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
+++ clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
@@ -1,6 +1,6 @@
 import * as vscode from 'vscode';
 import * as vscodelc from 'vscode-languageclient';
-
+import * as SM from './semantic-highlighting';
 /**
  * Method to get workspace configuration option
  * @param option name of the option (e.g. for clangd.path should be path)
@@ -91,6 +91,16 @@
 
   const clangdClient = new vscodelc.LanguageClient(
       'Clang Language Server', serverOptions, clientOptions);
+  const semanticHighlightingFeature = new SM.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(
+          SM.NotificationType,
+          semanticHighlightingFeature.handleNotification.bind(
+              semanticHighlightingFeature)));
+
   console.log('Clang Language Server is now active!');
   context.subscriptions.push(clangdClient.start());
   context.subscriptions.push(vscode.commands.registerCommand(
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to