Skip to content

Commit 6d488ea

Browse files
committed
ref value highlight
1 parent 69ff19c commit 6d488ea

File tree

8 files changed

+237
-91
lines changed

8 files changed

+237
-91
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,14 @@
560560
"description": "Enable template interpolation service that offers hover / definition / references in Vue interpolations."
561561
}
562562
}
563-
}
563+
},
564+
"semanticTokenScopes": [
565+
{
566+
"scopes": {
567+
"property.refValue": ["entity.name.function"]
568+
}
569+
}
570+
]
564571
},
565572
"devDependencies": {
566573
"@rollup/plugin-commonjs": "^17.1.0",

server/src/modes/script/javascript.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
CodeActionDataKind,
5050
OrganizeImportsActionData,
5151
RefactorActionData,
52-
SemanticTokenData
52+
SemanticTokenOffsetData
5353
} from '../../types';
5454
import { IServiceHost } from '../../services/typescriptService/serviceHost';
5555
import {
@@ -62,7 +62,12 @@ import * as Previewer from './previewer';
6262
import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken';
6363
import { EnvironmentService } from '../../services/EnvironmentService';
6464
import { getCodeActionKind } from './CodeActionKindConverter';
65-
import { FileRename, SemanticTokensBuilder } from 'vscode-languageserver';
65+
import { FileRename } from 'vscode-languageserver';
66+
import {
67+
addCompositionApiRefTokens,
68+
getTokenModifierFromClassification,
69+
getTokenTypeFromClassification
70+
} from './semanticToken';
6671

6772
// Todo: After upgrading to LS server 4.0, use CompletionContext for filtering trigger chars
6873
// https://microsoft.github.io/language-server-protocol/specification#completion-request-leftwards_arrow_with_hook
@@ -816,12 +821,12 @@ export async function getJavascriptMode(
816821
tsModule.SemanticClassificationFormat.TwentyTwenty
817822
);
818823

819-
const data: SemanticTokenData[] = [];
824+
const data: SemanticTokenOffsetData[] = [];
820825
let index = 0;
821826

822827
while (index < spans.length) {
823828
// [start, length, encodedClassification, start2, length2, encodedClassification2]
824-
const offset = spans[index++];
829+
const start = spans[index++];
825830
const length = spans[index++];
826831
const encodedClassification = spans[index++];
827832
const classificationType = getTokenTypeFromClassification(encodedClassification);
@@ -830,18 +835,29 @@ export async function getJavascriptMode(
830835
}
831836

832837
const modifierSet = getTokenModifierFromClassification(encodedClassification);
833-
const startPosition = scriptDoc.positionAt(offset);
834838

835839
data.push({
836-
line: startPosition.line,
837-
character: startPosition.character,
840+
start,
838841
length,
839842
classificationType,
840843
modifierSet
841844
});
842845
}
843846

844-
return data;
847+
const program = service.getProgram();
848+
if (program) {
849+
addCompositionApiRefTokens(tsModule, program, fileFsPath, data);
850+
}
851+
852+
return data.map(({ start, ...rest }) => {
853+
const startPosition = scriptDoc.positionAt(start);
854+
855+
return {
856+
...rest,
857+
line: startPosition.line,
858+
character: startPosition.character
859+
};
860+
});
845861
},
846862
dispose() {
847863
jsDocuments.dispose();
@@ -1179,16 +1195,3 @@ function convertTextSpan(range: Range, doc: TextDocument): ts.TextSpan {
11791195
length: end - start
11801196
};
11811197
}
1182-
1183-
function getTokenTypeFromClassification(tsClassification: number): number {
1184-
return (tsClassification >> TokenEncodingConsts.typeOffset) - 1;
1185-
}
1186-
1187-
function getTokenModifierFromClassification(tsClassification: number) {
1188-
return tsClassification & TokenEncodingConsts.modifierMask;
1189-
}
1190-
1191-
const enum TokenEncodingConsts {
1192-
typeOffset = 8,
1193-
modifierMask = (1 << typeOffset) - 1
1194-
}

server/src/modes/script/semanticToken.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,79 @@ export function getSemanticTokenLegends(): SemanticTokensLegend {
7979
tokenTypes
8080
};
8181
}
82+
83+
export function getTokenTypeFromClassification(tsClassification: number): number {
84+
return (tsClassification >> TokenEncodingConsts.typeOffset) - 1;
85+
}
86+
87+
export function getTokenModifierFromClassification(tsClassification: number) {
88+
return tsClassification & TokenEncodingConsts.modifierMask;
89+
}
90+
91+
const enum TokenEncodingConsts {
92+
typeOffset = 8,
93+
modifierMask = (1 << typeOffset) - 1
94+
}
95+
96+
export function addCompositionApiRefTokens(
97+
tsModule: RuntimeLibrary['typescript'],
98+
program: ts.Program,
99+
fileFsPath: string,
100+
exists: SemanticTokenOffsetData[]
101+
): void {
102+
const sourceFile = program.getSourceFile(fileFsPath);
103+
104+
if (!sourceFile) {
105+
return;
106+
}
107+
108+
const typeChecker = program.getTypeChecker();
109+
110+
walk(sourceFile, node => {
111+
const possiblyRefValue =
112+
ts.isIdentifier(node) && node.text === 'value' && ts.isPropertyAccessExpression(node.parent);
113+
if (!possiblyRefValue) {
114+
return;
115+
}
116+
const propertyAccess = node.parent;
117+
if (!ts.isPropertyAccessExpression(propertyAccess)) {
118+
return;
119+
}
120+
121+
let parentSymbol = typeChecker.getTypeAtLocation(propertyAccess.expression).symbol;
122+
if (!parentSymbol) {
123+
return;
124+
}
125+
126+
if (parentSymbol.flags & tsModule.SymbolFlags.Alias) {
127+
parentSymbol = typeChecker.getAliasedSymbol(parentSymbol);
128+
}
129+
130+
if (parentSymbol.name !== 'Ref') {
131+
return;
132+
}
133+
134+
const start = node.getStart();
135+
const length = node.getWidth();
136+
const exist = exists.find(token => token.start === start && token.length === length);
137+
const encodedModifier = 1 << TokenModifier.refValue;
138+
139+
if (exist) {
140+
exist.modifierSet |= encodedModifier;
141+
} else {
142+
exists.push({
143+
classificationType: TokenType.property,
144+
length: node.getEnd() - node.getStart(),
145+
modifierSet: encodedModifier,
146+
start: node.getStart()
147+
});
148+
}
149+
});
150+
}
151+
152+
function walk(node: ts.Node, callback: (node: ts.Node) => void) {
153+
node.forEachChild(child => {
154+
callback(child);
155+
walk(child, callback);
156+
});
157+
}

server/src/types.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ts from 'typescript';
12
import { LanguageId } from './embeddedSupport/embeddedSupport';
23

34
export interface DocumentContext {
@@ -36,10 +37,15 @@ export interface OrganizeImportsActionData extends BaseCodeActionData {
3637

3738
export type CodeActionData = RefactorActionData | CombinedFixActionData | OrganizeImportsActionData;
3839

39-
export interface SemanticTokenData {
40+
interface SemanticTokenClassification {
41+
classificationType: number;
42+
modifierSet: number;
43+
}
44+
45+
export interface SemanticTokenData extends SemanticTokenClassification {
4046
line: number;
4147
character: number;
4248
length: number;
43-
classificationType: number;
44-
modifierSet: number;
4549
}
50+
51+
export interface SemanticTokenOffsetData extends SemanticTokenClassification, ts.TextSpan {}
Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import assert from 'assert';
2-
import vscode from 'vscode';
3-
import { SemanticTokensParams, SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-protocol';
4-
import { showFile } from '../../../editorHelper';
5-
import { sameLineRange } from '../../../util';
1+
import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-protocol';
2+
import { getTokenRange, testSemanticTokens } from '../../../semanticTokenHelper';
63
import { getDocUri } from '../../path';
74

8-
function getTokenRange(line: number, startChar: number, identifier: string) {
9-
return sameLineRange(line, startChar, startChar + identifier.length);
10-
}
11-
12-
describe('Should update import when rename files', () => {
5+
describe('semantic tokens', () => {
136
const docPath = 'semanticTokens/Basic.vue';
147
const docUri = getDocUri(docPath);
8+
159
it('provide semantic tokens', async () => {
1610
await testSemanticTokens(docUri, [
1711
{
@@ -76,60 +70,4 @@ describe('Should update import when rename files', () => {
7670
}
7771
]);
7872
});
79-
80-
async function testSemanticTokens(uri: vscode.Uri, expected: UnEncodedSemanticTokenData[]) {
81-
await showFile(docUri);
82-
83-
const result = await vscode.commands.executeCommand<vscode.SemanticTokens>(
84-
'vscode.provideDocumentSemanticTokens',
85-
uri
86-
);
87-
assertResult(result!.data, encodeExpected(await getLegend(uri), expected));
88-
}
8973
});
90-
91-
/**
92-
* group result by tokens to better distinguish
93-
*/
94-
function assertResult(actual: Uint32Array, expected: number[]) {
95-
const actualGrouped = group(actual);
96-
const expectedGrouped = group(expected);
97-
98-
assert.deepStrictEqual(actualGrouped, expectedGrouped);
99-
}
100-
101-
function group(tokens: Uint32Array | number[]) {
102-
const result: number[][] = [];
103-
104-
let index = 0;
105-
while (index < tokens.length) {
106-
result.push(Array.from(tokens.slice(index, (index += 5))));
107-
}
108-
109-
return result;
110-
}
111-
112-
interface UnEncodedSemanticTokenData {
113-
range: vscode.Range;
114-
type: string;
115-
modifiers: string[];
116-
}
117-
118-
function encodeExpected(legend: vscode.SemanticTokensLegend, tokens: UnEncodedSemanticTokenData[]) {
119-
const builder = new vscode.SemanticTokensBuilder(legend);
120-
121-
for (const token of tokens) {
122-
builder.push(token.range, token.type, token.modifiers);
123-
}
124-
125-
return Array.from(builder.build().data);
126-
}
127-
128-
async function getLegend(uri: vscode.Uri): Promise<vscode.SemanticTokensLegend> {
129-
const res = await vscode.commands.executeCommand<vscode.SemanticTokensLegend>(
130-
'vscode.provideDocumentSemanticTokensLegend',
131-
uri
132-
);
133-
134-
return res!;
135-
}

test/semanticTokenHelper.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import assert from 'assert';
2+
import vscode from 'vscode';
3+
import { showFile } from './editorHelper';
4+
import { sameLineRange } from './util';
5+
6+
/**
7+
* group result by tokens to better distinguish
8+
*/
9+
export function assertResult(actual: Uint32Array, expected: number[]) {
10+
const actualGrouped = group(actual);
11+
const expectedGrouped = group(expected);
12+
13+
assert.deepStrictEqual(actualGrouped, expectedGrouped);
14+
}
15+
16+
function group(tokens: Uint32Array | number[]) {
17+
const result: number[][] = [];
18+
19+
let index = 0;
20+
while (index < tokens.length) {
21+
result.push(Array.from(tokens.slice(index, (index += 5))));
22+
}
23+
24+
return result;
25+
}
26+
27+
export interface UnEncodedSemanticTokenData {
28+
range: vscode.Range;
29+
type: string;
30+
modifiers: string[];
31+
}
32+
33+
function encodeExpected(legend: vscode.SemanticTokensLegend, tokens: UnEncodedSemanticTokenData[]) {
34+
const builder = new vscode.SemanticTokensBuilder(legend);
35+
36+
for (const token of tokens) {
37+
builder.push(token.range, token.type, token.modifiers);
38+
}
39+
40+
return Array.from(builder.build().data);
41+
}
42+
43+
async function getLegend(uri: vscode.Uri): Promise<vscode.SemanticTokensLegend> {
44+
const res = await vscode.commands.executeCommand<vscode.SemanticTokensLegend>(
45+
'vscode.provideDocumentSemanticTokensLegend',
46+
uri
47+
);
48+
49+
return res!;
50+
}
51+
52+
export async function testSemanticTokens(docUri: vscode.Uri, expected: UnEncodedSemanticTokenData[]) {
53+
await showFile(docUri);
54+
55+
const result = await vscode.commands.executeCommand<vscode.SemanticTokens>(
56+
'vscode.provideDocumentSemanticTokens',
57+
docUri
58+
);
59+
assertResult(result!.data, encodeExpected(await getLegend(docUri), expected));
60+
}
61+
62+
export function getTokenRange(line: number, startChar: number, identifier: string) {
63+
return sameLineRange(line, startChar, startChar + identifier.length);
64+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver-protocol';
2+
import { getTokenRange, testSemanticTokens } from '../../../semanticTokenHelper';
3+
import { getDocUri } from '../../path';
4+
5+
describe('semantic tokens', () => {
6+
const docPath = 'semanticTokens/Basic.vue';
7+
const docUri = getDocUri(docPath);
8+
9+
it('provide semantic tokens', async () => {
10+
await testSemanticTokens(docUri, [
11+
{
12+
type: SemanticTokenTypes.method,
13+
range: getTokenRange(7, 2, 'setup'),
14+
modifiers: [SemanticTokenModifiers.declaration]
15+
},
16+
{
17+
type: SemanticTokenTypes.variable,
18+
range: getTokenRange(8, 10, 'a'),
19+
modifiers: [SemanticTokenModifiers.readonly, SemanticTokenModifiers.declaration, 'local']
20+
},
21+
{
22+
type: SemanticTokenTypes.function,
23+
range: getTokenRange(8, 14, 'ref'),
24+
modifiers: []
25+
},
26+
{
27+
type: SemanticTokenTypes.variable,
28+
range: getTokenRange(10, 4, 'a'),
29+
modifiers: [SemanticTokenModifiers.readonly, 'local']
30+
},
31+
{
32+
type: SemanticTokenTypes.property,
33+
range: getTokenRange(10, 6, 'value'),
34+
modifiers: ['refValue']
35+
}
36+
]);
37+
});
38+
});

0 commit comments

Comments
 (0)