llvm-project/clang-tools-extra/clangd/clients/clangd-vscode/test/semantic-highlighting.test.ts

175 lines
7.3 KiB
TypeScript

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 ]));
});
});