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 a TextMate parser module to the vscode extension. It watches for changes 
to the vscode configuration and updates the colors when the current theme is 
changed.
The colors are saved in a singleton to allow for easy access when doing 
coloring later.

The TMColor class maps all scopes to the clangd TextMate scope indexes and 
discards any theme colors that are not relevant.


Repository:
  rG LLVM Github Monorepo

https://reviews.llvm.org/D65738

Files:
  clang-tools-extra/clangd/clients/clangd-vscode/package.json
  clang-tools-extra/clangd/clients/clangd-vscode/src/TextMate.ts
  clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts

Index: clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/clients/clangd-vscode/test/TextMate.test.ts
@@ -0,0 +1,32 @@
+/** The module 'assert' provides assertion methods from node */
+import * as assert from 'assert';
+
+import * as vscode from 'vscode';
+import {SemanticHighlighting} from '../src/TextMate';
+
+// TODO: add tests
+suite("Extension Tests", () => {
+  test('overrides for more specific themes', () => {
+    const scopes = [ 'a.b.c.d', 'a.b.f', 'a' ];
+    const colorPairs = [
+      [ [ 'a.b.c', 'a.b.d' ], '1' ],
+      [ 'a.b', '2' ],
+      [ 'a.b.c.d', '3' ],
+      [ 'a', '4' ],
+    ];
+    const tm = new SemanticHighlighting.TMColors(scopes);
+    colorPairs.forEach((p) => tm.setColor(p[0], p[1] as string));
+    assert.deepEqual(tm.getColor(0), '3');
+    assert.deepEqual(tm.getColor(1), '2');
+    assert.deepEqual(tm.getColor(2), '4');
+  });
+  test('Sets an instance of TMColors on setup.', async () => {
+    const scopes = [
+      'variable',
+    ];
+    const disp = await SemanticHighlighting.setupTMScopes(scopes, () => {});
+    assert.notEqual(SemanticHighlighting.TMColorProvider.get().getColors(),
+                    undefined);
+    disp.dispose();
+  });
+});
\ No newline at end of file
Index: clang-tools-extra/clangd/clients/clangd-vscode/src/TextMate.ts
===================================================================
--- /dev/null
+++ clang-tools-extra/clangd/clients/clangd-vscode/src/TextMate.ts
@@ -0,0 +1,174 @@
+import * as fs from 'fs';
+import * as jsonc from "jsonc-parser";
+import * as path from 'path';
+import * as vscode from 'vscode';
+
+export namespace SemanticHighlighting {
+export class TMColors {
+  // The clangd tm scopes.
+  private scopes: string[];
+  // Mapping from a clangd scope index to a color hex string.
+  private colors: string[];
+  // The current best matching scope for every index.
+  private colorScopes: string[];
+  constructor(scopes: string[]) {
+    this.scopes = scopes;
+    this.colors = this.scopes.map(() => '#000');
+    this.colorScopes = this.scopes.map(() => '');
+  }
+
+  setColor(scope: string|Array<string>, color: string) {
+    if (scope instanceof Array) {
+      scope.forEach((s: string) => this.setColor(s, color));
+      return;
+    }
+
+    // Find the associated clangd scope(s) index for this scope.
+    // If "scope" is a candiate for a clangd scope the clangd scope must have
+    // "scope" as a prefix.
+    const allCandidates =
+        this.scopes.map((s, i) => ({s : s, i : i}))
+            .filter(({s}) => s.substr(0, scope.length) === scope);
+    // If this scope is more specific than any of current scopes for the clangd
+    // scopes it should be replaced. As both options are prefixes of the clangd
+    // scope it's enough to compare lengths.
+    allCandidates.forEach(({i}) => {
+      if (scope.length > this.colorScopes[i].length) {
+        this.colorScopes[i] = scope;
+        this.colors[i] = color;
+      }
+    });
+  }
+
+  getColor(idx: number) { return this.colors[idx]; }
+}
+
+// Singleton for reading/writing TM scopes/colors.
+export class TMColorProvider {
+  private static instance: TMColorProvider = new TMColorProvider();
+  private colors: TMColors = undefined;
+  static get() { return TMColorProvider.instance; }
+
+  setColors(colors: TMColors) { this.colors = colors; }
+  getColors(): TMColors { return this.colors; }
+}
+
+/**
+ * @param scopes The TextMate scopes clangd sends on initialize.
+ * @param cb A callback that is called every time the theme changes and the new
+ *     theme has been loaded (not called on the first load).
+ */
+export async function setupTMScopes(scopes: string[],
+                                    cb: Function): Promise<vscode.Disposable> {
+  let oldThemeName = '';
+  async function setTMColors() {
+    const name =
+        vscode.workspace.getConfiguration('workbench').get('colorTheme');
+    if (typeof name != 'string') {
+      console.warn('The current theme name is not a string, is: ' +
+                       (typeof name) + ', value: ',
+                   name);
+      return;
+    }
+
+    if (oldThemeName == name) {
+      return;
+    }
+
+    oldThemeName = name;
+    TMColorProvider.get().setColors(
+        await getTextMateColors(scopes, name as string));
+  }
+
+  // Initialize the TM scopes and colors.
+  await setTMColors();
+
+  // Need to change the color configuration if a theme changes otherwise the
+  // highlightings will have the wrong colors.
+  return vscode.workspace.onDidChangeConfiguration(
+      async (conf: vscode.ConfigurationChangeEvent) => {
+        if (conf.affectsConfiguration('workbench'))
+          // Configuration affected the workbench meaning the current theme
+          // might have changed.
+          await setTMColors();
+        cb();
+      });
+}
+
+// Gets a TM theme with themeName and returns class with the mapping from the
+// clangd scope index to a color.
+async function getTextMateColors(scopes: string[],
+                                 themeName: string): Promise<TMColors> {
+  const fileContents = await getFullNamedTheme(themeName);
+  const tmColors = new TMColors(scopes);
+  fileContents.forEach((content) => {
+    if (!content.tokenColors)
+      return;
+    content.tokenColors.forEach((rule: any) => {
+      if (!rule.scope || !rule.settings || !rule.settings.foreground)
+        return;
+
+      tmColors.setColor(rule.scope, rule.settings.foreground);
+    });
+  });
+
+  return tmColors;
+}
+
+// Gets a TextMate theme by its name and all its included themes.
+async function getFullNamedTheme(themeName: string): Promise<any[]> {
+  const extension =
+      vscode.extensions.all.find((extension: vscode.Extension<any>) => {
+        const contribs = extension.packageJSON.contributes;
+        if (!contribs || !contribs.themes)
+          return false;
+        return contribs.themes.some((theme: any) => theme.id === themeName ||
+                                                    theme.label === themeName);
+      });
+
+  if (!extension) {
+    return Promise.reject('Could not find a theme with name: ' + themeName);
+  }
+
+  const extensionInfo = extension.packageJSON.contributes.themes.find(
+      (theme: any) => theme.id === themeName || theme.label === themeName);
+  return recursiveGetTextMateGrammarPath(
+      path.join(extension.extensionPath, extensionInfo.path));
+}
+
+// TM grammars can include other TM grammars, this function recursively gets all
+// of them.
+async function recursiveGetTextMateGrammarPath(fullPath: string):
+    Promise<any[]> {
+  // If there is an error opening a file, the TM files that were correctly found
+  // and parsed further up the chain should be returned. Otherwise there will be
+  // no highlightings at all.
+  try {
+    const contents = await readFileText(fullPath);
+    const parsed = jsonc.parse(contents);
+    if (parsed.include)
+      // Get all includes and merge into a flat list of parsed json.
+      return [
+        ...(await recursiveGetTextMateGrammarPath(
+            path.join(path.dirname(fullPath), parsed.include))),
+        parsed
+      ];
+    return [ parsed ];
+  } catch (err) {
+    console.warn('Could not open file: ' + fullPath + ', error: ', err);
+  }
+
+  return [];
+}
+
+function readFileText(path: string): Promise<string> {
+  return new Promise((res, rej) => {
+    fs.readFile(path, 'utf8', (err, data) => {
+      if (err) {
+        rej(err);
+      }
+      res(data);
+    });
+  });
+}
+}
Index: clang-tools-extra/clangd/clients/clangd-vscode/package.json
===================================================================
--- clang-tools-extra/clangd/clients/clangd-vscode/package.json
+++ clang-tools-extra/clangd/clients/clangd-vscode/package.json
@@ -36,14 +36,15 @@
         "test": "node ./node_modules/vscode/bin/test"
     },
     "dependencies": {
+        "jsonc-parser": "^2.1.0",
         "vscode-languageclient": "^5.3.0-next.6",
         "vscode-languageserver": "^5.3.0-next.6"
     },
     "devDependencies": {
         "@types/mocha": "^2.2.32",
         "@types/node": "^6.0.40",
-        "mocha": "^5.2.0",
         "clang-format": "1.2.4",
+        "mocha": "^5.2.0",
         "typescript": "^2.0.3",
         "vscode": "^1.1.0"
     },
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to