Skip to content

Commit f9db434

Browse files
authored
(feat) Semantic tokens (#752)
#71 This implements the semantic token for TypeScript. Because of TypeScript's implementation, this feature doesn't work with JavaScript.
1 parent c8718b3 commit f9db434

File tree

12 files changed

+462
-7
lines changed

12 files changed

+462
-7
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {
2+
SemanticTokensLegend,
3+
SemanticTokenModifiers,
4+
SemanticTokenTypes
5+
} from 'vscode-languageserver';
6+
7+
/**
8+
* extended from https://github.com/microsoft/TypeScript/blob/35c8df04ad959224fad9037e340c1e50f0540a49/src/services/classifier2020.ts#L9
9+
* so that we don't have to map it into our own legend
10+
*/
11+
export const enum TokenType {
12+
class,
13+
enum,
14+
interface,
15+
namespace,
16+
typeParameter,
17+
type,
18+
parameter,
19+
variable,
20+
enumMember,
21+
property,
22+
function,
23+
member,
24+
25+
// svelte
26+
event
27+
}
28+
29+
/**
30+
* adopted from https://github.com/microsoft/TypeScript/blob/35c8df04ad959224fad9037e340c1e50f0540a49/src/services/classifier2020.ts#L13
31+
* so that we don't have to map it into our own legend
32+
*/
33+
export const enum TokenModifier {
34+
declaration,
35+
static,
36+
async,
37+
readonly,
38+
defaultLibrary,
39+
local
40+
}
41+
42+
export function getSemanticTokenLegends(): SemanticTokensLegend {
43+
const tokenModifiers: string[] = [];
44+
45+
([
46+
[TokenModifier.declaration, SemanticTokenModifiers.declaration],
47+
[TokenModifier.static, SemanticTokenModifiers.static],
48+
[TokenModifier.async, SemanticTokenModifiers.async],
49+
[TokenModifier.readonly, SemanticTokenModifiers.readonly],
50+
[TokenModifier.defaultLibrary, SemanticTokenModifiers.defaultLibrary],
51+
[TokenModifier.local, 'local']
52+
] as const).forEach(([tsModifier, legend]) => (tokenModifiers[tsModifier] = legend));
53+
54+
const tokenTypes: string[] = [];
55+
56+
([
57+
[TokenType.class, SemanticTokenTypes.class],
58+
[TokenType.enum, SemanticTokenTypes.enum],
59+
[TokenType.interface, SemanticTokenTypes.interface],
60+
[TokenType.namespace, SemanticTokenTypes.namespace],
61+
[TokenType.typeParameter, SemanticTokenTypes.typeParameter],
62+
[TokenType.type, SemanticTokenTypes.type],
63+
[TokenType.parameter, SemanticTokenTypes.parameter],
64+
[TokenType.variable, SemanticTokenTypes.variable],
65+
[TokenType.enumMember, SemanticTokenTypes.enumMember],
66+
[TokenType.property, SemanticTokenTypes.property],
67+
[TokenType.function, SemanticTokenTypes.function],
68+
69+
// member is renamed to method in vscode codebase to match LSP default
70+
[TokenType.member, SemanticTokenTypes.method],
71+
[TokenType.event, SemanticTokenTypes.event]
72+
] as const).forEach(([tokenType, legend]) => (tokenTypes[tokenType] = legend));
73+
74+
return {
75+
tokenModifiers,
76+
tokenTypes
77+
};
78+
}

packages/language-server/src/ls-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const defaultLSConfig: LSConfig = {
1717
codeActions: { enable: true },
1818
rename: { enable: true },
1919
selectionRange: { enable: true },
20-
signatureHelp: { enable: true }
20+
signatureHelp: { enable: true },
21+
semanticTokens: { enable: true }
2122
},
2223
css: {
2324
enable: true,
@@ -93,6 +94,9 @@ export interface LSTypescriptConfig {
9394
signatureHelp: {
9495
enable: boolean;
9596
};
97+
semanticTokens: {
98+
enable: boolean;
99+
}
96100
}
97101

98102
export interface LSCSSConfig {

packages/language-server/src/plugins/PluginHost.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Range,
1818
ReferenceContext,
1919
SelectionRange,
20+
SemanticTokens,
2021
SignatureHelp,
2122
SignatureHelpContext,
2223
SymbolInformation,
@@ -400,6 +401,23 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
400401
}
401402
}
402403

404+
async getSemanticTokens(textDocument: TextDocumentIdentifier, range?: Range) {
405+
const document = this.getDocument(textDocument.uri);
406+
if (!document) {
407+
throw new Error('Cannot call methods on an unopened document');
408+
}
409+
410+
return (
411+
(await this.execute<SemanticTokens>(
412+
'getSemanticTokens',
413+
[document, range],
414+
ExecuteMode.FirstNonNull
415+
)) ?? {
416+
data: []
417+
}
418+
);
419+
}
420+
403421
onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): void {
404422
for (const support of this.plugins) {
405423
support.onWatchFileChanges?.(onWatchFileChangesParas);

packages/language-server/src/plugins/interfaces.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CompletionContext, FileChangeType, SignatureHelpContext } from 'vscode-languageserver';
1+
import { CompletionContext, FileChangeType, SemanticTokens, SignatureHelpContext } from 'vscode-languageserver';
22
import {
33
CodeAction,
44
CodeActionContext,
@@ -141,6 +141,13 @@ export interface SelectionRangeProvider {
141141
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
142142
}
143143

144+
export interface SemanticTokensProvider {
145+
getSemanticTokens(
146+
textDocument: Document,
147+
range?: Range
148+
): Resolvable<SemanticTokens>
149+
}
150+
144151
export interface OnWatchFileChangesPara {
145152
fileName: string;
146153
changeType: FileChangeType;
@@ -162,7 +169,8 @@ type ProviderBase = DiagnosticsProvider &
162169
CodeActionsProvider &
163170
FindReferencesProvider &
164171
RenameProvider &
165-
SignatureHelpProvider;
172+
SignatureHelpProvider &
173+
SemanticTokensProvider;
166174

167175
export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider;
168176

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
CompletionList,
1818
SelectionRange,
1919
SignatureHelp,
20-
SignatureHelpContext
20+
SignatureHelpContext,
21+
SemanticTokens
2122
} from 'vscode-languageserver';
2223
import {
2324
Document,
@@ -43,7 +44,8 @@ import {
4344
SelectionRangeProvider,
4445
SignatureHelpProvider,
4546
UpdateImportsProvider,
46-
OnWatchFileChangesPara
47+
OnWatchFileChangesPara,
48+
SemanticTokensProvider
4749
} from '../interfaces';
4850
import { SnapshotFragment } from './DocumentSnapshot';
4951
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
@@ -62,6 +64,7 @@ import { FindReferencesProviderImpl } from './features/FindReferencesProvider';
6264
import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider';
6365
import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
6466
import { SnapshotManager } from './SnapshotManager';
67+
import { SemanticTokensProviderImpl } from './features/SemanticTokensProvider';
6568

6669
export class TypeScriptPlugin
6770
implements
@@ -75,6 +78,7 @@ export class TypeScriptPlugin
7578
FindReferencesProvider,
7679
SelectionRangeProvider,
7780
SignatureHelpProvider,
81+
SemanticTokensProvider,
7882
OnWatchFileChanges,
7983
CompletionsProvider<CompletionEntryWithIdentifer> {
8084
private readonly configManager: LSConfigManager;
@@ -88,6 +92,7 @@ export class TypeScriptPlugin
8892
private readonly findReferencesProvider: FindReferencesProviderImpl;
8993
private readonly selectionRangeProvider: SelectionRangeProviderImpl;
9094
private readonly signatureHelpProvider: SignatureHelpProviderImpl;
95+
private readonly semanticTokensProvider: SemanticTokensProviderImpl;
9196

9297
constructor(
9398
docManager: DocumentManager,
@@ -112,6 +117,7 @@ export class TypeScriptPlugin
112117
this.findReferencesProvider = new FindReferencesProviderImpl(this.lsAndTsDocResolver);
113118
this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver);
114119
this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver);
120+
this.semanticTokensProvider = new SemanticTokensProviderImpl(this.lsAndTsDocResolver);
115121
}
116122

117123
async getDiagnostics(document: Document): Promise<Diagnostic[]> {
@@ -401,6 +407,16 @@ export class TypeScriptPlugin
401407
return this.signatureHelpProvider.getSignatureHelp(document, position, context);
402408
}
403409

410+
async getSemanticTokens(textDocument: Document, range?: Range): Promise<SemanticTokens> {
411+
if (!this.featureEnabled('semanticTokens')) {
412+
return {
413+
data: []
414+
};
415+
}
416+
417+
return this.semanticTokensProvider.getSemanticTokens(textDocument, range);
418+
}
419+
404420
private getLSAndTSDoc(document: Document) {
405421
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
406422
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import ts from 'typescript';
2+
import {
3+
Range,
4+
SemanticTokens,
5+
SemanticTokensBuilder
6+
} from 'vscode-languageserver';
7+
import { Document } from '../../../lib/documents';
8+
import { SemanticTokensProvider } from '../../interfaces';
9+
import { SnapshotFragment } from '../DocumentSnapshot';
10+
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
11+
import { convertToTextSpan } from '../utils';
12+
13+
export class SemanticTokensProviderImpl implements SemanticTokensProvider {
14+
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
15+
16+
async getSemanticTokens(textDocument: Document, range?: Range): Promise<SemanticTokens> {
17+
const { lang, tsDoc } = this.lsAndTsDocResolver.getLSAndTSDoc(textDocument);
18+
const fragment = await tsDoc.getFragment();
19+
const textSpan = range
20+
? convertToTextSpan(range, fragment)
21+
: {
22+
start: 0,
23+
length: tsDoc.parserError
24+
? fragment.text.length
25+
: // This is appended by svelte2tsx, there's nothing mappable afterwards
26+
fragment.text.lastIndexOf('return { props:') || fragment.text.length
27+
};
28+
29+
const { spans } = lang.getEncodedSemanticClassifications(
30+
tsDoc.filePath,
31+
textSpan,
32+
ts.SemanticClassificationFormat.TwentyTwenty
33+
);
34+
35+
const builder = new SemanticTokensBuilder();
36+
let index = 0;
37+
38+
while (index < spans.length) {
39+
// [start, length, encodedClassification, start2, length2, encodedClassification2]
40+
const generatedOffset = spans[index++];
41+
const generatedLength = spans[index++];
42+
const encodedClassification = spans[index++];
43+
const classificationType = this.getTokenTypeFromClassification(encodedClassification);
44+
if (classificationType < 0) {
45+
continue;
46+
}
47+
48+
const originalPosition = this.mapToOrigin(
49+
textDocument,
50+
fragment,
51+
generatedOffset,
52+
generatedLength
53+
);
54+
if (!originalPosition) {
55+
continue;
56+
}
57+
58+
const [line, character, length] = originalPosition;
59+
60+
// remove identifers whose start and end mapped to the same location
61+
// like the svelte2tsx inserted render function
62+
if (!length) {
63+
continue;
64+
}
65+
66+
const modifier = this.getTokenModifierFromClassification(encodedClassification);
67+
68+
builder.push(line, character, length, classificationType , modifier);
69+
}
70+
71+
return builder.build();
72+
}
73+
74+
private mapToOrigin(
75+
document: Document,
76+
fragment: SnapshotFragment,
77+
generatedOffset: number,
78+
generatedLength: number
79+
): [line: number, character: number, length: number] | undefined {
80+
const startPosition = fragment.getOriginalPosition(fragment.positionAt(generatedOffset));
81+
82+
if (startPosition.line < 0) {
83+
return;
84+
}
85+
86+
const endPosition = fragment.getOriginalPosition(
87+
fragment.positionAt(generatedOffset + generatedLength)
88+
);
89+
const startOffset = document.offsetAt(startPosition);
90+
const endOffset = document.offsetAt(endPosition);
91+
92+
return [startPosition.line, startPosition.character, endOffset - startOffset];
93+
}
94+
95+
/**
96+
* TSClassification = (TokenType + 1) << TokenEncodingConsts.typeOffset + TokenModifier
97+
*/
98+
private getTokenTypeFromClassification(tsClassification: number): number {
99+
return (tsClassification >> TokenEncodingConsts.typeOffset) - 1;
100+
}
101+
102+
private getTokenModifierFromClassification(tsClassification: number) {
103+
return tsClassification & TokenEncodingConsts.modifierMask;
104+
}
105+
}
106+
107+
const enum TokenEncodingConsts {
108+
typeOffset = 8,
109+
modifierMask = (1 << typeOffset) - 1
110+
}

packages/language-server/src/plugins/typescript/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,13 @@ export function getTsCheckComment(str = ''): string | undefined {
286286
}
287287
}
288288
}
289+
290+
export function convertToTextSpan(range: Range, fragment: SnapshotFragment): ts.TextSpan {
291+
const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
292+
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
293+
294+
return {
295+
start,
296+
length: end - start
297+
};
298+
}

0 commit comments

Comments
 (0)