From 34538b9844cd69976e64a4404945730220ca936c Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 7 Mar 2025 22:23:36 +0800 Subject: [PATCH] fix(language-server): make sure vueCompilerOptions are consistent between language server and ts plugin --- extensions/vscode/src/nodeClientMain.ts | 18 ++- .../language-server/lib/hybridModeProject.ts | 80 ---------- packages/language-server/node.ts | 147 ++++++++++++++---- .../language-server/tests/completions.spec.ts | 10 +- packages/typescript-plugin/index.ts | 51 +++--- 5 files changed, 155 insertions(+), 151 deletions(-) delete mode 100644 packages/language-server/lib/hybridModeProject.ts diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 64a2b7ea0e..3d8824c7f4 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -91,13 +91,17 @@ export const { activate, deactivate } = defineExtension(async () => { if (!tsserver) { return; } - const res = await tsserver.executeImpl(command, args, { - isAsync: true, - expectsResult: true, - lowPriority: true, - requireSemantic: true, - })[0]; - return res.body; + try { + const res = await tsserver.executeImpl(command, args, { + isAsync: true, + expectsResult: true, + lowPriority: true, + requireSemantic: true, + })[0]; + return res.body; + } catch { + // noop + } }); return client; diff --git a/packages/language-server/lib/hybridModeProject.ts b/packages/language-server/lib/hybridModeProject.ts deleted file mode 100644 index 4161ff9b80..0000000000 --- a/packages/language-server/lib/hybridModeProject.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Language, LanguagePlugin, LanguageServer, LanguageServerProject, ProjectContext, ProviderResult } from '@volar/language-server'; -import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject'; -import { createLanguage } from '@vue/language-core'; -import { createLanguageService, createUriMap, LanguageService } from '@vue/language-service'; -import { URI } from 'vscode-uri'; - -export function createHybridModeProject( - create: () => ProviderResult<{ - languagePlugins: LanguagePlugin[]; - setup?(options: { - language: Language; - project: ProjectContext; - }): void; - }> -) { - let simpleLs: Promise | undefined; - let server: LanguageServer; - - const tsconfigProjects = createUriMap>(); - const project: LanguageServerProject = { - setup(_server) { - server = _server; - server.fileWatcher.onDidChangeWatchedFiles(({ changes }) => { - for (const change of changes) { - const changeUri = URI.parse(change.uri); - if (tsconfigProjects.has(changeUri)) { - tsconfigProjects.get(changeUri)?.then(project => project.dispose()); - tsconfigProjects.delete(changeUri); - } - } - }); - }, - async getLanguageService() { - simpleLs ??= createLs(server); - return await simpleLs; - }, - getExistingLanguageServices() { - return Promise.all([ - ...tsconfigProjects.values(), - simpleLs, - ].filter(promise => !!promise)); - }, - reload() { - for (const ls of [ - ...tsconfigProjects.values(), - simpleLs, - ]) { - ls?.then(ls => ls.dispose()); - } - tsconfigProjects.clear(); - simpleLs = undefined; - }, - }; - - return project; - - async function createLs(server: LanguageServer) { - const { languagePlugins, setup } = await create(); - const language = createLanguage([ - { getLanguageId: uri => server.documents.get(uri)?.languageId }, - ...languagePlugins, - ], createUriMap(), uri => { - const document = server.documents.get(uri); - if (document) { - language.scripts.set(uri, document.getSnapshot(), document.languageId); - } - else { - language.scripts.delete(uri); - } - }); - const project: ProjectContext = {}; - setup?.({ language, project }); - return createLanguageService( - language, - server.languageServicePlugins, - createLanguageServiceEnvironment(server, [...server.workspaceFolders.all]), - project - ); - } -} diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index ef95fa749b..f35c5cd91e 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -1,7 +1,10 @@ +import type { LanguageServer } from '@volar/language-server'; +import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject'; import { createConnection, createServer, loadTsdkByPath } from '@volar/language-server/node'; -import { createVueLanguagePlugin, getDefaultCompilerOptions } from '@vue/language-core'; -import { getHybridModeLanguageServicePlugins } from '@vue/language-service'; -import { createHybridModeProject } from './lib/hybridModeProject'; +import { createLanguage, createParsedCommandLine, createVueLanguagePlugin, getDefaultCompilerOptions } from '@vue/language-core'; +import { createLanguageService, createUriMap, getHybridModeLanguageServicePlugins, LanguageService } from '@vue/language-service'; +import type * as ts from 'typescript'; +import { URI } from 'vscode-uri'; import type { VueInitializationOptions } from './lib/types'; const connection = createConnection(); @@ -20,62 +23,138 @@ connection.onInitialize(params => { } const { typescript: ts } = loadTsdkByPath(options.typescript.tsdk, params.locale); + const tsconfigProjects = createUriMap(); + + server.fileWatcher.onDidChangeWatchedFiles(({ changes }) => { + for (const change of changes) { + const changeUri = URI.parse(change.uri); + if (tsconfigProjects.has(changeUri)) { + tsconfigProjects.get(changeUri)!.dispose(); + tsconfigProjects.delete(changeUri); + } + } + }); + + let simpleLs: LanguageService | undefined; + return server.initialize( params, - createHybridModeProject( - () => { - const commandLine = { - vueOptions: getDefaultCompilerOptions(), - options: ts.getDefaultCompilerOptions(), - }; - return { - languagePlugins: [ - createVueLanguagePlugin( - ts, - commandLine.options, - commandLine.vueOptions, - uri => uri.fsPath.replace(/\\/g, '/') - ), - ], - setup({ project }) { - project.vue = { compilerOptions: commandLine.vueOptions }; - }, - }; - } - ), + { + setup() { }, + async getLanguageService(uri) { + if (uri.scheme === 'file' && options.typescript.requestForwardingCommand) { + const fileName = uri.fsPath.replace(/\\/g, '/'); + const projectInfo = await sendTsRequest( + ts.server.protocol.CommandTypes.ProjectInfo, + { + file: fileName, + needFileNameList: false, + } satisfies ts.server.protocol.ProjectInfoRequestArgs + ); + if (projectInfo) { + const { configFileName } = projectInfo; + let ls = tsconfigProjects.get(URI.file(configFileName)); + if (!ls) { + ls = createLs(server, configFileName); + tsconfigProjects.set(URI.file(configFileName), ls); + } + return ls; + } + } + return simpleLs ??= createLs(server, undefined); + }, + getExistingLanguageServices() { + return Promise.all([ + ...tsconfigProjects.values(), + simpleLs, + ].filter(promise => !!promise)); + }, + reload() { + for (const ls of [ + ...tsconfigProjects.values(), + simpleLs, + ]) { + ls?.dispose(); + } + tsconfigProjects.clear(); + simpleLs = undefined; + }, + }, getHybridModeLanguageServicePlugins(ts, options.typescript.requestForwardingCommand ? { collectExtractProps(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:collectExtractProps', args]); + return sendTsRequest('vue:collectExtractProps', args); }, getComponentDirectives(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getComponentDirectives', args]); + return sendTsRequest('vue:getComponentDirectives', args); }, getComponentEvents(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getComponentEvents', args]); + return sendTsRequest('vue:getComponentEvents', args); }, getComponentNames(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getComponentNames', args]); + return sendTsRequest('vue:getComponentNames', args); }, getComponentProps(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getComponentProps', args]); + return sendTsRequest('vue:getComponentProps', args); }, getElementAttrs(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getElementAttrs', args]); + return sendTsRequest('vue:getElementAttrs', args); }, getElementNames(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getElementNames', args]); + return sendTsRequest('vue:getElementNames', args); }, getImportPathForFile(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getImportPathForFile', args]); + return sendTsRequest('vue:getImportPathForFile', args); }, getPropertiesAtLocation(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getPropertiesAtLocation', args]); + return sendTsRequest('vue:getPropertiesAtLocation', args); }, getQuickInfoAtPosition(...args) { - return connection.sendRequest(options.typescript.requestForwardingCommand!, ['vue:getQuickInfoAtPosition', args]); + return sendTsRequest('vue:getQuickInfoAtPosition', args); }, } : undefined) ); + + function sendTsRequest(command: string, args: any): Promise { + return connection.sendRequest(options.typescript.requestForwardingCommand!, [command, args]); + } + + function createLs(server: LanguageServer, tsconfig: string | undefined) { + const commonLine = tsconfig + ? createParsedCommandLine(ts, ts.sys, tsconfig) + : { + options: ts.getDefaultCompilerOptions(), + vueOptions: getDefaultCompilerOptions(), + }; + const language = createLanguage( + [ + { + getLanguageId: uri => server.documents.get(uri)?.languageId, + }, + createVueLanguagePlugin( + ts, + commonLine.options, + commonLine.vueOptions, + uri => uri.fsPath.replace(/\\/g, '/') + ), + ], + createUriMap(), + uri => { + const document = server.documents.get(uri); + if (document) { + language.scripts.set(uri, document.getSnapshot(), document.languageId); + } + else { + language.scripts.delete(uri); + } + } + ); + return createLanguageService( + language, + server.languageServicePlugins, + createLanguageServiceEnvironment(server, [...server.workspaceFolders.all]), + { vue: { compilerOptions: commonLine.vueOptions } } + ); + } }); connection.onInitialized(server.initialized); diff --git a/packages/language-server/tests/completions.spec.ts b/packages/language-server/tests/completions.spec.ts index ac17319dd9..de37946c8c 100644 --- a/packages/language-server/tests/completions.spec.ts +++ b/packages/language-server/tests/completions.spec.ts @@ -50,6 +50,12 @@ test('#4670', async () => { [ "onclick", "ondblclick", + "v-on:auxclick", + "@auxclick", + "v-on:click", + "@click", + "v-on:dblclick", + "@dblclick", ] `); }); @@ -179,6 +185,8 @@ test('HTML tags and built-in components', async () => { "component", "slot", "template", + "BaseTransition", + "Fixture", ] `); }); @@ -244,7 +252,7 @@ test('Slot name', async () => { `, 'default'); }); -test.skip('#2454', async () => { +test('#2454', async () => { await requestCompletionItemToVueServer('fixture.vue', 'vue', `