Skip to content

Commit 4b8fa93

Browse files
refactor(language-service): re-implement component tag semantic tokens in TypeScript plugin (#3915)
1 parent a918fcd commit 4b8fa93

File tree

7 files changed

+82
-122
lines changed

7 files changed

+82
-122
lines changed

extensions/vscode/package.json

-26
Original file line numberDiff line numberDiff line change
@@ -85,32 +85,6 @@
8585
"url": "./dist/schemas/vue-tsconfig.deprecated.schema.json"
8686
}
8787
],
88-
"semanticTokenScopes": [
89-
{
90-
"language": "vue",
91-
"scopes": {
92-
"component": [
93-
"support.class.component.vue"
94-
]
95-
}
96-
},
97-
{
98-
"language": "markdown",
99-
"scopes": {
100-
"component": [
101-
"support.class.component.vue"
102-
]
103-
}
104-
},
105-
{
106-
"language": "html",
107-
"scopes": {
108-
"component": [
109-
"support.class.component.vue"
110-
]
111-
}
112-
}
113-
],
11488
"languages": [
11589
{
11690
"id": "vue",

extensions/vscode/src/nodeClientMain.ts

+1-28
Original file line numberDiff line numberDiff line change
@@ -106,31 +106,7 @@ export async function activate(context: vscode.ExtensionContext) {
106106
serverOptions,
107107
clientOptions,
108108
);
109-
client.start().then(() => {
110-
const legend = client.initializeResult?.capabilities.semanticTokensProvider?.legend;
111-
if (!legend) {
112-
console.error('Server does not support semantic tokens');
113-
return;
114-
}
115-
// When tsserver has provided semantic tokens for the .vue file, VSCode will no longer request semantic tokens from the Vue language server, so it needs to be provided here again.
116-
vscode.languages.registerDocumentSemanticTokensProvider(documentSelector, {
117-
async provideDocumentSemanticTokens(document) {
118-
const tokens = await client.sendRequest(lsp.SemanticTokensRequest.type, {
119-
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
120-
} satisfies lsp.SemanticTokensParams);
121-
return client.protocol2CodeConverter.asSemanticTokens(tokens);
122-
},
123-
}, legend);
124-
vscode.languages.registerDocumentRangeSemanticTokensProvider(documentSelector, {
125-
async provideDocumentRangeSemanticTokens(document, range) {
126-
const tokens = await client.sendRequest(lsp.SemanticTokensRangeRequest.type, {
127-
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
128-
range: client.code2ProtocolConverter.asRange(range),
129-
} satisfies lsp.SemanticTokensRangeParams);
130-
return client.protocol2CodeConverter.asSemanticTokens(tokens);
131-
},
132-
}, legend);
133-
});
109+
client.start();
134110

135111
volarLabs.addLanguageClient(client);
136112

@@ -193,9 +169,6 @@ function updateProviders(client: lsp.LanguageClient) {
193169
capabilities.workspace.fileOperations.willRename = undefined;
194170
}
195171

196-
// TODO: disalbe for now because this break ts plugin semantic tokens
197-
capabilities.semanticTokensProvider = undefined;
198-
199172
return initializeFeatures.call(client, ...args);
200173
};
201174
}

packages/language-server/node.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ let tsdk: ReturnType<typeof loadTsdkByPath>;
1515

1616
connection.listen();
1717

18-
connection.onInitialize(params => {
18+
connection.onInitialize(async params => {
1919

2020
const options: VueInitializationOptions = params.initializationOptions;
2121

@@ -29,7 +29,7 @@ connection.onInitialize(params => {
2929
}
3030
}
3131

32-
return server.initialize(
32+
const result = await server.initialize(
3333
params,
3434
createSimpleProjectProviderFactory(),
3535
{
@@ -81,6 +81,11 @@ connection.onInitialize(params => {
8181
},
8282
},
8383
);
84+
85+
// handle by tsserver + @vue/typescript-plugin
86+
result.capabilities.semanticTokensProvider = undefined;
87+
88+
return result;
8489
});
8590

8691
connection.onInitialized(() => {

packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function create(ts: typeof import('typescript')): ServicePlugin {
7676
}
7777

7878
function sleep(ms: number) {
79-
return new Promise<void>(resolve => setTimeout(resolve, ms));
79+
return new Promise(resolve => setTimeout(resolve, ms));
8080
}
8181

8282
function isTsDocument(document: TextDocument) {

packages/language-service/lib/plugins/vue-template.ts

-65
Original file line numberDiff line numberDiff line change
@@ -303,71 +303,6 @@ export function create(
303303
];
304304
}
305305
},
306-
307-
async provideDocumentSemanticTokens(document, range, legend, token) {
308-
309-
if (!isSupportedDocument(document))
310-
return;
311-
312-
const result = await baseServiceInstance.provideDocumentSemanticTokens?.(document, range, legend, token) ?? [];
313-
const scanner = getScanner(baseServiceInstance, document);
314-
if (!scanner)
315-
return;
316-
317-
const [virtualCode] = context.documents.getVirtualCodeByUri(document.uri);
318-
if (!virtualCode)
319-
return;
320-
321-
for (const map of context.documents.getMaps(virtualCode)) {
322-
323-
const code = context.language.files.get(map.sourceDocument.uri)?.generated?.code;
324-
if (!(code instanceof VueGeneratedCode))
325-
continue;
326-
327-
const templateScriptData = await namedPipeClient.getComponentNames(code.fileName) ?? [];
328-
const components = new Set([
329-
...templateScriptData,
330-
...templateScriptData.map(hyphenateTag),
331-
]);
332-
const offsetRange = {
333-
start: document.offsetAt(range.start),
334-
end: document.offsetAt(range.end),
335-
};
336-
337-
let token = scanner.scan();
338-
339-
while (token !== html.TokenType.EOS) {
340-
341-
const tokenOffset = scanner.getTokenOffset();
342-
343-
// TODO: fix source map perf and break in while condition
344-
if (tokenOffset > offsetRange.end)
345-
break;
346-
347-
if (tokenOffset >= offsetRange.start && (token === html.TokenType.StartTag || token === html.TokenType.EndTag)) {
348-
349-
const tokenText = scanner.getTokenText();
350-
351-
if (components.has(tokenText) || tokenText.indexOf('.') >= 0) {
352-
353-
const tokenLength = scanner.getTokenLength();
354-
const tokenPosition = document.positionAt(tokenOffset);
355-
356-
if (components.has(tokenText)) {
357-
let tokenType = legend.tokenTypes.indexOf('component');
358-
if (tokenType === -1) {
359-
tokenType = legend.tokenTypes.indexOf('class');
360-
}
361-
result.push([tokenPosition.line, tokenPosition.character, tokenLength, tokenType, 0]);
362-
}
363-
}
364-
}
365-
token = scanner.scan();
366-
}
367-
}
368-
369-
return result;
370-
},
371306
};
372307

373308
async function provideHtmlData(sourceDocumentUri: string, vueCode: VueGeneratedCode) {

packages/typescript-plugin/index.ts

+58
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createFileRegistry, resolveCommonLanguageId } from '@vue/language-core'
55
import { projects } from './lib/utils';
66
import * as vue from '@vue/language-core';
77
import { startNamedPipeServer } from './lib/server';
8+
import { _getComponentNames } from './lib/requests/componentInfos';
89

910
const windowsPathReg = /\\/g;
1011
const externalFiles = new WeakMap<ts.server.Project, string[]>();
@@ -62,6 +63,7 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory {
6263
startNamedPipeServer();
6364

6465
const getCompletionsAtPosition = info.languageService.getCompletionsAtPosition;
66+
const getEncodedSemanticClassifications = info.languageService.getEncodedSemanticClassifications;
6567

6668
info.languageService.getCompletionsAtPosition = (fileName, position, options) => {
6769
const result = getCompletionsAtPosition(fileName, position, options);
@@ -70,6 +72,62 @@ function createLanguageServicePlugin(): ts.server.PluginModuleFactory {
7072
}
7173
return result;
7274
};
75+
info.languageService.getEncodedSemanticClassifications = (fileName, span, format) => {
76+
const result = getEncodedSemanticClassifications(fileName, span, format);
77+
const file = files.get(fileName);
78+
if (
79+
file?.generated?.code instanceof vue.VueGeneratedCode
80+
&& file.generated.code.sfc.template
81+
) {
82+
const validComponentNames = _getComponentNames(ts, info.languageService, file.generated.code, vueOptions);
83+
const components = new Set([
84+
...validComponentNames,
85+
...validComponentNames.map(vue.hyphenateTag),
86+
]);
87+
const { template } = file.generated.code.sfc;
88+
const spanTemplateRange = [
89+
span.start - template.startTagEnd,
90+
span.start + span.length - template.startTagEnd,
91+
] as const;
92+
template.ast?.children.forEach(function visit(node) {
93+
if (node.loc.end.offset <= spanTemplateRange[0] || node.loc.start.offset >= spanTemplateRange[1]) {
94+
return;
95+
}
96+
if (node.type === 1 satisfies vue.CompilerDOM.NodeTypes.ELEMENT) {
97+
if (components.has(node.tag)) {
98+
result.spans.push(
99+
node.loc.start.offset + node.loc.source.indexOf(node.tag) + template.startTagEnd,
100+
node.tag.length,
101+
256, // class
102+
);
103+
if (template.lang === 'html' && !node.isSelfClosing) {
104+
result.spans.push(
105+
node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) + template.startTagEnd,
106+
node.tag.length,
107+
256, // class
108+
);
109+
}
110+
}
111+
for (const child of node.children) {
112+
visit(child);
113+
}
114+
}
115+
else if (node.type === 9 satisfies vue.CompilerDOM.NodeTypes.IF) {
116+
for (const branch of node.branches) {
117+
for (const child of branch.children) {
118+
visit(child);
119+
}
120+
}
121+
}
122+
else if (node.type === 11 satisfies vue.CompilerDOM.NodeTypes.FOR) {
123+
for (const child of node.children) {
124+
visit(child);
125+
}
126+
}
127+
});
128+
}
129+
return result;
130+
};
73131
}
74132

75133
return info.languageService;

packages/typescript-plugin/lib/requests/componentInfos.ts

+15
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ export function getComponentNames(fileName: string) {
200200
?? [];
201201
}
202202

203+
export function _getComponentNames(
204+
ts: typeof import('typescript'),
205+
tsLs: ts.LanguageService,
206+
vueCode: vue.VueGeneratedCode,
207+
vueOptions: vue.VueCompilerOptions,
208+
) {
209+
return getVariableType(ts, tsLs, vueCode, '__VLS_components')
210+
?.type
211+
?.getProperties()
212+
.map(c => c.name)
213+
.filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_'))
214+
.filter(entry => !vueOptions.nativeTags.includes(entry))
215+
?? [];
216+
}
217+
203218
export function getElementAttrs(fileName: string, tagName: string) {
204219
const match = getProject(fileName);
205220
if (!match) {

0 commit comments

Comments
 (0)