From 1e408b8d568469ca0966bee18b4bc1508fd12659 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Fri, 20 Dec 2024 04:26:20 +0800 Subject: [PATCH 01/45] fix(language-core): only generate the props it needs in generic components --- .../lib/codegen/script/scriptSetup.ts | 62 ++++++++++--------- .../tsc/tests/__snapshots__/dts.spec.ts.snap | 14 ++--- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index bd4b7345e8..3f2a959ee1 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -66,7 +66,7 @@ export function* generateScriptSetup( } yield `return {} as {${newLine}` - + ` props: ${ctx.localTypes.PrettifyLocal} & __VLS_BuiltInPublicProps,${newLine}` + + ` props: ${ctx.localTypes.PrettifyLocal}<__VLS_OwnProps & __VLS_PublicProps & __VLS_TemplateResult['attrs']> & __VLS_BuiltInPublicProps,${newLine}` + ` expose(exposed: import('${options.vueCompilerOptions.lib}').ShallowUnwrapRef<${scriptSetupRanges.defineExpose ? 'typeof __VLS_exposed' : '{}'}>): void,${newLine}` + ` attrs: any,${newLine}` + ` slots: __VLS_TemplateResult['slots'],${newLine}` @@ -368,37 +368,39 @@ function* generateComponentProps( scriptSetup: NonNullable, scriptSetupRanges: ScriptSetupRanges ): Generator { - yield `const __VLS_fnComponent = (await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; - - if (scriptSetupRanges.defineProps?.arg) { - yield `props: `; - yield generateSfcBlockSection( - scriptSetup, - scriptSetupRanges.defineProps.arg.start, - scriptSetupRanges.defineProps.arg.end, - codeFeatures.navigation - ); - yield `,${newLine}`; + if (scriptSetup.generic) { + yield `const __VLS_fnComponent = (await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; + + if (scriptSetupRanges.defineProps?.arg) { + yield `props: `; + yield generateSfcBlockSection( + scriptSetup, + scriptSetupRanges.defineProps.arg.start, + scriptSetupRanges.defineProps.arg.end, + codeFeatures.navigation + ); + yield `,${newLine}`; + } + + yield* generateEmitsOption(options, scriptSetupRanges); + + yield `})${endOfLine}`; + + yield `type __VLS_BuiltInPublicProps = ${options.vueCompilerOptions.target >= 3.4 + ? `import('${options.vueCompilerOptions.lib}').PublicProps` + : options.vueCompilerOptions.target >= 3.0 + ? `import('${options.vueCompilerOptions.lib}').VNodeProps` + + ` & import('${options.vueCompilerOptions.lib}').AllowedComponentProps` + + ` & import('${options.vueCompilerOptions.lib}').ComponentCustomProps` + : `globalThis.JSX.IntrinsicAttributes` + }`; + yield endOfLine; + + yield `type __VLS_OwnProps = `; + yield `${ctx.localTypes.OmitKeepDiscriminatedUnion}['$props'], keyof __VLS_BuiltInPublicProps>`; + yield endOfLine; } - yield* generateEmitsOption(options, scriptSetupRanges); - - yield `})${endOfLine}`; - - yield `type __VLS_BuiltInPublicProps = ${options.vueCompilerOptions.target >= 3.4 - ? `import('${options.vueCompilerOptions.lib}').PublicProps` - : options.vueCompilerOptions.target >= 3.0 - ? `import('${options.vueCompilerOptions.lib}').VNodeProps` - + ` & import('${options.vueCompilerOptions.lib}').AllowedComponentProps` - + ` & import('${options.vueCompilerOptions.lib}').ComponentCustomProps` - : `globalThis.JSX.IntrinsicAttributes` - }`; - yield endOfLine; - - yield `let __VLS_functionalComponentProps!: `; - yield `${ctx.localTypes.OmitKeepDiscriminatedUnion}['$props'], keyof __VLS_BuiltInPublicProps>`; - yield endOfLine; - if (scriptSetupRanges.defineProp.length) { yield `const __VLS_defaults = {${newLine}`; for (const defineProp of scriptSetupRanges.defineProp) { diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index 7c6e412db5..6959c4430d 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -5,10 +5,10 @@ exports[`vue-tsc-dts > Input: #4577/main.vue, Output: #4577/main.vue.d.ts 1`] = value: string; }; declare const _default: (__VLS_props: NonNullable>["props"], __VLS_ctx?: __VLS_PrettifyLocal>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable>["expose"], __VLS_setup?: Promise<{ - props: __VLS_PrettifyLocal & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, never> & Partial<{}> & { + props: __VLS_PrettifyLocal & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, never> & { nonGeneric: string; rows: Row[]; - }> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + } & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: { @@ -69,7 +69,7 @@ exports[`vue-tsc-dts > Input: events/component-generic.vue, Output: events/compo "declare const _default: (__VLS_props: NonNullable>["props"], __VLS_ctx?: __VLS_PrettifyLocal>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable>["expose"], __VLS_setup?: Promise<{ props: __VLS_PrettifyLocal & Omit<{ readonly onFoo?: (value: string) => any; - } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onFoo"> & Partial<{}> & {}> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onFoo"> & {} & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: {}; @@ -89,11 +89,11 @@ exports[`vue-tsc-dts > Input: generic/component.vue, Output: generic/component.v props: __VLS_PrettifyLocal & Omit<{ readonly "onUpdate:title"?: (value: string) => any; readonly onBar?: (data: number) => any; - } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & Partial<{}> & ({ + } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & ({ title?: string; } & { foo: number; - })> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + }) & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; }>): void; @@ -123,11 +123,11 @@ exports[`vue-tsc-dts > Input: generic/custom-extension-component.cext, Output: g props: __VLS_PrettifyLocal & Omit<{ readonly "onUpdate:title"?: (value: string) => any; readonly onBar?: (data: number) => any; - } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & Partial<{}> & ({ + } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:title" | "onBar"> & ({ title?: string; } & { foo: number; - })> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); + }) & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{ baz: number; }>): void; From 06df71bdca16f9e68f7549d9652082d6b3a58b6b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:26:50 +0000 Subject: [PATCH 02/45] ci(lint): auto-fix --- .../language-core/lib/codegen/script/scriptSetup.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index 3f2a959ee1..5b7e0bceea 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -370,7 +370,7 @@ function* generateComponentProps( ): Generator { if (scriptSetup.generic) { yield `const __VLS_fnComponent = (await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`; - + if (scriptSetupRanges.defineProps?.arg) { yield `props: `; yield generateSfcBlockSection( @@ -381,11 +381,11 @@ function* generateComponentProps( ); yield `,${newLine}`; } - + yield* generateEmitsOption(options, scriptSetupRanges); - + yield `})${endOfLine}`; - + yield `type __VLS_BuiltInPublicProps = ${options.vueCompilerOptions.target >= 3.4 ? `import('${options.vueCompilerOptions.lib}').PublicProps` : options.vueCompilerOptions.target >= 3.0 @@ -395,7 +395,7 @@ function* generateComponentProps( : `globalThis.JSX.IntrinsicAttributes` }`; yield endOfLine; - + yield `type __VLS_OwnProps = `; yield `${ctx.localTypes.OmitKeepDiscriminatedUnion}['$props'], keyof __VLS_BuiltInPublicProps>`; yield endOfLine; From 3862e76f32cc910b26860fe422d34bcb1917b0e5 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 20:55:34 +0800 Subject: [PATCH 03/45] perf(typescript-plugin): use named pipe servers more efficiently (#5070) --- .../language-server/lib/hybridModeProject.ts | 16 +- .../lib/plugins/vue-template.ts | 8 +- packages/typescript-plugin/lib/client.ts | 103 ++--- .../lib/requests/componentInfos.ts | 27 +- packages/typescript-plugin/lib/server.ts | 297 +++++++++++---- packages/typescript-plugin/lib/utils.ts | 356 ++++++++---------- 6 files changed, 466 insertions(+), 341 deletions(-) diff --git a/packages/language-server/lib/hybridModeProject.ts b/packages/language-server/lib/hybridModeProject.ts index 5a606bd530..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 { getReadyNamedPipePaths, onSomePipeReadyCallbacks, searchNamedPipeServerForFile } 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 }) => { @@ -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/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 ce8d82db99..04a5f777b1 100644 --- a/packages/typescript-plugin/lib/client.ts +++ b/packages/typescript-plugin/lib/client.ts @@ -1,96 +1,99 @@ -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 -export function getComponentProps( - ...args: Parameters -) { - return sendRequest>({ - type: 'getComponentProps', - args, - }); +export async function getComponentProps(fileName: string, componentName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + const componentAndProps = await server.componentNamesAndProps.get(fileName); + if (!componentAndProps) { + return; + } + return componentAndProps[componentName]; } 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, - }); +export async function getComponentNames(fileName: string) { + const server = await getBestServer(fileName); + if (!server) { + return; + } + const componentAndProps = server.componentNamesAndProps.get(fileName); + if (!componentAndProps) { + return; + } + return Object.keys(componentAndProps); } 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/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 c3b83c3f5c..f618f8f5c8 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -8,10 +8,9 @@ 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 { - type: 'containsFile' +export type RequestType = 'containsFile' | 'projectInfo' | 'collectExtractProps' | 'getImportPathForFile' @@ -21,10 +20,25 @@ export interface Request { | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' - | 'getComponentNames' | 'getElementAttrs'; - args: [fileName: string, ...rest: any]; -} + +export type RequestData = [ + seq: number, + type: RequestType, + fileName: string, + ...args: any[], +]; + +export type ResponseData = [ + seq: number, + data: any, +]; + +export type NotificationData = [ + type: 'componentAndPropsUpdated', + fileName: string, + data: any, +]; export interface ProjectInfo { name: string; @@ -38,85 +52,53 @@ export async function startNamedPipeServer( language: Language, projectKind: ts.server.ProjectKind.Inferred | ts.server.ProjectKind.Configured ) { + let lastProjectVersion: string | undefined; + + const requestContext: RequestContext = { + typescript: ts, + languageService: info.languageService, + languageServiceHost: info.languageServiceHost, + language: language, + isTsPlugin: true, + getFileId: (fileName: string) => fileName, + }; + const dataChunks: Buffer[] = []; + const componentNamesAndProps = new Map(); + const allConnections = new Set(); 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]; - const requestContext: RequestContext = { - typescript: ts, - languageService: info.languageService, - languageServiceHost: info.languageServiceHost, - language: language, - isTsPlugin: true, - getFileId: (fileName: string) => fileName, - }; - if (request.type === 'containsFile') { - sendResponse( - info.project.containsFile(ts.server.toNormalizedPath(fileName)) - ); - } - else if (request.type === 'projectInfo') { - sendResponse({ - name: info.project.getProjectName(), - kind: info.project.projectKind, - currentDirectory: info.project.getCurrentDirectory(), - } satisfies ProjectInfo); - } - else if (request.type === 'collectExtractProps') { - const result = collectExtractProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getImportPathForFile') { - const result = getImportPathForFile.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getPropertiesAtLocation') { - const result = getPropertiesAtLocation.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getQuickInfoAtPosition') { - const result = getQuickInfoAtPosition.apply(requestContext, request.args as any); - sendResponse(result); - } - // Component Infos - else if (request.type === 'getComponentProps') { - const result = getComponentProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getComponentEvents') { - const result = getComponentEvents.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getTemplateContextProps') { - const result = getTemplateContextProps.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getComponentNames') { - const result = getComponentNames.apply(requestContext, request.args as any); - sendResponse(result); - } - else if (request.type === 'getElementAttrs') { - const result = getElementAttrs.apply(requestContext, request.args as any); - sendResponse(result); - } - else { - console.warn('[Vue Named Pipe Server] Unknown request type:', request.type); + allConnections.add(connection); + + connection.on('end', () => { + allConnections.delete(connection); + }); + 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 { + onRequest(connection, JSON.parse(json)); + } catch (e) { + console.error('[Vue Named Pipe Server] JSON parse error:', e); + } + } } }); 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 (const [fileName, data] of componentNamesAndProps) { + notify(connection, 'componentAndPropsUpdated', fileName, data); } }); - 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(); @@ -130,6 +112,167 @@ export async function startNamedPipeServer( break; } } + + updateWhile(); + + async function updateWhile() { + while (true) { + await sleep(500); + const projectVersion = info.project.getProjectVersion(); + if (lastProjectVersion === projectVersion) { + continue; + } + const connections = [...allConnections].filter(c => !c.destroyed); + if (!connections.length) { + continue; + } + const token = info.languageServiceHost.getCancellationToken?.(); + const openedScriptInfos = info.project.getRootScriptInfos().filter(info => info.isScriptOpen()); + if (!openedScriptInfos.length) { + continue; + } + 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; + } + } + + 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'); + } + + async 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 { + 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) { + 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) { diff --git a/packages/typescript-plugin/lib/utils.ts b/packages/typescript-plugin/lib/utils.ts index ebc318aec5..72315e49b4 100644 --- a/packages/typescript-plugin/lib/utils.ts +++ b/packages/typescript-plugin/lib/utils.ts @@ -3,211 +3,216 @@ 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 { NotificationData, 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` - : `/tmp`; -const toFullPath = (file: string) => { - if (platform === 'win32') { - return pipeDir + '\\' + file; + ? `\\\\.\\pipe\\` + : `/tmp/`; + +export function getServerPath(kind: ts.server.ProjectKind, id: number) { + if (kind === 1 satisfies ts.server.ProjectKind.Configured) { + return `${pipeDir}vue-named-pipe-${version}-configured-${id}`; + } else { + return `${pipeDir}vue-named-pipe-${version}-inferred-${id}`; } - else { - return pipeDir + '/' + file; +} + +class NamedPipeServer { + path: string; + connecting = false; + projectInfo?: ProjectInfo; + containsFileCache = new Map>(); + componentNamesAndProps = new Map>(); + + constructor(kind: ts.server.ProjectKind, id: number) { + this.path = getServerPath(kind, id); } -}; -const configuredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-configured-`); -const inferredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-inferred-`); -const pipes = new Map(); -export const onSomePipeReadyCallbacks: (() => void)[] = []; + containsFile(fileName: string) { + if (this.projectInfo) { + if (!this.containsFileCache.has(fileName)) { + this.containsFileCache.set(fileName, (async () => { + const res = await this.request('containsFile', fileName); + if (typeof res !== 'boolean') { + // If the request fails, delete the cache + this.containsFileCache.delete(fileName); + } + return res; + })()); + } + return this.containsFileCache.get(fileName); + } + } -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 { } + update() { + if (!this.connecting && !this.projectInfo) { + this.connecting = true; + this.connect(); } - pipes.delete(namedPipePath); - socket.end(); - }); - socket.on('timeout', () => { - pipes.delete(namedPipePath); - socket.end(); - }); -} + } -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}`; -} + 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.containsFileCache.clear(); + onServerReady.forEach(cb => cb()); + } else { + this.close(); + } + }); + this.socket.on('error', err => { + if ((err as any).code === 'ECONNREFUSED') { + try { + console.log('Deleteing invalid named pipe file:', this.path); + fs.promises.unlink(this.path); + } catch { } + } + this.close(); + }); + this.socket.on('timeout', () => { + this.close(); + }); + } -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); + close() { + this.connecting = false; + this.projectInfo = undefined; + this.socket?.end(); + } + + 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')) { + this.dataChunks.length = 0; + const results = text.split('\n\n'); + for (let result of results) { + result = result.trim(); + if (!result) { + continue; + } + try { + 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); + } + } } - else if (!pipes.has(inferredPipe)) { - pipes.set(inferredPipe, 'unknown'); - waitingForNamedPipeServerReady(inferredPipe); + } + + onNotification(type: NotificationData[0], fileName: string, data: any) { + // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`); + if (type === 'componentAndPropsUpdated') { + this.componentNamesAndProps.set(fileName, data); } } - return { - configured: configuredPipes, - inferred: inferredPipes, - }; + + request(requestType: RequestData[1], fileName: string, ...args: any[]) { + 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); + }); + this.socket!.write(JSON.stringify([seq, requestType, fileName, ...args] satisfies RequestData) + '\n\n'); + }); + } } -export 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 { } - } - 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); - }); +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 async function searchNamedPipeServerForFile(fileName: string) { - const paths = await getReadyNamedPipePaths(); +export async function getBestServer(fileName: string) { + for (const server of configuredServers) { + server.update(); + } - const configuredServers = (await Promise.all( - paths.configured.map(async path => { - // Find existing servers - const socket = await connect(path); - if (typeof socket !== 'object') { + let servers = (await Promise.all( + configuredServers.map(async server => { + const projectInfo = server.projectInfo; + if (!projectInfo) { return; } - - // Find servers containing the current file - const containsFile = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, socket); + const containsFile = await server.containsFile(fileName); if (!containsFile) { - socket.end(); - return; - } - - // Get project info for each server - const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket); - if (!projectInfo) { - socket.end(); return; } - - return { - socket, - projectInfo, - }; + 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 +237,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 186ab3dc92d3c1abb563c4b151af792442ae4c56 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:55:59 +0000 Subject: [PATCH 04/45] ci(lint): auto-fix --- packages/typescript-plugin/lib/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index f618f8f5c8..550f766b17 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -182,7 +182,7 @@ export async function startNamedPipeServer( connection.write(JSON.stringify([type, fileName, data] satisfies NotificationData) + '\n\n'); } - async function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { + function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { if (requestType === 'projectInfo') { sendResponse({ name: info.project.getProjectName(), From 128e58b29c84e389646626b5d6d16c43e2a47a2a Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 20:59:05 +0800 Subject: [PATCH 05/45] chore: format --- packages/typescript-plugin/lib/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-plugin/lib/server.ts b/packages/typescript-plugin/lib/server.ts index 550f766b17..ea59999530 100644 --- a/packages/typescript-plugin/lib/server.ts +++ b/packages/typescript-plugin/lib/server.ts @@ -182,7 +182,7 @@ export async function startNamedPipeServer( connection.write(JSON.stringify([type, fileName, data] satisfies NotificationData) + '\n\n'); } - function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { + function onRequest(connection: net.Socket, [seq, requestType, ...args]: RequestData) { if (requestType === 'projectInfo') { sendResponse({ name: info.project.getProjectName(), From 3216627bcb5086882dcfcff22040e341b87698ba Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Fri, 20 Dec 2024 21:04:36 +0800 Subject: [PATCH 06/45] fix(language-core): do not resolve type of components and directives by spreading --- .../lib/codegen/script/template.ts | 50 ++++++++++--------- .../lib/codegen/template/index.ts | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index 78c937d392..982e37a909 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -54,16 +54,19 @@ function* generateTemplateCtx(options: ScriptCodegenOptions): Generator { } function* generateTemplateComponents(options: ScriptCodegenOptions): Generator { - const exps: Code[] = []; + const types: Code[] = []; if (options.sfc.script && options.scriptRanges?.exportDefault?.componentsOption) { const { componentsOption } = options.scriptRanges.exportDefault; - exps.push([ + yield `const __VLS_componentsOption = ` + yield [ options.sfc.script.content.slice(componentsOption.start, componentsOption.end), 'script', componentsOption.start, codeFeatures.navigation, - ]); + ]; + yield endOfLine; + types.push(`typeof __VLS_componentsOption`); } let nameType: Code | undefined; @@ -76,52 +79,51 @@ function* generateTemplateComponents(options: ScriptCodegenOptions): Generator { ` + types.push( + `{ [K in ${nameType}]: typeof __VLS_self & (new () => { ` + getSlotsPropertyName(options.vueCompilerOptions.target) + `: typeof ${options.scriptSetupRanges?.defineSlots?.name ?? `__VLS_slots`} }) }` ); } - exps.push(`{} as NonNullable`); - exps.push(`__VLS_ctx`); + types.push(`typeof __VLS_ctx`); - yield `const __VLS_localComponents = {${newLine}`; - for (const type of exps) { - yield `...`; + yield `type __VLS_LocalComponents =`; + for (const type of types) { + yield ` & `; yield type; - yield `,${newLine}`; } - yield `}${endOfLine}`; + yield endOfLine; - yield `let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents${endOfLine}`; + yield `let __VLS_components!: __VLS_LocalComponents & __VLS_GlobalComponents${endOfLine}`; } export function* generateTemplateDirectives(options: ScriptCodegenOptions): Generator { - const exps: Code[] = []; + const types: Code[] = []; if (options.sfc.script && options.scriptRanges?.exportDefault?.directivesOption) { const { directivesOption } = options.scriptRanges.exportDefault; - exps.push([ + yield `const __VLS_directivesOption = `; + yield [ options.sfc.script.content.slice(directivesOption.start, directivesOption.end), 'script', directivesOption.start, codeFeatures.navigation, - ]); + ]; + yield endOfLine; + types.push(`typeof __VLS_directivesOption`); } - exps.push(`{} as NonNullable`); - exps.push(`__VLS_ctx`); + types.push(`typeof __VLS_ctx`); - yield `const __VLS_localDirectives = {${newLine}`; - for (const type of exps) { - yield `...`; + yield `type __VLS_LocalDirectives =`; + for (const type of types) { + yield ` & `; yield type; - yield `,${newLine}`; } - yield `}${endOfLine}`; + yield endOfLine; - yield `let __VLS_directives!: typeof __VLS_localDirectives & __VLS_GlobalDirectives${endOfLine}`; + yield `let __VLS_directives!: __VLS_LocalDirectives & __VLS_GlobalDirectives${endOfLine}`; } function* generateTemplateBody( diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index 1edad7e844..1dd9458aa3 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -139,7 +139,7 @@ function* generatePreResolveComponents(options: TemplateCodegenOptions): Generat } components.add(node.tag); yield newLine; - yield ` & __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', typeof __VLS_localComponents, `; + yield ` & __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', __VLS_LocalComponents, `; yield getPossibleOriginalComponentNames(node.tag, false) .map(name => `'${name}'`) .join(', '); From 4fff8928a4f726d1b2e8f0df650eff5ba4ad0490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:52:51 +0800 Subject: [PATCH 07/45] fix(language-core): generate macros after script setup content (#5071) --- .../language-core/lib/codegen/script/index.ts | 23 --------- .../lib/codegen/script/scriptSetup.ts | 49 ++++++++++++++----- packages/tsc/tests/typecheck.spec.ts | 4 ++ .../tsc/failureFixtures/#5071/tsconfig.json | 4 ++ .../tsc/failureFixtures/#5071/withScript.vue | 6 +++ .../failureFixtures/#5071/withoutScript.vue | 3 ++ test-workspace/tsc/tsconfig.json | 1 + 7 files changed, 56 insertions(+), 34 deletions(-) create mode 100644 test-workspace/tsc/failureFixtures/#5071/tsconfig.json create mode 100644 test-workspace/tsc/failureFixtures/#5071/withScript.vue create mode 100644 test-workspace/tsc/failureFixtures/#5071/withoutScript.vue diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index d805ec1ef3..9c975f325d 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -84,7 +84,6 @@ export function* generateScript(options: ScriptCodegenOptions): Generator -): Generator { - const definePropProposalA = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition'; - const definePropProposalB = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition'; - - if (definePropProposalA || definePropProposalB) { - yield `type __VLS_PropOptions = Exclude, import('${options.vueCompilerOptions.lib}').PropType>${endOfLine}`; - if (definePropProposalA) { - yield `declare function defineProp(name: string, options: ({ required: true } | { default: T }) & __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; - yield `declare function defineProp(name?: string, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; - } - if (definePropProposalB) { - yield `declare function defineProp(value: T | (() => T), required?: boolean, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; - yield `declare function defineProp(value: T | (() => T) | undefined, required: true, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; - yield `declare function defineProp(value?: T | (() => T), required?: boolean, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; - } - } -} diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index 5b7e0bceea..bb6ad08d80 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -17,7 +17,6 @@ export function* generateScriptSetupImports( 0, codeFeatures.all, ]; - yield newLine; } export function* generateScriptSetup( @@ -96,16 +95,6 @@ function* generateSetupFunction( scriptSetupRanges: ScriptSetupRanges, syntax: 'return' | 'export default' | undefined ): Generator { - if (options.vueCompilerOptions.target >= 3.3) { - yield `const { `; - for (const macro of Object.keys(options.vueCompilerOptions.macros)) { - if (!ctx.bindingNames.has(macro) && macro !== 'templateRef') { - yield macro + `, `; - } - } - yield `} = await import('${options.vueCompilerOptions.lib}')${endOfLine}`; - } - ctx.scriptSetupGeneratedOffset = options.getGeneratedLength() - scriptSetupRanges.importSectionEndOffset; let setupCodeModifies: [Code[], number, number][] = []; @@ -280,6 +269,8 @@ function* generateSetupFunction( yield generateSfcBlockSection(scriptSetup, nextStart, scriptSetup.content.length, codeFeatures.all); yield* generateScriptSectionPartiallyEnding(scriptSetup.name, scriptSetup.content.length, '#3632/scriptSetup.vue'); + yield* generateMacros(options, ctx); + yield* generateDefineProp(options, scriptSetup); if (scriptSetupRanges.defineProps?.typeArg && scriptSetupRanges.withDefaults?.arg) { // fix https://github.com/vuejs/language-tools/issues/1187 @@ -317,6 +308,42 @@ function* generateSetupFunction( } } +function* generateMacros( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext +): Generator { + if (options.vueCompilerOptions.target >= 3.3) { + yield `declare const { `; + for (const macro of Object.keys(options.vueCompilerOptions.macros)) { + if (!ctx.bindingNames.has(macro)) { + yield `${macro}, `; + } + } + yield `}: typeof import('${options.vueCompilerOptions.lib}')${endOfLine}`; + } +} + +function* generateDefineProp( + options: ScriptCodegenOptions, + scriptSetup: NonNullable +): Generator { + const definePropProposalA = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition'; + const definePropProposalB = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition') || options.vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition'; + + if (definePropProposalA || definePropProposalB) { + yield `type __VLS_PropOptions = Exclude, import('${options.vueCompilerOptions.lib}').PropType>${endOfLine}`; + if (definePropProposalA) { + yield `declare function defineProp(name: string, options: ({ required: true } | { default: T }) & __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(name?: string, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + } + if (definePropProposalB) { + yield `declare function defineProp(value: T | (() => T), required?: boolean, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(value: T | (() => T) | undefined, required: true, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + yield `declare function defineProp(value?: T | (() => T), required?: boolean, options?: __VLS_PropOptions): import('${options.vueCompilerOptions.lib}').ComputedRef${endOfLine}`; + } + } +} + function* generateDefineWithType( scriptSetup: NonNullable, statement: TextRange, diff --git a/packages/tsc/tests/typecheck.spec.ts b/packages/tsc/tests/typecheck.spec.ts index ac97fb2c35..9ca92a87df 100644 --- a/packages/tsc/tests/typecheck.spec.ts +++ b/packages/tsc/tests/typecheck.spec.ts @@ -13,6 +13,8 @@ describe(`vue-tsc`, () => { "test-workspace/tsc/failureFixtures/#3632/both.vue(7,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", + "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/#5071/withScript.vue(1,19): error TS1005: ';' expected.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", @@ -32,6 +34,8 @@ describe(`vue-tsc`, () => { "test-workspace/tsc/failureFixtures/#3632/both.vue(7,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", + "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/#5071/withScript.vue(1,19): error TS1005: ';' expected.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", diff --git a/test-workspace/tsc/failureFixtures/#5071/tsconfig.json b/test-workspace/tsc/failureFixtures/#5071/tsconfig.json new file mode 100644 index 0000000000..52ecbeb3c3 --- /dev/null +++ b/test-workspace/tsc/failureFixtures/#5071/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ "**/*" ] +} diff --git a/test-workspace/tsc/failureFixtures/#5071/withScript.vue b/test-workspace/tsc/failureFixtures/#5071/withScript.vue new file mode 100644 index 0000000000..f2acf4d1eb --- /dev/null +++ b/test-workspace/tsc/failureFixtures/#5071/withScript.vue @@ -0,0 +1,6 @@ + + + diff --git a/test-workspace/tsc/failureFixtures/#5071/withoutScript.vue b/test-workspace/tsc/failureFixtures/#5071/withoutScript.vue new file mode 100644 index 0000000000..0a5bb66ff8 --- /dev/null +++ b/test-workspace/tsc/failureFixtures/#5071/withoutScript.vue @@ -0,0 +1,3 @@ + diff --git a/test-workspace/tsc/tsconfig.json b/test-workspace/tsc/tsconfig.json index 219f289b2f..bee2125e24 100644 --- a/test-workspace/tsc/tsconfig.json +++ b/test-workspace/tsc/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./failureFixtures/#3632" }, // { "path": "./failureFixtures/#4569" }, // TODO: not working with --build flag + { "path": "./failureFixtures/#5071" }, { "path": "./failureFixtures/directives" }, { "path": "./passedFixtures/#1886" }, From e816866333d3888f3363e699f0b8dde65f52c734 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Fri, 20 Dec 2024 23:09:36 +0800 Subject: [PATCH 08/45] fix(language-core): disable type inference of `useAttrs` and `$attrs` --- .../lib/codegen/script/scriptSetup.ts | 23 ++++++++++--------- .../lib/codegen/template/index.ts | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index bb6ad08d80..0d7fbeffeb 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -170,17 +170,18 @@ function* generateSetupFunction( ]); } } - for (const { callExp } of scriptSetupRanges.useAttrs) { - setupCodeModifies.push([ - [`(`], - callExp.start, - callExp.start - ], [ - [` as __VLS_TemplateResult['attrs'] & Record)`], - callExp.end, - callExp.end - ]); - } + // TODO: circular reference + // for (const { callExp } of scriptSetupRanges.useAttrs) { + // setupCodeModifies.push([ + // [`(`], + // callExp.start, + // callExp.start + // ], [ + // [` as __VLS_TemplateResult['attrs'] & Record)`], + // callExp.end, + // callExp.end + // ]); + // } for (const { callExp, exp, arg } of scriptSetupRanges.useCssModule) { setupCodeModifies.push([ [`(`], diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index 1dd9458aa3..4fd82c74da 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -35,7 +35,8 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator Date: Fri, 20 Dec 2024 23:14:22 +0800 Subject: [PATCH 09/45] test: sort tsc result --- packages/tsc/tests/typecheck.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/tsc/tests/typecheck.spec.ts b/packages/tsc/tests/typecheck.spec.ts index 9ca92a87df..8ed14447e2 100644 --- a/packages/tsc/tests/typecheck.spec.ts +++ b/packages/tsc/tests/typecheck.spec.ts @@ -6,18 +6,18 @@ describe(`vue-tsc`, () => { test(`TypeScript - Stable`, () => { expect( - getTscOutput('stable') + getTscOutput('stable').sort() ).toMatchInlineSnapshot(` [ "test-workspace/tsc/failureFixtures/#3632/both.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/both.vue(7,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", - "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", "test-workspace/tsc/failureFixtures/#5071/withScript.vue(1,19): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", - "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", ] `); }); @@ -27,18 +27,18 @@ describe(`vue-tsc`, () => { test.skipIf(!isUpdateEvent && isGithubActions)(`TypeScript - Next`, () => { expect( - getTscOutput('next') + getTscOutput('next').sort() ).toMatchInlineSnapshot(` [ "test-workspace/tsc/failureFixtures/#3632/both.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/both.vue(7,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", - "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", "test-workspace/tsc/failureFixtures/#5071/withScript.vue(1,19): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/#5071/withoutScript.vue(2,26): error TS1005: ';' expected.", + "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", - "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", ] `); }); From 44f1c2843a4475068884aeb67c997aef44547fe1 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 20 Dec 2024 23:21:29 +0800 Subject: [PATCH 10/45] test: exclude useAttrs test --- .../fallthroughAttributes_strictTemplate/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/tsconfig.json b/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/tsconfig.json index e93dd518af..9295825aeb 100644 --- a/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/tsconfig.json +++ b/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/tsconfig.json @@ -5,4 +5,5 @@ "strictTemplates": true, }, "include": [ "**/*" ], + "exclude": [ "useAttrs/main.vue" ], } From 0a085fe5faffd23166c85e18dcf190a403ad944a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Sat, 21 Dec 2024 03:39:44 +0800 Subject: [PATCH 11/45] fix(language-core): correct type and completion support of `vue:` event (#4969) Co-authored-by: Johnson Chu --- .../lib/codegen/template/elementEvents.ts | 72 ++++++++----------- .../lib/codegen/template/elementProps.ts | 2 +- .../lib/plugins/vue-template.ts | 8 ++- 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts index b8646dd7f0..e6ec49a56a 100644 --- a/packages/language-core/lib/codegen/template/elementEvents.ts +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -1,8 +1,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import type * as ts from 'typescript'; -import type { Code, VueCodeInformation } from '../../types'; -import { hyphenateAttr } from '../../utils/shared'; +import type { Code } from '../../types'; import { combineLastMapping, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import type { TemplateCodegenContext } from './context'; @@ -32,9 +31,18 @@ export function* generateElementEvents( propsVar = ctx.getInternalVariable(); yield `let ${propsVar}!: __VLS_FunctionalComponentProps${endOfLine}`; } - const originalPropName = camelize('on-' + prop.arg.loc.source); - yield `const ${ctx.getInternalVariable()}: __VLS_NormalizeComponentEvent = {${newLine}`; - yield* generateEventArg(ctx, prop.arg, true); + let source = prop.arg.loc.source; + let start = prop.arg.loc.start.offset; + let propPrefix = 'on'; + let emitPrefix = ''; + if (source.startsWith('vue:')) { + source = source.slice('vue:'.length); + start = start + 'vue:'.length; + propPrefix = 'onVnode'; + emitPrefix = 'vnode-'; + } + yield `const ${ctx.getInternalVariable()}: __VLS_NormalizeComponentEvent = {${newLine}`; + yield* generateEventArg(ctx, source, start, propPrefix); yield `: `; yield* generateEventExpression(options, ctx, prop); yield `}${endOfLine}`; @@ -43,54 +51,36 @@ export function* generateElementEvents( return usedComponentEventsVar; } -const eventArgFeatures: VueCodeInformation = { - navigation: { - // @click-outside -> onClickOutside - resolveRenameNewName(newName) { - return camelize('on-' + newName); - }, - // onClickOutside -> @click-outside - resolveRenameEditText(newName) { - const hName = hyphenateAttr(newName); - if (hyphenateAttr(newName).startsWith('on-')) { - return camelize(hName.slice('on-'.length)); - } - return newName; - }, - }, -}; - export function* generateEventArg( ctx: TemplateCodegenContext, - arg: CompilerDOM.SimpleExpressionNode, - enableHover: boolean + name: string, + start: number, + directive = 'on' ): Generator { - const features = enableHover - ? { - ...ctx.codeFeatures.withoutHighlightAndCompletion, - ...eventArgFeatures, - } - : eventArgFeatures; - if (variableNameRegex.test(camelize(arg.loc.source))) { - yield ['', 'template', arg.loc.start.offset, features]; - yield `on`; + const features = { + ...ctx.codeFeatures.withoutHighlightAndCompletion, + ...ctx.codeFeatures.navigationWithoutRename, + }; + if (variableNameRegex.test(camelize(name))) { + yield ['', 'template', start, features]; + yield directive; yield* generateCamelized( - capitalize(arg.loc.source), - arg.loc.start.offset, + capitalize(name), + start, combineLastMapping ); } else { yield* wrapWith( - arg.loc.start.offset, - arg.loc.end.offset, + start, + start + name.length, features, `'`, - ['', 'template', arg.loc.start.offset, combineLastMapping], - 'on', + ['', 'template', start, combineLastMapping], + directive, ...generateCamelized( - capitalize(arg.loc.source), - arg.loc.start.offset, + capitalize(name), + start, combineLastMapping ), `'` diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 20e2bc7ff4..1557aad18c 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -44,7 +44,7 @@ export function* generateElementProps( ) { if (!isComponent) { yield `...{ `; - yield* generateEventArg(ctx, prop.arg, true); + yield* generateEventArg(ctx, prop.arg.loc.source, prop.arg.loc.start.offset); yield `: `; yield* generateEventExpression(options, ctx, prop); yield `},`; diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 54f4db2638..d76c55d2ef 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -557,7 +557,11 @@ export function create( } const { attrs, propsInfo, events } = tagInfo; - const props = propsInfo.map(prop => prop.name); + const props = propsInfo.map(prop => + hyphenateTag(prop.name).startsWith('on-vnode-') + ? 'onVue:' + prop.name.slice('onVnode'.length) + : prop.name + ); const attributes: html.IAttributeData[] = []; const _tsCodegen = tsCodegen.get(vueCode._sfc); @@ -894,7 +898,7 @@ export function create( } else if (isEvent) { item.kind = 23 satisfies typeof vscode.CompletionItemKind.Event; - if (propName.startsWith('vnode-')) { + if (propName.startsWith('vue:')) { tokens.push('\u0004'); } } From d6faebbde0cb61b5626b4347faaa7a0fd2d5b318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Sat, 21 Dec 2024 03:42:16 +0800 Subject: [PATCH 12/45] refactor(language-service): consistent style of source and virtual code operation (#5053) --- .../lib/ideFeatures/nameCasing.ts | 28 +-- .../lib/plugins/vue-autoinsert-dotvalue.ts | 3 +- .../plugins/vue-complete-define-assignment.ts | 9 +- .../lib/plugins/vue-document-drop.ts | 22 +- .../lib/plugins/vue-document-links.ts | 94 ++++---- .../lib/plugins/vue-extract-file.ts | 31 ++- .../lib/plugins/vue-inlayhints.ts | 127 +++++----- .../language-service/lib/plugins/vue-sfc.ts | 56 ++--- .../lib/plugins/vue-template.ts | 224 ++++++++++-------- .../lib/plugins/vue-twoslash-queries.ts | 14 +- packages/typescript-plugin/lib/common.ts | 9 +- .../lib/requests/collectExtractProps.ts | 15 +- 12 files changed, 347 insertions(+), 285 deletions(-) diff --git a/packages/language-service/lib/ideFeatures/nameCasing.ts b/packages/language-service/lib/ideFeatures/nameCasing.ts index 859301b100..c2040ba1f2 100644 --- a/packages/language-service/lib/ideFeatures/nameCasing.ts +++ b/packages/language-service/lib/ideFeatures/nameCasing.ts @@ -19,21 +19,20 @@ export async function convertTagName( return; } - const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueVirtualCode)) { + const root = sourceFile?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } - const desc = rootCode._sfc; - if (!desc.template) { + const { template } = root._sfc; + if (!template) { return; } - const template = desc.template; const document = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await tsPluginClient?.getComponentNames(rootCode.fileName) ?? []; - const tags = getTemplateTagsAndAttrs(rootCode); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const tags = getTemplateTagsAndAttrs(root); for (const [tagName, { offsets }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); @@ -67,26 +66,25 @@ export async function convertAttrName( return; } - const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueVirtualCode)) { + const root = sourceFile?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } - const desc = rootCode._sfc; - if (!desc.template) { + const { template } = root._sfc; + if (!template) { return; } - const template = desc.template; const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await tsPluginClient?.getComponentNames(rootCode.fileName) ?? []; - const tags = getTemplateTagsAndAttrs(rootCode); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const tags = getTemplateTagsAndAttrs(root); for (const [tagName, { attrs }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); if (componentName) { - const props = (await tsPluginClient?.getComponentProps(rootCode.fileName, componentName) ?? []).map(prop => prop.name); + const props = (await tsPluginClient?.getComponentProps(root.fileName, componentName) ?? []).map(prop => prop.name); for (const [attrName, { offsets }] of attrs) { const propName = props.find(prop => prop === attrName || hyphenateAttr(prop) === attrName); if (propName) { diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 68143c99f9..6d0265545c 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -57,7 +57,8 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); if (!sourceScript) { diff --git a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts index 139abab53a..8e2e98129d 100644 --- a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts +++ b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts @@ -23,15 +23,15 @@ export function create(): LanguageServicePlugin { return; } - const result: vscode.CompletionItem[] = []; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!sourceScript || !virtualCode) { + if (!sourceScript?.generated || !virtualCode) { return; } - const root = sourceScript?.generated?.root; + const root = sourceScript.generated.root; if (!(root instanceof VueVirtualCode)) { return; } @@ -43,6 +43,7 @@ export function create(): LanguageServicePlugin { return; } + const result: vscode.CompletionItem[] = []; const mappings = [...context.language.maps.forEach(virtualCode)]; addDefineCompletionItem( diff --git a/packages/language-service/lib/plugins/vue-document-drop.ts b/packages/language-service/lib/plugins/vue-document-drop.ts index 44ad8b6cee..79fbb2d67b 100644 --- a/packages/language-service/lib/plugins/vue-document-drop.ts +++ b/packages/language-service/lib/plugins/vue-document-drop.ts @@ -33,11 +33,15 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - const vueVirtualCode = sourceScript?.generated?.root; - if (!sourceScript || !virtualCode || !(vueVirtualCode instanceof VueVirtualCode)) { + if (!sourceScript?.generated) { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -55,7 +59,7 @@ export function create( baseName = baseName.slice(0, baseName.lastIndexOf('.')); const newName = capitalize(camelize(baseName)); - const { _sfc: sfc } = vueVirtualCode; + const sfc = root._sfc; const script = sfc.scriptSetup ?? sfc.script; if (!script) { @@ -63,26 +67,26 @@ export function create( } const additionalEdit: vscode.WorkspaceEdit = {}; - const code = [...forEachEmbeddedCode(vueVirtualCode)].find(code => code.id === (sfc.scriptSetup ? 'scriptsetup_raw' : 'script_raw'))!; + const code = [...forEachEmbeddedCode(root)].find(code => code.id === (sfc.scriptSetup ? 'scriptsetup_raw' : 'script_raw'))!; const lastImportNode = getLastImportNode(ts, script.ast); const incomingFileName = context.project.typescript?.uriConverter.asFileName(URI.parse(importUri)) ?? URI.parse(importUri).fsPath.replace(/\\/g, '/'); let importPath: string | undefined; - const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(vueVirtualCode); + const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root); if (tsPluginClient && serviceScript) { const tsDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, serviceScript.code.id); const tsDocument = context.documents.get(tsDocumentUri, serviceScript.code.languageId, serviceScript.code.snapshot); const preferences = await getUserPreferences(context, tsDocument); - const importPathRequest = await tsPluginClient.getImportPathForFile(vueVirtualCode.fileName, incomingFileName, preferences); + const importPathRequest = await tsPluginClient.getImportPathForFile(root.fileName, incomingFileName, preferences); if (importPathRequest) { importPath = importPathRequest; } } if (!importPath) { - importPath = path.relative(path.dirname(vueVirtualCode.fileName), incomingFileName) + importPath = path.relative(path.dirname(root.fileName), incomingFileName) || importUri.slice(importUri.lastIndexOf('/') + 1); if (!importPath.startsWith('./') && !importPath.startsWith('../')) { diff --git a/packages/language-service/lib/plugins/vue-document-links.ts b/packages/language-service/lib/plugins/vue-document-links.ts index dbb423e111..aae8265e30 100644 --- a/packages/language-service/lib/plugins/vue-document-links.ts +++ b/packages/language-service/lib/plugins/vue-document-links.ts @@ -13,63 +13,69 @@ export function create(): LanguageServicePlugin { return { provideDocumentLinks(document) { - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!sourceScript?.generated || virtualCode?.id !== 'template') { + return; + } - if (sourceScript?.generated?.root instanceof VueVirtualCode && virtualCode?.id === 'template') { + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } - const result: vscode.DocumentLink[] = []; - const codegen = tsCodegen.get(sourceScript.generated.root._sfc); - const scopedClasses = codegen?.generatedTemplate.get()?.scopedClasses ?? []; - const styleClasses = new Map(); - const option = sourceScript.generated.root.vueCompilerOptions.experimentalResolveStyleCssClasses; + const result: vscode.DocumentLink[] = []; + const codegen = tsCodegen.get(root._sfc); + const scopedClasses = codegen?.generatedTemplate.get()?.scopedClasses ?? []; + const styleClasses = new Map(); + const option = root.vueCompilerOptions.experimentalResolveStyleCssClasses; - for (let i = 0; i < sourceScript.generated.root._sfc.styles.length; i++) { - const style = sourceScript.generated.root._sfc.styles[i]; - if (option === 'always' || (option === 'scoped' && style.scoped)) { - for (const className of style.classNames) { - if (!styleClasses.has(className.text.slice(1))) { - styleClasses.set(className.text.slice(1), []); - } - styleClasses.get(className.text.slice(1))!.push({ - index: i, - style, - classOffset: className.offset, - }); + for (let i = 0; i < root._sfc.styles.length; i++) { + const style = root._sfc.styles[i]; + if (option === 'always' || (option === 'scoped' && style.scoped)) { + for (const className of style.classNames) { + if (!styleClasses.has(className.text.slice(1))) { + styleClasses.set(className.text.slice(1), []); } + styleClasses.get(className.text.slice(1))!.push({ + index: i, + style, + classOffset: className.offset, + }); } } + } - for (const { className, offset } of scopedClasses) { - const styles = styleClasses.get(className); - if (styles) { - for (const style of styles) { - const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index); - const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + style.index); - if (!styleVirtualCode) { - continue; - } - const styleDocument = context.documents.get(styleDocumentUri, styleVirtualCode.languageId, styleVirtualCode.snapshot); - const start = styleDocument.positionAt(style.classOffset); - const end = styleDocument.positionAt(style.classOffset + className.length + 1); - result.push({ - range: { - start: document.positionAt(offset), - end: document.positionAt(offset + className.length), - }, - target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, - }); + for (const { className, offset } of scopedClasses) { + const styles = styleClasses.get(className); + if (styles) { + for (const style of styles) { + const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index); + const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + style.index); + if (!styleVirtualCode) { + continue; } + const styleDocument = context.documents.get(styleDocumentUri, styleVirtualCode.languageId, styleVirtualCode.snapshot); + const start = styleDocument.positionAt(style.classOffset); + const end = styleDocument.positionAt(style.classOffset + className.length + 1); + result.push({ + range: { + start: document.positionAt(offset), + end: document.positionAt(offset + className.length), + }, + target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, + }); } } - - return result; } + + return result; }, }; }, diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index de17245580..c580096e58 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -35,16 +35,21 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { return; } - const { _sfc: sfc } = sourceScript.generated.root; - const script = sfc.scriptSetup ?? sfc.script; + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + const sfc = root._sfc; + const script = sfc.scriptSetup ?? sfc.script; if (!sfc.template || !script) { return; } @@ -71,19 +76,22 @@ export function create( const { uri, range, newName } = codeAction.data as ActionData; const [startOffset, endOffset]: [number, number] = range; + const parsedUri = URI.parse(uri); const decoded = context.decodeEmbeddedDocumentUri(parsedUri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { return codeAction; } - const document = context.documents.get(parsedUri, virtualCode.languageId, virtualCode.snapshot); - const sfcDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); - const { _sfc: sfc } = sourceScript.generated.root; - const script = sfc.scriptSetup ?? sfc.script; + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return codeAction; + } + const sfc = root._sfc; + const script = sfc.scriptSetup ?? sfc.script; if (!sfc.template || !script) { return codeAction; } @@ -93,13 +101,16 @@ export function create( return codeAction; } - const toExtract = await tsPluginClient?.collectExtractProps(sourceScript.generated.root.fileName, templateCodeRange) ?? []; + const toExtract = await tsPluginClient?.collectExtractProps(root.fileName, templateCodeRange) ?? []; if (!toExtract) { return codeAction; } const templateInitialIndent = await context.env.getConfiguration!('vue.format.template.initialIndent') ?? true; const scriptInitialIndent = await context.env.getConfiguration!('vue.format.script.initialIndent') ?? false; + + const document = context.documents.get(parsedUri, virtualCode.languageId, virtualCode.snapshot); + const sfcDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); const newUri = sfcDocument.uri.slice(0, sfcDocument.uri.lastIndexOf('/') + 1) + `${newName}.vue`; const lastImportNode = getLastImportNode(ts, script.ast); diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 0282eed999..0891adcb43 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -15,80 +15,89 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { return { async provideInlayHints(document, range) { - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!(virtualCode instanceof VueVirtualCode)) { + return; + } - if (virtualCode instanceof VueVirtualCode) { + const settings: Record = {}; + async function getSettingEnabled(key: string) { + return settings[key] ??= await context.env.getConfiguration?.(key) ?? false; + } - const codegen = tsCodegen.get(virtualCode._sfc); - const inlayHints = [ - ...codegen?.generatedTemplate.get()?.inlayHints ?? [], - ...codegen?.generatedScript.get()?.inlayHints ?? [], - ]; - const scriptSetupRanges = codegen?.scriptSetupRanges.get(); + const result: vscode.InlayHint[] = []; + + const codegen = tsCodegen.get(virtualCode._sfc); + const inlayHints = [ + ...codegen?.generatedTemplate.get()?.inlayHints ?? [], + ...codegen?.generatedScript.get()?.inlayHints ?? [], + ]; + const scriptSetupRanges = codegen?.scriptSetupRanges.get(); - if (scriptSetupRanges?.defineProps?.destructured && virtualCode._sfc.scriptSetup?.ast) { - const setting = 'vue.inlayHints.destructuredProps'; - settings[setting] ??= await context.env.getConfiguration?.(setting) ?? false; + if (scriptSetupRanges?.defineProps?.destructured && virtualCode._sfc.scriptSetup?.ast) { + const setting = 'vue.inlayHints.destructuredProps'; + const enabled = await getSettingEnabled(setting); - if (settings[setting]) { - for (const [prop, isShorthand] of findDestructuredProps( + if (enabled) { + for (const [prop, isShorthand] of findDestructuredProps( ts, virtualCode._sfc.scriptSetup.ast, scriptSetupRanges.defineProps.destructured )) { - const name = prop.text; - const end = prop.getEnd(); - const pos = isShorthand ? end : end - name.length; - const label = isShorthand ? `: props.${name}` : 'props.'; - inlayHints.push({ - blockName: 'scriptSetup', - offset: pos, - setting, - label, - }); - } + const name = prop.text; + const end = prop.getEnd(); + const pos = isShorthand ? end : end - name.length; + const label = isShorthand ? `: props.${name}` : 'props.'; + inlayHints.push({ + blockName: 'scriptSetup', + offset: pos, + setting, + label, + }); } } + } - const blocks = [ - virtualCode._sfc.template, - virtualCode._sfc.script, - virtualCode._sfc.scriptSetup, - ]; - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - - for (const hint of inlayHints) { - - const block = blocks.find(block => block?.name === hint.blockName); - const hintOffset = (block?.startTagEnd ?? 0) + hint.offset; - - if (hintOffset >= start && hintOffset <= end) { - - settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; - - if (!settings[hint.setting]) { - continue; - } - - result.push({ - label: hint.label, - paddingRight: hint.paddingRight, - paddingLeft: hint.paddingLeft, - position: document.positionAt(hintOffset), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - tooltip: hint.tooltip ? { - kind: 'markdown', - value: hint.tooltip, - } : undefined, - }); - } + const blocks = [ + virtualCode._sfc.template, + virtualCode._sfc.script, + virtualCode._sfc.scriptSetup, + ]; + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + + for (const hint of inlayHints) { + const block = blocks.find(block => block?.name === hint.blockName); + if (!block) { + continue; + } + + const hintOffset = block.startTagEnd + hint.offset; + if (hintOffset < start || hintOffset >= end) { + continue; } + + const enabled = await getSettingEnabled(hint.setting); + if (!enabled) { + continue; + } + + result.push({ + label: hint.label, + paddingRight: hint.paddingRight, + paddingLeft: hint.paddingLeft, + position: document.positionAt(hintOffset), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + tooltip: hint.tooltip ? { + kind: 'markdown', + value: hint.tooltip, + } : undefined, + }); } + return result; }, }; diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index 849f59bf55..a94ded1900 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -1,5 +1,5 @@ import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service'; -import * as vue from '@vue/language-core'; +import { VueVirtualCode } from '@vue/language-core'; import { create as createHtmlService } from 'volar-service-html'; import * as html from 'vscode-html-languageservice'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -18,12 +18,12 @@ export function create(): LanguageServicePlugin { return [sfcDataProvider]; }, async getFormattingOptions(document, options, context) { - return await worker(document, context, async vueCode => { + return await worker(document, context, async root => { const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; const blockTypes = ['template', 'script', 'style']; - for (const customBlock of vueCode._sfc.customBlocks) { + for (const customBlock of root._sfc.customBlocks) { blockTypes.push(customBlock.type); } @@ -53,7 +53,7 @@ export function create(): LanguageServicePlugin { provideDocumentLinks: undefined, async resolveEmbeddedCodeFormattingOptions(sourceScript, virtualCode, options) { - if (sourceScript.generated?.root instanceof vue.VueVirtualCode) { + if (sourceScript.generated?.root instanceof VueVirtualCode) { if (virtualCode.id === 'script_raw' || virtualCode.id === 'scriptsetup_raw') { if (await context.env.getConfiguration?.('vue.format.script.initialIndent') ?? false) { options.initialIndentLevel++; @@ -74,54 +74,54 @@ export function create(): LanguageServicePlugin { }, provideDocumentSymbols(document) { - return worker(document, context, vueSourceFile => { + return worker(document, context, root => { const result: vscode.DocumentSymbol[] = []; - const descriptor = vueSourceFile._sfc; + const sfc = root._sfc; - if (descriptor.template) { + if (sfc.template) { result.push({ name: 'template', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.template.start), - end: document.positionAt(descriptor.template.end), + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.end), }, selectionRange: { - start: document.positionAt(descriptor.template.start), - end: document.positionAt(descriptor.template.startTagEnd), + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.startTagEnd), }, }); } - if (descriptor.script) { + if (sfc.script) { result.push({ name: 'script', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.script.start), - end: document.positionAt(descriptor.script.end), + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.end), }, selectionRange: { - start: document.positionAt(descriptor.script.start), - end: document.positionAt(descriptor.script.startTagEnd), + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.startTagEnd), }, }); } - if (descriptor.scriptSetup) { + if (sfc.scriptSetup) { result.push({ name: 'script setup', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.scriptSetup.start), - end: document.positionAt(descriptor.scriptSetup.end), + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.end), }, selectionRange: { - start: document.positionAt(descriptor.scriptSetup.start), - end: document.positionAt(descriptor.scriptSetup.startTagEnd), + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.startTagEnd), }, }); } - for (const style of descriptor.styles) { + for (const style of sfc.styles) { let name = 'style'; if (style.scoped) { name += ' scoped'; @@ -142,7 +142,7 @@ export function create(): LanguageServicePlugin { }, }); } - for (const customBlock of descriptor.customBlocks) { + for (const customBlock of sfc.customBlocks) { result.push({ name: `${customBlock.type}`, kind: 2 satisfies typeof vscode.SymbolKind.Module, @@ -237,14 +237,16 @@ export function create(): LanguageServicePlugin { }, }; - function worker(document: TextDocument, context: LanguageServiceContext, callback: (vueSourceFile: vue.VueVirtualCode) => T) { + function worker(document: TextDocument, context: LanguageServiceContext, callback: (root: VueVirtualCode) => T) { if (document.languageId !== 'vue-root-tags') { return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (sourceScript?.generated?.root instanceof vue.VueVirtualCode) { - return callback(sourceScript.generated.root); + const root = sourceScript?.generated?.root; + if (root instanceof VueVirtualCode) { + return callback(root); } } } diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index d76c55d2ef..e2c8320dc3 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -162,14 +162,17 @@ export function create( let sync: (() => Promise) | undefined; let currentVersion: number | undefined; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (sourceScript?.generated?.root instanceof VueVirtualCode) { + const root = sourceScript?.generated?.root; + + if (root instanceof VueVirtualCode) { // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token); - sync = (await provideHtmlData(vueCompilerOptions, sourceScript.id, sourceScript.generated.root)).sync; + sync = (await provideHtmlData(vueCompilerOptions, sourceScript!.id, root)).sync; currentVersion = await sync(); } @@ -211,7 +214,6 @@ export function create( return; } - const result: vscode.InlayHint[] = []; const uri = URI.parse(document.uri); const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); @@ -220,101 +222,108 @@ export function create( return; } - const code = context.language.scripts.get(decoded[0])?.generated?.root; + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + const scanner = getScanner(baseServiceInstance, document); + if (!scanner) { + return; + } - if (code instanceof VueVirtualCode && scanner) { - - // visualize missing required props - const casing = await getNameCasing(context, decoded[0]); - const components = await tsPluginClient?.getComponentNames(code.fileName) ?? []; - const componentProps: Record = {}; - let token: html.TokenType; - let current: { - unburnedRequiredProps: string[]; - labelOffset: number; - insertOffset: number; - } | undefined; - while ((token = scanner.scan()) !== html.TokenType.EOS) { - if (token === html.TokenType.StartTag) { - const tagName = scanner.getTokenText(); - const checkTag = tagName.includes('.') - ? tagName - : components.find(component => component === tagName || hyphenateTag(component) === tagName); - if (checkTag) { - 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(), - insertOffset: scanner.getTokenOffset() + scanner.getTokenLength(), - }; - } + const result: vscode.InlayHint[] = []; + + // visualize missing required props + const casing = await getNameCasing(context, decoded[0]); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const componentProps: Record = {}; + let token: html.TokenType; + let current: { + unburnedRequiredProps: string[]; + labelOffset: number; + insertOffset: number; + } | undefined; + + while ((token = scanner.scan()) !== html.TokenType.EOS) { + if (token === html.TokenType.StartTag) { + const tagName = scanner.getTokenText(); + const checkTag = tagName.includes('.') + ? tagName + : components.find(component => component === tagName || hyphenateTag(component) === tagName); + if (checkTag) { + componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(root.fileName, checkTag) ?? []) + .filter(prop => prop.required) + .map(prop => prop.name); + current = { + unburnedRequiredProps: [...componentProps[checkTag]], + labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), + insertOffset: scanner.getTokenOffset() + scanner.getTokenLength(), + }; } - else if (token === html.TokenType.AttributeName) { - if (current) { - let attrText = scanner.getTokenText(); + } + else if (token === html.TokenType.AttributeName) { + if (current) { + let attrText = scanner.getTokenText(); - if (attrText === 'v-bind') { - current.unburnedRequiredProps = []; + if (attrText === 'v-bind') { + current.unburnedRequiredProps = []; + } + else { + // remove modifiers + if (attrText.includes('.')) { + attrText = attrText.split('.')[0]; } - else { - // remove modifiers - if (attrText.includes('.')) { - attrText = attrText.split('.')[0]; - } - // normalize - if (attrText.startsWith('v-bind:')) { - attrText = attrText.slice('v-bind:'.length); - } - else if (attrText.startsWith(':')) { - attrText = attrText.slice(':'.length); - } - else if (attrText.startsWith('v-model:')) { - attrText = attrText.slice('v-model:'.length); - } - else if (attrText === 'v-model') { - attrText = vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? - } - else if (attrText.startsWith('v-on:')) { - attrText = 'on-' + hyphenateAttr(attrText.slice('v-on:'.length)); - } - else if (attrText.startsWith('@')) { - attrText = 'on-' + hyphenateAttr(attrText.slice('@'.length)); - } - - current.unburnedRequiredProps = current.unburnedRequiredProps.filter(propName => { - return attrText !== propName - && attrText !== hyphenateAttr(propName); - }); + // normalize + if (attrText.startsWith('v-bind:')) { + attrText = attrText.slice('v-bind:'.length); } - } - } - else if (token === html.TokenType.StartTagSelfClose || token === html.TokenType.StartTagClose) { - if (current) { - for (const requiredProp of current.unburnedRequiredProps) { - result.push({ - label: `${requiredProp}!`, - paddingLeft: true, - position: document.positionAt(current.labelOffset), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - textEdits: [{ - range: { - start: document.positionAt(current.insertOffset), - end: document.positionAt(current.insertOffset), - }, - newText: ` :${casing.attr === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp}=`, - }], - }); + else if (attrText.startsWith(':')) { + attrText = attrText.slice(':'.length); + } + else if (attrText.startsWith('v-model:')) { + attrText = attrText.slice('v-model:'.length); } - current = undefined; + else if (attrText === 'v-model') { + attrText = vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? + } + else if (attrText.startsWith('v-on:')) { + attrText = 'on-' + hyphenateAttr(attrText.slice('v-on:'.length)); + } + else if (attrText.startsWith('@')) { + attrText = 'on-' + hyphenateAttr(attrText.slice('@'.length)); + } + + current.unburnedRequiredProps = current.unburnedRequiredProps.filter(propName => { + return attrText !== propName + && attrText !== hyphenateAttr(propName); + }); } } - if (token === html.TokenType.AttributeName || token === html.TokenType.AttributeValue) { - if (current) { - current.insertOffset = scanner.getTokenOffset() + scanner.getTokenLength(); + } + else if (token === html.TokenType.StartTagSelfClose || token === html.TokenType.StartTagClose) { + if (current) { + for (const requiredProp of current.unburnedRequiredProps) { + result.push({ + label: `${requiredProp}!`, + paddingLeft: true, + position: document.positionAt(current.labelOffset), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + textEdits: [{ + range: { + start: document.positionAt(current.insertOffset), + end: document.positionAt(current.insertOffset), + }, + newText: ` :${casing.attr === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp}=`, + }], + }); } + current = undefined; + } + } + if (token === html.TokenType.AttributeName || token === html.TokenType.AttributeValue) { + if (current) { + current.insertOffset = scanner.getTokenOffset() + scanner.getTokenLength(); } } } @@ -341,7 +350,6 @@ export function create( return; } - const originalResult = await baseServiceInstance.provideDiagnostics?.(document, token); const uri = URI.parse(document.uri); const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); @@ -350,13 +358,14 @@ export function create( return; } - const code = context.language.scripts.get(decoded[0])?.generated?.root; - if (!(code instanceof VueVirtualCode)) { + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } + const originalResult = await baseServiceInstance.provideDiagnostics?.(document, token); const templateErrors: vscode.Diagnostic[] = []; - const { template } = code._sfc; + const { template } = root._sfc; if (template) { @@ -396,27 +405,34 @@ export function create( }, provideDocumentSemanticTokens(document, range, legend) { + if (!isSupportedDocument(document)) { return; } + if (!context.project.vue) { return; } const vueCompilerOptions = context.project.vue.compilerOptions; + const languageService = context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'); if (!languageService) { return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if ( - !sourceScript - || !(sourceScript.generated?.root instanceof VueVirtualCode) - || !sourceScript.generated.root._sfc.template - ) { - return []; + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { + return; } - const { template } = sourceScript.generated.root._sfc; + + const { template } = root._sfc; + if (!template) { + return; + } + const spans = getComponentSpans.call( { files: context.language.scripts, @@ -424,13 +440,15 @@ export function create( typescript: ts, vueOptions: vueCompilerOptions, }, - sourceScript.generated.root, + root, template, { start: document.offsetAt(range.start), length: document.offsetAt(range.end) - document.offsetAt(range.start), - }); + } + ); const classTokenIndex = legend.tokenTypes.indexOf('class'); + return spans.map(span => { const start = document.positionAt(span.start); return [ diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index e35b89c243..c011f3cbfb 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,5 +1,5 @@ import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service'; -import * as vue from '@vue/language-core'; +import { VueVirtualCode } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; import { URI } from 'vscode-uri'; @@ -18,10 +18,16 @@ export function create( return { async provideInlayHints(document, range) { - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(sourceScript?.generated?.root instanceof vue.VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -40,7 +46,7 @@ export function create( for (const [pointerPosition, hoverOffset] of hoverOffsets) { const map = context.language.maps.get(virtualCode, sourceScript); for (const [sourceOffset] of map.toSourceLocation(hoverOffset)) { - const quickInfo = await tsPluginClient?.getQuickInfoAtPosition(sourceScript.generated.root.fileName, sourceOffset); + const quickInfo = await tsPluginClient?.getQuickInfoAtPosition(root.fileName, sourceOffset); if (quickInfo) { inlayHints.push({ position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index c83453c349..152bf8fa00 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -194,13 +194,14 @@ function getEncodedSemanticClassifications( return (filePath, span, format) => { const fileName = filePath.replace(windowsPathReg, '/'); const result = getEncodedSemanticClassifications(fileName, span, format); - const file = language.scripts.get(asScriptId(fileName)); - if (file?.generated?.root instanceof VueVirtualCode) { - const { template } = file.generated.root._sfc; + const sourceScript = language.scripts.get(asScriptId(fileName)); + const root = sourceScript?.generated?.root; + if (root instanceof VueVirtualCode) { + const { template } = root._sfc; if (template) { for (const componentSpan of getComponentSpans.call( { typescript: ts, languageService }, - file.generated.root, + root, template, { start: span.start - template.startTagEnd, diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index a3e203d3e9..0c4ee82265 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -8,8 +8,13 @@ export function collectExtractProps( ) { const { typescript: ts, languageService, language, isTsPlugin, getFileId } = this; - const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { + const sourceScript = language.scripts.get(getFileId(fileName)); + if (!sourceScript?.generated) { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -21,9 +26,9 @@ export function collectExtractProps( const program = languageService.getProgram()!; const sourceFile = program.getSourceFile(fileName)!; const checker = program.getTypeChecker(); - const script = volarFile.generated?.languagePlugin.typescript?.getServiceScript(volarFile.generated.root); + const script = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root); const maps = script ? [...language.maps.forEach(script.code)].map(([_sourceScript, map]) => map) : []; - const sfc = volarFile.generated.root._sfc; + const sfc = root._sfc; sourceFile.forEachChild(function visit(node) { if ( @@ -35,7 +40,7 @@ export function collectExtractProps( const { name } = node; for (const map of maps) { let mapped = false; - for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? volarFile.snapshot.getLength() : 0))) { + for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? sourceScript.snapshot.getLength() : 0))) { if ( source[0] >= sfc.template!.startTagEnd + templateCodeRange[0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange[1] From 1cbf715a58daf75c4262fb40c31887feef9a18ed Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 19:42:40 +0000 Subject: [PATCH 13/45] ci(lint): auto-fix --- packages/language-service/lib/plugins/vue-inlayhints.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 0891adcb43..3cee77fb07 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -43,10 +43,10 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { if (enabled) { for (const [prop, isShorthand] of findDestructuredProps( - ts, - virtualCode._sfc.scriptSetup.ast, - scriptSetupRanges.defineProps.destructured - )) { + ts, + virtualCode._sfc.scriptSetup.ast, + scriptSetupRanges.defineProps.destructured + )) { const name = prop.text; const end = prop.getEnd(); const pos = isShorthand ? end : end - name.length; From a161283483dc15df5855d12c616bd82c64360910 Mon Sep 17 00:00:00 2001 From: rioj7 <38918937+rioj7@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:46:41 +0100 Subject: [PATCH 14/45] docs: fix broken marketplace page (#5004) --- extensions/vscode/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md index 0271f714fe..6dbe71267a 100644 --- a/extensions/vscode/README.md +++ b/extensions/vscode/README.md @@ -259,13 +259,13 @@ Finally you need to make VS Code recognize your new extension and automatically -| Command | Title | -| ------------------------------ | ------------------------------------------------------ | -| `vue.action.restartServer` | Vue: Restart Vue and TS servers | -| `vue.action.doctor` | Vue: Doctor | -| `vue.action.writeVirtualFiles` | Vue (Debug): Write Virtual Files | -| `vue.action.splitEditors` | Vue: Split <script>,