Skip to content

Commit 0e917c6

Browse files
authored
feat: forward tsserver's semantic tokens via language server (#5512)
1 parent 0ee2d15 commit 0e917c6

File tree

11 files changed

+280
-145
lines changed

11 files changed

+280
-145
lines changed

extensions/vscode/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@
217217
]
218218
}
219219
],
220+
"semanticTokenScopes": [
221+
{
222+
"language": "vue",
223+
"scopes": {
224+
"component": [
225+
"support.class.component.vue"
226+
]
227+
}
228+
},
229+
{
230+
"language": "markdown",
231+
"scopes": {
232+
"component": [
233+
"support.class.component.vue"
234+
]
235+
}
236+
},
237+
{
238+
"language": "html",
239+
"scopes": {
240+
"component": [
241+
"support.class.component.vue"
242+
]
243+
}
244+
}
245+
],
220246
"breakpoints": [
221247
{
222248
"language": "vue"

packages/language-plugin-pug/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@volar/source-map": "2.4.19",
17-
"volar-service-pug": "0.0.64"
17+
"volar-service-pug": "0.0.65"
1818
},
1919
"devDependencies": {
2020
"@types/node": "^22.10.4",

packages/language-server/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ connection.onInitialize(params => {
139139
} satisfies ts.server.protocol.DocumentHighlightsRequestArgs,
140140
);
141141
},
142+
getEncodedSemanticClassifications(fileName, span) {
143+
return sendTsServerRequest(
144+
'_vue:encodedSemanticClassifications-full',
145+
{
146+
file: fileName,
147+
...span,
148+
format: ts.SemanticClassificationFormat.TwentyTwenty,
149+
} satisfies ts.server.protocol.EncodedSemanticClassificationsRequestArgs,
150+
);
151+
},
142152
async getQuickInfoAtPosition(fileName, { line, character }) {
143153
const result = await sendTsServerRequest<ts.server.protocol.QuickInfoResponseBody>(
144154
'_vue:' + ts.server.protocol.CommandTypes.Quickinfo,

packages/language-service/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import { create as createPugFormatPlugin } from 'volar-service-pug-beautify';
1313
import { create as createTypeScriptDocCommentTemplatePlugin } from 'volar-service-typescript/lib/plugins/docCommentTemplate';
1414
import { create as createTypeScriptSyntacticPlugin } from 'volar-service-typescript/lib/plugins/syntactic';
1515
import { create as createCssPlugin } from './lib/plugins/css';
16+
import { create as createTypescriptSemanticTokensPlugin } from './lib/plugins/typescript-semantic-tokens';
1617
import { create as createVueAutoDotValuePlugin } from './lib/plugins/vue-autoinsert-dotvalue';
1718
import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoinsert-space';
1819
import { create as createVueCompilerDomErrorsPlugin } from './lib/plugins/vue-compiler-dom-errors';
20+
import { create as createVueComponentSemanticTokensPlugin } from './lib/plugins/vue-component-semantic-tokens';
1921
import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments';
2022
import { create as createVueDocumentDropPlugin } from './lib/plugins/vue-document-drop';
2123
import { create as createVueDocumentHighlightsPlugin } from './lib/plugins/vue-document-highlights';
@@ -51,10 +53,12 @@ export function createVueLanguageServicePlugins(
5153
createJsonPlugin(),
5254
createPugFormatPlugin(),
5355
createTypeScriptDocCommentTemplatePlugin(ts),
56+
createTypescriptSemanticTokensPlugin(getTsPluginClient),
5457
createTypeScriptSyntacticPlugin(ts),
5558
createVueAutoAddSpacePlugin(),
5659
createVueAutoDotValuePlugin(ts, getTsPluginClient),
5760
createVueCompilerDomErrorsPlugin(),
61+
createVueComponentSemanticTokensPlugin(getTsPluginClient),
5862
createVueDocumentDropPlugin(ts, getTsPluginClient),
5963
createVueDocumentLinksPlugin(),
6064
createVueDirectiveCommentsPlugin(),
@@ -77,9 +81,5 @@ export function createVueLanguageServicePlugins(
7781
if (tsPluginClient) {
7882
plugins.push(createVueDocumentHighlightsPlugin(tsPluginClient.getDocumentHighlights));
7983
}
80-
for (const plugin of plugins) {
81-
// avoid affecting TS plugin
82-
delete plugin.capabilities.semanticTokensProvider;
83-
}
8484
return plugins;
8585
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
2+
import { VueVirtualCode } from '@vue/language-core';
3+
import { convertClassificationsToSemanticTokens } from 'volar-service-typescript/lib/semanticFeatures/semanticTokens';
4+
import { URI } from 'vscode-uri';
5+
6+
export function create(
7+
getTsPluginClient?: (
8+
context: LanguageServiceContext,
9+
) => import('@vue/typescript-plugin/lib/requests').Requests | undefined,
10+
): LanguageServicePlugin {
11+
return {
12+
name: 'typescript-highlights',
13+
capabilities: {
14+
semanticTokensProvider: {
15+
legend: {
16+
tokenTypes: [
17+
'namespace',
18+
'class',
19+
'enum',
20+
'interface',
21+
'typeParameter',
22+
'type',
23+
'parameter',
24+
'variable',
25+
'property',
26+
'enumMember',
27+
'function',
28+
'method',
29+
],
30+
tokenModifiers: [
31+
'declaration',
32+
'readonly',
33+
'static',
34+
'async',
35+
'defaultLibrary',
36+
'local',
37+
],
38+
},
39+
},
40+
},
41+
create(context) {
42+
const tsPluginClient = getTsPluginClient?.(context);
43+
44+
return {
45+
async provideDocumentSemanticTokens(document, range, legend) {
46+
const uri = URI.parse(document.uri);
47+
const decoded = context.decodeEmbeddedDocumentUri(uri);
48+
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
49+
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
50+
if (!sourceScript?.generated || virtualCode?.id !== 'main') {
51+
return;
52+
}
53+
54+
const root = sourceScript.generated.root;
55+
if (!(root instanceof VueVirtualCode)) {
56+
return;
57+
}
58+
59+
const start = document.offsetAt(range.start);
60+
const end = document.offsetAt(range.end);
61+
const span = {
62+
start: start,
63+
length: end - start,
64+
};
65+
const classifications = await tsPluginClient?.getEncodedSemanticClassifications(
66+
root.fileName,
67+
span,
68+
);
69+
70+
if (classifications) {
71+
return convertClassificationsToSemanticTokens(document, span, legend, classifications);
72+
}
73+
},
74+
};
75+
},
76+
};
77+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from '@volar/language-service';
2+
import { forEachElementNode, hyphenateTag, VueVirtualCode } from '@vue/language-core';
3+
import type * as ts from 'typescript';
4+
import { URI } from 'vscode-uri';
5+
6+
export function create(
7+
getTsPluginClient?: (
8+
context: LanguageServiceContext,
9+
) => import('@vue/typescript-plugin/lib/requests').Requests | undefined,
10+
): LanguageServicePlugin {
11+
return {
12+
name: 'vue-component-highlights',
13+
capabilities: {
14+
semanticTokensProvider: {
15+
legend: {
16+
tokenTypes: ['component'],
17+
tokenModifiers: [],
18+
},
19+
},
20+
},
21+
create(context) {
22+
const tsPluginClient = getTsPluginClient?.(context);
23+
24+
return {
25+
async provideDocumentSemanticTokens(document, range, legend) {
26+
const uri = URI.parse(document.uri);
27+
const decoded = context.decodeEmbeddedDocumentUri(uri);
28+
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
29+
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
30+
if (!sourceScript?.generated || virtualCode?.id !== 'template') {
31+
return;
32+
}
33+
34+
const root = sourceScript.generated.root;
35+
if (!(root instanceof VueVirtualCode)) {
36+
return;
37+
}
38+
39+
const { template } = root.sfc;
40+
if (!template) {
41+
return;
42+
}
43+
44+
const result: SemanticToken[] = [];
45+
46+
const tokenTypes = legend.tokenTypes.indexOf('component');
47+
const componentSpans = await getComponentSpans(root.fileName, template, {
48+
start: document.offsetAt(range.start),
49+
length: document.offsetAt(range.end) - document.offsetAt(range.start),
50+
});
51+
52+
for (const span of componentSpans) {
53+
const position = document.positionAt(span.start);
54+
result.push([
55+
position.line,
56+
position.character,
57+
span.length,
58+
tokenTypes,
59+
0,
60+
]);
61+
}
62+
return result;
63+
},
64+
};
65+
66+
async function getComponentSpans(
67+
fileName: string,
68+
template: NonNullable<VueVirtualCode['_sfc']['template']>,
69+
spanTemplateRange: ts.TextSpan,
70+
) {
71+
const result: ts.TextSpan[] = [];
72+
const validComponentNames = await tsPluginClient?.getComponentNames(fileName) ?? [];
73+
const elements = new Set(await tsPluginClient?.getElementNames(fileName) ?? []);
74+
const components = new Set([
75+
...validComponentNames,
76+
...validComponentNames.map(hyphenateTag),
77+
]);
78+
79+
if (template.ast) {
80+
for (const node of forEachElementNode(template.ast)) {
81+
if (
82+
node.loc.end.offset <= spanTemplateRange.start
83+
|| node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length)
84+
) {
85+
continue;
86+
}
87+
if (components.has(node.tag) && !elements.has(node.tag)) {
88+
let start = node.loc.start.offset;
89+
if (template.lang === 'html') {
90+
start += '<'.length;
91+
}
92+
result.push({
93+
start,
94+
length: node.tag.length,
95+
});
96+
if (template.lang === 'html' && !node.isSelfClosing) {
97+
result.push({
98+
start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag),
99+
length: node.tag.length,
100+
});
101+
}
102+
}
103+
}
104+
}
105+
return result;
106+
}
107+
},
108+
};
109+
}

packages/language-service/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121
"@vue/language-core": "3.0.1",
2222
"@vue/shared": "^3.5.0",
2323
"path-browserify": "^1.0.1",
24-
"volar-service-css": "0.0.64",
25-
"volar-service-emmet": "0.0.64",
26-
"volar-service-html": "0.0.64",
27-
"volar-service-json": "0.0.64",
28-
"volar-service-pug": "0.0.64",
29-
"volar-service-pug-beautify": "0.0.64",
30-
"volar-service-typescript": "0.0.64",
24+
"volar-service-css": "0.0.65",
25+
"volar-service-emmet": "0.0.65",
26+
"volar-service-html": "0.0.65",
27+
"volar-service-json": "0.0.65",
28+
"volar-service-pug": "0.0.65",
29+
"volar-service-pug-beautify": "0.0.65",
30+
"volar-service-typescript": "0.0.65",
3131
"vscode-html-languageservice": "^5.2.0",
3232
"vscode-uri": "^3.0.8"
3333
},

packages/typescript-plugin/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export = createLanguageServicePlugin(
9393
session.addProtocolHandler('_vue:documentHighlights-full', ({ arguments: args }) => {
9494
return (session as any).handlers.get('documentHighlights-full')?.({ arguments: args });
9595
});
96+
session.addProtocolHandler('_vue:encodedSemanticClassifications-full', ({ arguments: args }) => {
97+
return (session as any).handlers.get('encodedSemanticClassifications-full')?.({ arguments: args });
98+
});
9699
session.addProtocolHandler('_vue:quickinfo', ({ arguments: args }) => {
97100
return (session as any).handlers.get('quickinfo')?.({ arguments: args });
98101
});

0 commit comments

Comments
 (0)