From 30de10062a78bbb77081c9b73dea4d5a19009716 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 08:50:41 +0800 Subject: [PATCH 01/16] refactor(typescript-plugin): use named pipe servers more efficiently --- .../language-server/lib/hybridModeProject.ts | 14 +- packages/typescript-plugin/lib/client.ts | 85 +++-- packages/typescript-plugin/lib/server.ts | 129 +++++--- packages/typescript-plugin/lib/utils.ts | 313 ++++++++---------- 4 files changed, 266 insertions(+), 275 deletions(-) diff --git a/packages/language-server/lib/hybridModeProject.ts b/packages/language-server/lib/hybridModeProject.ts index 5a606bd530..6d034bc8bd 100644 --- a/packages/language-server/lib/hybridModeProject.ts +++ b/packages/language-server/lib/hybridModeProject.ts @@ -2,7 +2,7 @@ import type { Language, LanguagePlugin, LanguageServer, LanguageServerProject, P import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject'; import { createLanguage } from '@vue/language-core'; import { createLanguageService, createUriMap, LanguageService } from '@vue/language-service'; -import { getReadyNamedPipePaths, onSomePipeReadyCallbacks, searchNamedPipeServerForFile } from '@vue/typescript-plugin/lib/utils'; +import { configuredServers, getBestServer, inferredServers, onSomePipeReadyCallbacks } from '@vue/typescript-plugin/lib/utils'; import { URI } from 'vscode-uri'; export function createHybridModeProject( @@ -38,16 +38,20 @@ export function createHybridModeProject( }); const end = Date.now() + 60000; const pipeWatcher = setInterval(() => { - getReadyNamedPipePaths(); + for (const server of configuredServers) { + server.update(); + } + for (const server of inferredServers) { + server.update(); + } if (Date.now() > end) { clearInterval(pipeWatcher); } - }, 1000); + }, 2500); }, async getLanguageService(uri) { const fileName = asFileName(uri); - const namedPipeServer = (await searchNamedPipeServerForFile(fileName)); - namedPipeServer?.socket.end(); + const namedPipeServer = await getBestServer(fileName); if (namedPipeServer?.projectInfo?.kind === 1) { const tsconfig = namedPipeServer.projectInfo.name; const tsconfigUri = URI.file(tsconfig); diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index ce8d82db99..32baccbbef 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,40 +1,40 @@ -import type { Request } from './server'; -import { searchNamedPipeServerForFile, sendRequestWorker } from './utils'; +import type { RequestData } from './server'; +import { getBestServer } from './utils'; export function collectExtractProps( ...args: Parameters ) { - return sendRequest>({ - type: 'collectExtractProps', - args, - }); + return sendRequest>( + 'collectExtractProps', + ...args + ); } export async function getImportPathForFile( ...args: Parameters ) { - return await sendRequest>({ - type: 'getImportPathForFile', - args, - }); + return await sendRequest>( + 'getImportPathForFile', + ...args + ); } export async function getPropertiesAtLocation( ...args: Parameters ) { - return await sendRequest>({ - type: 'getPropertiesAtLocation', - args, - }); + return await sendRequest>( + 'getPropertiesAtLocation', + ...args + ); } export function getQuickInfoAtPosition( ...args: Parameters ) { - return sendRequest>({ - type: 'getQuickInfoAtPosition', - args, - }); + return sendRequest>( + 'getQuickInfoAtPosition', + ...args + ); } // Component Infos @@ -42,55 +42,52 @@ export function getQuickInfoAtPosition( export function getComponentProps( ...args: Parameters ) { - return sendRequest>({ - type: 'getComponentProps', - args, - }); + return sendRequest>( + 'getComponentProps', + ...args + ); } export function getComponentEvents( ...args: Parameters ) { - return sendRequest>({ - type: 'getComponentEvents', - args, - }); + return sendRequest>( + 'getComponentEvents', + ...args + ); } export function getTemplateContextProps( ...args: Parameters ) { - return sendRequest>({ - type: 'getTemplateContextProps', - args, - }); + return sendRequest>( + 'getTemplateContextProps', + ...args + ); } export function getComponentNames( ...args: Parameters ) { - return sendRequest>({ - type: 'getComponentNames', - args, - }); + return sendRequest>( + 'getComponentNames', + ...args + ); } export function getElementAttrs( ...args: Parameters ) { - return sendRequest>({ - type: 'getElementAttrs', - args, - }); + return sendRequest>( + 'getElementAttrs', + ...args + ); } -async function sendRequest(request: Request) { - const server = (await searchNamedPipeServerForFile(request.args[0])); +async function sendRequest(requestType: RequestData[1], fileName: string, ...rest: any[]) { + const server = (await getBestServer(fileName)); if (!server) { - console.warn('[Vue Named Pipe Client] No server found for', request.args[0]); return; } - const res = await sendRequestWorker(request, server.socket); - server.socket.end(); - return res; + return server.request(requestType, fileName, ...rest); } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index c3b83c3f5c..d9ce0cb7f4 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -8,9 +8,10 @@ import { getImportPathForFile } from './requests/getImportPathForFile'; import { getPropertiesAtLocation } from './requests/getPropertiesAtLocation'; import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition'; import type { RequestContext } from './requests/types'; -import { connect, getNamedPipePath } from './utils'; +import { getServerPath } from './utils'; -export interface Request { +export type RequestData = [ + seq: number, type: 'containsFile' | 'projectInfo' | 'collectExtractProps' @@ -22,9 +23,15 @@ export interface Request { | 'getComponentEvents' | 'getTemplateContextProps' | 'getComponentNames' - | 'getElementAttrs'; - args: [fileName: string, ...rest: any]; -} + | 'getElementAttrs', + fileName: string, + ...args: any[], +]; + +export type ResponseData = [ + seq: number, + data: any, +]; export interface ProjectInfo { name: string; @@ -39,14 +46,9 @@ export async function startNamedPipeServer( projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { const server = net.createServer(connection => { - connection.on('data', data => { - const text = data.toString(); - if (text === 'ping') { - connection.write('pong'); - return; - } - const request: Request = JSON.parse(text); - const fileName = request.args[0]; + connection.on('data', text => { + const json = text.toString(); + const [seq, requestType, ...args]: RequestData = JSON.parse(json); const requestContext: RequestContext = { typescript: ts, languageService: info.languageService, @@ -55,68 +57,70 @@ export async function startNamedPipeServer( isTsPlugin: true, getFileId: (fileName: string) => fileName, }; - if (request.type === 'containsFile') { - sendResponse( - info.project.containsFile(ts.server.toNormalizedPath(fileName)) - ); - } - else if (request.type === 'projectInfo') { + if (requestType === 'projectInfo') { sendResponse({ name: info.project.getProjectName(), kind: info.project.projectKind, currentDirectory: info.project.getCurrentDirectory(), } satisfies ProjectInfo); + return; } - else if (request.type === 'collectExtractProps') { - const result = collectExtractProps.apply(requestContext, request.args as any); + else if (requestType === 'containsFile') { + sendResponse( + info.project.containsFile(ts.server.toNormalizedPath(args[0])) + ); + } + else if (requestType === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getImportPathForFile') { - const result = getImportPathForFile.apply(requestContext, request.args as any); + else if (requestType === 'getImportPathForFile') { + const result = getImportPathForFile.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(requestContext, request.args as any); + else if (requestType === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(requestContext, request.args as any); + else if (requestType === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, args as any); sendResponse(result); } // Component Infos - else if (request.type === 'getComponentProps') { - const result = getComponentProps.apply(requestContext, request.args as any); + else if (requestType === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getComponentEvents') { - const result = getComponentEvents.apply(requestContext, request.args as any); + else if (requestType === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(requestContext, request.args as any); + else if (requestType === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getComponentNames') { - const result = getComponentNames.apply(requestContext, request.args as any); + else if (requestType === 'getComponentNames') { + const result = getComponentNames.apply(requestContext, args as any); sendResponse(result); } - else if (request.type === 'getElementAttrs') { - const result = getElementAttrs.apply(requestContext, request.args as any); + else if (requestType === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, args as any); sendResponse(result); } else { - console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + console.warn('[Vue Named Pipe Server] Unknown request:', json); + debugger; + } + + function sendResponse(data: any | undefined) { + connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); } }); connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message)); - - function sendResponse(data: any | undefined) { - connection.write(JSON.stringify(data ?? null) + '\n\n'); - } }); - for (let i = 0; i < 20; i++) { - const path = getNamedPipePath(projectKind, i); + for (let i = 0; i < 10; i++) { + const path = getServerPath(projectKind, i); const socket = await connect(path, 100); if (typeof socket === 'object') { socket.end(); @@ -132,6 +136,43 @@ export async function startNamedPipeServer( } } +function connect(namedPipePath: string, timeout?: number) { + return new Promise(resolve => { + const socket = net.connect(namedPipePath); + if (timeout) { + socket.setTimeout(timeout); + } + const onConnect = () => { + cleanup(); + resolve(socket); + }; + const onError = (err: any) => { + if (err.code === 'ECONNREFUSED') { + try { + console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); + fs.promises.unlink(namedPipePath); + } catch { } + } + cleanup(); + resolve('error'); + socket.end(); + }; + const onTimeout = () => { + cleanup(); + resolve('timeout'); + socket.end(); + }; + const cleanup = () => { + socket.off('connect', onConnect); + socket.off('error', onError); + socket.off('timeout', onTimeout); + }; + socket.on('connect', onConnect); + socket.on('error', onError); + socket.on('timeout', onTimeout); + }); +} + function tryListen(server: net.Server, namedPipePath: string) { return new Promise(resolve => { const onSuccess = () => { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index ebc318aec5..ac3d5c93e2 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -3,7 +3,7 @@ import * as net from 'node:net'; import * as os from 'node:os'; import * as path from 'node:path'; import type * as ts from 'typescript'; -import type { ProjectInfo, Request } from './server'; +import type { ProjectInfo, RequestData, ResponseData } from './server'; export { TypeScriptProjectHost } from '@volar/typescript'; @@ -20,194 +20,176 @@ const toFullPath = (file: string) => { return pipeDir + '/' + file; } }; -const configuredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-configured-`); -const inferredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-inferred-`); -const pipes = new Map(); -export const onSomePipeReadyCallbacks: (() => void)[] = []; - -function waitingForNamedPipeServerReady(namedPipePath: string) { - const socket = net.connect(namedPipePath); - const start = Date.now(); - socket.on('connect', () => { - console.log('[Vue Named Pipe Client] Connected:', namedPipePath, 'in', (Date.now() - start) + 'ms'); - socket.write('ping'); - }); - socket.on('data', () => { - console.log('[Vue Named Pipe Client] Ready:', namedPipePath, 'in', (Date.now() - start) + 'ms'); - pipes.set(namedPipePath, 'ready'); - socket.end(); - onSomePipeReadyCallbacks.forEach(cb => cb()); - }); - socket.on('error', err => { - if ((err as any).code === 'ECONNREFUSED') { - try { - console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); - fs.promises.unlink(namedPipePath); - } catch { } - } - pipes.delete(namedPipePath); - socket.end(); - }); - socket.on('timeout', () => { - pipes.delete(namedPipePath); - socket.end(); - }); +export function getServerPath(kind: ts.server.ProjectKind, id: number) { + if (kind === 1 satisfies ts.server.ProjectKind.Configured) { + return toFullPath(`vue-named-pipe-${version}-configured-${id}`); + } else { + return toFullPath(`vue-named-pipe-${version}-inferred-${id}`); + } } -export function getNamedPipePath(projectKind: ts.server.ProjectKind.Configured | ts.server.ProjectKind.Inferred, key: number) { - return projectKind === 1 satisfies ts.server.ProjectKind.Configured - ? `${configuredNamedPipePathPrefix}${key}` - : `${inferredNamedPipePathPrefix}${key}`; -} +class NamedPipeServer { + path: string; + connecting = false; + projectInfo?: ProjectInfo; + containsFileRequests = new Map>(); -export function getReadyNamedPipePaths() { - const configuredPipes: string[] = []; - const inferredPipes: string[] = []; - for (let i = 0; i < 20; i++) { - const configuredPipe = getNamedPipePath(1 satisfies ts.server.ProjectKind.Configured, i); - const inferredPipe = getNamedPipePath(0 satisfies ts.server.ProjectKind.Inferred, i); - if (pipes.get(configuredPipe) === 'ready') { - configuredPipes.push(configuredPipe); - } - else if (!pipes.has(configuredPipe)) { - pipes.set(configuredPipe, 'unknown'); - waitingForNamedPipeServerReady(configuredPipe); - } - if (pipes.get(inferredPipe) === 'ready') { - inferredPipes.push(inferredPipe); - } - else if (!pipes.has(inferredPipe)) { - pipes.set(inferredPipe, 'unknown'); - waitingForNamedPipeServerReady(inferredPipe); + constructor(kind: ts.server.ProjectKind, id: number) { + this.path = getServerPath(kind, id); + } + + containsFile(fileName: string) { + if (this.projectInfo) { + const containsFile = this.request('containsFile', fileName) + ?? this.request('containsFile', fileName); + this.containsFileRequests.set(fileName, containsFile); + return containsFile; } } - return { - configured: configuredPipes, - inferred: inferredPipes, - }; -} -export function connect(namedPipePath: string, timeout?: number) { - return new Promise(resolve => { - const socket = net.connect(namedPipePath); - if (timeout) { - socket.setTimeout(timeout); + update() { + if (!this.connecting && !this.projectInfo) { + this.connecting = true; + this.connect(); } - const onConnect = () => { - cleanup(); - resolve(socket); - }; - const onError = (err: any) => { - if (err.code === 'ECONNREFUSED') { + } + + connect() { + this.socket = net.connect(this.path); + this.socket.on('data', this.onData.bind(this)); + this.socket.on('connect', async () => { + const projectInfo = await this.request('projectInfo', ''); + if (projectInfo) { + console.log('TSServer project ready:', projectInfo.name); + this.projectInfo = projectInfo; + this.containsFileRequests.clear(); + } else { + this.close(); + } + }); + this.socket.on('error', err => { + if ((err as any).code === 'ECONNREFUSED') { try { - console.log('[Vue Named Pipe Client] Deleting:', namedPipePath); - fs.promises.unlink(namedPipePath); + console.log('Deleteing invalid named pipe file:', this.path); + fs.promises.unlink(this.path); } catch { } } - pipes.delete(namedPipePath); - cleanup(); - resolve('error'); - socket.end(); - }; - const onTimeout = () => { - cleanup(); - resolve('timeout'); - socket.end(); - }; - const cleanup = () => { - socket.off('connect', onConnect); - socket.off('error', onError); - socket.off('timeout', onTimeout); - }; - socket.on('connect', onConnect); - socket.on('error', onError); - socket.on('timeout', onTimeout); - }); -} + this.close(); + }); + this.socket.on('timeout', () => { + this.close(); + }); + } -export async function searchNamedPipeServerForFile(fileName: string) { - const paths = await getReadyNamedPipePaths(); + close() { + this.connecting = false; + this.projectInfo = undefined; + this.socket?.end(); + } - const configuredServers = (await Promise.all( - paths.configured.map(async path => { - // Find existing servers - const socket = await connect(path); - if (typeof socket !== 'object') { - return; + socket?: net.Socket; + seq = 0; + dataChunks: Buffer[] = []; + requestHandlers: Map void> = new Map(); + + onData(chunk: Buffer) { + this.dataChunks.push(chunk); + const data = Buffer.concat(this.dataChunks); + const text = data.toString(); + if (text.endsWith('\n\n')) { + const results = text.split('\n\n'); + for (let result of results) { + result = result.trim(); + if (!result) { + continue; + } + try { + const [seq, res]: ResponseData = JSON.parse(result.trim()); + this.requestHandlers.get(seq)?.(res); + } catch (e) { + console.error('JSON parse error:', e); + } } + } + } - // Find servers containing the current file - const containsFile = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, socket); - if (!containsFile) { - socket.end(); - return; - } + request(requestType: RequestData[1], fileName: string, ...args: any[]) { + return new Promise(resolve => { + const seq = this.seq++; + this.requestHandlers.set(seq, data => { + this.requestHandlers.delete(seq); + resolve(data); + }); + this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); + }); + } +} + +export const configuredServers: NamedPipeServer[] = []; +export const inferredServers: NamedPipeServer[] = []; + +for (let i = 0; i < 10; i++) { + configuredServers.push(new NamedPipeServer(1 satisfies ts.server.ProjectKind.Configured, i)); + inferredServers.push(new NamedPipeServer(0 satisfies ts.server.ProjectKind.Inferred, i)); +} + +export const onSomePipeReadyCallbacks: (() => void)[] = []; - // Get project info for each server - const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket); +export async function getBestServer(fileName: string) { + for (const server of configuredServers) { + server.update(); + } + + let servers = (await Promise.all( + configuredServers.map(async server => { + const projectInfo = server.projectInfo; if (!projectInfo) { - socket.end(); return; } - - return { - socket, - projectInfo, - }; + const containsFile = await server.containsFile(fileName); + if (!containsFile) { + return; + } + return server; }) )).filter(server => !!server); // Sort servers by tsconfig - configuredServers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo.name, b.projectInfo.name)); + servers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo!.name, b.projectInfo!.name)); - if (configuredServers.length) { - // Close all but the first server - for (let i = 1; i < configuredServers.length; i++) { - configuredServers[i].socket.end(); - } + if (servers.length) { // Return the first server - return configuredServers[0]; + return servers[0]; } - const inferredServers = (await Promise.all( - paths.inferred.map(async namedPipePath => { - // Find existing servers - const socket = await connect(namedPipePath); - if (typeof socket !== 'object') { - return; - } + for (const server of inferredServers) { + server.update(); + } - // Get project info for each server - const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket); + servers = (await Promise.all( + inferredServers.map(server => { + const projectInfo = server.projectInfo; if (!projectInfo) { - socket.end(); return; } - // Check if the file is in the project's directory - if (!path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) { - return { - socket, - projectInfo, - }; + if (path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) { + return; } + return server; }) )).filter(server => !!server); // Sort servers by directory - inferredServers.sort((a, b) => - b.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length - - a.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length + servers.sort((a, b) => + b.projectInfo!.currentDirectory.replace(/\\/g, '/').split('/').length + - a.projectInfo!.currentDirectory.replace(/\\/g, '/').split('/').length ); - if (inferredServers.length) { - // Close all but the first server - for (let i = 1; i < inferredServers.length; i++) { - inferredServers[i].socket.end(); - } + if (servers.length) { // Return the first server - return inferredServers[0]; + return servers[0]; } } @@ -232,36 +214,3 @@ function isFileInDir(fileName: string, dir: string) { const relative = path.relative(dir, fileName); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); } - -export function sendRequestWorker(request: Request, socket: net.Socket) { - return new Promise(resolve => { - let dataChunks: Buffer[] = []; - const onData = (chunk: Buffer) => { - dataChunks.push(chunk); - const data = Buffer.concat(dataChunks); - const text = data.toString(); - if (text.endsWith('\n\n')) { - let json = null; - try { - json = JSON.parse(text); - } catch (e) { - console.error('[Vue Named Pipe Client] Failed to parse response:', text); - } - cleanup(); - resolve(json); - } - }; - const onError = (err: any) => { - console.error('[Vue Named Pipe Client] Error:', err.message); - cleanup(); - resolve(undefined); - }; - const cleanup = () => { - socket.off('data', onData); - socket.off('error', onError); - }; - socket.on('data', onData); - socket.on('error', onError); - socket.write(JSON.stringify(request)); - }); -} From 686d7a4524a1c619e94dfc0734ad2b776b4369e3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 08:59:49 +0800 Subject: [PATCH 02/16] Update server.ts --- packages/typescript-plugin/lib/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index d9ce0cb7f4..b0bd25b31d 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -63,7 +63,6 @@ export async function startNamedPipeServer( kind: info.project.projectKind, currentDirectory: info.project.getCurrentDirectory(), } satisfies ProjectInfo); - return; } else if (requestType === 'containsFile') { sendResponse( From acb7a9493a146cacdac3756a259f353f3ffcae65 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 09:02:06 +0800 Subject: [PATCH 03/16] onServerReady --- packages/language-server/lib/hybridModeProject.ts | 4 ++-- packages/typescript-plugin/lib/utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/language-server/lib/hybridModeProject.ts b/packages/language-server/lib/hybridModeProject.ts index 6d034bc8bd..e9747126bd 100644 --- a/packages/language-server/lib/hybridModeProject.ts +++ b/packages/language-server/lib/hybridModeProject.ts @@ -2,7 +2,7 @@ import type { Language, LanguagePlugin, LanguageServer, LanguageServerProject, P import { createLanguageServiceEnvironment } from '@volar/language-server/lib/project/simpleProject'; import { createLanguage } from '@vue/language-core'; import { createLanguageService, createUriMap, LanguageService } from '@vue/language-service'; -import { configuredServers, getBestServer, inferredServers, onSomePipeReadyCallbacks } from '@vue/typescript-plugin/lib/utils'; +import { configuredServers, getBestServer, inferredServers, onServerReady } from '@vue/typescript-plugin/lib/utils'; import { URI } from 'vscode-uri'; export function createHybridModeProject( @@ -24,7 +24,7 @@ export function createHybridModeProject( const project: LanguageServerProject = { setup(_server) { server = _server; - onSomePipeReadyCallbacks.push(() => { + onServerReady.push(() => { server.languageFeatures.requestRefresh(false); }); server.fileWatcher.onDidChangeWatchedFiles(({ changes }) => { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index ac3d5c93e2..d8735fa531 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -64,6 +64,7 @@ class NamedPipeServer { console.log('TSServer project ready:', projectInfo.name); this.projectInfo = projectInfo; this.containsFileRequests.clear(); + onServerReady.forEach(cb => cb()); } else { this.close(); } @@ -128,14 +129,13 @@ class NamedPipeServer { export const configuredServers: NamedPipeServer[] = []; export const inferredServers: NamedPipeServer[] = []; +export const onServerReady: (() => void)[] = []; for (let i = 0; i < 10; i++) { configuredServers.push(new NamedPipeServer(1 satisfies ts.server.ProjectKind.Configured, i)); inferredServers.push(new NamedPipeServer(0 satisfies ts.server.ProjectKind.Inferred, i)); } -export const onSomePipeReadyCallbacks: (() => void)[] = []; - export async function getBestServer(fileName: string) { for (const server of configuredServers) { server.update(); From dfb2a78e62c4f4192434a21b3dd39cfcd63c4bc7 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 14:42:29 +0800 Subject: [PATCH 04/16] Update utils.ts --- packages/typescript-plugin/lib/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index d8735fa531..621ac1338a 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -7,7 +7,10 @@ import type { ProjectInfo, RequestData, ResponseData } from './server'; export { TypeScriptProjectHost } from '@volar/typescript'; -const { version } = require('../package.json'); +let { version } = require('../package.json'); +if (version === '2.1.10') { + version += '-dev'; +} const platform = os.platform(); const pipeDir = platform === 'win32' ? `\\\\.\\pipe` From 88e0d4d39c3020e368dc37b9931c9742eab8d292 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 15:26:59 +0800 Subject: [PATCH 05/16] fix --- packages/typescript-plugin/lib/server.ts | 148 +++++++++++++---------- packages/typescript-plugin/lib/utils.ts | 22 +++- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index b0bd25b31d..27fae26659 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -45,74 +45,90 @@ export async function startNamedPipeServer( language: Language, projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { + const dataChunks: Buffer[] = []; const server = net.createServer(connection => { - connection.on('data', text => { - const json = text.toString(); - const [seq, requestType, ...args]: RequestData = JSON.parse(json); - const requestContext: RequestContext = { - typescript: ts, - languageService: info.languageService, - languageServiceHost: info.languageServiceHost, - language: language, - isTsPlugin: true, - getFileId: (fileName: string) => fileName, - }; - if (requestType === 'projectInfo') { - sendResponse({ - name: info.project.getProjectName(), - kind: info.project.projectKind, - currentDirectory: info.project.getCurrentDirectory(), - } satisfies ProjectInfo); - } - else if (requestType === 'containsFile') { - sendResponse( - info.project.containsFile(ts.server.toNormalizedPath(args[0])) - ); - } - else if (requestType === 'collectExtractProps') { - const result = collectExtractProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getImportPathForFile') { - const result = getImportPathForFile.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(requestContext, args as any); - sendResponse(result); - } - // Component Infos - else if (requestType === 'getComponentProps') { - const result = getComponentProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getComponentEvents') { - const result = getComponentEvents.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getComponentNames') { - const result = getComponentNames.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getElementAttrs') { - const result = getElementAttrs.apply(requestContext, args as any); - sendResponse(result); - } - else { - console.warn('[Vue Named Pipe Server] Unknown request:', json); - debugger; - } + connection.on('data', buffer => { + dataChunks.push(buffer); + const text = dataChunks.toString(); + if (text.endsWith('\n\n')) { + dataChunks.length = 0; + const requests = text.split('\n\n'); + for (let json of requests) { + json = json.trim(); + if (!json) { + continue; + } + try { + const [seq, requestType, ...args]: RequestData = JSON.parse(json); + const requestContext: RequestContext = { + typescript: ts, + languageService: info.languageService, + languageServiceHost: info.languageServiceHost, + language: language, + isTsPlugin: true, + getFileId: (fileName: string) => fileName, + }; + if (requestType === 'projectInfo') { + sendResponse({ + name: info.project.getProjectName(), + kind: info.project.projectKind, + currentDirectory: info.project.getCurrentDirectory(), + } satisfies ProjectInfo); + } + else if (requestType === 'containsFile') { + sendResponse( + info.project.containsFile(ts.server.toNormalizedPath(args[0])) + ); + } + else if (requestType === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getImportPathForFile') { + const result = getImportPathForFile.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, args as any); + sendResponse(result); + } + // Component Infos + else if (requestType === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentNames') { + const result = getComponentNames.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, args as any); + sendResponse(result); + } + else { + console.warn('[Vue Named Pipe Server] Unknown request:', json); + debugger; + } - function sendResponse(data: any | undefined) { - connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); + function sendResponse(data: any | undefined) { + connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); + } + } catch (e) { + console.error('[Vue Named Pipe Server] JSON parse error:', e); + } + } } }); connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message)); diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 621ac1338a..d1a48208ba 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -44,8 +44,14 @@ class NamedPipeServer { containsFile(fileName: string) { if (this.projectInfo) { - const containsFile = this.request('containsFile', fileName) - ?? this.request('containsFile', fileName); + const containsFile = this.containsFileRequests.get(fileName) + ?? (async () => { + const res = await this.request('containsFile', fileName); + if (typeof res !== 'boolean') { + this.containsFileRequests.delete(fileName); + } + return res; + })(); this.containsFileRequests.set(fileName, containsFile); return containsFile; } @@ -102,6 +108,7 @@ class NamedPipeServer { const data = Buffer.concat(this.dataChunks); const text = data.toString(); if (text.endsWith('\n\n')) { + this.dataChunks.length = 0; const results = text.split('\n\n'); for (let result of results) { result = result.trim(); @@ -119,12 +126,21 @@ class NamedPipeServer { } request(requestType: RequestData[1], fileName: string, ...args: any[]) { - return new Promise(resolve => { + return new Promise(resolve => { const seq = this.seq++; + // console.time(`[${seq}] ${requestType} ${fileName}`); this.requestHandlers.set(seq, data => { + // console.timeEnd(`[${seq}] ${requestType} ${fileName}`); this.requestHandlers.delete(seq); resolve(data); }); + setTimeout(() => { + if (this.requestHandlers.has(seq)) { + console.error(`[${seq}] ${requestType} ${fileName} timeout`); + this.requestHandlers.delete(seq); + resolve(undefined); + } + }, 5000); this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); }); } From 447d97ce23f404e43dd73414b1283966a8b3a2ea Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 16:16:48 +0800 Subject: [PATCH 06/16] Update utils.ts --- packages/typescript-plugin/lib/utils.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index d1a48208ba..19b5784003 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -13,22 +13,14 @@ if (version === '2.1.10') { } const platform = os.platform(); const pipeDir = platform === 'win32' - ? `\\\\.\\pipe` - : `/tmp`; -const toFullPath = (file: string) => { - if (platform === 'win32') { - return pipeDir + '\\' + file; - } - else { - return pipeDir + '/' + file; - } -}; + ? `\\\\.\\pipe\\` + : `/tmp/`; export function getServerPath(kind: ts.server.ProjectKind, id: number) { if (kind === 1 satisfies ts.server.ProjectKind.Configured) { - return toFullPath(`vue-named-pipe-${version}-configured-${id}`); + return `${pipeDir}vue-named-pipe-${version}-configured-${id}`; } else { - return toFullPath(`vue-named-pipe-${version}-inferred-${id}`); + return `${pipeDir}vue-named-pipe-${version}-inferred-${id}`; } } From 56e17be1b83caf354c609cedf56d4d13d0390523 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 22:18:43 +0800 Subject: [PATCH 07/16] wip --- packages/typescript-plugin/lib/client.ts | 15 +- packages/typescript-plugin/lib/server.ts | 194 ++++++++++++++--------- packages/typescript-plugin/lib/utils.ts | 53 +++++-- 3 files changed, 172 insertions(+), 90 deletions(-) diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 32baccbbef..264589a5f3 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -66,13 +66,12 @@ export function getTemplateContextProps( ); } -export function getComponentNames( - ...args: Parameters -) { - return sendRequest>( - 'getComponentNames', - ...args - ); +export async function getComponentNames(fileName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + return server.getComponentNames(fileName); } export function getElementAttrs( @@ -85,7 +84,7 @@ export function getElementAttrs( } async function sendRequest(requestType: RequestData[1], fileName: string, ...rest: any[]) { - const server = (await getBestServer(fileName)); + const server = await getBestServer(fileName); if (!server) { return; } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 27fae26659..d0697cda41 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -10,9 +10,7 @@ import { getQuickInfoAtPosition } from './requests/getQuickInfoAtPosition'; import type { RequestContext } from './requests/types'; import { getServerPath } from './utils'; -export type RequestData = [ - seq: number, - type: 'containsFile' +export type RequestType = 'containsFile' | 'projectInfo' | 'collectExtractProps' | 'getImportPathForFile' @@ -22,8 +20,13 @@ export type RequestData = [ | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' - | 'getComponentNames' - | 'getElementAttrs', + // | 'getComponentNames' + | 'getElementAttrs' + | 'subscribeComponentNames'; + +export type RequestData = [ + seq: number, + type: RequestType, fileName: string, ...args: any[], ]; @@ -33,6 +36,12 @@ export type ResponseData = [ data: any, ]; +export type NotificationData = [ + type: 'componentNamesUpdated', + fileName: string, + data: any, +]; + export interface ProjectInfo { name: string; kind: ts.server.ProjectKind; @@ -45,7 +54,18 @@ export async function startNamedPipeServer( language: Language, projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { + let lastProjectVersion = info.project.getProjectVersion(); + + const requestContext: RequestContext = { + typescript: ts, + languageService: info.languageService, + languageServiceHost: info.languageServiceHost, + language: language, + isTsPlugin: true, + getFileId: (fileName: string) => fileName, + }; const dataChunks: Buffer[] = []; + const componentNamesSubscriptions = new Map, Set]>(); const server = net.createServer(connection => { connection.on('data', buffer => { dataChunks.push(buffer); @@ -59,72 +79,7 @@ export async function startNamedPipeServer( continue; } try { - const [seq, requestType, ...args]: RequestData = JSON.parse(json); - const requestContext: RequestContext = { - typescript: ts, - languageService: info.languageService, - languageServiceHost: info.languageServiceHost, - language: language, - isTsPlugin: true, - getFileId: (fileName: string) => fileName, - }; - if (requestType === 'projectInfo') { - sendResponse({ - name: info.project.getProjectName(), - kind: info.project.projectKind, - currentDirectory: info.project.getCurrentDirectory(), - } satisfies ProjectInfo); - } - else if (requestType === 'containsFile') { - sendResponse( - info.project.containsFile(ts.server.toNormalizedPath(args[0])) - ); - } - else if (requestType === 'collectExtractProps') { - const result = collectExtractProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getImportPathForFile') { - const result = getImportPathForFile.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(requestContext, args as any); - sendResponse(result); - } - // Component Infos - else if (requestType === 'getComponentProps') { - const result = getComponentProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getComponentEvents') { - const result = getComponentEvents.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getComponentNames') { - const result = getComponentNames.apply(requestContext, args as any); - sendResponse(result); - } - else if (requestType === 'getElementAttrs') { - const result = getElementAttrs.apply(requestContext, args as any); - sendResponse(result); - } - else { - console.warn('[Vue Named Pipe Server] Unknown request:', json); - debugger; - } - - function sendResponse(data: any | undefined) { - connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); - } + onRequest(connection, JSON.parse(json)); } catch (e) { console.error('[Vue Named Pipe Server] JSON parse error:', e); } @@ -149,6 +104,103 @@ export async function startNamedPipeServer( break; } } + + setInterval(() => { + const projectVersion = info.project.getProjectVersion(); + if (lastProjectVersion !== projectVersion) { + lastProjectVersion = projectVersion; + onProjectUpdate(); + } + }, 500); + + function onProjectUpdate() { + for (const [fileName, [oldComponentNames, subscriptions]] of componentNamesSubscriptions) { + const connections = [...subscriptions].filter(connection => !connection.destroyed); + if (connections.length) { + const script = info.project.getScriptInfo(fileName); + if (script?.isScriptOpen()) { + const newComponentNames = getComponentNames.apply(requestContext, [fileName]); + if (JSON.stringify(oldComponentNames) !== JSON.stringify(newComponentNames)) { + // Update cache + componentNamesSubscriptions.set(fileName, [newComponentNames, subscriptions]); + // Notify + for (const connection of connections) { + notify(connection, 'componentNamesUpdated', fileName, newComponentNames); + } + } + } + } + } + } + + function notify(connection: net.Socket, type: NotificationData[0], fileName: string, data: any) { + connection.write(JSON.stringify([type, fileName, data] satisfies NotificationData) + '\n\n'); + } + + function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { + if (requestType === 'projectInfo') { + sendResponse({ + name: info.project.getProjectName(), + kind: info.project.projectKind, + currentDirectory: info.project.getCurrentDirectory(), + } satisfies ProjectInfo); + } + else if (requestType === 'containsFile') { + sendResponse( + info.project.containsFile(ts.server.toNormalizedPath(args[0])) + ); + } + else if (requestType === 'collectExtractProps') { + const result = collectExtractProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getImportPathForFile') { + const result = getImportPathForFile.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getPropertiesAtLocation') { + const result = getPropertiesAtLocation.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getQuickInfoAtPosition') { + const result = getQuickInfoAtPosition.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentProps') { + const result = getComponentProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getComponentEvents') { + const result = getComponentEvents.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getTemplateContextProps') { + const result = getTemplateContextProps.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'getElementAttrs') { + const result = getElementAttrs.apply(requestContext, args as any); + sendResponse(result); + } + else if (requestType === 'subscribeComponentNames') { + let subscriptions = componentNamesSubscriptions.get(args[0]); + if (!subscriptions) { + const result = getComponentNames.apply(requestContext, args as any); + subscriptions = [result, new Set()]; + componentNamesSubscriptions.set(args[0], subscriptions); + } + subscriptions[1].add(connection); + sendResponse(subscriptions[0]); + } + else { + console.warn('[Vue Named Pipe Server] Unknown request:', requestType); + debugger; + } + + function sendResponse(data: any | undefined) { + connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); + } + } } function connect(namedPipePath: string, timeout?: number) { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 19b5784003..24af273a53 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -3,7 +3,7 @@ import * as net from 'node:net'; import * as os from 'node:os'; import * as path from 'node:path'; import type * as ts from 'typescript'; -import type { ProjectInfo, RequestData, ResponseData } from './server'; +import type { NotificationData, ProjectInfo, RequestData, ResponseData } from './server'; export { TypeScriptProjectHost } from '@volar/typescript'; @@ -28,7 +28,8 @@ class NamedPipeServer { path: string; connecting = false; projectInfo?: ProjectInfo; - containsFileRequests = new Map>(); + containsFileCache = new Map>(); + componentNamesCache = new Map>(); constructor(kind: ts.server.ProjectKind, id: number) { this.path = getServerPath(kind, id); @@ -36,16 +37,33 @@ class NamedPipeServer { containsFile(fileName: string) { if (this.projectInfo) { - const containsFile = this.containsFileRequests.get(fileName) - ?? (async () => { + if (!this.containsFileCache.has(fileName)) { + this.containsFileCache.set(fileName, (async () => { const res = await this.request('containsFile', fileName); if (typeof res !== 'boolean') { - this.containsFileRequests.delete(fileName); + // If the request fails, delete the cache + this.containsFileCache.delete(fileName); } return res; - })(); - this.containsFileRequests.set(fileName, containsFile); - return containsFile; + })()); + } + return this.containsFileCache.get(fileName); + } + } + + getComponentNames(fileName: string) { + if (this.projectInfo) { + if (!this.componentNamesCache.has(fileName)) { + this.componentNamesCache.set(fileName, (async () => { + const res = await this.request('subscribeComponentNames', fileName); + if (!res) { + // If the request fails, delete the cache + this.componentNamesCache.delete(fileName); + } + return res; + })()); + } + return this.componentNamesCache.get(fileName); } } @@ -64,7 +82,7 @@ class NamedPipeServer { if (projectInfo) { console.log('TSServer project ready:', projectInfo.name); this.projectInfo = projectInfo; - this.containsFileRequests.clear(); + this.containsFileCache.clear(); onServerReady.forEach(cb => cb()); } else { this.close(); @@ -108,8 +126,14 @@ class NamedPipeServer { continue; } try { - const [seq, res]: ResponseData = JSON.parse(result.trim()); - this.requestHandlers.get(seq)?.(res); + const data: ResponseData | NotificationData = JSON.parse(result.trim()); + if (typeof data[0] === 'number') { + const [seq, res] = data; + this.requestHandlers.get(seq)?.(res); + } else { + const [type, fileName, res] = data; + this.onNotification(type, fileName, res); + } } catch (e) { console.error('JSON parse error:', e); } @@ -117,6 +141,13 @@ class NamedPipeServer { } } + onNotification(type: string, fileName: string, data: any) { + // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`); + if (type === 'componentNamesUpdated') { + this.componentNamesCache.set(fileName, data); + } + } + request(requestType: RequestData[1], fileName: string, ...args: any[]) { return new Promise(resolve => { const seq = this.seq++; From ca33241a21732c3aae761061ed9a477412ca6fe9 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 23:39:54 +0800 Subject: [PATCH 08/16] wip --- .../lib/plugins/vue-template.ts | 8 +++-- packages/typescript-plugin/lib/client.ts | 23 +++++++----- .../lib/requests/componentInfos.ts | 27 +++++++------- packages/typescript-plugin/lib/server.ts | 35 +++++++++++++------ packages/typescript-plugin/lib/utils.ts | 16 +++++---- 5 files changed, 69 insertions(+), 40 deletions(-) diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index ae6f2bbf7f..54f4db2638 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -242,7 +242,9 @@ export function create( ? tagName : components.find(component => component === tagName || hyphenateTag(component) === tagName); if (checkTag) { - componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(code.fileName, checkTag, true) ?? []).map(prop => prop.name); + componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(code.fileName, checkTag) ?? []) + .filter(prop => prop.required) + .map(prop => prop.name); current = { unburnedRequiredProps: [...componentProps[checkTag]], labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), @@ -469,7 +471,7 @@ export function create( const promises: Promise[] = []; const tagInfos = new Map(); @@ -1010,7 +1012,7 @@ function parseLabel(label: string) { return { name, leadingSlash - } + }; } function generateItemKey(type: InternalItemId, tag: string, prop: string) { diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 264589a5f3..242af9b650 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -39,13 +39,16 @@ export function getQuickInfoAtPosition( // Component Infos -export function getComponentProps( - ...args: Parameters -) { - return sendRequest>( - 'getComponentProps', - ...args - ); +export async function getComponentProps(fileName: string, componentName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + const componentAndProps = await server.getAllComponentAndProps(fileName); + if (!componentAndProps) { + return; + } + return componentAndProps[componentName]; } export function getComponentEvents( @@ -71,7 +74,11 @@ export async function getComponentNames(fileName: string) { if (!server) { return; } - return server.getComponentNames(fileName); + const componentAndProps = await server.getAllComponentAndProps(fileName); + if (!componentAndProps) { + return; + } + return Object.keys(componentAndProps); } export function getElementAttrs( diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index c345b33cc0..d1fa344292 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -6,8 +6,7 @@ import type { RequestContext } from './types'; export function getComponentProps( this: RequestContext, fileName: string, - tag: string, - requiredOnly = false + tag: string ) { const { typescript: ts, language, languageService, getFileId } = this; const volarFile = language.scripts.get(getFileId(fileName)); @@ -47,7 +46,11 @@ export function getComponentProps( } } - const result = new Map(); + const result = new Map(); for (const sig of componentType.getCallSignatures()) { const propParam = sig.parameters[0]; @@ -55,12 +58,11 @@ export function getComponentProps( const propsType = checker.getTypeOfSymbolAtLocation(propParam, components.node); const props = propsType.getProperties(); for (const prop of props) { - if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) { - const name = prop.name; - const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()); + const name = prop.name; + const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined; + const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined; - result.set(name, { name, commentMarkdown }); - } + result.set(name, { name, required, commentMarkdown }); } } } @@ -75,12 +77,11 @@ export function getComponentProps( if (prop.flags & ts.SymbolFlags.Method) { // #2443 continue; } - if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) { - const name = prop.name; - const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()); + const name = prop.name; + const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined; + const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined; - result.set(name, { name, commentMarkdown }); - } + result.set(name, { name, required, commentMarkdown }); } } } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index d0697cda41..6705c68f4a 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -22,7 +22,7 @@ export type RequestType = 'containsFile' | 'getTemplateContextProps' // | 'getComponentNames' | 'getElementAttrs' - | 'subscribeComponentNames'; + | 'subscribeAllComponentAndProps'; export type RequestData = [ seq: number, @@ -37,7 +37,7 @@ export type ResponseData = [ ]; export type NotificationData = [ - type: 'componentNamesUpdated', + type: 'componentAndPropsUpdated', fileName: string, data: any, ]; @@ -65,7 +65,7 @@ export async function startNamedPipeServer( getFileId: (fileName: string) => fileName, }; const dataChunks: Buffer[] = []; - const componentNamesSubscriptions = new Map, Set]>(); + const componentNamesSubscriptions = new Map, Set]>(); const server = net.createServer(connection => { connection.on('data', buffer => { dataChunks.push(buffer); @@ -114,18 +114,18 @@ export async function startNamedPipeServer( }, 500); function onProjectUpdate() { - for (const [fileName, [oldComponentNames, subscriptions]] of componentNamesSubscriptions) { + for (const [fileName, [oldData, subscriptions]] of componentNamesSubscriptions) { const connections = [...subscriptions].filter(connection => !connection.destroyed); if (connections.length) { const script = info.project.getScriptInfo(fileName); if (script?.isScriptOpen()) { - const newComponentNames = getComponentNames.apply(requestContext, [fileName]); - if (JSON.stringify(oldComponentNames) !== JSON.stringify(newComponentNames)) { + const newData = getComponentAndProps.apply(requestContext, [fileName]); + if (JSON.stringify(oldData) !== JSON.stringify(newData)) { // Update cache - componentNamesSubscriptions.set(fileName, [newComponentNames, subscriptions]); + componentNamesSubscriptions.set(fileName, [newData, subscriptions]); // Notify for (const connection of connections) { - notify(connection, 'componentNamesUpdated', fileName, newComponentNames); + notify(connection, 'componentAndPropsUpdated', fileName, newData); } } } @@ -182,10 +182,10 @@ export async function startNamedPipeServer( const result = getElementAttrs.apply(requestContext, args as any); sendResponse(result); } - else if (requestType === 'subscribeComponentNames') { + else if (requestType === 'subscribeAllComponentAndProps') { let subscriptions = componentNamesSubscriptions.get(args[0]); if (!subscriptions) { - const result = getComponentNames.apply(requestContext, args as any); + const result = getComponentAndProps.apply(requestContext, args as any); subscriptions = [result, new Set()]; componentNamesSubscriptions.set(args[0], subscriptions); } @@ -201,6 +201,21 @@ export async function startNamedPipeServer( connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); } } + + function getComponentAndProps(fileName: string) { + const result: Record = {}; + for (const component of getComponentNames.apply(requestContext, [fileName]) ?? []) { + const props = getComponentProps.apply(requestContext, [fileName, component]); + if (props) { + result[component] = props; + } + } + return result; + } } function connect(namedPipePath: string, timeout?: number) { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 24af273a53..3f681cae71 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -29,7 +29,7 @@ class NamedPipeServer { connecting = false; projectInfo?: ProjectInfo; containsFileCache = new Map>(); - componentNamesCache = new Map>(); + componentNamesCache = new Map(); constructor(kind: ts.server.ProjectKind, id: number) { this.path = getServerPath(kind, id); @@ -51,11 +51,11 @@ class NamedPipeServer { } } - getComponentNames(fileName: string) { + getAllComponentAndProps(fileName: string) { if (this.projectInfo) { if (!this.componentNamesCache.has(fileName)) { this.componentNamesCache.set(fileName, (async () => { - const res = await this.request('subscribeComponentNames', fileName); + const res = await this.request('subscribeAllComponentAndProps', fileName); if (!res) { // If the request fails, delete the cache this.componentNamesCache.delete(fileName); @@ -63,7 +63,11 @@ class NamedPipeServer { return res; })()); } - return this.componentNamesCache.get(fileName); + return this.componentNamesCache.get(fileName) as Promise>; } } @@ -141,9 +145,9 @@ class NamedPipeServer { } } - onNotification(type: string, fileName: string, data: any) { + onNotification(type: NotificationData[0], fileName: string, data: any) { // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`); - if (type === 'componentNamesUpdated') { + if (type === 'componentAndPropsUpdated') { this.componentNamesCache.set(fileName, data); } } From cd1049ac8c15f23777f1ef069fd8941aa24dbb08 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Thu, 19 Dec 2024 23:48:10 +0800 Subject: [PATCH 09/16] Update utils.ts --- packages/typescript-plugin/lib/utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 3f681cae71..02cbc0778f 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -161,13 +161,13 @@ class NamedPipeServer { this.requestHandlers.delete(seq); resolve(data); }); - setTimeout(() => { - if (this.requestHandlers.has(seq)) { - console.error(`[${seq}] ${requestType} ${fileName} timeout`); - this.requestHandlers.delete(seq); - resolve(undefined); - } - }, 5000); + // setTimeout(() => { + // if (this.requestHandlers.has(seq)) { + // console.error(`[${seq}] ${requestType} ${fileName} timeout`); + // this.requestHandlers.delete(seq); + // resolve(undefined); + // } + // }, 5000); this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); }); } From 38737e37088a71d14bb84819e17d0bce59858703 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 05:12:10 +0800 Subject: [PATCH 10/16] wip --- packages/typescript-plugin/lib/server.ts | 46 +++++++++++++++++------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 6705c68f4a..dd11753812 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -65,7 +65,7 @@ export async function startNamedPipeServer( getFileId: (fileName: string) => fileName, }; const dataChunks: Buffer[] = []; - const componentNamesSubscriptions = new Map, Set]>(); + const componentNamesSubscriptions = new Map>, Set]>(); const server = net.createServer(connection => { connection.on('data', buffer => { dataChunks.push(buffer); @@ -105,21 +105,35 @@ export async function startNamedPipeServer( } } - setInterval(() => { - const projectVersion = info.project.getProjectVersion(); - if (lastProjectVersion !== projectVersion) { - lastProjectVersion = projectVersion; - onProjectUpdate(); + updateWhile(); + + async function updateWhile() { + while (true) { + const projectVersion = info.project.getProjectVersion(); + if (lastProjectVersion !== projectVersion) { + lastProjectVersion = projectVersion; + await onProjectUpdate(); + } + await sleep(500); } - }, 500); + } + + async function onProjectUpdate() { + const token = info.languageServiceHost.getCancellationToken?.(); - function onProjectUpdate() { for (const [fileName, [oldData, subscriptions]] of componentNamesSubscriptions) { const connections = [...subscriptions].filter(connection => !connection.destroyed); if (connections.length) { const script = info.project.getScriptInfo(fileName); if (script?.isScriptOpen()) { - const newData = getComponentAndProps.apply(requestContext, [fileName]); + await sleep(0); + if (token?.isCancellationRequested()) { + return; + } + const newData = await getComponentAndProps.apply(requestContext, [fileName, token]); + if (token?.isCancellationRequested()) { + return; + } if (JSON.stringify(oldData) !== JSON.stringify(newData)) { // Update cache componentNamesSubscriptions.set(fileName, [newData, subscriptions]); @@ -133,11 +147,15 @@ export async function startNamedPipeServer( } } + function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + function notify(connection: net.Socket, type: NotificationData[0], fileName: string, data: any) { connection.write(JSON.stringify([type, fileName, data] satisfies NotificationData) + '\n\n'); } - function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { + async function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { if (requestType === 'projectInfo') { sendResponse({ name: info.project.getProjectName(), @@ -185,7 +203,7 @@ export async function startNamedPipeServer( else if (requestType === 'subscribeAllComponentAndProps') { let subscriptions = componentNamesSubscriptions.get(args[0]); if (!subscriptions) { - const result = getComponentAndProps.apply(requestContext, args as any); + const result = await getComponentAndProps.apply(requestContext, args as any); subscriptions = [result, new Set()]; componentNamesSubscriptions.set(args[0], subscriptions); } @@ -202,13 +220,17 @@ export async function startNamedPipeServer( } } - function getComponentAndProps(fileName: string) { + async function getComponentAndProps(fileName: string, token?: ts.HostCancellationToken) { const result: Record = {}; for (const component of getComponentNames.apply(requestContext, [fileName]) ?? []) { + await sleep(0); + if (token?.isCancellationRequested()) { + return; + } const props = getComponentProps.apply(requestContext, [fileName, component]); if (props) { result[component] = props; From 6074d373253717b39cad1be220e03dccdea87844 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 05:15:33 +0800 Subject: [PATCH 11/16] Update server.ts --- packages/typescript-plugin/lib/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index dd11753812..ec18db7091 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -65,7 +65,7 @@ export async function startNamedPipeServer( getFileId: (fileName: string) => fileName, }; const dataChunks: Buffer[] = []; - const componentNamesSubscriptions = new Map>, Set]>(); + const componentNamesSubscriptions = new Map | Awaited>, Set]>(); const server = net.createServer(connection => { connection.on('data', buffer => { dataChunks.push(buffer); @@ -203,12 +203,12 @@ export async function startNamedPipeServer( else if (requestType === 'subscribeAllComponentAndProps') { let subscriptions = componentNamesSubscriptions.get(args[0]); if (!subscriptions) { - const result = await getComponentAndProps.apply(requestContext, args as any); + const result = getComponentAndProps.apply(requestContext, args as any); subscriptions = [result, new Set()]; componentNamesSubscriptions.set(args[0], subscriptions); } subscriptions[1].add(connection); - sendResponse(subscriptions[0]); + sendResponse(await subscriptions[0]); } else { console.warn('[Vue Named Pipe Server] Unknown request:', requestType); From 8009f6bc187a82feb85f20ee8a1184c29463a881 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 05:17:00 +0800 Subject: [PATCH 12/16] Update utils.ts --- packages/typescript-plugin/lib/utils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 02cbc0778f..7b873f33fb 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -161,13 +161,6 @@ class NamedPipeServer { this.requestHandlers.delete(seq); resolve(data); }); - // setTimeout(() => { - // if (this.requestHandlers.has(seq)) { - // console.error(`[${seq}] ${requestType} ${fileName} timeout`); - // this.requestHandlers.delete(seq); - // resolve(undefined); - // } - // }, 5000); this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); }); } From 939002ff31024effb0b5250b7f4bc033b4f3f56f Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 05:17:13 +0800 Subject: [PATCH 13/16] Update server.ts --- packages/typescript-plugin/lib/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index ec18db7091..9970bdd103 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -20,7 +20,6 @@ export type RequestType = 'containsFile' | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' - // | 'getComponentNames' | 'getElementAttrs' | 'subscribeAllComponentAndProps'; From 968999d10bf6e2b0b3415ebeee9661c243bef2d3 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 05:19:09 +0800 Subject: [PATCH 14/16] Update server.ts --- packages/typescript-plugin/lib/server.ts | 40 +++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 9970bdd103..df82799d48 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -122,25 +122,27 @@ export async function startNamedPipeServer( for (const [fileName, [oldData, subscriptions]] of componentNamesSubscriptions) { const connections = [...subscriptions].filter(connection => !connection.destroyed); - if (connections.length) { - const script = info.project.getScriptInfo(fileName); - if (script?.isScriptOpen()) { - await sleep(0); - if (token?.isCancellationRequested()) { - return; - } - const newData = await getComponentAndProps.apply(requestContext, [fileName, token]); - if (token?.isCancellationRequested()) { - return; - } - if (JSON.stringify(oldData) !== JSON.stringify(newData)) { - // Update cache - componentNamesSubscriptions.set(fileName, [newData, subscriptions]); - // Notify - for (const connection of connections) { - notify(connection, 'componentAndPropsUpdated', fileName, newData); - } - } + if (!connections.length) { + continue; + } + const script = info.project.getScriptInfo(fileName); + if (!script?.isScriptOpen()) { + continue; + } + await sleep(0); + if (token?.isCancellationRequested()) { + return; + } + const newData = await getComponentAndProps.apply(requestContext, [fileName, token]); + if (token?.isCancellationRequested()) { + return; + } + if (JSON.stringify(oldData) !== JSON.stringify(newData)) { + // Update cache + componentNamesSubscriptions.set(fileName, [newData, subscriptions]); + // Notify + for (const connection of connections) { + notify(connection, 'componentAndPropsUpdated', fileName, newData); } } } From 1021e631272fe7e10437cd2e69991a41e37dc621 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 16:47:04 +0800 Subject: [PATCH 15/16] Update server.ts --- packages/typescript-plugin/lib/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index df82799d48..548fb47f7e 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -129,7 +129,7 @@ export async function startNamedPipeServer( if (!script?.isScriptOpen()) { continue; } - await sleep(0); + await sleep(10); if (token?.isCancellationRequested()) { return; } @@ -228,7 +228,7 @@ export async function startNamedPipeServer( commentMarkdown?: string; }[]> = {}; for (const component of getComponentNames.apply(requestContext, [fileName]) ?? []) { - await sleep(0); + await sleep(10); if (token?.isCancellationRequested()) { return; } From 5d77adfca0bbb1ae2b83ae1e88f7d268b9570f7d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 20:51:32 +0800 Subject: [PATCH 16/16] done --- packages/typescript-plugin/lib/client.ts | 4 +- packages/typescript-plugin/lib/server.ts | 119 +++++++++++------------ packages/typescript-plugin/lib/utils.ts | 28 ++---- 3 files changed, 66 insertions(+), 85 deletions(-) diff --git a/packages/typescript-plugin/lib/client.ts b/packages/typescript-plugin/lib/client.ts index 242af9b650..04a5f777b1 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -44,7 +44,7 @@ export async function getComponentProps(fileName: string, componentName: string) if (!server) { return; } - const componentAndProps = await server.getAllComponentAndProps(fileName); + const componentAndProps = await server.componentNamesAndProps.get(fileName); if (!componentAndProps) { return; } @@ -74,7 +74,7 @@ export async function getComponentNames(fileName: string) { if (!server) { return; } - const componentAndProps = await server.getAllComponentAndProps(fileName); + const componentAndProps = server.componentNamesAndProps.get(fileName); if (!componentAndProps) { return; } diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 548fb47f7e..f618f8f5c8 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -20,8 +20,7 @@ export type RequestType = 'containsFile' | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' - | 'getElementAttrs' - | 'subscribeAllComponentAndProps'; + | 'getElementAttrs'; export type RequestData = [ seq: number, @@ -53,7 +52,7 @@ export async function startNamedPipeServer( language: Language, projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { - let lastProjectVersion = info.project.getProjectVersion(); + let lastProjectVersion: string | undefined; const requestContext: RequestContext = { typescript: ts, @@ -64,8 +63,14 @@ export async function startNamedPipeServer( getFileId: (fileName: string) => fileName, }; const dataChunks: Buffer[] = []; - const componentNamesSubscriptions = new Map | Awaited>, Set]>(); + const componentNamesAndProps = new Map(); + const allConnections = new Set(); const server = net.createServer(connection => { + allConnections.add(connection); + + connection.on('end', () => { + allConnections.delete(connection); + }); connection.on('data', buffer => { dataChunks.push(buffer); const text = dataChunks.toString(); @@ -86,6 +91,10 @@ export async function startNamedPipeServer( } }); connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message)); + + for (const [fileName, data] of componentNamesAndProps) { + notify(connection, 'componentAndPropsUpdated', fileName, data); + } }); for (let i = 0; i < 10; i++) { @@ -108,43 +117,60 @@ export async function startNamedPipeServer( async function updateWhile() { while (true) { + await sleep(500); const projectVersion = info.project.getProjectVersion(); - if (lastProjectVersion !== projectVersion) { - lastProjectVersion = projectVersion; - await onProjectUpdate(); + if (lastProjectVersion === projectVersion) { + continue; } - await sleep(500); - } - } - - async function onProjectUpdate() { - const token = info.languageServiceHost.getCancellationToken?.(); - - for (const [fileName, [oldData, subscriptions]] of componentNamesSubscriptions) { - const connections = [...subscriptions].filter(connection => !connection.destroyed); + const connections = [...allConnections].filter(c => !c.destroyed); if (!connections.length) { continue; } - const script = info.project.getScriptInfo(fileName); - if (!script?.isScriptOpen()) { + const token = info.languageServiceHost.getCancellationToken?.(); + const openedScriptInfos = info.project.getRootScriptInfos().filter(info => info.isScriptOpen()); + if (!openedScriptInfos.length) { continue; } - await sleep(10); - if (token?.isCancellationRequested()) { - return; - } - const newData = await getComponentAndProps.apply(requestContext, [fileName, token]); - if (token?.isCancellationRequested()) { - return; - } - if (JSON.stringify(oldData) !== JSON.stringify(newData)) { - // Update cache - componentNamesSubscriptions.set(fileName, [newData, subscriptions]); - // Notify - for (const connection of connections) { - notify(connection, 'componentAndPropsUpdated', fileName, newData); + for (const scriptInfo of openedScriptInfos) { + await sleep(10); + if (token?.isCancellationRequested()) { + break; + } + let newData: Record | undefined = {}; + const componentNames = getComponentNames.apply(requestContext, [scriptInfo.fileName]); + // const testProps = getComponentProps.apply(requestContext, [scriptInfo.fileName, 'HelloWorld']); + // debugger; + for (const component of componentNames ?? []) { + await sleep(10); + if (token?.isCancellationRequested()) { + newData = undefined; + break; + } + const props = getComponentProps.apply(requestContext, [scriptInfo.fileName, component]); + if (props) { + newData[component] = props; + } + } + if (!newData) { + // Canceled + break; + } + const oldDataJson = componentNamesAndProps.get(scriptInfo.fileName); + const newDataJson = JSON.stringify(newData); + if (oldDataJson !== newDataJson) { + // Update cache + componentNamesAndProps.set(scriptInfo.fileName, newDataJson); + // Notify + for (const connection of connections) { + notify(connection, 'componentAndPropsUpdated', scriptInfo.fileName, newData); + } } } + lastProjectVersion = projectVersion; } } @@ -201,16 +227,6 @@ export async function startNamedPipeServer( const result = getElementAttrs.apply(requestContext, args as any); sendResponse(result); } - else if (requestType === 'subscribeAllComponentAndProps') { - let subscriptions = componentNamesSubscriptions.get(args[0]); - if (!subscriptions) { - const result = getComponentAndProps.apply(requestContext, args as any); - subscriptions = [result, new Set()]; - componentNamesSubscriptions.set(args[0], subscriptions); - } - subscriptions[1].add(connection); - sendResponse(await subscriptions[0]); - } else { console.warn('[Vue Named Pipe Server] Unknown request:', requestType); debugger; @@ -220,25 +236,6 @@ export async function startNamedPipeServer( connection.write(JSON.stringify([seq, data ?? null]) + '\n\n'); } } - - async function getComponentAndProps(fileName: string, token?: ts.HostCancellationToken) { - const result: Record = {}; - for (const component of getComponentNames.apply(requestContext, [fileName]) ?? []) { - await sleep(10); - if (token?.isCancellationRequested()) { - return; - } - const props = getComponentProps.apply(requestContext, [fileName, component]); - if (props) { - result[component] = props; - } - } - return result; - } } function connect(namedPipePath: string, timeout?: number) { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index 7b873f33fb..72315e49b4 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -29,7 +29,11 @@ class NamedPipeServer { connecting = false; projectInfo?: ProjectInfo; containsFileCache = new Map>(); - componentNamesCache = new Map(); + componentNamesAndProps = new Map>(); constructor(kind: ts.server.ProjectKind, id: number) { this.path = getServerPath(kind, id); @@ -51,26 +55,6 @@ class NamedPipeServer { } } - getAllComponentAndProps(fileName: string) { - if (this.projectInfo) { - if (!this.componentNamesCache.has(fileName)) { - this.componentNamesCache.set(fileName, (async () => { - const res = await this.request('subscribeAllComponentAndProps', fileName); - if (!res) { - // If the request fails, delete the cache - this.componentNamesCache.delete(fileName); - } - return res; - })()); - } - return this.componentNamesCache.get(fileName) as Promise>; - } - } - update() { if (!this.connecting && !this.projectInfo) { this.connecting = true; @@ -148,7 +132,7 @@ class NamedPipeServer { onNotification(type: NotificationData[0], fileName: string, data: any) { // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`); if (type === 'componentAndPropsUpdated') { - this.componentNamesCache.set(fileName, data); + this.componentNamesAndProps.set(fileName, data); } }