semantic-highlighting.test.ts 7.33 KB
import * as assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';

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 semanticHighlighting.parseThemeFile(themePath);
    const getScopeRule = (scope: string) =>
        scopeColorRules.find((v) => v.scope === scope);
    assert.equal(scopeColorRules.length, 3);
    assert.deepEqual(getScopeRule('a'), {scope : 'a', foreground : '#fff'});
    assert.deepEqual(getScopeRule('b'), {scope : 'b', foreground : '#000'});
    assert.deepEqual(getScopeRule('c'), {scope : 'c', foreground : '#bcd'});
  });
  test('Decodes tokens correctly', () => {
    const testCases: string[] = [
      'AAAAAAABAAA=', 'AAAAAAADAAkAAAAEAAEAAA==',
      'AAAAAAADAAkAAAAEAAEAAAAAAAoAAQAA'
    ];
    const expected = [
      [ {character : 0, scopeIndex : 0, length : 1} ],
      [
        {character : 0, scopeIndex : 9, length : 3},
        {character : 4, scopeIndex : 0, length : 1}
      ],
      [
        {character : 0, scopeIndex : 9, length : 3},
        {character : 4, scopeIndex : 0, length : 1},
        {character : 10, scopeIndex : 0, length : 1}
      ]
    ];
    testCases.forEach(
        (testCase, i) => assert.deepEqual(
            semanticHighlighting.decodeTokens(testCase), expected[i]));
  });
  test('ScopeRules overrides for more specific themes', () => {
    const rules = [
      {scope : 'variable.other.css', foreground : '1'},
      {scope : 'variable.other', foreground : '2'},
      {scope : 'storage', foreground : '3'},
      {scope : 'storage.static', foreground : '4'},
      {scope : 'storage', foreground : '5'},
      {scope : 'variable.other.parameter', foreground : '6'},
    ];
    const tm = new semanticHighlighting.ThemeRuleMatcher(rules);
    assert.deepEqual(tm.getBestThemeRule('variable.other.cpp').scope,
                     'variable.other');
    assert.deepEqual(tm.getBestThemeRule('storage.static').scope,
                     'storage.static');
    assert.deepEqual(
        tm.getBestThemeRule('storage'),
        rules[2]); // Match the first element if there are duplicates.
    assert.deepEqual(tm.getBestThemeRule('variable.other.parameter').scope,
                     'variable.other.parameter');
    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;
        };

    const fileUri1 = vscode.Uri.parse('file:///file1');
    const fileUri2 = vscode.Uri.parse('file:///file2');
    const fileUri1Str = fileUri1.toString();
    const fileUri2Str = fileUri2.toString();

    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: vscode.Uri) {
        this.applicationUriHistory.push(fileUri.toString());
      }
      // Override to make it accessible from the test.
      getDecorationRanges(fileUri: vscode.Uri) {
        return super.getDecorationRanges(fileUri);
      }
      // Override to make tests not depend on visible text editors.
      getVisibleTextEditorUris() { return [ fileUri1, fileUri2 ]; }
    }
    const highlighter = new MockHighlighter(scopeTable);
    const tm = new semanticHighlighting.ThemeRuleMatcher([
      {scope : 'variable', foreground : '1'},
      {scope : 'entity.type', foreground : '2'},
    ]);
    // Recolorizes when initialized.
    highlighter.highlight(fileUri1, []);
    assert.deepEqual(highlighter.applicationUriHistory, [ fileUri1Str ]);
    highlighter.initialize(tm);
    assert.deepEqual(highlighter.applicationUriHistory,
                     [ fileUri1Str, fileUri1Str, fileUri2Str ]);
    // 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(fileUri1, highlightingsInLine);
    assert.deepEqual(highlighter.applicationUriHistory,
                     [ fileUri1Str, fileUri1Str, fileUri2Str, fileUri1Str ]);
    assert.deepEqual(highlighter.getDecorationRanges(fileUri1),
                     createHighlightingScopeRanges(highlightingsInLine));
    // Keeps state separate between files.
    const highlightingsInLine1:
        semanticHighlighting.SemanticHighlightingLine = {
      line : 1,
      tokens : [
        {character : 2, length : 1, scopeIndex : 0},
      ]
    };
    highlighter.highlight(fileUri2, [ highlightingsInLine1 ]);
    assert.deepEqual(
        highlighter.applicationUriHistory,
        [ fileUri1Str, fileUri1Str, fileUri2Str, fileUri1Str, fileUri2Str ]);
    assert.deepEqual(highlighter.getDecorationRanges(fileUri2),
                     createHighlightingScopeRanges([ highlightingsInLine1 ]));
    // Does full colorizations.
    highlighter.highlight(fileUri1, [ highlightingsInLine1 ]);
    assert.deepEqual(highlighter.applicationUriHistory, [
      fileUri1Str, fileUri1Str, fileUri2Str, fileUri1Str, fileUri2Str,
      fileUri1Str
    ]);
    // After the incremental update to line 1, the old highlightings at line 1
    // will no longer exist in the array.
    assert.deepEqual(
        highlighter.getDecorationRanges(fileUri1),
        createHighlightingScopeRanges(
            [ highlightingsInLine1, ...highlightingsInLine.slice(1) ]));
    // Closing a text document removes all highlightings for the file and no
    // other files.
    highlighter.removeFileHighlightings(fileUri1);
    assert.deepEqual(highlighter.getDecorationRanges(fileUri1), []);
    assert.deepEqual(highlighter.getDecorationRanges(fileUri2),
                     createHighlightingScopeRanges([ highlightingsInLine1 ]));
  });
});