diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 36f3d92551..7b9ca77de1 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -1,6 +1,7 @@ import { createLabsInfo } from '@volar/vscode'; import * as lsp from '@volar/vscode/node'; import * as protocol from '@vue/language-server/protocol'; +import type { Requests } from '@vue/typescript-plugin/lib/requests/index'; import * as fs from 'node:fs'; import { defineExtension, executeCommand, extensionContext, onDeactivate } from 'reactive-vscode'; import * as vscode from 'vscode'; @@ -86,7 +87,38 @@ export const { activate, deactivate } = defineExtension(async () => { updateProviders(client); - client.onRequest('tsserverRequest', async ([command, args]) => { + client.onRequest('tsserverRequest', executeCommand); + + const cachedData = new Map(); + const allowCacheCommands = new Set<`vue:${keyof Requests}`>([ + 'vue:getComponentNames', + 'vue:getComponentProps', + 'vue:getComponentEvents', + 'vue:getComponentDirectives', + 'vue:getElementNames', + 'vue:getElementAttrs' + ]); + + listenProjectVersion(); + + return client; + + async function listenProjectVersion() { + while (true) { + await sleep(500); + const isProjectUpdated = await executeCommand(['vue:isProjectUpdated', []]); + if (isProjectUpdated === 'yes') { + cachedData.clear(); + } + } + } + + async function executeCommand([command, args]: [string, any[]]) { + const key = command + ':' + JSON.stringify(args); + if (cachedData.has(key)) { + return cachedData.get(key); + } + const tsserver = (globalThis as any).__TSSERVER__?.semantic; if (!tsserver) { return; @@ -98,13 +130,16 @@ export const { activate, deactivate } = defineExtension(async () => { lowPriority: true, requireSemantic: true, })[0]; - return res.body; + const { body } = res; + + if (allowCacheCommands.has(command as any)) { + cachedData.set(key, body); + } + return body; } catch { // noop } - }); - - return client; + } } ); @@ -133,6 +168,10 @@ function updateProviders(client: lsp.LanguageClient) { }; } +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + try { const tsExtension = vscode.extensions.getExtension('vscode.typescript-language-features')!; const readFileSync = fs.readFileSync; diff --git a/packages/language-server/node.ts b/packages/language-server/node.ts index a892205b7b..cdd6d3d7f8 100644 --- a/packages/language-server/node.ts +++ b/packages/language-server/node.ts @@ -91,11 +91,11 @@ connection.onInitialize(params => { collectExtractProps(...args) { return sendTsRequest('vue:collectExtractProps', args); }, - getComponentDirectives(...args) { - return sendTsRequest('vue:getComponentDirectives', args); + getImportPathForFile(...args) { + return sendTsRequest('vue:getImportPathForFile', args); }, - getComponentEvents(...args) { - return sendTsRequest('vue:getComponentEvents', args); + getPropertiesAtLocation(...args) { + return sendTsRequest('vue:getPropertiesAtLocation', args); }, getComponentNames(...args) { return sendTsRequest('vue:getComponentNames', args); @@ -103,18 +103,18 @@ connection.onInitialize(params => { getComponentProps(...args) { return sendTsRequest('vue:getComponentProps', args); }, + getComponentEvents(...args) { + return sendTsRequest('vue:getComponentEvents', args); + }, + getComponentDirectives(...args) { + return sendTsRequest('vue:getComponentDirectives', args); + }, getElementAttrs(...args) { return sendTsRequest('vue:getElementAttrs', args); }, getElementNames(...args) { return sendTsRequest('vue:getElementNames', args); }, - getImportPathForFile(...args) { - return sendTsRequest('vue:getImportPathForFile', args); - }, - getPropertiesAtLocation(...args) { - return sendTsRequest('vue:getPropertiesAtLocation', args); - }, getDocumentHighlights(fileName, position) { return sendTsRequest( 'documentHighlights-full', // internal command diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 01cd1f94df..3577b8419b 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -32,7 +32,7 @@ import { create as createVueTemplatePlugin } from './lib/plugins/vue-template'; import { create as createVueTwoslashQueriesPlugin } from './lib/plugins/vue-twoslash-queries'; import { parse, VueCompilerOptions } from '@vue/language-core'; -import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/common'; +import { proxyLanguageServiceForVue } from '@vue/typescript-plugin/lib/proxy'; import { collectExtractProps } from '@vue/typescript-plugin/lib/requests/collectExtractProps'; import { getComponentDirectives } from '@vue/typescript-plugin/lib/requests/getComponentDirectives'; import { getComponentEvents } from '@vue/typescript-plugin/lib/requests/getComponentEvents'; diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index bcaaf30276..9673089e88 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -244,6 +244,7 @@ export function create( provideTags: () => { if (!components) { promises.push((async () => { + console.log("[VVVIP] service getComponentNames", Date.now()); components = (await tsPluginClient?.getComponentNames(vueCode.fileName) ?? []) .filter(name => name !== 'Transition' @@ -294,17 +295,19 @@ export function create( if (!tagInfo) { promises.push((async () => { - const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? []; - const propInfos = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? []; - const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? []; - const directives = await tsPluginClient?.getComponentDirectives(vueCode.fileName) ?? []; + const [attrs, propInfos, events, directives] = await Promise.all([ + tsPluginClient?.getElementAttrs(vueCode.fileName, tag), + tsPluginClient?.getComponentProps(vueCode.fileName, tag), + tsPluginClient?.getComponentEvents(vueCode.fileName, tag), + tsPluginClient?.getComponentDirectives(vueCode.fileName), + ]); tagInfos.set(tag, { - attrs, - propInfos: propInfos.filter(prop => + attrs: attrs ?? [], + propInfos: propInfos?.filter(prop => !prop.name.startsWith('ref_') - ), - events, - directives, + ) ?? [], + events: events ?? [], + directives: directives ?? [], }); version++; })()); diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index f164f4d77b..e664ca2ba3 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -1,20 +1,11 @@ import { createLanguageServicePlugin } from '@volar/typescript/lib/quickstart/createLanguageServicePlugin'; import * as vue from '@vue/language-core'; import type * as ts from 'typescript'; -import { proxyLanguageServiceForVue } from './lib/common'; -import { collectExtractProps } from './lib/requests/collectExtractProps'; -import { getComponentDirectives } from './lib/requests/getComponentDirectives'; -import { getComponentEvents } from './lib/requests/getComponentEvents'; -import { getComponentNames } from './lib/requests/getComponentNames'; -import { getComponentProps } from './lib/requests/getComponentProps'; -import { getElementAttrs } from './lib/requests/getElementAttrs'; -import { getElementNames } from './lib/requests/getElementNames'; -import { getImportPathForFile } from './lib/requests/getImportPathForFile'; -import { getPropertiesAtLocation } from './lib/requests/getPropertiesAtLocation'; -import type { RequestContext } from './lib/requests/types'; +import { addVueCommands } from './lib/commands'; +import { proxyLanguageServiceForVue } from './lib/proxy'; const windowsPathReg = /\\/g; -const project2Service = new WeakMap(); +const project2Service = new Map(); export = createLanguageServicePlugin( (ts, info) => { @@ -26,8 +17,6 @@ export = createLanguageServicePlugin( id => id ); - addVueCommands(); - return { languagePlugins: [languagePlugin], setup: language => { @@ -35,6 +24,8 @@ export = createLanguageServicePlugin( info.languageService = proxyLanguageServiceForVue(ts, language, info.languageService, vueOptions, fileName => fileName); + addVueCommands(ts, info, project2Service); + // #3963 const timer = setInterval(() => { if (info.project['program']) { @@ -54,97 +45,5 @@ export = createLanguageServicePlugin( return vue.createParsedCommandLineByJson(ts, ts.sys, info.languageServiceHost.getCurrentDirectory(), {}).vueOptions; } } - - // https://github.com/JetBrains/intellij-plugins/blob/6435723ad88fa296b41144162ebe3b8513f4949b/Angular/src-js/angular-service/src/index.ts#L69 - function addVueCommands() { - const projectService = info.project.projectService; - projectService.logger.info("Vue: called handler processing " + info.project.projectKind); - - const session = info.session; - if (session == undefined) { - projectService.logger.info("Vue: there is no session in info."); - return; - } - if (session.addProtocolHandler == undefined) { - // addProtocolHandler was introduced in TS 4.4 or 4.5 in 2021, see https://github.com/microsoft/TypeScript/issues/43893 - projectService.logger.info("Vue: there is no addProtocolHandler method."); - return; - } - if ((session as any).vueCommandsAdded) { - return; - } - - (session as any).vueCommandsAdded = true; - - session.addProtocolHandler('vue:collectExtractProps', ({ arguments: args }) => { - return { - response: collectExtractProps.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getImportPathForFile', ({ arguments: args }) => { - return { - response: getImportPathForFile.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getPropertiesAtLocation', ({ arguments: args }) => { - return { - response: getPropertiesAtLocation.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getComponentNames', ({ arguments: args }) => { - return { - response: getComponentNames.apply(getRequestContext(args[0]), args) ?? [], - }; - }); - session.addProtocolHandler('vue:getComponentProps', ({ arguments: args }) => { - return { - response: getComponentProps.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getComponentEvents', ({ arguments: args }) => { - return { - response: getComponentEvents.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getComponentDirectives', ({ arguments: args }) => { - return { - response: getComponentDirectives.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getElementAttrs', ({ arguments: args }) => { - return { - response: getElementAttrs.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('vue:getElementNames', ({ arguments: args }) => { - return { - response: getElementNames.apply(getRequestContext(args[0]), args), - }; - }); - - projectService.logger.info('Vue specific commands are successfully added.'); - } - - function getRequestContext(fileName: string): RequestContext { - const fileAndProject = (info.session as any).getFileAndProject({ - file: fileName, - projectFileName: undefined, - }) as { - file: ts.server.NormalizedPath; - project: ts.server.Project; - }; - const service = project2Service.get(fileAndProject.project); - if (!service) { - throw 'No RequestContext'; - } - return { - typescript: ts, - languageService: service[2], - languageServiceHost: service[1], - language: service[0], - isTsPlugin: true, - getFileId: (fileName: string) => fileName, - }; - } } ); diff --git a/packages/typescript-plugin/lib/commands.ts b/packages/typescript-plugin/lib/commands.ts new file mode 100644 index 0000000000..e33b883a3c --- /dev/null +++ b/packages/typescript-plugin/lib/commands.ts @@ -0,0 +1,160 @@ +import { FileMap, type IScriptSnapshot, type Language } from '@vue/language-core'; +import type * as ts from 'typescript'; +import { collectExtractProps } from './requests/collectExtractProps'; +import { getComponentDirectives } from './requests/getComponentDirectives'; +import { getComponentEvents } from './requests/getComponentEvents'; +import { getComponentNames } from './requests/getComponentNames'; +import { getComponentProps } from './requests/getComponentProps'; +import { getElementAttrs } from './requests/getElementAttrs'; +import { getElementNames } from './requests/getElementNames'; +import { getImportPathForFile } from './requests/getImportPathForFile'; +import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation'; +import type { RequestContext } from './requests/types'; + +// https://github.com/JetBrains/intellij-plugins/blob/6435723ad88fa296b41144162ebe3b8513f4949b/Angular/src-js/angular-service/src/index.ts#L69 +export function addVueCommands( + ts: typeof import('typescript'), + info: ts.server.PluginCreateInfo, + project2Service: Map +) { + const projectService = info.project.projectService; + projectService.logger.info("Vue: called handler processing " + info.project.projectKind); + + const session = info.session; + if (session == undefined) { + projectService.logger.info("Vue: there is no session in info."); + return; + } + if (session.addProtocolHandler == undefined) { + // addProtocolHandler was introduced in TS 4.4 or 4.5 in 2021, see https://github.com/microsoft/TypeScript/issues/43893 + projectService.logger.info("Vue: there is no addProtocolHandler method."); + return; + } + if ((session as any).vueCommandsAdded) { + return; + } + (session as any).vueCommandsAdded = true; + + interface ScriptInfo { + version: string; + snapshot?: IScriptSnapshot; + } + + const isCaseSensitive = info.languageServiceHost.useCaseSensitiveFileNames?.() ?? false; + let lastProjectVersion: string | undefined; + let scriptInfos = new FileMap(isCaseSensitive); + + session.addProtocolHandler('vue:isProjectUpdated', () => { + const projectVersion = info.project.getProjectVersion(); + if (projectVersion === lastProjectVersion) { + return { response: 'no' }; + } + lastProjectVersion = projectVersion; + + const [, [language, languageServiceHost]] = [...project2Service].find( + ([project]) => project.projectKind === ts.server.ProjectKind.Configured + )!; + + const fileNames = languageServiceHost.getScriptFileNames(); + const infos = new FileMap(isCaseSensitive); + let isAnyScriptVersionChanged = false; + let isAnyScriptSnapshotChanged = false; + + for (const file of fileNames) { + const scriptVersion = languageServiceHost.getScriptVersion(file); + const scriptInfo = scriptInfos.get(file) ?? { version: "" }; + infos.set(file, scriptInfo); + if (scriptInfo.version === scriptVersion) { + continue; + } + scriptInfo.version = scriptVersion; + isAnyScriptVersionChanged = true; + + const volarFile = language.scripts.get(file); + const root = volarFile?.generated?.root; + const serviceScript = volarFile?.generated?.languagePlugin.typescript?.getServiceScript(root!); + if (!serviceScript) { + isAnyScriptSnapshotChanged = true; + continue; + } + + const { snapshot } = serviceScript.code; + if (scriptInfo.snapshot !== snapshot) { + scriptInfo.snapshot = snapshot; + isAnyScriptSnapshotChanged = true; + } + } + scriptInfos = infos; + + return { response: isAnyScriptSnapshotChanged || !isAnyScriptVersionChanged ? 'yes' : 'no' }; + }); + session.addProtocolHandler('vue:collectExtractProps', ({ arguments: args }) => { + return { + response: collectExtractProps.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getImportPathForFile', ({ arguments: args }) => { + return { + response: getImportPathForFile.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getPropertiesAtLocation', ({ arguments: args }) => { + return { + response: getPropertiesAtLocation.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getComponentNames', ({ arguments: args }) => { + return { + response: getComponentNames.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getComponentProps', ({ arguments: args }) => { + return { + response: getComponentProps.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getComponentEvents', ({ arguments: args }) => { + return { + response: getComponentEvents.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getComponentDirectives', ({ arguments: args }) => { + return { + response: getComponentDirectives.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getElementAttrs', ({ arguments: args }) => { + return { + response: getElementAttrs.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('vue:getElementNames', ({ arguments: args }) => { + return { + response: getElementNames.apply(getRequestContext(args[0]), args), + }; + }); + + projectService.logger.info('Vue specific commands are successfully added.'); + + function getRequestContext(fileName: string): RequestContext { + const fileAndProject = (info.session as any).getFileAndProject({ + file: fileName, + projectFileName: undefined, + }) as { + file: ts.server.NormalizedPath; + project: ts.server.Project; + }; + const service = project2Service.get(fileAndProject.project); + if (!service) { + throw 'No RequestContext'; + } + return { + typescript: ts, + languageService: service[2], + languageServiceHost: service[1], + language: service[0], + isTsPlugin: true, + getFileId: (fileName: string) => fileName, + }; + } +} diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/proxy.ts similarity index 100% rename from packages/typescript-plugin/lib/common.ts rename to packages/typescript-plugin/lib/proxy.ts