diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 5dcdc9eac1..7cb80eecdd 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -474,7 +474,7 @@ "items": { "type": "string" }, - "default": [], + "default": [ ], "description": "List any additional file extensions that should be processed as Vue files (requires restart)." }, "vue.server.fullCompletionList": { @@ -595,6 +595,11 @@ "type": "boolean", "default": false, "description": "Show inlay hints for component options wrapper for type support." + }, + "vue.inlayHints.vbindShorthand": { + "type": "boolean", + "default": false, + "description": "Show inlay hints for v-bind shorthand." } } }, @@ -737,7 +742,7 @@ "devDependencies": { "@types/semver": "^7.5.3", "@types/vscode": "^1.82.0", - "@volar/vscode": "~1.11.1", + "@volar/vscode": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "@vue/language-server": "1.8.26", "esbuild": "latest", diff --git a/extensions/vscode/src/common.ts b/extensions/vscode/src/common.ts index 0adef5beb4..15ba18f2bb 100644 --- a/extensions/vscode/src/common.ts +++ b/extensions/vscode/src/common.ts @@ -1,5 +1,6 @@ import { activateAutoInsertion, + activateDocumentDropEdit, activateFindFileReferences, activateReloadProjects, activateServerSys, @@ -102,8 +103,17 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang javascriptreact: true, typescriptreact: true, }; + const selectors: vscode.DocumentFilter[] = [{ language: 'vue' }]; + + if (config.server.petiteVue.supportHtmlFile) { + selectors.push({ language: 'html' }); + } + if (config.server.vitePress.supportMdFile) { + selectors.push({ language: 'markdown' }); + } activateAutoInsertion([syntacticClient, semanticClient], document => supportedLanguages[document.languageId]); + activateDocumentDropEdit(selectors, semanticClient); activateWriteVirtualFiles('volar.action.writeVirtualFiles', semanticClient); activateFindFileReferences('volar.vue.findAllFileReferences', semanticClient); activateTsConfigStatusItem('volar.openTsconfig', semanticClient, @@ -151,14 +161,14 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang } function activateServerMaxOldSpaceSizeChange() { - vscode.workspace.onDidChangeConfiguration((e) => { + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('vue.server.runtime') || e.affectsConfiguration('vue.server.path')) { requestReloadVscode(); } if (e.affectsConfiguration('vue')) { vscode.commands.executeCommand('volar.action.restartServer'); } - }); + })); } async function activateRestartRequest() { diff --git a/extensions/vscode/src/features/dragImport.ts b/extensions/vscode/src/features/dragImport.ts deleted file mode 100644 index 46878bd4ed..0000000000 --- a/extensions/vscode/src/features/dragImport.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { GetDragAndDragImportEditsRequest, TagNameCasing } from '@vue/language-server'; -import * as vscode from 'vscode'; -import type { BaseLanguageClient, DocumentFilter, InsertTextFormat } from 'vscode-languageclient'; -import { tagNameCasings } from './nameCasing'; -import { config } from '../config'; - -export async function register(context: vscode.ExtensionContext, client: BaseLanguageClient) { - - const selectors: DocumentFilter[] = [{ language: 'vue' }]; - - if (config.server.petiteVue.supportHtmlFile) { - selectors.push({ language: 'html' }); - } - if (config.server.vitePress.supportMdFile) { - selectors.push({ language: 'markdown' }); - } - - context.subscriptions.push( - vscode.languages.registerDocumentDropEditProvider( - selectors, - { - async provideDocumentDropEdits(document, _position, dataTransfer) { - for (const [mimeType, item] of dataTransfer) { - if (mimeType === 'text/uri-list') { - const uri = item.value as string; - if ( - uri.endsWith('.vue') - || (uri.endsWith('.md') && config.server.vitePress.supportMdFile) - ) { - const response = await client.sendRequest(GetDragAndDragImportEditsRequest.type, { - uri: document.uri.toString(), - importUri: uri, - casing: tagNameCasings.get(document.uri.toString()) ?? TagNameCasing.Pascal, - }); - if (!response) { - return; - } - const additionalEdit = new vscode.WorkspaceEdit(); - for (const edit of response.additionalEdits) { - additionalEdit.replace( - document.uri, - new vscode.Range( - edit.range.start.line, - edit.range.start.character, - edit.range.end.line, - edit.range.end.character, - ), - edit.newText - ); - } - return { - insertText: response.insertTextFormat === 2 satisfies typeof InsertTextFormat.Snippet - ? new vscode.SnippetString(response.insertText) - : response.insertText, - additionalEdit, - }; - } - } - } - }, - } - ), - ); -} diff --git a/extensions/vscode/src/features/nameCasing.ts b/extensions/vscode/src/features/nameCasing.ts index 7209b5d761..d1a03d96f1 100644 --- a/extensions/vscode/src/features/nameCasing.ts +++ b/extensions/vscode/src/features/nameCasing.ts @@ -11,24 +11,25 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa await client.start(); + const disposes: vscode.Disposable[] = []; const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); statusBar.command = 'volar.action.nameCasing'; update(vscode.window.activeTextEditor?.document); - const d_1 = vscode.window.onDidChangeActiveTextEditor(e => { + disposes.push(vscode.window.onDidChangeActiveTextEditor(e => { update(e?.document); - }); - const d_2 = vscode.workspace.onDidChangeConfiguration(() => { + })); + disposes.push(vscode.workspace.onDidChangeConfiguration(() => { attrNameCasings.clear(); tagNameCasings.clear(); update(vscode.window.activeTextEditor?.document); - }); - const d_3 = vscode.workspace.onDidCloseTextDocument((doc) => { + })); + disposes.push(vscode.workspace.onDidCloseTextDocument((doc) => { attrNameCasings.delete(doc.uri.toString()); tagNameCasings.delete(doc.uri.toString()); - }); - const d_4 = vscode.commands.registerCommand('volar.action.nameCasing', async () => { + })); + disposes.push(vscode.commands.registerCommand('volar.action.nameCasing', async () => { if (!vscode.window.activeTextEditor?.document) return; @@ -80,14 +81,11 @@ export async function activate(_context: vscode.ExtensionContext, client: BaseLa await convertAttr(vscode.window.activeTextEditor, AttrNameCasing.Camel); } updateStatusBarText(); - }); + })); client.onDidChangeState(e => { if (e.newState === State.Stopped) { - d_1.dispose(); - d_2.dispose(); - d_3.dispose(); - d_4.dispose(); + disposes.forEach(d => d.dispose()); statusBar.dispose(); } }); diff --git a/extensions/vscode/src/nodeClientMain.ts b/extensions/vscode/src/nodeClientMain.ts index 6f9345af9c..5f31748249 100644 --- a/extensions/vscode/src/nodeClientMain.ts +++ b/extensions/vscode/src/nodeClientMain.ts @@ -18,7 +18,7 @@ export async function activate(context: vscode.ExtensionContext) { let cancellationPipeUpdateKey: string | undefined; let serverPathStatusItem: vscode.StatusBarItem | undefined; - vscode.workspace.onDidChangeTextDocument((e) => { + context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e) => { let key = e.document.uri.toString() + '|' + e.document.version; if (cancellationPipeUpdateKey === undefined) { cancellationPipeUpdateKey = key; @@ -28,7 +28,7 @@ export async function activate(context: vscode.ExtensionContext) { cancellationPipeUpdateKey = key; fs.writeFileSync(cancellationPipeName, ''); } - }); + })); await commonActivate(context, ( id, diff --git a/package.json b/package.json index 92a6a85a90..815abf6fc0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@lerna-lite/cli": "latest", "@lerna-lite/publish": "latest", - "@volar/language-service": "~1.11.1", + "@volar/language-service": "2.0.0-alpha.0", "typescript": "latest", "vite": "latest", "vitest": "latest" diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index 4deb45fb13..85742c89cf 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -13,7 +13,7 @@ "directory": "packages/component-meta" }, "dependencies": { - "@volar/typescript": "~1.11.1", + "@volar/typescript": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "path-browserify": "^1.0.1", "vue-component-type-helpers": "1.8.26" diff --git a/packages/component-meta/src/base.ts b/packages/component-meta/src/base.ts index c5b684399c..1f1dd25c33 100644 --- a/packages/component-meta/src/base.ts +++ b/packages/component-meta/src/base.ts @@ -3,7 +3,7 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import * as path from 'path-browserify'; import { code as typeHelpersCode } from 'vue-component-type-helpers'; import { code as vue2TypeHelpersCode } from 'vue-component-type-helpers/vue2'; -import { createLanguageServiceHost, decorateLanguageService } from '@volar/typescript'; +import { createLanguage, decorateLanguageService } from '@volar/typescript'; import type { MetaCheckerOptions, @@ -33,6 +33,7 @@ export function createCheckerByJsonConfigBase( checkerOptions, rootDir, path.join(rootDir, 'jsconfig.json.global.vue'), + undefined ); } @@ -48,6 +49,7 @@ export function createCheckerBase( checkerOptions, path.dirname(tsconfig), tsconfig + '.global.vue', + tsconfig, ); } @@ -57,6 +59,7 @@ function createCheckerWorker( checkerOptions: MetaCheckerOptions, rootPath: string, globalComponentName: string, + configFileName: string | undefined, ) { /** @@ -68,9 +71,8 @@ function createCheckerWorker( let projectVersion = 0; const scriptSnapshots = new Map(); - const _host: vue.TypeScriptLanguageHost = { - workspacePath: rootPath, - rootPath: rootPath, + const _host: vue.TypeScriptProjectHost = { + getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), getCompilationSettings: () => parsedCommandLine.options, getScriptFileNames: () => fileNames, @@ -84,10 +86,13 @@ function createCheckerWorker( } return scriptSnapshots.get(fileName); }, + getFileId: fileName => fileName, + getFileName: id => id, + getLanguageId: vue.resolveCommonLanguageId, }; return { - ...baseCreate(ts, _host, vue.resolveVueCompilerOptions(parsedCommandLine.vueOptions), checkerOptions, globalComponentName), + ...baseCreate(ts, configFileName, _host, vue.resolveVueCompilerOptions(parsedCommandLine.vueOptions), checkerOptions, globalComponentName), updateFile(fileName: string, text: string) { fileName = fileName.replace(windowsPathReg, '/'); scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(text)); @@ -112,59 +117,60 @@ function createCheckerWorker( export function baseCreate( ts: typeof import('typescript/lib/tsserverlibrary'), - _host: vue.TypeScriptLanguageHost, + configFileName: string | undefined, + host: vue.TypeScriptProjectHost, vueCompilerOptions: vue.VueCompilerOptions, checkerOptions: MetaCheckerOptions, globalComponentName: string, ) { const globalComponentSnapshot = ts.ScriptSnapshot.fromString(''); const metaSnapshots: Record = {}; - const host = new Proxy>({ - getScriptFileNames: () => { - const names = _host.getScriptFileNames(); - return [ - ...names, - ...names.map(getMetaFileName), - globalComponentName, - getMetaFileName(globalComponentName), - ]; - }, - getScriptSnapshot: fileName => { - if (isMetaFileName(fileName)) { - if (!metaSnapshots[fileName]) { - metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); - } - return metaSnapshots[fileName]; - } - else if (fileName === globalComponentName) { - return globalComponentSnapshot; - } - else { - return _host.getScriptSnapshot(fileName); - } - }, - }, { - get(target, prop) { - if (prop in target) { - return target[prop as keyof typeof target]; + const getScriptFileNames = host.getScriptFileNames; + const getScriptSnapshot = host.getScriptSnapshot; + host.getScriptFileNames = () => { + const names = getScriptFileNames(); + return [ + ...names, + ...names.map(getMetaFileName), + globalComponentName, + getMetaFileName(globalComponentName), + ]; + }; + host.getScriptSnapshot = (fileName) => { + if (isMetaFileName(fileName)) { + if (!metaSnapshots[fileName]) { + metaSnapshots[fileName] = ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName)); } - return _host[prop as keyof typeof _host]; - }, - }) as vue.TypeScriptLanguageHost; - const vueLanguages = vue.createLanguages( + return metaSnapshots[fileName]; + } + else if (fileName === globalComponentName) { + return globalComponentSnapshot; + } + else { + return getScriptSnapshot(fileName); + } + }; + + const vueLanguagePlugins = vue.createLanguages( ts, host.getCompilationSettings(), vueCompilerOptions, ); - const core = vue.createLanguageContext(host, vueLanguages); - const tsLsHost = createLanguageServiceHost(core, ts, ts.sys); - const tsLs = ts.createLanguageService(tsLsHost); + const language = createLanguage( + ts, + ts.sys, + vueLanguagePlugins, + configFileName, + host, + ); + const { languageServiceHost } = language.typescript!; + const tsLs = ts.createLanguageService(languageServiceHost); - decorateLanguageService(core.virtualFiles, tsLs, false); + decorateLanguageService(language.files, tsLs, false); if (checkerOptions.forceUseTs) { - const getScriptKind = tsLsHost.getScriptKind; - tsLsHost.getScriptKind = (fileName) => { + const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost); + languageServiceHost.getScriptKind = (fileName) => { if (fileName.endsWith('.vue.js')) { return ts.ScriptKind.TS; } @@ -281,7 +287,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} .map((prop) => { const { resolveNestedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveNestedProperties(prop); }) @@ -300,7 +306,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const printer = ts.createPrinter(checkerOptions.printer); const snapshot = host.getScriptSnapshot(componentPath)!; - const vueSourceFile = core.virtualFiles.getSource(componentPath)?.root; + const vueSourceFile = language.files.getSourceFile(componentPath)?.virtualFile?.[0]; const vueDefaults = vueSourceFile && exportName === 'default' ? (vueSourceFile instanceof vue.VueFile ? readVueComponentDefaultProps(vueSourceFile, printer, ts, vueCompilerOptions) : {}) : {}; @@ -345,7 +351,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} const { resolveEventSignature, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveEventSignature(call); }).filter(event => event.name); @@ -365,7 +371,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveSlotProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveSlotProperties(prop); }); @@ -388,7 +394,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode} return properties.map((prop) => { const { resolveExposedProperties, - } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, core); + } = createSchemaResolvers(typeChecker, symbolNode!, checkerOptions, ts, language); return resolveExposedProperties(prop); }); @@ -447,7 +453,7 @@ function createSchemaResolvers( symbolNode: ts.Expression, { rawType, schema: options, noDeclarations }: MetaCheckerOptions, ts: typeof import('typescript/lib/tsserverlibrary'), - core: vue.LanguageContext, + core: vue.Language, ) { const visited = new Set(); @@ -638,12 +644,12 @@ function createSchemaResolvers( } function getDeclaration(declaration: ts.Declaration): Declaration | undefined { const fileName = declaration.getSourceFile().fileName; - const [virtualFile] = core.virtualFiles.getVirtualFile(fileName); + const [virtualFile] = core.files.getVirtualFile(fileName); if (virtualFile) { - const maps = core.virtualFiles.getMaps(virtualFile); + const maps = core.files.getMaps(virtualFile); for (const [source, [_, map]] of maps) { - const start = map.toSourceOffset(declaration.getStart()); - const end = map.toSourceOffset(declaration.getEnd()); + const start = map.getSourceOffset(declaration.getStart()); + const end = map.getSourceOffset(declaration.getEnd()); if (start && end) { return { file: source, diff --git a/packages/language-core/package.json b/packages/language-core/package.json index 70ac7e1cb9..2b0694e4d0 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -13,13 +13,11 @@ "directory": "packages/language-core" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/source-map": "~1.11.1", - "@vue/compiler-dom": "^3.3.0", - "@vue/shared": "^3.3.0", + "@volar/language-core": "2.0.0-alpha.0", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "muggle-string": "^0.3.1", "path-browserify": "^1.0.1", "vue-template-compiler": "^2.7.14" }, @@ -27,7 +25,7 @@ "@types/minimatch": "^5.1.2", "@types/node": "latest", "@types/path-browserify": "^1.0.1", - "@vue/compiler-sfc": "^3.3.0" + "@vue/compiler-sfc": "^3.4.0" }, "peerDependencies": { "typescript": "*" diff --git a/packages/language-core/schemas/vue-tsconfig.deprecated.schema.json b/packages/language-core/schemas/vue-tsconfig.deprecated.schema.json index 2a4b89da79..94224badac 100644 --- a/packages/language-core/schemas/vue-tsconfig.deprecated.schema.json +++ b/packages/language-core/schemas/vue-tsconfig.deprecated.schema.json @@ -57,6 +57,12 @@ "experimentalRfc436": { "deprecated": true }, + "hooks": { + "deprecated": true, + "type": "array", + "description": "Deprecated since v2.0.0.", + "markdownDescription": "https://github.com/vuejs/language-tools/pull/2217" + }, "narrowingTypesInInlineHandlers": { "deprecated": true }, diff --git a/packages/language-core/schemas/vue-tsconfig.schema.json b/packages/language-core/schemas/vue-tsconfig.schema.json index 2f40e2fcf3..07944fa142 100644 --- a/packages/language-core/schemas/vue-tsconfig.schema.json +++ b/packages/language-core/schemas/vue-tsconfig.schema.json @@ -61,10 +61,6 @@ "default": [ ], "markdownDescription": "Plugins to be used in the SFC compiler." }, - "hooks": { - "type": "array", - "markdownDescription": "https://github.com/vuejs/language-tools/pull/2217" - }, "optionsWrapper": { "type": "array", "default": [ diff --git a/packages/language-core/src/generators/script.ts b/packages/language-core/src/generators/script.ts index 72776cec2d..111b4c12bd 100644 --- a/packages/language-core/src/generators/script.ts +++ b/packages/language-core/src/generators/script.ts @@ -1,18 +1,15 @@ -import { FileRangeCapabilities, MirrorBehaviorCapabilities } from '@volar/language-core'; -import * as SourceMaps from '@volar/source-map'; -import { Segment, getLength } from '@volar/source-map'; -import * as muggle from 'muggle-string'; +import { Mapping } from '@volar/language-core'; import * as path from 'path-browserify'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import type * as templateGen from '../generators/template'; import type { ScriptRanges } from '../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../parsers/scriptSetupRanges'; -import type { TextRange, VueCompilerOptions } from '../types'; +import type { Code, CodeAndStack, SfcBlock, VueCompilerOptions } from '../types'; import { Sfc } from '../types'; import { getSlotsPropertyName, hyphenateTag } from '../utils/shared'; -import { walkInterpolationFragment } from '../utils/transform'; +import { eachInterpolationSegment } from '../utils/transform'; +import { disableAllFeatures, enableAllFeatures, withStack } from './utils'; -export function generate( +export function* generate( ts: typeof import('typescript/lib/tsserverlibrary'), fileName: string, script: Sfc['script'], @@ -21,14 +18,19 @@ export function generate( lang: string, scriptRanges: ScriptRanges | undefined, scriptSetupRanges: ScriptSetupRanges | undefined, - htmlGen: ReturnType | undefined, + templateCodegen: { + tsCodes: Code[]; + tsCodegenStacks: string[]; + tagNames: Set; + accessedGlobalVariables: Set; + hasSlot: boolean; + } | undefined, compilerOptions: ts.CompilerOptions, vueCompilerOptions: VueCompilerOptions, + getGeneratedLength: () => number, + linkedCodeMappings: Mapping[] = [], codegenStack: boolean, -) { - - const [codes, codeStacks] = codegenStack ? muggle.track([] as Segment[]) : [[], []]; - const mirrorBehaviorMappings: SourceMaps.Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; +): Generator { //#region monkey fix: https://github.com/vuejs/language-tools/pull/2113 if (!script && !scriptSetup) { @@ -69,100 +71,105 @@ export function generate( WithTemplateSlots: false, PropsChildren: false, }; - - codes.push(`/* __placeholder__ */\n`); + const _ = codegenStack ? withStack : (code: Code): CodeAndStack => [code, '']; let generatedTemplate = false; + let scriptSetupGeneratedOffset: number | undefined; - generateSrc(); - generateScriptSetupImports(); - generateScriptContentBeforeExportDefault(); - generateScriptSetupAndTemplate(); - generateHelperTypes(); - generateScriptContentAfterExportDefault(); + yield _(`/* __placeholder__ */\n`); + yield* generateSrc(); + yield* generateScriptSetupImports(); + yield* generateScriptContentBeforeExportDefault(); + yield* generateScriptSetupAndTemplate(); + yield* generateHelperTypes(); + yield* generateScriptContentAfterExportDefault(); if (!generatedTemplate) { - generateTemplate(false); + yield* generateTemplate(false); } if (scriptSetup) { - // for code action edits - codes.push([ - '', - 'scriptSetup', - scriptSetup.content.length, - {}, - ]); + yield _(['', 'scriptSetup', scriptSetup.content.length, disableAllFeatures({ verification: true })]); } - return { - codes, - codeStacks, - mirrorBehaviorMappings, - }; - - function generateHelperTypes() { + function* generateHelperTypes(): Generator { if (usedHelperTypes.DefinePropsToOptions) { if (compilerOptions.exactOptionalPropertyTypes) { - codes.push(`type __VLS_TypePropsToRuntimeProps = { [K in keyof T]-?: {} extends Pick ? { type: import('${vueCompilerOptions.lib}').PropType } : { type: import('${vueCompilerOptions.lib}').PropType, required: true } };\n`); + yield _(`type __VLS_TypePropsToRuntimeProps = {\n` + + ` [K in keyof T]-?: {} extends Pick\n` + + ` ? { type: import('${vueCompilerOptions.lib}').PropType }\n` + + ` : { type: import('${vueCompilerOptions.lib}').PropType, required: true }\n` + + `};\n`); } else { - codes.push(`type __VLS_NonUndefinedable = T extends undefined ? never : T;\n`); - codes.push(`type __VLS_TypePropsToRuntimeProps = { [K in keyof T]-?: {} extends Pick ? { type: import('${vueCompilerOptions.lib}').PropType<__VLS_NonUndefinedable> } : { type: import('${vueCompilerOptions.lib}').PropType, required: true } };\n`); + yield _(`type __VLS_NonUndefinedable = T extends undefined ? never : T;\n`); + yield _(`type __VLS_TypePropsToRuntimeProps = {\n` + + ` [K in keyof T]-?: {} extends Pick\n` + + ` ? { type: import('${vueCompilerOptions.lib}').PropType<__VLS_NonUndefinedable> }\n` + + ` : { type: import('${vueCompilerOptions.lib}').PropType, required: true }\n` + + `};\n`); } } if (usedHelperTypes.MergePropDefaults) { - codes.push(`type __VLS_WithDefaults = { - // use 'keyof Pick' instead of 'keyof P' to keep props jsdoc - [K in keyof Pick]: K extends keyof D ? __VLS_Prettify : P[K] - };\n`); - codes.push(`type __VLS_Prettify = { [K in keyof T]: T[K]; } & {};\n`); + yield _(`type __VLS_WithDefaults = {\n` + // use 'keyof Pick' instead of 'keyof P' to keep props jsdoc + + ` [K in keyof Pick]: K extends keyof D\n` + + ` ? __VLS_Prettify\n` + + ` : P[K]\n` + + `};\n`); + yield _(`type __VLS_Prettify = { [K in keyof T]: T[K]; } & {};\n`); } if (usedHelperTypes.WithTemplateSlots) { - codes.push( - `type __VLS_WithTemplateSlots = T & { new(): {\n`, - `${getSlotsPropertyName(vueCompilerOptions.target)}: S;\n`, - ); + yield _(`type __VLS_WithTemplateSlots = T & {\n` + + ` new(): {\n` + + ` ${getSlotsPropertyName(vueCompilerOptions.target)}: S;\n`); if (vueCompilerOptions.jsxSlots) { usedHelperTypes.PropsChildren = true; - codes.push(`$props: __VLS_PropsChildren;\n`); + yield _(` $props: __VLS_PropsChildren;\n`); } - codes.push( - `} };\n`, - ); + yield _(` }\n` + + `};\n`); } if (usedHelperTypes.PropsChildren) { - codes.push(`type __VLS_PropsChildren = { [K in keyof (boolean extends (JSX.ElementChildrenAttribute extends never ? true : false) ? never : JSX.ElementChildrenAttribute)]?: S; };\n`); + yield _(`type __VLS_PropsChildren = {\n` + + ` [K in keyof (\n` + + ` boolean extends (\n` + + ` JSX.ElementChildrenAttribute extends never\n` + + ` ? true\n` + + ` : false\n` + + ` )\n` + + ` ? never\n` + + ` : JSX.ElementChildrenAttribute\n` + + ` )]?: S;\n` + + `};\n`); } } - function generateSrc() { + function* generateSrc(): Generator { if (!script?.src) return; let src = script.src; - if (src.endsWith('.d.ts')) src = src.substring(0, src.length - '.d.ts'.length); - else if (src.endsWith('.ts')) src = src.substring(0, src.length - '.ts'.length); - else if (src.endsWith('.tsx')) src = src.substring(0, src.length - '.tsx'.length) + '.jsx'; + if (src.endsWith('.d.ts')) + src = src.substring(0, src.length - '.d.ts'.length); + else if (src.endsWith('.ts')) + src = src.substring(0, src.length - '.ts'.length); + else if (src.endsWith('.tsx')) + src = src.substring(0, src.length - '.tsx'.length) + '.jsx'; - if (!src.endsWith('.js') && !src.endsWith('.jsx')) src = src + '.js'; + if (!src.endsWith('.js') && !src.endsWith('.jsx')) + src = src + '.js'; - codes.push(`export * from `); - codes.push([ + yield _(`export * from `); + yield _([ `'${src}'`, 'script', - [script.srcOffset - 1, script.srcOffset + script.src.length + 1], - { - ...FileRangeCapabilities.full, - rename: src === script.src ? true : { - normalize: undefined, - apply(newName) { - if ( - newName.endsWith('.jsx') - || newName.endsWith('.js') - ) { + script.srcOffset - 1, + enableAllFeatures({ + navigation: src === script.src ? true : { + shouldRename: () => false, + resolveRenameEditText(newName) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { newName = newName.split('.').slice(0, -1).join('.'); } if (script?.src?.endsWith('.d.ts')) { @@ -177,214 +184,197 @@ export function generate( return newName; }, }, - }, + }), ]); - codes.push(`;\n`); - codes.push(`export { default } from '${src}';\n`); + yield _(`;\n`); + yield _(`export { default } from '${src}';\n`); } - function generateScriptContentBeforeExportDefault() { + function* generateScriptContentBeforeExportDefault(): Generator { if (!script) return; - if (!!scriptSetup && scriptRanges?.exportDefault) { - addVirtualCode('script', 0, scriptRanges.exportDefault.expression.start); - } - else { - let isExportRawObject = false; - if (scriptRanges?.exportDefault) { - isExportRawObject = script.content.substring(scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end).startsWith('{'); - } - if (isExportRawObject && vueCompilerOptions.optionsWrapper.length === 2 && scriptRanges?.exportDefault) { - addVirtualCode('script', 0, scriptRanges.exportDefault.expression.start); - codes.push(vueCompilerOptions.optionsWrapper[0]); - { - codes.push(['', 'script', scriptRanges.exportDefault.expression.start, { - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: vueCompilerOptions.optionsWrapper[0], - tooltip: [ - 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', - 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', - ].join('\n\n'), - } - } as any]); - addVirtualCode('script', scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end); - codes.push(['', 'script', scriptRanges.exportDefault.expression.end, { - __hint: { - setting: 'vue.inlayHints.optionsWrapper', - label: vueCompilerOptions.optionsWrapper[1], - tooltip: '', - } - } as any]); - } - codes.push(vueCompilerOptions.optionsWrapper[1]); - addVirtualCode('script', scriptRanges.exportDefault.expression.end, script.content.length); - } - else { - addVirtualCode('script', 0, script.content.length); - } - } + if (!!scriptSetup && scriptRanges?.exportDefault) + return yield _(generateSourceCode(script, 0, scriptRanges.exportDefault.expression.start)); + + let isExportRawObject = false; + if (scriptRanges?.exportDefault) + isExportRawObject = script.content.substring(scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end).startsWith('{'); + + if (!isExportRawObject || !vueCompilerOptions.optionsWrapper.length || !scriptRanges?.exportDefault) + return yield _(generateSourceCode(script, 0, script.content.length)); + + yield _(generateSourceCode(script, 0, scriptRanges.exportDefault.expression.start)); + yield _(vueCompilerOptions.optionsWrapper[0]); + yield _(['', 'script', scriptRanges.exportDefault.expression.start, disableAllFeatures({ + __hint: { + setting: 'vue.inlayHints.optionsWrapper', + label: vueCompilerOptions.optionsWrapper[0], + tooltip: [ + 'This is virtual code that is automatically wrapped for type support, it does not affect your runtime behavior, you can customize it via `vueCompilerOptions.optionsWrapper` option in tsconfig / jsconfig.', + 'To hide it, you can set `"vue.inlayHints.optionsWrapper": false` in IDE settings.', + ].join('\n\n'), + } + })]); + yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.expression.end)); + yield _(['', 'script', scriptRanges.exportDefault.expression.end, disableAllFeatures({ + __hint: { + setting: 'vue.inlayHints.optionsWrapper', + label: vueCompilerOptions.optionsWrapper[1], + tooltip: '', + } + })]); + yield _(vueCompilerOptions.optionsWrapper[1]); + yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.end, script.content.length)); } - function generateScriptContentAfterExportDefault() { + function* generateScriptContentAfterExportDefault(): Generator { if (!script) return; - if (!!scriptSetup && scriptRanges?.exportDefault) { - addVirtualCode('script', scriptRanges.exportDefault.expression.end, script.content.length); - } + if (!!scriptSetup && scriptRanges?.exportDefault) + yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.end, script.content.length)); } - function generateScriptSetupImports() { - - if (!scriptSetup) + function* generateScriptSetupImports(): Generator { + if (!scriptSetup || !scriptSetupRanges) return; - if (!scriptSetupRanges) - return; - - codes.push([ + yield _([ scriptSetup.content.substring(0, Math.max(scriptSetupRanges.importSectionEndOffset, scriptSetupRanges.leadingCommentEndOffset)) + '\n', 'scriptSetup', 0, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); } - function generateScriptSetupAndTemplate() { - - if (!scriptSetup || !scriptSetupRanges) { + function* generateScriptSetupAndTemplate(): Generator { + if (!scriptSetup || !scriptSetupRanges) return; - } - const definePropMirrors: Record = {}; - let scriptSetupGeneratedOffset: number | undefined; + const definePropMirrors = new Map(); if (scriptSetup.generic) { if (!scriptRanges?.exportDefault) { - codes.push('export default '); + yield _(`export default `); } - codes.push(`(<`); - codes.push([ + yield _(`(<`); + yield _([ scriptSetup.generic, scriptSetup.name, scriptSetup.genericOffset, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); - if (!scriptSetup.generic.endsWith(',')) { - codes.push(`,`); + if (!scriptSetup.generic.endsWith(`,`)) { + yield _(`,`); } - codes.push(`>`); - codes.push('(\n'); - codes.push(`__VLS_props: Awaited['props'],\n`); - codes.push(`__VLS_ctx?: __VLS_Prettify, 'attrs' | 'emit' | 'slots'>>,\n`); // use __VLS_Prettify for less dts code - codes.push(`__VLS_expose?: NonNullable>['expose'],\n`); - codes.push('__VLS_setup = (async () => {\n'); - scriptSetupGeneratedOffset = generateSetupFunction(true, 'none', definePropMirrors); + yield _(`>`); + yield _(`(\n` + + ` __VLS_props: Awaited['props'],\n` + + ` __VLS_ctx?: __VLS_Prettify, 'attrs' | 'emit' | 'slots'>>,\n` // use __VLS_Prettify for less dts code + + ` __VLS_expose?: NonNullable>['expose'],\n` + + ` __VLS_setup = (async () => {\n`); + + yield* generateSetupFunction(true, 'none', definePropMirrors); //#region props - codes.push(`const __VLS_fnComponent = `); - codes.push(`(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); + yield _(`const __VLS_fnComponent = ` + + `(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); if (scriptSetupRanges.props.define?.arg) { - codes.push(`props: `); - addExtraReferenceVirtualCode('scriptSetup', scriptSetupRanges.props.define.arg.start, scriptSetupRanges.props.define.arg.end); - codes.push(`,\n`); - + yield _(` props: `); + yield _(generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.props.define.arg.start, scriptSetupRanges.props.define.arg.end)); + yield _(`,\n`); } if (scriptSetupRanges.emits.define) { - codes.push( - `emits: ({} as __VLS_NormalizeEmits),\n`, - ); + yield _(` emits: ({} as __VLS_NormalizeEmits),\n`); } - codes.push(`});\n`); + yield _(`});\n`); + if (scriptSetupRanges.defineProp.length) { - codes.push(`const __VLS_defaults = {\n`); + yield _(`const __VLS_defaults = {\n`); for (const defineProp of scriptSetupRanges.defineProp) { if (defineProp.defaultValue) { if (defineProp.name) { - codes.push(scriptSetup.content.substring(defineProp.name.start, defineProp.name.end)); + yield _(scriptSetup.content.substring(defineProp.name.start, defineProp.name.end)); } else { - codes.push('modelValue'); + yield _('modelValue'); } - codes.push(`: `); - codes.push(scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end)); - codes.push(`,\n`); + yield _(`: `); + yield _(scriptSetup.content.substring(defineProp.defaultValue.start, defineProp.defaultValue.end)); + yield _(`,\n`); } } - codes.push(`};\n`); + yield _(`};\n`); } - codes.push(`let __VLS_fnPropsTypeOnly!: {}`); // TODO: reuse __VLS_fnPropsTypeOnly even without generic, and remove __VLS_propsOption_defineProp + + yield _(`let __VLS_fnPropsTypeOnly!: {}`); // TODO: reuse __VLS_fnPropsTypeOnly even without generic, and remove __VLS_propsOption_defineProp if (scriptSetupRanges.props.define?.typeArg) { - codes.push(` & `); - addVirtualCode('scriptSetup', scriptSetupRanges.props.define.typeArg.start, scriptSetupRanges.props.define.typeArg.end); + yield _(` & `); + yield _(generateSourceCode(scriptSetup, scriptSetupRanges.props.define.typeArg.start, scriptSetupRanges.props.define.typeArg.end)); } if (scriptSetupRanges.defineProp.length) { - codes.push(` & {\n`); + yield _(` & {\n`); for (const defineProp of scriptSetupRanges.defineProp) { let propName = 'modelValue'; if (defineProp.name) { propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirrorStart = muggle.getLength(codes); - definePropMirrors[propName] = [propMirrorStart, propMirrorStart + propName.length]; + definePropMirrors.set(propName, getGeneratedLength()); } - codes.push(`${propName}${defineProp.required ? '' : '?'}: `); + yield _(`${propName}${defineProp.required ? '' : '?'}: `); if (defineProp.type) { - codes.push(scriptSetup.content.substring(defineProp.type.start, defineProp.type.end)); + yield _(scriptSetup.content.substring(defineProp.type.start, defineProp.type.end)); } else if (defineProp.defaultValue) { - codes.push(`typeof __VLS_defaults['`); - codes.push(propName); - codes.push(`']`); + yield _(`typeof __VLS_defaults['`); + yield _(propName); + yield _(`']`); } else { - codes.push(`any`); + yield _(`any`); } - codes.push(',\n'); + yield _(',\n'); } - codes.push(`}`); + yield _(`}`); } - codes.push(`;\n`); - codes.push(`let __VLS_fnPropsDefineComponent!: InstanceType['$props']`); - codes.push(`;\n`); - codes.push(`let __VLS_fnPropsSlots!: `); + yield _(`;\n`); + + yield _(`let __VLS_fnPropsDefineComponent!: InstanceType['$props'];\n`); + yield _(`let __VLS_fnPropsSlots!: `); if (scriptSetupRanges.slots.define && vueCompilerOptions.jsxSlots) { usedHelperTypes.PropsChildren = true; - codes.push(`__VLS_PropsChildren`); + yield _(`__VLS_PropsChildren`); } else { - codes.push(`{}`); - } - codes.push(`;\n`); - codes.push( - `let __VLS_defaultProps!: `, - `import('${vueCompilerOptions.lib}').VNodeProps`, - `& import('${vueCompilerOptions.lib}').AllowedComponentProps`, - `& import('${vueCompilerOptions.lib}').ComponentCustomProps`, - `;\n`, - ); + yield _(`{}`); + } + yield _(`;\n`); + + yield _(`let __VLS_defaultProps!:\n` + + ` import('${vueCompilerOptions.lib}').VNodeProps\n` + + ` & import('${vueCompilerOptions.lib}').AllowedComponentProps\n` + + ` & import('${vueCompilerOptions.lib}').ComponentCustomProps;\n`); //#endregion - codes.push('return {} as {\n'); - codes.push(`props: __VLS_Prettify<__VLS_OmitKeepDiscriminatedUnion> & typeof __VLS_fnPropsSlots & typeof __VLS_defaultProps,\n`); - codes.push(`expose(exposed: import('${vueCompilerOptions.lib}').ShallowUnwrapRef<${scriptSetupRanges.expose.define ? 'typeof __VLS_exposed' : '{}'}>): void,\n`); - codes.push('attrs: any,\n'); - codes.push('slots: ReturnType,\n'); - codes.push(`emit: typeof ${scriptSetupRanges.emits.name ?? '__VLS_emit'},\n`); - codes.push('};\n'); - codes.push('})(),\n'); - codes.push(`) => ({} as import('${vueCompilerOptions.lib}').VNode & { __ctx?: Awaited }))`); + yield _(` return {} as {\n` + + ` props: __VLS_Prettify<__VLS_OmitKeepDiscriminatedUnion> & typeof __VLS_fnPropsSlots & typeof __VLS_defaultProps,\n` + + ` expose(exposed: import('${vueCompilerOptions.lib}').ShallowUnwrapRef<${scriptSetupRanges.expose.define ? 'typeof __VLS_exposed' : '{}'}>): void,\n` + + ` attrs: any,\n` + + ` slots: ReturnType,\n` + + ` emit: typeof ${scriptSetupRanges.emits.name ?? '__VLS_emit'},\n` + + ` };\n`); + yield _(` })(),\n`); // __VLS_setup = (async () => { + yield _(`) => ({} as import('${vueCompilerOptions.lib}').VNode & { __ctx?: Awaited }))`); } else if (!script) { // no script block, generate script setup code at root - scriptSetupGeneratedOffset = generateSetupFunction(false, 'export', definePropMirrors); + yield* generateSetupFunction(false, 'export', definePropMirrors); } else { if (!scriptRanges?.exportDefault) { - codes.push('export default '); + yield _(`export default `); } - codes.push('await (async () => {\n'); - scriptSetupGeneratedOffset = generateSetupFunction(false, 'return', definePropMirrors); - codes.push(`})()`); + yield _(`await (async () => {\n`); + yield* generateSetupFunction(false, 'return', definePropMirrors); + yield _(`})()`); } if (scriptSetupGeneratedOffset !== undefined) { @@ -393,142 +383,150 @@ export function generate( continue; } const propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const propMirror = definePropMirrors[propName]; - if (propMirror) { - mirrorBehaviorMappings.push({ - sourceRange: [defineProp.name.start + scriptSetupGeneratedOffset, defineProp.name.end + scriptSetupGeneratedOffset], - generatedRange: propMirror, - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], + const propMirror = definePropMirrors.get(propName); + if (propMirror !== undefined) { + linkedCodeMappings.push({ + sourceOffsets: [defineProp.name.start + scriptSetupGeneratedOffset], + generatedOffsets: [propMirror], + lengths: [defineProp.name.end - defineProp.name.start], + data: undefined, }); } } } } - function generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Record) { - - if (!scriptSetupRanges || !scriptSetup) { + function* generateSetupFunction(functional: boolean, mode: 'return' | 'export' | 'none', definePropMirrors: Map): Generator { + if (!scriptSetupRanges || !scriptSetup) return; - } const definePropProposalA = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=kevinEdition') || vueCompilerOptions.experimentalDefinePropProposal === 'kevinEdition'; const definePropProposalB = scriptSetup.content.trimStart().startsWith('// @experimentalDefinePropProposal=johnsonEdition') || vueCompilerOptions.experimentalDefinePropProposal === 'johnsonEdition'; if (vueCompilerOptions.target >= 3.3) { - codes.push('const { '); + yield _(`const { `); for (const macro of Object.keys(vueCompilerOptions.macros)) { if (!bindingNames.has(macro)) { - codes.push(macro, ', '); + yield _(macro + `, `); } } - codes.push(`} = await import('${vueCompilerOptions.lib}');\n`); + yield _(`} = await import('${vueCompilerOptions.lib}');\n`); } if (definePropProposalA) { - codes.push(` -declare function defineProp(name: string, options: { required: true } & Record): import('${vueCompilerOptions.lib}').ComputedRef; -declare function defineProp(name: string, options: { default: any } & Record): import('${vueCompilerOptions.lib}').ComputedRef; -declare function defineProp(name?: string, options?: any): import('${vueCompilerOptions.lib}').ComputedRef; -`.trim() + '\n'); + yield _(`declare function defineProp(name: string, options: { required: true } & Record): import('${vueCompilerOptions.lib}').ComputedRef;\n`); + yield _(`declare function defineProp(name: string, options: { default: any } & Record): import('${vueCompilerOptions.lib}').ComputedRef;\n`); + yield _(`declare function defineProp(name?: string, options?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); } if (definePropProposalB) { - codes.push(` -declare function defineProp(value: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef; -declare function defineProp(value: T | (() => T) | undefined, required: true, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef; -declare function defineProp(value?: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef; -`.trim() + '\n'); + yield _(`declare function defineProp(value: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); + yield _(`declare function defineProp(value: T | (() => T) | undefined, required: true, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); + yield _(`declare function defineProp(value?: T | (() => T), required?: boolean, rest?: any): import('${vueCompilerOptions.lib}').ComputedRef;\n`); } - const scriptSetupGeneratedOffset = muggle.getLength(codes) - scriptSetupRanges.importSectionEndOffset; + scriptSetupGeneratedOffset = getGeneratedLength() - scriptSetupRanges.importSectionEndOffset; - let setupCodeModifies: [() => void, number, number][] = []; + let setupCodeModifies: [Code[], number, number][] = []; if (scriptSetupRanges.props.define && !scriptSetupRanges.props.name) { const range = scriptSetupRanges.props.withDefaults ?? scriptSetupRanges.props.define; const statement = scriptSetupRanges.props.define.statement; if (statement.start === range.start && statement.end === range.end) { - setupCodeModifies.push([() => codes.push(`const __VLS_props = `), range.start, range.start]); + setupCodeModifies.push([[`const __VLS_props = `], range.start, range.start]); } else { - setupCodeModifies.push([() => { - codes.push(`const __VLS_props = `); - addVirtualCode('scriptSetup', range.start, range.end); - codes.push(`;\n`); - addVirtualCode('scriptSetup', statement.start, range.start); - codes.push(`__VLS_props`); - }, statement.start, range.end]); + setupCodeModifies.push([[ + `const __VLS_props = `, + generateSourceCode(scriptSetup, range.start, range.end), + `;\n`, + generateSourceCode(scriptSetup, statement.start, range.start), + `__VLS_props`, + ], statement.start, range.end]); } } if (scriptSetupRanges.slots.define && !scriptSetupRanges.slots.name) { - setupCodeModifies.push([() => codes.push(`const __VLS_slots = `), scriptSetupRanges.slots.define.start, scriptSetupRanges.slots.define.start]); + setupCodeModifies.push([[`const __VLS_slots = `], scriptSetupRanges.slots.define.start, scriptSetupRanges.slots.define.start]); } if (scriptSetupRanges.emits.define && !scriptSetupRanges.emits.name) { - setupCodeModifies.push([() => codes.push(`const __VLS_emit = `), scriptSetupRanges.emits.define.start, scriptSetupRanges.emits.define.start]); + setupCodeModifies.push([[`const __VLS_emit = `], scriptSetupRanges.emits.define.start, scriptSetupRanges.emits.define.start]); } if (scriptSetupRanges.expose.define) { - setupCodeModifies.push([() => { - if (scriptSetupRanges?.expose.define?.typeArg) { - codes.push(`let __VLS_exposed!: `); - addExtraReferenceVirtualCode('scriptSetup', scriptSetupRanges.expose.define.typeArg.start, scriptSetupRanges.expose.define.typeArg.end); - codes.push(`;\n`); - } - else if (scriptSetupRanges?.expose.define?.arg) { - codes.push(`const __VLS_exposed = `); - addExtraReferenceVirtualCode('scriptSetup', scriptSetupRanges.expose.define.arg.start, scriptSetupRanges.expose.define.arg.end); - codes.push(`;\n`); - } - else { - codes.push(`const __VLS_exposed = {};\n`); - } - }, scriptSetupRanges.expose.define.start, scriptSetupRanges.expose.define.start]); + if (scriptSetupRanges.expose.define?.typeArg) { + setupCodeModifies.push([ + [ + `let __VLS_exposed!: `, + generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.expose.define.typeArg.start, scriptSetupRanges.expose.define.typeArg.end), + `;\n`, + ], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } + else if (scriptSetupRanges.expose.define?.arg) { + setupCodeModifies.push([ + [ + `const __VLS_exposed = `, + generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.expose.define.arg.start, scriptSetupRanges.expose.define.arg.end), + `;\n`, + ], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } + else { + setupCodeModifies.push([ + [`const __VLS_exposed = {};\n`], + scriptSetupRanges.expose.define.start, + scriptSetupRanges.expose.define.start, + ]); + } } setupCodeModifies = setupCodeModifies.sort((a, b) => a[1] - b[1]); if (setupCodeModifies.length) { - addVirtualCode('scriptSetup', scriptSetupRanges.importSectionEndOffset, setupCodeModifies[0][1]); + yield _(generateSourceCode(scriptSetup, scriptSetupRanges.importSectionEndOffset, setupCodeModifies[0][1])); while (setupCodeModifies.length) { - const [generate, _, end] = setupCodeModifies.shift()!; - generate(); + const [codes, _start, end] = setupCodeModifies.shift()!; + for (const code of codes) { + yield _(code); + } if (setupCodeModifies.length) { const nextStart = setupCodeModifies[0][1]; - addVirtualCode('scriptSetup', end, nextStart); + yield _(generateSourceCode(scriptSetup, end, nextStart)); } else { - addVirtualCode('scriptSetup', end); + yield _(generateSourceCode(scriptSetup, end)); } } } else { - addVirtualCode('scriptSetup', scriptSetupRanges.importSectionEndOffset); + yield _(generateSourceCode(scriptSetup, scriptSetupRanges.importSectionEndOffset)); } if (scriptSetupRanges.props.define?.typeArg && scriptSetupRanges.props.withDefaults?.arg) { // fix https://github.com/vuejs/language-tools/issues/1187 - codes.push(`const __VLS_withDefaultsArg = (function (t: T) { return t })(`); - addExtraReferenceVirtualCode('scriptSetup', scriptSetupRanges.props.withDefaults.arg.start, scriptSetupRanges.props.withDefaults.arg.end); - codes.push(`);\n`); + yield _(`const __VLS_withDefaultsArg = (function (t: T) { return t })(`); + yield _(generateSourceCodeForExtraReference(scriptSetup, scriptSetupRanges.props.withDefaults.arg.start, scriptSetupRanges.props.withDefaults.arg.end)); + yield _(`);\n`); } if (!functional && scriptSetupRanges.defineProp.length) { - codes.push(`let __VLS_propsOption_defineProp!: {\n`); + yield _(`let __VLS_propsOption_defineProp!: {\n`); for (const defineProp of scriptSetupRanges.defineProp) { let propName = 'modelValue'; if (defineProp.name && defineProp.nameIsString) { // renaming support - addExtraReferenceVirtualCode('scriptSetup', defineProp.name.start, defineProp.name.end); + yield _(generateSourceCodeForExtraReference(scriptSetup, defineProp.name.start, defineProp.name.end)); } else if (defineProp.name) { propName = scriptSetup.content.substring(defineProp.name.start, defineProp.name.end); - const start = muggle.getLength(codes); - definePropMirrors[propName] = [start, start + propName.length]; - codes.push(propName); + const start = getGeneratedLength(); + definePropMirrors.set(propName, start); + yield _(propName); } else { - codes.push(propName); + yield _(propName); } - codes.push(`: `); + yield _(`: `); let type = 'any'; if (!defineProp.nameIsString) { @@ -539,182 +537,173 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: } if (defineProp.required) { - codes.push(`{ required: true, type: import('${vueCompilerOptions.lib}').PropType<${type}> },\n`); + yield _(`{ required: true, type: import('${vueCompilerOptions.lib}').PropType<${type}> },\n`); } else { - codes.push(`import('${vueCompilerOptions.lib}').PropType<${type}>,\n`); + yield _(`import('${vueCompilerOptions.lib}').PropType<${type}>,\n`); } } - codes.push(`};\n`); + yield _(`};\n`); } - generateTemplate(functional); + yield* generateTemplate(functional); if (mode === 'return' || mode === 'export') { - if (!vueCompilerOptions.skipTemplateCodegen && (htmlGen?.hasSlot || scriptSetupRanges?.slots.define)) { + if (!vueCompilerOptions.skipTemplateCodegen && (templateCodegen?.hasSlot || scriptSetupRanges?.slots.define)) { usedHelperTypes.WithTemplateSlots = true; - codes.push(`const __VLS_component = `); - generateComponent(functional); - codes.push(`;\n`); - codes.push(mode === 'return' ? 'return ' : 'export default '); - codes.push(`{} as __VLS_WithTemplateSlots>;\n`); + yield _(`const __VLS_component = `); + yield* generateComponent(functional); + yield _(`;\n`); + yield _(mode === 'return' ? 'return ' : 'export default '); + yield _(`{} as __VLS_WithTemplateSlots>;\n`); } else { - codes.push(mode === 'return' ? 'return ' : 'export default '); - generateComponent(functional); - codes.push(`;\n`); + yield _(mode === 'return' ? 'return ' : 'export default '); + yield* generateComponent(functional); + yield _(`;\n`); } } - - return scriptSetupGeneratedOffset; } - function generateComponent(functional: boolean) { - + function* generateComponent(functional: boolean): Generator { if (!scriptSetupRanges) return; - if (scriptRanges?.exportDefault && scriptRanges.exportDefault.expression.start !== scriptRanges.exportDefault.args.start) { + if (script && scriptRanges?.exportDefault && scriptRanges.exportDefault.expression.start !== scriptRanges.exportDefault.args.start) { // use defineComponent() from user space code if it exist - addVirtualCode('script', scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.args.start); - codes.push(`{\n`); + yield _(generateSourceCode(script, scriptRanges.exportDefault.expression.start, scriptRanges.exportDefault.args.start)); + yield _(`{\n`); } else { - codes.push(`(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); + yield _(`(await import('${vueCompilerOptions.lib}')).defineComponent({\n`); } - codes.push(`setup() {\n`); - codes.push(`return {\n`); - generateSetupReturns(); + yield _(`setup() {\n`); + yield _(`return {\n`); + yield* generateSetupReturns(); if (scriptSetupRanges.expose.define) { - codes.push(`...__VLS_exposed,\n`); + yield _(`...__VLS_exposed,\n`); } - codes.push(`};\n`); - codes.push(`},\n`); - generateComponentOptions(functional); - codes.push(`})`); + yield _(`};\n`); + yield _(`},\n`); + yield* generateComponentOptions(functional); + yield _(`})`); } - function generateComponentOptions(functional: boolean) { - if (scriptSetupRanges && !bypassDefineComponent) { + function* generateComponentOptions(functional: boolean): Generator { + if (scriptSetup && scriptSetupRanges && !bypassDefineComponent) { const ranges = scriptSetupRanges; - const propsCodegens: (() => void)[] = []; + const propsCodegens: (() => Generator)[] = []; if (ranges.props.define?.arg) { const arg = ranges.props.define.arg; - propsCodegens.push(() => { - addExtraReferenceVirtualCode('scriptSetup', arg.start, arg.end); + propsCodegens.push(function* () { + yield _(generateSourceCodeForExtraReference(scriptSetup!, arg.start, arg.end)); }); } if (ranges.props.define?.typeArg) { const typeArg = ranges.props.define.typeArg; - propsCodegens.push(() => { + propsCodegens.push(function* () { usedHelperTypes.DefinePropsToOptions = true; - codes.push(`{} as `); + yield _(`{} as `); if (ranges.props.withDefaults?.arg) { usedHelperTypes.MergePropDefaults = true; - codes.push(`__VLS_WithDefaults<`); + yield _(`__VLS_WithDefaults<`); } - codes.push(`__VLS_TypePropsToRuntimeProps<`); + yield _(`__VLS_TypePropsToRuntimeProps<`); if (functional) { - codes.push(`typeof __VLS_fnPropsTypeOnly`); + yield _(`typeof __VLS_fnPropsTypeOnly`); } else { - addExtraReferenceVirtualCode('scriptSetup', typeArg.start, typeArg.end); + yield _(generateSourceCodeForExtraReference(scriptSetup!, typeArg.start, typeArg.end)); } - codes.push(`>`); + yield _(`>`); if (ranges.props.withDefaults?.arg) { - codes.push(`, typeof __VLS_withDefaultsArg`); - codes.push(`>`); + yield _(`, typeof __VLS_withDefaultsArg`); + yield _(`>`); } }); } if (!functional && ranges.defineProp.length) { - propsCodegens.push(() => { - codes.push(`__VLS_propsOption_defineProp`); + propsCodegens.push(function* () { + yield _(`__VLS_propsOption_defineProp`); }); } if (propsCodegens.length === 1) { - codes.push(`props: `); + yield _(`props: `); for (const generate of propsCodegens) { - generate(); + yield* generate(); } - codes.push(`,\n`); + yield _(`,\n`); } else if (propsCodegens.length >= 2) { - codes.push(`props: {\n`); + yield _(`props: {\n`); for (const generate of propsCodegens) { - codes.push('...'); - generate(); - codes.push(',\n'); + yield _(`...`); + yield* generate(); + yield _(`,\n`); } - codes.push(`},\n`); + yield _(`},\n`); } if (ranges.emits.define) { - codes.push( - `emits: ({} as __VLS_NormalizeEmits),\n`, - ); + yield _(`emits: ({} as __VLS_NormalizeEmits),\n`); } } - if (scriptRanges?.exportDefault?.args) { - addVirtualCode('script', scriptRanges.exportDefault.args.start + 1, scriptRanges.exportDefault.args.end - 1); + if (script && scriptRanges?.exportDefault?.args) { + yield _(generateSourceCode(script, scriptRanges.exportDefault.args.start + 1, scriptRanges.exportDefault.args.end - 1)); } } - function generateSetupReturns() { + function* generateSetupReturns(): Generator { if (scriptSetupRanges && bypassDefineComponent) { // fill $props if (scriptSetupRanges.props.define) { // NOTE: defineProps is inaccurate for $props - codes.push(`$props: __VLS_makeOptional(${scriptSetupRanges.props.name ?? `__VLS_props`}),\n`); - codes.push(`...${scriptSetupRanges.props.name ?? `__VLS_props`},\n`); + yield _(`$props: __VLS_makeOptional(${scriptSetupRanges.props.name ?? `__VLS_props`}),\n`); + yield _(`...${scriptSetupRanges.props.name ?? `__VLS_props`},\n`); } // fill $emit if (scriptSetupRanges.emits.define) { - codes.push(`$emit: ${scriptSetupRanges.emits.name ?? '__VLS_emit'},\n`); + yield _(`$emit: ${scriptSetupRanges.emits.name ?? '__VLS_emit'},\n`); } } } - function generateTemplate(functional: boolean) { + function* generateTemplate(functional: boolean): Generator { generatedTemplate = true; if (!vueCompilerOptions.skipTemplateCodegen) { - - generateExportOptions(); - generateConstNameOption(); - - codes.push(`function __VLS_template() {\n`); - - const templateGened = generateTemplateContext(); - - codes.push(`}\n`); - - generateComponentForTemplateUsage(functional, templateGened.cssIds); + yield* generateExportOptions(); + yield* generateConstNameOption(); + yield _(`function __VLS_template() {\n`); + const cssIds = new Set(); + yield* generateTemplateContext(cssIds); + yield _(`}\n`); + yield* generateComponentForTemplateUsage(functional, cssIds); } else { - codes.push(`function __VLS_template() {\n`); + yield _(`function __VLS_template() {\n`); const templateUsageVars = [...getTemplateUsageVars()]; - codes.push(`// @ts-ignore\n`); - codes.push(`[${templateUsageVars.join(', ')}]\n`); - codes.push(`return {};\n`); - codes.push(`}\n`); + yield _(`// @ts-ignore\n`); + yield _(`[${templateUsageVars.join(', ')}]\n`); + yield _(`return {};\n`); + yield _(`}\n`); } } - function generateComponentForTemplateUsage(functional: boolean, cssIds: Set) { + function* generateComponentForTemplateUsage(functional: boolean, cssIds: Set): Generator { if (scriptSetup && scriptSetupRanges) { - codes.push(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({\n`); - codes.push(`setup() {\n`); - codes.push(`return {\n`); - generateSetupReturns(); + yield _(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({\n`); + yield _(`setup() {\n`); + yield _(`return {\n`); + yield* generateSetupReturns(); // bindings const templateUsageVars = getTemplateUsageVars(); for (const [content, bindings] of [ @@ -728,119 +717,114 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: if (!templateUsageVars.has(varName) && !cssIds.has(varName)) { continue; } - const templateStart = getLength(codes); - codes.push(varName); - const templateEnd = getLength(codes); - codes.push(`: ${varName} as typeof `); - - const scriptStart = getLength(codes); - codes.push(varName); - const scriptEnd = getLength(codes); - codes.push(',\n'); - - mirrorBehaviorMappings.push({ - sourceRange: [scriptStart, scriptEnd], - generatedRange: [templateStart, templateEnd], - data: [ - MirrorBehaviorCapabilities.full, - MirrorBehaviorCapabilities.full, - ], + const templateOffset = getGeneratedLength(); + yield _(varName); + yield _(`: ${varName} as typeof `); + + const scriptOffset = getGeneratedLength(); + yield _(varName); + yield _(`,\n`); + + linkedCodeMappings.push({ + sourceOffsets: [scriptOffset], + generatedOffsets: [templateOffset], + lengths: [varName.length], + data: undefined, }); } } - codes.push(`};\n`); // return { - codes.push(`},\n`); // setup() { - generateComponentOptions(functional); - codes.push(`});\n`); // defineComponent({ + yield _(`};\n`); // return { + yield _(`},\n`); // setup() { + yield* generateComponentOptions(functional); + yield _(`});\n`); // defineComponent { } else if (script) { - codes.push(`let __VLS_internalComponent!: typeof import('./${path.basename(fileName)}')['default'];\n`); + yield _(`let __VLS_internalComponent!: typeof import('./${path.basename(fileName)}')['default'];\n`); } else { - codes.push(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({});\n`); + yield _(`const __VLS_internalComponent = (await import('${vueCompilerOptions.lib}')).defineComponent({});\n`); } } - function generateExportOptions() { - codes.push(`\n`); - codes.push(`const __VLS_componentsOption = `); + function* generateExportOptions(): Generator { + yield _(`\n`); + yield _(`const __VLS_componentsOption = `); if (script && scriptRanges?.exportDefault?.componentsOption) { const componentsOption = scriptRanges.exportDefault.componentsOption; - codes.push([ + yield _([ script.content.substring(componentsOption.start, componentsOption.end), 'script', componentsOption.start, - { - references: true, - rename: true, - }, + disableAllFeatures({ + navigation: true, + }), ]); } else { - codes.push('{}'); + yield _(`{}`); } - codes.push(`;\n`); + yield _(`;\n`); } - function generateConstNameOption() { - codes.push(`\n`); + function* generateConstNameOption(): Generator { + yield _(`\n`); if (script && scriptRanges?.exportDefault?.nameOption) { const nameOption = scriptRanges.exportDefault.nameOption; - codes.push(`const __VLS_name = `); - codes.push(`${script.content.substring(nameOption.start, nameOption.end)} as const`); - codes.push(`;\n`); + yield _(`const __VLS_name = `); + yield _(`${script.content.substring(nameOption.start, nameOption.end)} as const`); + yield _(`;\n`); } else if (scriptSetup) { - codes.push(`let __VLS_name!: '${path.basename(fileName.substring(0, fileName.lastIndexOf('.')))}';\n`); + yield _(`let __VLS_name!: '${path.basename(fileName.substring(0, fileName.lastIndexOf('.')))}';\n`); } else { - codes.push(`const __VLS_name = undefined;\n`); + yield _(`const __VLS_name = undefined;\n`); } } - function generateTemplateContext() { + function* generateTemplateContext(cssIds = new Set()): Generator { const useGlobalThisTypeInCtx = fileName.endsWith('.html'); - codes.push(`let __VLS_ctx!: ${useGlobalThisTypeInCtx ? 'typeof globalThis &' : ''}`); - codes.push(`InstanceType<__VLS_PickNotAny {}>> & {\n`); + yield _(`let __VLS_ctx!: ${useGlobalThisTypeInCtx ? 'typeof globalThis &' : ''}`); + yield _(`InstanceType<__VLS_PickNotAny {}>> & {\n`); /* CSS Module */ for (let i = 0; i < styles.length; i++) { const style = styles[i]; if (style.module) { - codes.push(`${style.module}: Record & __VLS_Prettify<{}`); + yield _(`${style.module}: Record & __VLS_Prettify<{}`); for (const className of style.classNames) { - generateCssClassProperty( + yield* generateCssClassProperty( i, - className.text.substring(1), - { start: className.offset, end: className.offset + className.text.length }, + className.text, + className.offset, 'string', false, true, ); } - codes.push('>;\n'); + yield _(`>;\n`); } } - codes.push(`};\n`); + yield _(`};\n`); /* Components */ - codes.push('/* Components */\n'); - codes.push(`let __VLS_otherComponents!: NonNullable & typeof __VLS_componentsOption;\n`); - codes.push(`let __VLS_own!: __VLS_SelfComponent { ${getSlotsPropertyName(vueCompilerOptions.target)}: typeof ${scriptSetupRanges?.slots?.name ?? '__VLS_slots'} })>;\n`); - codes.push(`let __VLS_localComponents!: typeof __VLS_otherComponents & Omit;\n`); - codes.push(`let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx;\n`); // for html completion, TS references... + yield _(`/* Components */\n`); + yield _(`let __VLS_otherComponents!: NonNullable & typeof __VLS_componentsOption;\n`); + yield _(`let __VLS_own!: __VLS_SelfComponent { ${getSlotsPropertyName(vueCompilerOptions.target)}: typeof ${scriptSetupRanges?.slots?.name ?? '__VLS_slots'} })>;\n`); + yield _(`let __VLS_localComponents!: typeof __VLS_otherComponents & Omit;\n`); + yield _(`let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx;\n`); // for html completion, TS references... /* Style Scoped */ - codes.push('/* Style Scoped */\n'); - codes.push('type __VLS_StyleScopedClasses = {}'); + yield _(`/* Style Scoped */\n`); + yield _(`type __VLS_StyleScopedClasses = {}`); for (let i = 0; i < styles.length; i++) { const style = styles[i]; const option = vueCompilerOptions.experimentalResolveStyleCssClasses; if (option === 'always' || (option === 'scoped' && style.scoped)) { for (const className of style.classNames) { - generateCssClassProperty( + yield* generateCssClassProperty( i, - className.text.substring(1), - { start: className.offset, end: className.offset + className.text.length }, + className.text, + className.offset, 'boolean', true, !style.module, @@ -848,152 +832,147 @@ declare function defineProp(value?: T | (() => T), required?: boolean, rest?: } } } - codes.push(';\n'); - codes.push('let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[];\n'); + yield _(`;\n`); + yield _('let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[];\n'); - codes.push(`/* CSS variable injection */\n`); - const cssIds = generateCssVars(); - codes.push(`/* CSS variable injection end */\n`); + yield _(`/* CSS variable injection */\n`); + yield* generateCssVars(cssIds); + yield _(`/* CSS variable injection end */\n`); - if (htmlGen) { - muggle.setTracking(false); - for (const s of htmlGen.codes) { - codes.push(s); - } - muggle.setTracking(true); - for (const s of htmlGen.codeStacks) { - codeStacks.push(s); + if (templateCodegen) { + for (let i = 0; i < templateCodegen.tsCodes.length; i++) { + yield [ + templateCodegen.tsCodes[i], + templateCodegen.tsCodegenStacks[i], + ]; } } - - if (!htmlGen) { - codes.push(`// no template\n`); + else { + yield _(`// no template\n`); if (!scriptSetupRanges?.slots.define) { - codes.push(`const __VLS_slots = {};\n`); + yield _(`const __VLS_slots = {};\n`); } } - codes.push(`return ${scriptSetupRanges?.slots.name ?? '__VLS_slots'};\n`); + yield _(`return ${scriptSetupRanges?.slots.name ?? '__VLS_slots'};\n`); - return { cssIds }; - - function generateCssClassProperty(styleIndex: number, className: string, classRange: TextRange, propertyType: string, optional: boolean, referencesCodeLens: boolean) { - codes.push(`\n & { `); - codes.push([ - '', - 'style_' + styleIndex, - classRange.start, - { - references: true, - referencesCodeLens, - }, - ]); - codes.push(`'`); - codes.push([ - className, - 'style_' + styleIndex, - [classRange.start, classRange.end], - { - references: true, - rename: { - normalize: normalizeCssRename, - apply: applyCssRename, - }, + } + function* generateCssClassProperty( + styleIndex: number, + classNameWithDot: string, + offset: number, + propertyType: string, + optional: boolean, + referencesCodeLens: boolean + ): Generator { + yield _(`\n & { `); + yield _([ + '', + 'style_' + styleIndex, + offset, + disableAllFeatures({ + navigation: true, + __referencesCodeLens: referencesCodeLens, + }), + ]); + yield _(`'`); + yield _([ + '', + 'style_' + styleIndex, + offset, + disableAllFeatures({ + navigation: { + resolveRenameNewName: normalizeCssRename, + resolveRenameEditText: applyCssRename, }, - ]); - codes.push(`'`); - codes.push([ - '', - 'style_' + styleIndex, - classRange.end, - {}, - ]); - codes.push(`${optional ? '?' : ''}: ${propertyType}`); - codes.push(` }`); - } - function generateCssVars() { - - const emptyLocalVars = new Map(); - const identifiers = new Set(); - - for (const style of styles) { - for (const cssBind of style.cssVars) { - walkInterpolationFragment( - ts, - cssBind.text, - ts.createSourceFile('/a.txt', cssBind.text, ts.ScriptTarget.ESNext), - (frag, fragOffset, onlyForErrorMapping) => { - if (fragOffset === undefined) { - codes.push(frag); - } - else { - codes.push([ - frag, - style.name, - cssBind.offset + fragOffset, - onlyForErrorMapping - ? { diagnostic: true } - : FileRangeCapabilities.full, - ]); - } - }, - emptyLocalVars, - identifiers, - vueCompilerOptions, - ); - codes.push(';\n'); + }), + ]); + yield _([ + classNameWithDot.substring(1), + 'style_' + styleIndex, + offset + 1, + disableAllFeatures({ __combineLastMappping: true }), + ]); + yield _(`'`); + yield _([ + '', + 'style_' + styleIndex, + offset + classNameWithDot.length, + disableAllFeatures({}), + ]); + yield _(`${optional ? '?' : ''}: ${propertyType}`); + yield _(` }`); + } + function* generateCssVars(cssIds: Set): Generator { + + const emptyLocalVars = new Map(); + + for (const style of styles) { + for (const cssBind of style.cssVars) { + for (const [segment, offset, onlyError] of eachInterpolationSegment( + ts, + cssBind.text, + ts.createSourceFile('/a.txt', cssBind.text, ts.ScriptTarget.ESNext), + emptyLocalVars, + cssIds, + vueCompilerOptions, + )) { + if (offset === undefined) { + yield _(segment); + } + else { + yield _([ + segment, + style.name, + cssBind.offset + offset, + onlyError + ? disableAllFeatures({ verification: true }) + : enableAllFeatures({}), + ]); + } } + yield _(`;\n`); } - - return identifiers; } } function getTemplateUsageVars() { const usageVars = new Set(); - if (htmlGen) { + if (templateCodegen) { // fix import components unused report for (const varName of bindingNames) { - if (!!htmlGen.tagNames[varName] || !!htmlGen.tagNames[hyphenateTag(varName)]) { + if (templateCodegen.tagNames.has(varName) || templateCodegen.tagNames.has(hyphenateTag(varName))) { usageVars.add(varName); } } - for (const tag of Object.keys(htmlGen.tagNames)) { + for (const tag of Object.keys(templateCodegen.tagNames)) { if (tag.indexOf('.') >= 0) { usageVars.add(tag.split('.')[0]); } } - for (const _id of htmlGen.accessedGlobalVariables) { + for (const _id of templateCodegen.accessedGlobalVariables) { usageVars.add(_id); } } return usageVars; } - function addVirtualCode(vueTag: 'script' | 'scriptSetup', start: number, end?: number) { - muggle.offsetStack(); - codes.push([ - (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), - vueTag, + function generateSourceCode(block: SfcBlock, start: number, end?: number): Code { + return [ + block.content.substring(start, end), + block.name, start, - FileRangeCapabilities.full, // diagnostic also working for setup() returns unused in template checking - ]); - muggle.resetOffsetStack(); + enableAllFeatures({}), // diagnostic also working for setup() returns unused in template checking + ]; } - function addExtraReferenceVirtualCode(vueTag: 'script' | 'scriptSetup', start: number, end: number) { - muggle.offsetStack(); - codes.push([ - (vueTag === 'script' ? script : scriptSetup)!.content.substring(start, end), - vueTag, + function generateSourceCodeForExtraReference(block: SfcBlock, start: number, end: number): Code { + return [ + block.content.substring(start, end), + block.name, start, - { - references: true, - definition: true, - rename: true, - }, - ]); - muggle.resetOffsetStack(); + disableAllFeatures({ navigation: true }), + ]; } } diff --git a/packages/language-core/src/generators/template.ts b/packages/language-core/src/generators/template.ts index f95543da24..2f787017dd 100644 --- a/packages/language-core/src/generators/template.ts +++ b/packages/language-core/src/generators/template.ts @@ -1,18 +1,18 @@ -import { FileRangeCapabilities } from '@volar/language-core'; -import { Segment } from '@volar/source-map'; +import { toString } from '@volar/language-core'; import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import { minimatch } from 'minimatch'; -import * as muggle from 'muggle-string'; import type * as ts from 'typescript/lib/tsserverlibrary'; -import { Sfc, VueCompilerOptions } from '../types'; +import { Code, CodeAndStack, Sfc, VueCodeInformation, VueCompilerOptions } from '../types'; import { hyphenateAttr, hyphenateTag } from '../utils/shared'; -import { collectVars, walkInterpolationFragment } from '../utils/transform'; - -const capabilitiesPresets = { - all: FileRangeCapabilities.full, - allWithHiddenParam: { - ...FileRangeCapabilities.full, __hint: { +import { collectVars, eachInterpolationSegment } from '../utils/transform'; +import { disableAllFeatures, enableAllFeatures, getStack, mergeFeatureSettings } from './utils'; + +const presetInfos = { + disabledAll: disableAllFeatures({}), + all: enableAllFeatures({}), + allWithHiddenParam: enableAllFeatures({ + __hint: { setting: 'vue.inlayHints.inlineHandlerLeading', label: '$event =>', tooltip: [ @@ -21,20 +21,20 @@ const capabilitiesPresets = { '[More info](https://github.com/vuejs/language-tools/issues/2445#issuecomment-1444771420)', ].join('\n\n'), paddingRight: true, - } /* TODO */ - } as FileRangeCapabilities, - noDiagnostic: { ...FileRangeCapabilities.full, diagnostic: false } satisfies FileRangeCapabilities, - diagnosticOnly: { diagnostic: true } satisfies FileRangeCapabilities, - tagHover: { hover: true } satisfies FileRangeCapabilities, - event: { hover: true, diagnostic: true } satisfies FileRangeCapabilities, - tagReference: { references: true, definition: true, rename: { normalize: undefined, apply: noEditApply } } satisfies FileRangeCapabilities, - attr: { hover: true, diagnostic: true, references: true, definition: true, rename: true } satisfies FileRangeCapabilities, - attrReference: { references: true, definition: true, rename: true } satisfies FileRangeCapabilities, - slotProp: { references: true, definition: true, rename: true, diagnostic: true } satisfies FileRangeCapabilities, - scopedClassName: { references: true, definition: true, rename: true, completion: true } satisfies FileRangeCapabilities, - slotName: { hover: true, diagnostic: true, references: true, definition: true, completion: true } satisfies FileRangeCapabilities, - slotNameExport: { hover: true, diagnostic: true, references: true, definition: true, /* referencesCodeLens: true */ } satisfies FileRangeCapabilities, - refAttr: { references: true, definition: true, rename: true } satisfies FileRangeCapabilities, + } + }), + noDiagnostics: enableAllFeatures({ verification: false }), + diagnosticOnly: disableAllFeatures({ verification: true }), + tagHover: disableAllFeatures({ semantic: { shouldHighlight: () => false } }), + event: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true }), + tagReference: disableAllFeatures({ navigation: { shouldRename: () => false } }), + attr: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true }), + attrReference: disableAllFeatures({ navigation: true }), + slotProp: disableAllFeatures({ navigation: true, verification: true }), + scopedClassName: disableAllFeatures({ navigation: true, completion: true }), + slotName: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, completion: true }), + slotNameExport: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, /* __navigationCodeLens: true */ }), + refAttr: disableAllFeatures({ navigation: true }), }; const formatBrackets = { normal: ['`${', '}`;'] as [string, string], @@ -63,9 +63,9 @@ const transformContext: CompilerDOM.TransformContext = { expressionPlugins: ['typescript'], }; -type Code = Segment; +type _CodeAndStack = [codeType: 'ts' | 'tsFormat' | 'inlineCss', ...codeAndStack: CodeAndStack]; -export function generate( +export function* generate( ts: typeof import('typescript/lib/tsserverlibrary'), compilerOptions: ts.CompilerOptions, vueCompilerOptions: VueCompilerOptions, @@ -78,15 +78,58 @@ export function generate( codegenStack: boolean, ) { + const processDirectiveComment = (code: Code) => { + if (ignoreError && typeof code !== 'string') { + const data = code[3]; + if (data.verification) { + code[3] = { + ...data, + verification: false, + }; + } + } + if (expectErrorToken && typeof code !== 'string') { + const token = expectErrorToken; + const data = code[3]; + if (data.verification) { + code[3] = { + ...data, + verification: { + shouldReport: () => { + token.errors++; + return false; + }, + }, + }; + } + } + return code; + }; + const _ts = codegenStack + ? (code: Code): _CodeAndStack => ['ts', processDirectiveComment(code), getStack()] + : (code: Code): _CodeAndStack => ['ts', processDirectiveComment(code), '']; + const _tsFormat = codegenStack + ? (code: Code): _CodeAndStack => ['tsFormat', code, getStack()] + : (code: Code): _CodeAndStack => ['tsFormat', code, '']; + const _inlineCss = codegenStack + ? (code: Code): _CodeAndStack => ['inlineCss', code, getStack()] + : (code: Code): _CodeAndStack => ['inlineCss', code, '']; const nativeTags = new Set(vueCompilerOptions.nativeTags); - const [codes, codeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const [formatCodes, formatCodeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const [cssCodes, cssCodeStacks] = codegenStack ? muggle.track([] as Code[]) : [[], []]; - const slots = new Map(); + const slots = new Map(); const slotExps = new Map(); - const tagNames = collectTagOffsets(); + const tagOffsetsMap = collectTagOffsets(); const localVars = new Map(); - const tempVars: ReturnType[] = []; + const tempVars: { + text: string, + isShorthand: boolean, + offset: number, + }[][] = []; const accessedGlobalVariables = new Set(); const scopedClasses: { className: string, offset: number; }[] = []; const blockConditions: string[] = []; @@ -94,10 +137,10 @@ export function generate( const componentCtxVar2EmitEventsVar = new Map(); let hasSlot = false; - let elementIndex = 0; - let ignoreStart: undefined | number; - let expectedErrorStart: undefined | number; + let ignoreError = false; + let expectErrorToken: { errors: number; } | undefined; let expectedErrorNode: CompilerDOM.CommentNode | undefined; + let elementIndex = 0; if (slotsAssignName) { localVars.set(slotsAssignName, 1); @@ -107,189 +150,37 @@ export function generate( localVars.set(propsAssignName, 1); } - generatePreResolveComponents(); + yield* generatePreResolveComponents(); if (template.ast) { - visitNode(template.ast, undefined, undefined, undefined); + yield* generateAstNode(template.ast, undefined, undefined, undefined); } - generateStyleScopedClasses(); + yield* generateStyleScopedClasses(); if (!hasScriptSetupSlots) { - codes.push( - 'var __VLS_slots!:', - ...createSlotsTypeCode(), - ';\n', - ); + yield _ts('var __VLS_slots!:'); + yield* generateSlotsType(); + yield _ts(';\n'); } - generateAutoImportCompletionCode(); + yield* generateExtraAutoImport(); return { - codes, - codeStacks, - formatCodes, - formatCodeStacks, - cssCodes, - cssCodeStacks, - tagNames, + tagOffsetsMap, accessedGlobalVariables, hasSlot, }; - function createSlotsTypeCode(): Code[] { - const codes: Code[] = []; - for (const [exp, slot] of slotExps) { - hasSlot = true; - codes.push(`Partial, (_: typeof ${slot.varName}) => any>> &\n`); - } - codes.push(`{\n`); - for (const [name, slot] of slots) { - hasSlot = true; - codes.push( - ...createObjectPropertyCode([ - name, - 'template', - slot.loc, - { - ...capabilitiesPresets.slotNameExport, - referencesCodeLens: true, - }, - ], slot.nodeLoc), - ); - codes.push(`?(_: typeof ${slot.varName}): any,\n`); - } - codes.push(`}`); - return codes; - } - - function generateStyleScopedClasses() { - codes.push(`if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {\n`); - for (const { className, offset } of scopedClasses) { - codes.push(`__VLS_styleScopedClasses[`); - codes.push(...createStringLiteralKeyCode([ - className, - 'template', - offset, - { - ...capabilitiesPresets.scopedClassName, - displayWithLink: stylesScopedClasses.has(className), - }, - ])); - codes.push(`];\n`); - } - codes.push('}\n'); - } - - function toCanonicalComponentName(tagText: string) { - return validTsVarReg.test(tagText) ? tagText : capitalize(camelize(tagText.replace(colonReg, '-'))); - } - - function getPossibleOriginalComponentName(tagText: string) { - return [...new Set([ - // order is important: https://github.com/vuejs/language-tools/issues/2010 - capitalize(camelize(tagText)), - camelize(tagText), - tagText, - ])]; - } - - function generatePreResolveComponents() { - - codes.push(`let __VLS_resolvedLocalAndGlobalComponents!: {}\n`); - - for (const tagName in tagNames) { - - if (nativeTags.has(tagName)) - continue; - - const isNamespacedTag = tagName.indexOf('.') >= 0; - if (isNamespacedTag) - continue; - - codes.push( - `& __VLS_WithComponent<'${toCanonicalComponentName(tagName)}', typeof __VLS_localComponents, `, - // order is important: https://github.com/vuejs/language-tools/issues/2010 - `"${capitalize(camelize(tagName))}", `, - `"${camelize(tagName)}", `, - `"${tagName}"`, - '>\n', - ); - } - - codes.push(`;\n`); - - for (const tagName in tagNames) { - - const tagOffsets = tagNames[tagName]; - const tagRanges: [number, number][] = tagOffsets.map(offset => [offset, offset + tagName.length]); - const names = nativeTags.has(tagName) ? [tagName] : getPossibleOriginalComponentName(tagName); - - for (const name of names) { - for (const tagRange of tagRanges) { - codes.push( - nativeTags.has(tagName) ? '__VLS_intrinsicElements' : '__VLS_components', - ...createPropertyAccessCode([ - name, - 'template', - tagRange, - { - ...capabilitiesPresets.tagReference, - rename: { - normalize: tagName === name ? capabilitiesPresets.tagReference.rename.normalize : camelizeComponentName, - apply: getTagRenameApply(tagName), - }, - ...nativeTags.has(tagName) ? { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - } : {}, - }, - ]), - ';', - ); - } - } - codes.push('\n'); - - if (nativeTags.has(tagName)) - continue; - - const isNamespacedTag = tagName.indexOf('.') >= 0; - if (isNamespacedTag) - continue; - - codes.push( - '// @ts-ignore\n', // #2304 - '[', - ); - const validName = toCanonicalComponentName(tagName); - for (const tagRange of tagRanges) { - codes.push([ - validName, - 'template', - tagRange, - { - completion: { - additional: true, - autoImportOnly: true, - }, - }, - ]); - codes.push(','); - } - codes.push(`];\n`); - } - } - function collectTagOffsets() { - const tagOffsetsMap: Record = {}; + const tagOffsetsMap = new Map(); if (!template.ast) { return tagOffsetsMap; } - walkElementNodes(template.ast, node => { + for (const node of eachElementNode(template.ast)) { if (node.tag === 'slot') { // ignore } @@ -297,16 +188,20 @@ export function generate( for (const prop of node.props) { if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === 'is' && prop.value) { const tag = prop.value.content; - tagOffsetsMap[tag] ??= []; - tagOffsetsMap[tag].push(prop.value.loc.start.offset + prop.value.loc.source.lastIndexOf(tag)); + let offsets = tagOffsetsMap.get(tag); + if (!offsets) { + tagOffsetsMap.set(tag, offsets = []); + } + offsets.push(prop.value.loc.start.offset + prop.value.loc.source.lastIndexOf(tag)); break; } } } else { - tagOffsetsMap[node.tag] ??= []; - - const offsets = tagOffsetsMap[node.tag]; + let offsets = tagOffsetsMap.get(node.tag); + if (!offsets) { + tagOffsetsMap.set(node.tag, offsets = []); + } const source = template.content.substring(node.loc.start.offset); const startTagOffset = node.loc.start.offset + source.indexOf(node.tag); @@ -318,75 +213,199 @@ export function generate( } } } - }); + } return tagOffsetsMap; } - function resolveComment() { - if (ignoreStart !== undefined) { - for (let i = ignoreStart; i < codes.length; i++) { - const code = codes[i]; - if (typeof code === 'string') { - continue; + function* generateExpectErrorComment(): Generator<_CodeAndStack> { + + if (expectErrorToken && expectedErrorNode) { + const token = expectErrorToken; + yield _ts([ + '', + 'template', + expectedErrorNode.loc.start.offset, + disableAllFeatures({ + verification: { + shouldReport: () => token.errors === 0, + }, + }), + ]); + yield _ts('// @ts-expect-error __VLS_TS_EXPECT_ERROR'); + yield _ts([ + '', + 'template', + expectedErrorNode.loc.end.offset, + disableAllFeatures({ __combineLastMappping: true }), + ]); + yield _ts('\n;\n'); + } + + ignoreError = false; + expectErrorToken = undefined; + expectedErrorNode = undefined; + } + + function* generateCanonicalComponentName(tagText: string, offset: number, info: VueCodeInformation): Generator<_CodeAndStack> { + if (validTsVarReg.test(tagText)) { + yield _ts([tagText, 'template', offset, info]); + } + else { + yield* generateCamelized( + capitalize(tagText.replace(colonReg, '-')), + offset, + info + ); + } + } + + function* generateSlotsType(): Generator<_CodeAndStack> { + for (const [exp, slot] of slotExps) { + hasSlot = true; + yield _ts(`Partial, (_: typeof ${slot.varName}) => any>> &\n`); + } + yield _ts(`{\n`); + for (const [_, slot] of slots) { + hasSlot = true; + if (slot.name && slot.loc !== undefined) { + yield* generateObjectProperty( + slot.name, + slot.loc, + mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true })), + slot.nodeLoc + ); + } + else { + yield _ts(['', 'template', slot.tagRange[0], mergeFeatureSettings(presetInfos.slotNameExport, disableAllFeatures({ __referencesCodeLens: true }))]); + yield _ts('default'); + yield _ts(['', 'template', slot.tagRange[1], disableAllFeatures({ __combineLastMappping: true })]); + } + yield _ts(`?(_: typeof ${slot.varName}): any,\n`); + } + yield _ts(`}`); + } + + function* generateStyleScopedClasses(): Generator<_CodeAndStack> { + yield _ts(`if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {\n`); + for (const { className, offset } of scopedClasses) { + yield _ts(`__VLS_styleScopedClasses[`); + yield* generateStringLiteralKey( + className, + offset, + mergeFeatureSettings( + presetInfos.scopedClassName, + disableAllFeatures({ __displayWithLink: stylesScopedClasses.has(className) }), + ), + ); + yield _ts(`];\n`); + } + yield _ts('}\n'); + } + + function* generatePreResolveComponents(): Generator<_CodeAndStack> { + + yield _ts(`let __VLS_resolvedLocalAndGlobalComponents!: {}\n`); + + for (const [tagName] of tagOffsetsMap) { + + if (nativeTags.has(tagName)) + continue; + + const isNamespacedTag = tagName.indexOf('.') >= 0; + if (isNamespacedTag) + continue; + + yield _ts(`& __VLS_WithComponent<'${getCanonicalComponentName(tagName)}', typeof __VLS_localComponents, `); + // order is important: https://github.com/vuejs/language-tools/issues/2010 + yield _ts(`"${capitalize(camelize(tagName))}", `); + yield _ts(`"${camelize(tagName)}", `); + yield _ts(`"${tagName}"`); + yield _ts('>\n'); + } + + yield _ts(`;\n`); + + for (const [tagName, tagOffsets] of tagOffsetsMap) { + + for (const tagOffset of tagOffsets) { + if (nativeTags.has(tagName)) { + yield _ts('__VLS_intrinsicElements'); + yield* generatePropertyAccess( + tagName, + tagOffset, + mergeFeatureSettings( + presetInfos.tagReference, + { + navigation: true + }, + ...(nativeTags.has(tagName) ? [ + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ] : []), + ), + ); + yield _ts(';'); } - const cap = code[3]; - if (cap.diagnostic) { - code[3] = { - ...cap, - diagnostic: false, - }; + else if (validTsVarReg.test(camelize(tagName))) { + for (const shouldCapitalize of tagName[0] === tagName.toUpperCase() ? [false] : [true, false]) { + const expectName = shouldCapitalize ? capitalize(camelize(tagName)) : camelize(tagName); + yield _ts('__VLS_components.'); + yield* generateCamelized( + shouldCapitalize ? capitalize(tagName) : tagName, + tagOffset, + mergeFeatureSettings( + presetInfos.tagReference, + { + navigation: { + resolveRenameNewName: tagName !== expectName ? camelizeComponentName : undefined, + resolveRenameEditText: getTagRenameApply(tagName), + } + }, + ...(nativeTags.has(tagName) ? [ + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ] : []), + ), + ); + yield _ts(';'); + } } } - ignoreStart = undefined; - } - if (expectedErrorStart !== undefined && expectedErrorStart !== codes.length && expectedErrorNode) { - let errors = 0; - const suppressError = () => { - errors++; - return false; - }; - for (let i = expectedErrorStart; i < codes.length; i++) { - const code = codes[i]; - if (typeof code === 'string') { - continue; - } - const cap = code[3]; - if (cap.diagnostic) { - code[3] = { - ...cap, - diagnostic: { - shouldReport: suppressError, - }, - }; + yield _ts('\n'); + + if ( + !nativeTags.has(tagName) + && validTsVarReg.test(camelize(tagName)) + ) { + yield _ts('// @ts-ignore\n'); // #2304 + yield _ts('['); + for (const tagOffset of tagOffsets) { + yield* generateCamelized( + capitalize(tagName), + tagOffset, + disableAllFeatures({ + completion: { + isAdditional: true, + onlyImport: true, + }, + }), + ); + yield _ts(','); } + yield _ts(`];\n`); } - codes.push( - [ - '// @ts-expect-error __VLS_TS_EXPECT_ERROR', - 'template', - [expectedErrorNode.loc.start.offset, expectedErrorNode.loc.end.offset], - { - diagnostic: { - shouldReport: () => errors === 0, - }, - }, - ], - '\n;\n', - ); - expectedErrorStart = undefined; - expectedErrorNode = undefined; } } - function visitNode( + function* generateAstNode( node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.InterpolationNode | CompilerDOM.CompoundExpressionNode | CompilerDOM.TextNode | CompilerDOM.SimpleExpressionNode, parentEl: CompilerDOM.ElementNode | undefined, prevNode: CompilerDOM.TemplateChildNode | undefined, componentCtxVar: string | undefined, - ): void { + ): Generator<_CodeAndStack> { - resolveComment(); + yield* generateExpectErrorComment(); if (prevNode?.type === CompilerDOM.NodeTypes.COMMENT) { const commentText = prevNode.content.trim().split(' ')[0]; @@ -394,10 +413,10 @@ export function generate( return; } else if (commentText.match(/^@vue-ignore\b[\s\S]*/)) { - ignoreStart = codes.length; + ignoreError = true; } else if (commentText.match(/^@vue-expect-error\b[\s\S]*/)) { - expectedErrorStart = codes.length; + expectErrorToken = { errors: 0 }; expectedErrorNode = prevNode; } } @@ -405,33 +424,33 @@ export function generate( if (node.type === CompilerDOM.NodeTypes.ROOT) { let prev: CompilerDOM.TemplateChildNode | undefined; for (const childNode of node.children) { - visitNode(childNode, parentEl, prev, componentCtxVar); + yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); prev = childNode; } - resolveComment(); + yield* generateExpectErrorComment(); } else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { const vForNode = getVForNode(node); const vIfNode = getVIfNode(node); if (vForNode) { - visitVForNode(vForNode, parentEl, componentCtxVar); + yield* generateVFor(vForNode, parentEl, componentCtxVar); } else if (vIfNode) { - visitVIfNode(vIfNode, parentEl, componentCtxVar); + yield* generateVIf(vIfNode, parentEl, componentCtxVar); } else { - visitElementNode(node, parentEl, componentCtxVar); + yield* generateElement(node, parentEl, componentCtxVar); } } else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) { // {{ var }} - visitNode(node.content, parentEl, undefined, componentCtxVar); + yield* generateAstNode(node.content, parentEl, undefined, componentCtxVar); } else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) { // {{ ... }} {{ ... }} for (const childNode of node.children) { if (typeof childNode === 'object') { - visitNode(childNode, parentEl, undefined, componentCtxVar); + yield* generateAstNode(childNode, parentEl, undefined, componentCtxVar); } } } @@ -452,42 +471,38 @@ export function generate( content = content + rightCharacter; } - codes.push( - ...createInterpolationCode( - content, - node.content.loc, - start, - capabilitiesPresets.all, - '(', - ');\n', - ), + yield* generateInterpolation( + content, + node.content.loc, + start, + presetInfos.all, + '(', + ');\n', ); const lines = content.split('\n'); - formatCodes.push( - ...createFormatCode( - content, - start, - lines.length <= 1 ? formatBrackets.curly : [ - formatBrackets.curly[0], - lines[lines.length - 1].trim() === '' ? '' : formatBrackets.curly[1], - ], - ), + yield* generateTsFormat( + content, + start, + lines.length <= 1 ? formatBrackets.curly : [ + formatBrackets.curly[0], + lines[lines.length - 1].trim() === '' ? '' : formatBrackets.curly[1], + ], ); } else if (node.type === CompilerDOM.NodeTypes.IF) { // v-if / v-else-if / v-else - visitVIfNode(node, parentEl, componentCtxVar); + yield* generateVIf(node, parentEl, componentCtxVar); } else if (node.type === CompilerDOM.NodeTypes.FOR) { // v-for - visitVForNode(node, parentEl, componentCtxVar); + yield* generateVFor(node, parentEl, componentCtxVar); } else if (node.type === CompilerDOM.NodeTypes.TEXT) { // not needed progress } } - function visitVIfNode(node: CompilerDOM.IfNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined) { + function* generateVIf(node: CompilerDOM.IfNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { let originalBlockConditionsLength = blockConditions.length; @@ -496,52 +511,50 @@ export function generate( const branch = node.branches[i]; if (i === 0) - codes.push('if'); + yield _ts('if'); else if (branch.condition) - codes.push('else if'); + yield _ts('else if'); else - codes.push('else'); + yield _ts('else'); let addedBlockCondition = false; if (branch.condition?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - codes.push(` `); - const beforeCodeLength = codes.length; - codes.push( - ...createInterpolationCode( - branch.condition.content, - branch.condition.loc, - branch.condition.loc.start.offset, - capabilitiesPresets.all, - '(', - ')', - ), + yield _ts(` `); + yield* generateInterpolation( + branch.condition.content, + branch.condition.loc, + branch.condition.loc.start.offset, + presetInfos.all, + '(', + ')', ); - const afterCodeLength = codes.length; - - formatCodes.push( - ...createFormatCode( - branch.condition.content, - branch.condition.loc.start.offset, - formatBrackets.normal, - ), + blockConditions.push( + toString( + [...generateInterpolation(branch.condition.content, branch.condition.loc, undefined, undefined, '(', ')')] + .map(code => code[1]) + ) ); - - blockConditions.push(muggle.toString(codes.slice(beforeCodeLength, afterCodeLength))); addedBlockCondition = true; + + yield* generateTsFormat( + branch.condition.content, + branch.condition.loc.start.offset, + formatBrackets.normal, + ); } - codes.push(` {\n`); + yield _ts(` {\n`); let prev: CompilerDOM.TemplateChildNode | undefined; for (const childNode of branch.children) { - visitNode(childNode, parentEl, prev, componentCtxVar); + yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); prev = childNode; } - resolveComment(); + yield* generateExpectErrorComment(); - generateAutoImportCompletionCode(); - codes.push('}\n'); + yield* generateExtraAutoImport(); + yield _ts('}\n'); if (addedBlockCondition) { blockConditions[blockConditions.length - 1] = `!(${blockConditions[blockConditions.length - 1]})`; @@ -551,14 +564,14 @@ export function generate( blockConditions.length = originalBlockConditionsLength; } - function visitVForNode(node: CompilerDOM.ForNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined) { + function* generateVFor(node: CompilerDOM.ForNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { const { source, value, key, index } = node.parseResult; const leftExpressionRange = value ? { start: (value ?? key ?? index).loc.start.offset, end: (index ?? key ?? value).loc.end.offset } : undefined; const leftExpressionText = leftExpressionRange ? node.loc.source.substring(leftExpressionRange.start - node.loc.start.offset, leftExpressionRange.end - node.loc.start.offset) : undefined; const forBlockVars: string[] = []; - codes.push(`for (const [`); + yield _ts(`for (const [`); if (leftExpressionRange && leftExpressionText) { const collectAst = createTsAst(node.parseResult, `const [${leftExpressionText}]`); @@ -567,41 +580,37 @@ export function generate( for (const varName of forBlockVars) localVars.set(varName, (localVars.get(varName) ?? 0) + 1); - codes.push([leftExpressionText, 'template', leftExpressionRange.start, capabilitiesPresets.all]); - formatCodes.push(...createFormatCode(leftExpressionText, leftExpressionRange.start, formatBrackets.normal)); + yield _ts([leftExpressionText, 'template', leftExpressionRange.start, presetInfos.all]); + yield* generateTsFormat(leftExpressionText, leftExpressionRange.start, formatBrackets.normal); } - codes.push(`] of __VLS_getVForSourceType`); + yield _ts(`] of __VLS_getVForSourceType`); if (source.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - codes.push( + yield _ts('('); + yield* generateInterpolation( + source.content, + source.loc, + source.loc.start.offset, + presetInfos.all, '(', - ...createInterpolationCode( - source.content, - source.loc, - source.loc.start.offset, - capabilitiesPresets.all, - '(', - ')', - ), - '!)', // #3102 - ') {\n', + ')', ); + yield _ts('!)'); // #3102 + yield _ts(') {\n'); let prev: CompilerDOM.TemplateChildNode | undefined; for (const childNode of node.children) { - visitNode(childNode, parentEl, prev, componentCtxVar); + yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); prev = childNode; } - resolveComment(); + yield* generateExpectErrorComment(); - generateAutoImportCompletionCode(); - codes.push('}\n'); + yield* generateExtraAutoImport(); + yield _ts('}\n'); - formatCodes.push( - ...createFormatCode( - source.content, - source.loc.start.offset, - formatBrackets.normal, - ), + yield* generateTsFormat( + source.content, + source.loc.start.offset, + formatBrackets.normal, ); } @@ -609,9 +618,9 @@ export function generate( localVars.set(varName, localVars.get(varName)! - 1); } - function visitElementNode(node: CompilerDOM.ElementNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined) { + function* generateElement(node: CompilerDOM.ElementNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { - codes.push(`{\n`); + yield _ts(`{\n`); const startTagOffset = node.loc.start.offset + template.content.substring(node.loc.start.offset).indexOf(node.tag); let endTagOffset = !node.isSelfClosing && template.lang === 'html' ? node.loc.start.offset + node.loc.source.lastIndexOf(node.tag) : undefined; @@ -655,144 +664,128 @@ export function generate( const isIntrinsicElement = nativeTags.has(tag) && tagOffsets.length; if (isIntrinsicElement) { - codes.push( - 'const ', - var_originalComponent, - ` = __VLS_intrinsicElements[`, - ...createStringLiteralKeyCode([ - tag, - 'template', - tagOffsets[0], - capabilitiesPresets.diagnosticOnly, - ]), - '];\n', + yield _ts('const '); + yield _ts(var_originalComponent); + yield _ts(` = __VLS_intrinsicElements[`); + yield* generateStringLiteralKey( + tag, + tagOffsets[0], + presetInfos.diagnosticOnly, ); + yield _ts('];\n'); } else if (isNamespacedTag) { - codes.push( - `const ${var_originalComponent} = `, - ...createInterpolationCode(tag, node.loc, startTagOffset, capabilitiesPresets.all, '', ''), - ';\n', - ); + yield _ts(`const ${var_originalComponent} = `); + yield* generateInterpolation(tag, node.loc, startTagOffset, presetInfos.all, '', ''); + yield _ts(';\n'); } else if (dynamicTagExp) { - codes.push( - `const ${var_originalComponent} = `, - ...createInterpolationCode(dynamicTagExp.loc.source, dynamicTagExp.loc, dynamicTagExp.loc.start.offset, capabilitiesPresets.all, '(', ')'), - ';\n', - ); + yield _ts(`const ${var_originalComponent} = `); + yield* generateInterpolation(dynamicTagExp.loc.source, dynamicTagExp.loc, dynamicTagExp.loc.start.offset, presetInfos.all, '(', ')'); + yield _ts(';\n'); } else { - codes.push( - `const ${var_originalComponent} = ({} as `, - ); - for (const componentName of getPossibleOriginalComponentName(tag)) { - codes.push( - `'${componentName}' extends keyof typeof __VLS_ctx ? `, - `{ '${toCanonicalComponentName(tag)}': typeof __VLS_ctx`, - ...createPropertyAccessCode(componentName), - ` }: `, + yield _ts(`const ${var_originalComponent} = ({} as `); + for (const componentName of getPossibleOriginalComponentNames(tag)) { + yield _ts(`'${componentName}' extends keyof typeof __VLS_ctx ? `); + yield _ts(`{ '${getCanonicalComponentName(tag)}': typeof __VLS_ctx`); + yield* generatePropertyAccess(componentName); + yield _ts(` }: `); + } + yield _ts(`typeof __VLS_resolvedLocalAndGlobalComponents)`); + if (tagOffsets.length) { + yield* generatePropertyAccess( + getCanonicalComponentName(tag), + tagOffsets[0], + presetInfos.diagnosticOnly, ); } - codes.push( - `typeof __VLS_resolvedLocalAndGlobalComponents)`, - ...(tagOffsets.length - ? createPropertyAccessCode([ - toCanonicalComponentName(tag), - 'template', - [tagOffsets[0], tagOffsets[0] + tag.length], - capabilitiesPresets.diagnosticOnly, - ]) - : createPropertyAccessCode(toCanonicalComponentName(tag)) - ), - ';\n', - ); + else { + yield* generatePropertyAccess(getCanonicalComponentName(tag)); + } + yield _ts(';\n'); } if (isIntrinsicElement) { - codes.push(`const ${var_functionalComponent} = __VLS_elementAsFunctionalComponent(${var_originalComponent});\n`,); + yield _ts(`const ${var_functionalComponent} = __VLS_elementAsFunctionalComponent(${var_originalComponent});\n`); } else { - codes.push( - `const ${var_functionalComponent} = __VLS_asFunctionalComponent(`, - `${var_originalComponent}, `, - `new ${var_originalComponent}({`, - ...createPropsCode(node, props, 'extraReferences'), - '})', - ');\n', - ); + yield _ts(`const ${var_functionalComponent} = __VLS_asFunctionalComponent(`); + yield _ts(`${var_originalComponent}, `); + yield _ts(`new ${var_originalComponent}({`); + yield* generateProps(node, props, 'extraReferences'); + yield _ts('})'); + yield _ts(');\n'); } for (const offset of tagOffsets) { if (isNamespacedTag || dynamicTagExp || isIntrinsicElement) { continue; } - const key = toCanonicalComponentName(tag); - codes.push(`({} as { ${key}: typeof ${var_originalComponent} }).`); - codes.push( - [ - key, - 'template', - [offset, offset + tag.length], - { - ...capabilitiesPresets.tagHover, - ...capabilitiesPresets.diagnosticOnly, - }, - ], - ';\n', + yield _ts(`({} as { ${getCanonicalComponentName(tag)}: typeof ${var_originalComponent} }).`); + yield* generateCanonicalComponentName( + tag, + offset, + mergeFeatureSettings( + presetInfos.tagHover, + presetInfos.diagnosticOnly, + ), ); + yield _ts(';\n'); } if (vueCompilerOptions.strictTemplates) { // with strictTemplates, generate once for props type-checking + instance type - codes.push( - `const ${var_componentInstance} = ${var_functionalComponent}(`, - // diagnostic start - tagOffsets.length ? ['', 'template', tagOffsets[0], capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly] - : '', - '{ ', - ...createPropsCode(node, props, 'normal', propsFailedExps), - '}', - // diagnostic end - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, capabilitiesPresets.diagnosticOnly] - : '', - `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`, + yield _ts(`const ${var_componentInstance} = ${var_functionalComponent}(`); + // diagnostic start + yield _ts( + tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] + : '' + ); + yield _ts('{ '); + yield* generateProps(node, props, 'normal', propsFailedExps); + yield _ts('}'); + // diagnostic end + yield _ts( + tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] + : '' ); + yield _ts(`, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`); } else { // without strictTemplates, this only for instacne type - codes.push( - `const ${var_componentInstance} = ${var_functionalComponent}(`, - '{ ', - ...createPropsCode(node, props, 'extraReferences'), - '}', - `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`, - ); + yield _ts(`const ${var_componentInstance} = ${var_functionalComponent}(`); + yield _ts('{ '); + yield* generateProps(node, props, 'extraReferences'); + yield _ts('}'); + yield _ts(`, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}));\n`); // and this for props type-checking - codes.push( - `({} as (props: __VLS_FunctionalComponentProps & Record) => void)(`, - // diagnostic start - tagOffsets.length ? ['', 'template', tagOffsets[0], capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly] - : '', - '{ ', - ...createPropsCode(node, props, 'normal', propsFailedExps), - '}', - // diagnostic end - tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, capabilitiesPresets.diagnosticOnly] - : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, capabilitiesPresets.diagnosticOnly] - : '', - `);\n`, + yield _ts(`({} as (props: __VLS_FunctionalComponentProps & Record) => void)(`); + // diagnostic start + yield _ts( + tagOffsets.length ? ['', 'template', tagOffsets[0], presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset, presetInfos.diagnosticOnly] + : '' + ); + yield _ts('{ '); + yield* generateProps(node, props, 'normal', propsFailedExps); + yield _ts('}'); + // diagnostic end + yield _ts( + tagOffsets.length ? ['', 'template', tagOffsets[0] + tag.length, presetInfos.diagnosticOnly] + : dynamicTagExp ? ['', 'template', startTagOffset + tag.length, presetInfos.diagnosticOnly] + : '' ); + yield _ts(`);\n`); } if (tag !== 'template' && tag !== 'slot') { componentCtxVar = `__VLS_${elementIndex++}`; const componentEventsVar = `__VLS_${elementIndex++}`; - codes.push(`const ${componentCtxVar} = __VLS_pickFunctionalComponentCtx(${var_originalComponent}, ${var_componentInstance})!;\n`); - codes.push(`let ${componentEventsVar}!: __VLS_NormalizeEmits;\n`); + yield _ts(`const ${componentCtxVar} = __VLS_pickFunctionalComponentCtx(${var_originalComponent}, ${var_componentInstance})!;\n`); + yield _ts(`let ${componentEventsVar}!: __VLS_NormalizeEmits;\n`); componentCtxVar2EmitEventsVar.set(componentCtxVar, componentEventsVar); parentEl = node; } @@ -800,30 +793,26 @@ export function generate( //#region // fix https://github.com/vuejs/language-tools/issues/1775 for (const failedExp of propsFailedExps) { - codes.push( - ...createInterpolationCode( - failedExp.loc.source, - failedExp.loc, - failedExp.loc.start.offset, - capabilitiesPresets.all, - '(', - ')', - ), - ';\n', + yield* generateInterpolation( + failedExp.loc.source, + failedExp.loc, + failedExp.loc.start.offset, + presetInfos.all, + '(', + ')', ); + yield _ts(';\n'); const fb = formatBrackets.normal; if (fb) { - formatCodes.push( - ...createFormatCode( - failedExp.loc.source, - failedExp.loc.start.offset, - fb, - ), + yield* generateTsFormat( + failedExp.loc.source, + failedExp.loc.start.offset, + fb, ); } } - generateInlineCss(props); + yield* generateInlineCss(props); const vScope = props.find(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && (prop.name === 'scope' || prop.name === 'data')); let inScope = false; @@ -834,33 +823,33 @@ export function generate( const scopeVar = `__VLS_${elementIndex++}`; const condition = `__VLS_withScope(__VLS_ctx, ${scopeVar})`; - codes.push(`const ${scopeVar} = `); - codes.push([ + yield _ts(`const ${scopeVar} = `); + yield _ts([ vScope.exp.loc.source, 'template', vScope.exp.loc.start.offset, - capabilitiesPresets.all, + presetInfos.all, ]); - codes.push(';\n'); - codes.push(`if (${condition}) {\n`); + yield _ts(';\n'); + yield _ts(`if (${condition}) {\n`); blockConditions.push(condition); inScope = true; } - generateDirectives(node); - generateElReferences(node); // + yield* generateDirectives(node); + yield* generateReferencesForElements(node); // if (shouldGenerateScopedClasses) { - generateClassScoped(node); + yield* generateReferencesForScopedCssClasses(node); } if (componentCtxVar) { - generateEvents(node, var_functionalComponent, var_componentInstance, componentCtxVar); + yield* generateEvents(node, var_functionalComponent, var_componentInstance, componentCtxVar); } if (node.tag === 'slot') { - generateSlot(node, startTagOffset); + yield* generateSlot(node, startTagOffset); } if (inScope) { - codes.push('}\n'); + yield _ts('}\n'); blockConditions.length = originalConditionsNum; } //#endregion @@ -871,70 +860,61 @@ export function generate( hasSlotElements.add(parentEl); } const slotBlockVars: string[] = []; - codes.push(`{\n`); + yield _ts(`{\n`); let hasProps = false; if (slotDir?.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - formatCodes.push( - ...createFormatCode( - slotDir.exp.content, - slotDir.exp.loc.start.offset, - formatBrackets.params, - ), + yield* generateTsFormat( + slotDir.exp.content, + slotDir.exp.loc.start.offset, + formatBrackets.params, ); const slotAst = createTsAst(slotDir, `(${slotDir.exp.content}) => {}`); collectVars(ts, slotAst, slotBlockVars); hasProps = true; if (slotDir.exp.content.indexOf(':') === -1) { - codes.push( - 'const [', - [ - slotDir.exp.content, - 'template', - slotDir.exp.loc.start.offset, - capabilitiesPresets.all, - ], - `] = __VLS_getSlotParams(`, - ); + yield _ts('const ['); + yield _ts([ + slotDir.exp.content, + 'template', + slotDir.exp.loc.start.offset, + presetInfos.all, + ]); + yield _ts(`] = __VLS_getSlotParams(`); } else { - codes.push( - 'const ', - [ - slotDir.exp.content, - 'template', - slotDir.exp.loc.start.offset, - capabilitiesPresets.all, - ], - ` = __VLS_getSlotParam(`, - ); + yield _ts('const '); + yield _ts([ + slotDir.exp.content, + 'template', + slotDir.exp.loc.start.offset, + presetInfos.all, + ]); + yield _ts(` = __VLS_getSlotParam(`); } } - codes.push( - ['', 'template', (slotDir.arg ?? slotDir).loc.start.offset, capabilitiesPresets.diagnosticOnly], - `(${componentCtxVar}.slots!)`, - ...( - (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content) - ? createPropertyAccessCode([ - slotDir.arg.loc.source, - 'template', - slotDir.arg.loc.start.offset, - slotDir.arg.isStatic ? capabilitiesPresets.slotName : capabilitiesPresets.all - ], slotDir.arg.loc) - : createPropertyAccessCode([ - 'default', - 'template', - [slotDir.loc.start.offset, slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0)], - { ...capabilitiesPresets.slotName, completion: false }, - ]) - ), - ['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, capabilitiesPresets.diagnosticOnly], - ); + yield _ts(['', 'template', (slotDir.arg ?? slotDir).loc.start.offset, presetInfos.diagnosticOnly]); + yield _ts(`(${componentCtxVar}.slots!)`); + if (slotDir?.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && slotDir.arg.content) { + yield* generatePropertyAccess( + slotDir.arg.loc.source, + slotDir.arg.loc.start.offset, + slotDir.arg.isStatic ? presetInfos.slotName : presetInfos.all, + slotDir.arg.loc + ); + } + else { + yield _ts('.'); + yield _ts(['', 'template', slotDir.loc.start.offset, { ...presetInfos.slotName, completion: false }] satisfies Code); + yield _ts('default'); + yield _ts(['', 'template', slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), disableAllFeatures({ __combineLastMappping: true })] satisfies Code); + } + yield _ts(['', 'template', (slotDir.arg ?? slotDir).loc.end.offset, presetInfos.diagnosticOnly]); if (hasProps) { - codes.push(')'); + yield _ts(')'); } - codes.push(';\n'); + yield _ts(';\n'); slotBlockVars.forEach(varName => { localVars.set(varName, (localVars.get(varName) ?? 0) + 1); @@ -942,11 +922,11 @@ export function generate( let prev: CompilerDOM.TemplateChildNode | undefined; for (const childNode of node.children) { - visitNode(childNode, parentEl, prev, componentCtxVar); + yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); prev = childNode; } - resolveComment(); - generateAutoImportCompletionCode(); + yield* generateExpectErrorComment(); + yield* generateExtraAutoImport(); slotBlockVars.forEach(varName => { localVars.set(varName, localVars.get(varName)! - 1); @@ -956,49 +936,44 @@ export function generate( isStatic = slotDir.arg.isStatic; } if (isStatic && slotDir && !slotDir.arg) { - codes.push( - `${componentCtxVar}.slots!['`, - [ - '', - 'template', - slotDir.loc.start.offset + (slotDir.loc.source.startsWith('#') ? '#'.length : slotDir.loc.source.startsWith('v-slot:') ? 'v-slot:'.length : 0), - { completion: true }, - ], - `'/* empty slot name completion */]\n`, - ); + yield _ts(`${componentCtxVar}.slots!['`); + yield _ts([ + '', + 'template', + slotDir.loc.start.offset + ( + slotDir.loc.source.startsWith('#') + ? '#'.length : slotDir.loc.source.startsWith('v-slot:') + ? 'v-slot:'.length + : 0 + ), + disableAllFeatures({ completion: true }), + ]); + yield _ts(`'/* empty slot name completion */]\n`); } - codes.push(`}\n`); + yield _ts(`}\n`); } else { let prev: CompilerDOM.TemplateChildNode | undefined; for (const childNode of node.children) { - visitNode(childNode, parentEl, prev, componentCtxVar); + yield* generateAstNode(childNode, parentEl, prev, componentCtxVar); prev = childNode; } - resolveComment(); + yield* generateExpectErrorComment(); // fix https://github.com/vuejs/language-tools/issues/932 if (!hasSlotElements.has(node) && node.children.length) { - codes.push( - `(${componentCtxVar}.slots!)`, - ...createPropertyAccessCode([ - 'default', - 'template', - [ - node.children[0].loc.start.offset, - node.children[node.children.length - 1].loc.end.offset, - ], - { references: true }, - ]), - ';\n', - ); + yield _ts(`(${componentCtxVar}.slots!).`); + yield _ts(['', 'template', node.children[0].loc.start.offset, disableAllFeatures({ navigation: true })]); + yield _ts('default'); + yield _ts(['', 'template', node.children[node.children.length - 1].loc.end.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts(';\n'); } } - codes.push(`}\n`); + yield _ts(`}\n`); } - function generateEvents(node: CompilerDOM.ElementNode, componentVar: string, componentInstanceVar: string, componentCtxVar: string) { + function* generateEvents(node: CompilerDOM.ElementNode, componentVar: string, componentInstanceVar: string, componentCtxVar: string): Generator<_CodeAndStack> { for (const prop of node.props) { if ( @@ -1008,22 +983,24 @@ export function generate( ) { const eventsVar = componentCtxVar2EmitEventsVar.get(componentCtxVar); const eventVar = `__VLS_${elementIndex++}`; - codes.push( - `let ${eventVar} = { '${prop.arg.loc.source}': `, - `__VLS_pickEvent(${eventsVar}['${prop.arg.loc.source}'], ({} as __VLS_FunctionalComponentProps)`, - ...createPropertyAccessCode([ - camelize('on-' + prop.arg.loc.source), // onClickOutside - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], + yield _ts(`let ${eventVar} = { '${prop.arg.loc.source}': `); + yield _ts(`__VLS_pickEvent(`); + yield _ts(`${eventsVar}['${prop.arg.loc.source}'], `); + yield _ts(`({} as __VLS_FunctionalComponentProps)`); + const startCode: Code = [ + '', + 'template', + prop.arg.loc.start.offset, + mergeFeatureSettings( + presetInfos.attrReference, { - ...capabilitiesPresets.attrReference, - rename: { + navigation: { // @click-outside -> onClickOutside - normalize(newName) { + resolveRenameNewName(newName) { return camelize('on-' + newName); }, // onClickOutside -> @click-outside - apply(newName) { + resolveRenameEditText(newName) { const hName = hyphenateAttr(newName); if (hyphenateAttr(newName).startsWith('on-')) { return camelize(hName.slice('on-'.length)); @@ -1032,37 +1009,58 @@ export function generate( }, }, }, - ]), - `) };\n`, - `${eventVar} = { `, - ); + ), + ]; + if (validTsVarReg.test(camelize(prop.arg.loc.source))) { + yield _ts(`.`); + yield _ts(startCode); + yield _ts(`on`); + yield* generateCamelized( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + disableAllFeatures({ __combineLastMappping: true }), + ); + } + else { + yield _ts(`[`); + yield _ts(startCode); + yield _ts(`'`); + yield _ts(['', 'template', prop.arg.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts('on'); + yield* generateCamelized( + capitalize(prop.arg.loc.source), + prop.arg.loc.start.offset, + disableAllFeatures({ __combineLastMappping: true }), + ); + yield _ts(`'`); + yield _ts(['', 'template', prop.arg.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts(`]`); + } + yield _ts(`) };\n`); + yield _ts(`${eventVar} = { `); if (prop.arg.loc.source.startsWith('[') && prop.arg.loc.source.endsWith(']')) { - codes.push( - '[(', - ...createInterpolationCode( - prop.arg.loc.source.slice(1, -1), - prop.arg.loc, - prop.arg.loc.start.offset + 1, - capabilitiesPresets.all, - '', - '', - ), - ')!]', + yield _ts('[('); + yield* generateInterpolation( + prop.arg.loc.source.slice(1, -1), + prop.arg.loc, + prop.arg.loc.start.offset + 1, + presetInfos.all, + '', + '', ); + yield _ts(')!]'); } else { - codes.push( - ...createObjectPropertyCode([ - prop.arg.loc.source, - 'template', - prop.arg.loc.start.offset, - capabilitiesPresets.event, - ], prop.arg.loc) + yield* generateObjectProperty( + prop.arg.loc.source, + prop.arg.loc.start.offset, + presetInfos.event, + prop.arg.loc ); } - codes.push(`: `); - appendExpressionNode(prop); - codes.push(` };\n`); + yield _ts(`: `); + yield* appendExpressionNode(prop); + yield _ts(` };\n`); } else if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1071,107 +1069,99 @@ export function generate( ) { // for vue 2 nameless event // https://github.com/johnsoncodehk/vue-tsc/issues/67 - codes.push( - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - capabilitiesPresets.all, - '$event => {(', - ')}', - ), - ';\n', + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + presetInfos.all, + '$event => {(', + ')}', ); - formatCodes.push( - ...createFormatCode( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ), + yield _ts(';\n'); + yield* generateTsFormat( + prop.exp.content, + prop.exp.loc.start.offset, + formatBrackets.normal, ); } + } + } - function appendExpressionNode(prop: CompilerDOM.DirectiveNode) { - if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + function* appendExpressionNode(prop: CompilerDOM.DirectiveNode): Generator<_CodeAndStack> { + if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - const ast = createTsAst(prop.exp, prop.exp.content); - let isCompoundExpression = true; + const ast = createTsAst(prop.exp, prop.exp.content); + let isCompoundExpression = true; - if (ast.getChildCount() === 2) { // with EOF - ast.forEachChild(child_1 => { - if (ts.isExpressionStatement(child_1)) { - child_1.forEachChild(child_2 => { - if (ts.isArrowFunction(child_2)) { - isCompoundExpression = false; - } - else if (ts.isIdentifier(child_2)) { - isCompoundExpression = false; - } - }); + if (ast.getChildCount() === 2) { // with EOF + ast.forEachChild(child_1 => { + if (ts.isExpressionStatement(child_1)) { + child_1.forEachChild(child_2 => { + if (ts.isArrowFunction(child_2)) { + isCompoundExpression = false; } - else if (ts.isFunctionDeclaration(child_1)) { + else if (ts.isIdentifier(child_2)) { isCompoundExpression = false; } }); } + else if (ts.isFunctionDeclaration(child_1)) { + isCompoundExpression = false; + } + }); + } - let prefix = '('; - let suffix = ')'; - let isFirstMapping = true; - - if (isCompoundExpression) { + let prefix = '('; + let suffix = ')'; + let isFirstMapping = true; - codes.push('$event => {\n'); - localVars.set('$event', (localVars.get('$event') ?? 0) + 1); + if (isCompoundExpression) { - prefix = ''; - suffix = ''; - for (const blockCondition of blockConditions) { - prefix += `if (!(${blockCondition})) return;\n`; - } - } + yield _ts('$event => {\n'); + localVars.set('$event', (localVars.get('$event') ?? 0) + 1); - codes.push( - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - () => { - if (isCompoundExpression && isFirstMapping) { - isFirstMapping = false; - return capabilitiesPresets.allWithHiddenParam; - } - return capabilitiesPresets.all; - }, - prefix, - suffix, - ) - ); - - if (isCompoundExpression) { - localVars.set('$event', localVars.get('$event')! - 1); + prefix = ''; + suffix = ''; + for (const blockCondition of blockConditions) { + prefix += `if (!(${blockCondition})) return;\n`; + } + } - codes.push(';\n'); - generateAutoImportCompletionCode(); - codes.push('}\n'); + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + () => { + if (isCompoundExpression && isFirstMapping) { + isFirstMapping = false; + return presetInfos.allWithHiddenParam; } + return presetInfos.all; + }, + prefix, + suffix, + ); - formatCodes.push( - ...createFormatCode( - prop.exp.content, - prop.exp.loc.start.offset, - isCompoundExpression ? formatBrackets.event : formatBrackets.normal, - ), - ); - } - else { - codes.push(`() => {}`); - } + if (isCompoundExpression) { + localVars.set('$event', localVars.get('$event')! - 1); + + yield _ts(';\n'); + yield* generateExtraAutoImport(); + yield _ts('}\n'); } + + yield* generateTsFormat( + prop.exp.content, + prop.exp.loc.start.offset, + isCompoundExpression ? formatBrackets.event : formatBrackets.normal, + ); + } + else { + yield _ts(`() => {}`); } } - function createPropsCode(node: CompilerDOM.ElementNode, props: CompilerDOM.ElementNode['props'], mode: 'normal' | 'extraReferences', propsFailedExps?: CompilerDOM.SimpleExpressionNode[]): Code[] { + function* generateProps(node: CompilerDOM.ElementNode, props: CompilerDOM.ElementNode['props'], mode: 'normal' | 'extraReferences', propsFailedExps?: CompilerDOM.SimpleExpressionNode[]): Generator<_CodeAndStack> { let styleAttrNum = 0; let classAttrNum = 0; @@ -1187,41 +1177,27 @@ export function generate( classAttrNum++; } - const codes: Code[] = []; - - let caps_all: FileRangeCapabilities = capabilitiesPresets.all; - let caps_diagnosticOnly: FileRangeCapabilities = capabilitiesPresets.diagnosticOnly; - let caps_attr: FileRangeCapabilities = capabilitiesPresets.attr; + let caps_all: VueCodeInformation = presetInfos.all; + let caps_diagnosticOnly: VueCodeInformation = presetInfos.diagnosticOnly; + let caps_attr: VueCodeInformation = presetInfos.attr; if (mode === 'extraReferences') { - caps_all = { - references: caps_all.references, - rename: caps_all.rename, - }; - caps_diagnosticOnly = { - references: caps_diagnosticOnly.references, - rename: caps_diagnosticOnly.rename, - }; - caps_attr = { - references: caps_attr.references, - rename: caps_attr.rename, - }; - } - - codes.push(`...{ `); + caps_all = disableAllFeatures({ navigation: caps_all.navigation }); + caps_diagnosticOnly = disableAllFeatures({ navigation: caps_diagnosticOnly.navigation }); + caps_attr = disableAllFeatures({ navigation: caps_attr.navigation }); + } + + yield _ts(`...{ `); for (const prop of props) { if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'on' && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { - codes.push( - ...createObjectPropertyCode(camelize('on-' + prop.arg.loc.source)), - ': {} as any, ', - ); + yield _ts(`'${camelize('on-' + prop.arg.loc.source)}': {} as any, `); } } - codes.push(`}, `); + yield _ts(`}, `); const canCamelize = !nativeTags.has(node.tag) || node.tagType === CompilerDOM.ElementTypes.COMPONENT; @@ -1233,7 +1209,7 @@ export function generate( && (!prop.exp || prop.exp.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) ) { - let attrNameText = + let propName = prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ? prop.arg.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY ? prop.arg.content @@ -1241,15 +1217,15 @@ export function generate( : getModelValuePropName(node, vueCompilerOptions.target, vueCompilerOptions); if (prop.modifiers.some(m => m === 'prop' || m === 'attr')) { - attrNameText = attrNameText?.substring(1); + propName = propName?.substring(1); } if ( - attrNameText === undefined - || vueCompilerOptions.dataAttributes.some(pattern => minimatch(attrNameText!, pattern)) - || (attrNameText === 'style' && ++styleAttrNum >= 2) - || (attrNameText === 'class' && ++classAttrNum >= 2) - || (attrNameText === 'name' && node.tag === 'slot') // #2308 + propName === undefined + || vueCompilerOptions.dataAttributes.some(pattern => minimatch(propName!, pattern)) + || (propName === 'style' && ++styleAttrNum >= 2) + || (propName === 'class' && ++classAttrNum >= 2) + || (propName === 'name' && node.tag === 'slot') // #2308 ) { if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { propsFailedExps?.push(prop.exp); @@ -1257,161 +1233,123 @@ export function generate( continue; } - let camelized = false; - - if ( - canCamelize + const shouldCamelize = canCamelize && (!prop.arg || (prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.isStatic)) // isStatic - && hyphenateAttr(attrNameText) === attrNameText - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(attrNameText!, pattern)) - ) { - attrNameText = camelize(attrNameText); - camelized = true; - } - - // camelize name - codes.push([ - '', - 'template', - prop.loc.start.offset, - caps_diagnosticOnly, - ]); - if (!prop.arg) { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.loc.start.offset, prop.loc.start.offset + prop.loc.source.indexOf('=')], + && hyphenateAttr(propName) === propName + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(propName!, pattern)); + + yield _ts(['', 'template', prop.loc.start.offset, caps_diagnosticOnly]); + yield* generateObjectProperty( + propName, + prop.arg + ? prop.arg.loc.start.offset + : prop.loc.start.offset, + prop.arg + ? mergeFeatureSettings( caps_attr, - ], (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {})), - ); - } - else if (prop.exp?.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY) { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.start.offset + attrNameText.length], // patch style attr, - { - ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, - }, - }, - ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), - ); - } - else { - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], { - ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, - }, + navigation: caps_attr.navigation ? { + resolveRenameNewName: camelize, + resolveRenameEditText: shouldCamelize ? hyphenateAttr : undefined, + } : undefined, }, - ], (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {})), + ) + : caps_attr, + (prop.loc as any).name_2 ?? ((prop.loc as any).name_2 = {}), + shouldCamelize, + ); + yield _ts(': ('); + const isShorthand = prop.exp + && prop.exp.loc.start.offset === prop.exp.loc.end.offset + && prop.exp.content; + if ( + prop.exp + && !isShorthand + && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY + ) { // style='z-index: 2' will compile to {'z-index':'2'} + yield* generateInterpolation( + prop.exp.loc.source, + prop.exp.loc, + prop.exp.loc.start.offset, + caps_all, + '(', + ')', ); - } - codes.push(': ('); - if (prop.exp && !(prop.exp.constType === CompilerDOM.ConstantTypes.CAN_STRINGIFY)) { // style='z-index: 2' will compile to {'z-index':'2'} - codes.push( - ...createInterpolationCode( + if (mode === 'normal') { + yield* generateTsFormat( prop.exp.loc.source, - prop.exp.loc, prop.exp.loc.start.offset, + formatBrackets.normal, + ); + } + } + else if ( + prop.arg + && isShorthand + && prop.arg.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + ) { + const propVariableName = camelize(prop.arg.content); + if (validTsVarReg.test(propVariableName)) { + yield* generateInterpolation( + propVariableName, + prop.arg.loc, + prop.arg.loc.start.offset, caps_all, '(', ')', - ), - ); - if (mode === 'normal') { - formatCodes.push( - ...createFormatCode( - prop.exp.loc.source, - prop.exp.loc.start.offset, - formatBrackets.normal, - ), ); } } else { - codes.push('{}'); + yield _ts('{}'); } - codes.push(')'); - codes.push([ + yield _ts(')'); + yield _ts([ '', 'template', prop.loc.end.offset, caps_diagnosticOnly, ]); - codes.push(', '); + yield _ts(', '); } else if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { - let attrNameText = prop.name; - if ( - vueCompilerOptions.dataAttributes.some(pattern => minimatch(attrNameText!, pattern)) - || (attrNameText === 'style' && ++styleAttrNum >= 2) - || (attrNameText === 'class' && ++classAttrNum >= 2) - || (attrNameText === 'name' && node.tag === 'slot') // #2308 - ) { - continue; - } + vueCompilerOptions.dataAttributes.some(pattern => minimatch(prop.name, pattern)) + || (prop.name === 'style' && ++styleAttrNum >= 2) + || (prop.name === 'class' && ++classAttrNum >= 2) + || (prop.name === 'name' && node.tag === 'slot') // #2308 + ) continue; - let camelized = false; - - if ( - canCamelize + const shouldCamelize = canCamelize && hyphenateAttr(prop.name) === prop.name - && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(attrNameText!, pattern)) - ) { - attrNameText = camelize(prop.name); - camelized = true; - } + && !vueCompilerOptions.htmlAttributes.some(pattern => minimatch(prop.name, pattern)); - // camelize name - codes.push([ - '', - 'template', + yield _ts(['', 'template', prop.loc.start.offset, caps_diagnosticOnly]); + yield* generateObjectProperty( + prop.name, prop.loc.start.offset, - caps_diagnosticOnly, - ]); - codes.push( - ...createObjectPropertyCode([ - attrNameText, - 'template', - [prop.loc.start.offset, prop.loc.start.offset + prop.name.length], - { - ...caps_attr, - rename: { - normalize: camelize, - apply: camelized ? hyphenateAttr : noEditApply, - }, - }, - ], (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {})) + shouldCamelize + ? mergeFeatureSettings(caps_attr, { + navigation: caps_attr.navigation ? { + resolveRenameNewName: camelize, + resolveRenameEditText: hyphenateAttr, + } : undefined, + }) + : caps_attr, + (prop.loc as any).name_1 ?? ((prop.loc as any).name_1 = {}), + shouldCamelize, ); - codes.push(': ('); + yield _ts(': ('); if (prop.value) { - generateAttrValue(prop.value); + yield* generateAttrValue(prop.value, caps_all); } else { - codes.push('true'); + yield _ts('true'); } - codes.push(')'); - codes.push([ - '', - 'template', - prop.loc.end.offset, - caps_diagnosticOnly, - ]); - codes.push(', '); + yield _ts(')'); + yield _ts(['', 'template', prop.loc.end.offset, caps_diagnosticOnly]); + yield _ts(', '); } else if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1419,27 +1357,23 @@ export function generate( && !prop.arg && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { - codes.push( - ['', 'template', prop.exp.loc.start.offset, capabilitiesPresets.diagnosticOnly], - '...', - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - caps_all, - '(', - ')', - ), - ['', 'template', prop.exp.loc.end.offset, capabilitiesPresets.diagnosticOnly], - ', ', + yield _ts(['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly]); + yield _ts('...'); + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + caps_all, + '(', + ')', ); + yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); + yield _ts(', '); if (mode === 'normal') { - formatCodes.push( - ...createFormatCode( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ), + yield* generateTsFormat( + prop.exp.content, + prop.exp.loc.start.offset, + formatBrackets.normal, ); } } @@ -1448,34 +1382,9 @@ export function generate( // tsCodeGen.addText("/* " + [prop.type, prop.name, prop.arg?.loc.source, prop.exp?.loc.source, prop.loc.source].join(", ") + " */ "); } } - - return codes; - - function generateAttrValue(attrNode: CompilerDOM.TextNode) { - const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; - codes.push(char); - let start = attrNode.loc.start.offset; - let end = attrNode.loc.end.offset; - let content = attrNode.loc.source; - if ( - (content.startsWith('"') && content.endsWith('"')) - || (content.startsWith("'") && content.endsWith("'")) - ) { - start++; - end--; - content = content.slice(1, -1); - } - codes.push([ - toUnicodeIfNeed(content), - 'template', - [start, end], - caps_all, - ]); - codes.push(char); - } } - function generateInlineCss(props: CompilerDOM.ElementNode['props']) { + function* generateInlineCss(props: CompilerDOM.ElementNode['props']): Generator<_CodeAndStack> { for (const prop of props) { if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1490,19 +1399,22 @@ export function generate( const end = prop.arg.loc.source.lastIndexOf(endCrt); const content = prop.arg.loc.source.substring(start, end); - cssCodes.push(`x { `); - cssCodes.push([ + yield _inlineCss(`x { `); + yield _inlineCss([ content, 'template', prop.arg.loc.start.offset + start, - capabilitiesPresets.all, + enableAllFeatures({ + format: false, + structure: false, + }), ]); - cssCodes.push(` }\n`); + yield _inlineCss(` }\n`); } } } - function generateDirectives(node: CompilerDOM.ElementNode) { + function* generateDirectives(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1516,120 +1428,101 @@ export function generate( accessedGlobalVariables.add(camelize('v-' + prop.name)); if (prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic) { - codes.push( - ...createInterpolationCode( - prop.arg.content, - prop.arg.loc, - prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content), - capabilitiesPresets.all, - '(', - ')', - ), - ';\n', + yield* generateInterpolation( + prop.arg.content, + prop.arg.loc, + prop.arg.loc.start.offset + prop.arg.loc.source.indexOf(prop.arg.content), + presetInfos.all, + '(', + ')', ); - formatCodes.push( - ...createFormatCode( - prop.arg.content, - prop.arg.loc.start.offset, - formatBrackets.normal, - ), + yield _ts(';\n'); + yield* generateTsFormat( + prop.arg.content, + prop.arg.loc.start.offset, + formatBrackets.normal, ); } - codes.push( - [ - '', - 'template', - prop.loc.start.offset, - capabilitiesPresets.diagnosticOnly, - ], - `__VLS_directiveFunction(__VLS_ctx.`, - [ - camelize('v-' + prop.name), - 'template', - [prop.loc.start.offset, prop.loc.start.offset + 'v-'.length + prop.name.length], + yield _ts(['', 'template', prop.loc.start.offset, presetInfos.diagnosticOnly]); + yield _ts(`__VLS_directiveFunction(__VLS_ctx.`); + yield* generateCamelized( + 'v-' + prop.name, + prop.loc.start.offset, + mergeFeatureSettings( + presetInfos.noDiagnostics, { - ...capabilitiesPresets.noDiagnostic, completion: { // fix https://github.com/vuejs/language-tools/issues/1905 - additional: true, + isAdditional: true, }, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), }, }, - ], - ')', - '(', + ), ); + yield _ts(')'); + yield _ts('('); + if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - codes.push( - ['', 'template', prop.exp.loc.start.offset, capabilitiesPresets.diagnosticOnly], - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - capabilitiesPresets.all, - '(', - ')', - ), - ['', 'template', prop.exp.loc.end.offset, capabilitiesPresets.diagnosticOnly], + yield _ts(['', 'template', prop.exp.loc.start.offset, presetInfos.diagnosticOnly]); + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + presetInfos.all, + '(', + ')', ); - formatCodes.push( - ...createFormatCode( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ), + yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); + yield* generateTsFormat( + prop.exp.content, + prop.exp.loc.start.offset, + formatBrackets.normal, ); } else { - codes.push('undefined'); + yield _ts('undefined'); } - codes.push( - ')', - ['', 'template', prop.loc.end.offset, capabilitiesPresets.diagnosticOnly], - ';\n', - ); + yield _ts(')'); + yield _ts(['', 'template', prop.loc.end.offset, presetInfos.diagnosticOnly]); + yield _ts(';\n'); } } } - function generateElReferences(node: CompilerDOM.ElementNode) { + function* generateReferencesForElements(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === 'ref' && prop.value ) { - codes.push( - '// @ts-ignore\n', - ...createInterpolationCode( - prop.value.content, - prop.value.loc, - prop.value.loc.start.offset + 1, - capabilitiesPresets.refAttr, - '(', - ')', - ), - ';\n', + yield _ts('// @ts-ignore\n'); + yield* generateInterpolation( + prop.value.content, + prop.value.loc, + prop.value.loc.start.offset + 1, + presetInfos.refAttr, + '(', + ')', ); + yield _ts(';\n'); } } } - function generateClassScoped(node: CompilerDOM.ElementNode) { + function* generateReferencesForScopedCssClasses(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name === 'class' && prop.value ) { - let startOffset = prop.value.loc.start.offset; let tempClassName = ''; - for (const char of (prop.value.loc.source + ' ')) { if (char.trim() === '' || char === '"' || char === "'") { if (tempClassName !== '') { @@ -1650,40 +1543,38 @@ export function generate( && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.content === 'class' ) { - codes.push(`__VLS_styleScopedClasses = (`); - codes.push([ + yield _ts(`__VLS_styleScopedClasses = (`); + yield _ts([ prop.exp.content, 'template', prop.exp.loc.start.offset, - capabilitiesPresets.scopedClassName, + presetInfos.scopedClassName, ]); - codes.push(`);\n`); + yield _ts(`);\n`); } } } - function generateSlot(node: CompilerDOM.ElementNode, startTagOffset: number) { + function* generateSlot(node: CompilerDOM.ElementNode, startTagOffset: number): Generator<_CodeAndStack> { const varSlot = `__VLS_${elementIndex++}`; const slotNameExpNode = getSlotNameExpNode(); if (hasScriptSetupSlots) { - codes.push( - '__VLS_normalizeSlot(', - ['', 'template', node.loc.start.offset, capabilitiesPresets.diagnosticOnly], - `${slotsAssignName ?? '__VLS_slots'}[`, - ['', 'template', node.loc.start.offset, capabilitiesPresets.diagnosticOnly], - slotNameExpNode?.content ?? `('${getSlotName()}' as const)`, - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], - ']', - ['', 'template', node.loc.end.offset, capabilitiesPresets.diagnosticOnly], - ')?.(', - ['', 'template', startTagOffset, capabilitiesPresets.diagnosticOnly], - '{\n', - ); + yield _ts('__VLS_normalizeSlot('); + yield _ts(['', 'template', node.loc.start.offset, presetInfos.diagnosticOnly]); + yield _ts(`${slotsAssignName ?? '__VLS_slots'}[`); + yield _ts(['', 'template', node.loc.start.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts(slotNameExpNode?.content ?? `('${getSlotName()?.[0] ?? 'default'}' as const)`); + yield _ts(['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts(']'); + yield _ts(['', 'template', node.loc.end.offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts(')?.('); + yield _ts(['', 'template', startTagOffset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts('{\n'); } else { - codes.push(`var ${varSlot} = {\n`); + yield _ts(`var ${varSlot} = {\n`); } for (const prop of node.props) { if ( @@ -1691,18 +1582,16 @@ export function generate( && !prop.arg && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { - codes.push( - '...', - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, - '(', - ')', - ), - ',\n', + yield _ts('...'); + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + presetInfos.attrReference, + '(', + ')', ); + yield _ts(',\n'); } else if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1710,59 +1599,64 @@ export function generate( && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.content !== 'name' ) { - codes.push( - ...createObjectPropertyCode([ - prop.arg.content, - 'template', - [prop.arg.loc.start.offset, prop.arg.loc.end.offset], + yield* generateObjectProperty( + prop.arg.content, + prop.arg.loc.start.offset, + mergeFeatureSettings( + presetInfos.slotProp, { - ...capabilitiesPresets.slotProp, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.arg.content), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.arg.content), }, }, - ], prop.arg.loc), - ': ', - ...createInterpolationCode( - prop.exp.content, - prop.exp.loc, - prop.exp.loc.start.offset, - capabilitiesPresets.attrReference, - '(', - ')', ), - ',\n', + prop.arg.loc + ); + yield _ts(': '); + yield* generateInterpolation( + prop.exp.content, + prop.exp.loc, + prop.exp.loc.start.offset, + presetInfos.attrReference, + '(', + ')', ); + yield _ts(',\n'); } else if ( prop.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop.name !== 'name' // slot name ) { - codes.push( - ...createObjectPropertyCode([ - prop.name, - 'template', - prop.loc.start.offset, + yield* generateObjectProperty( + prop.name, + prop.loc.start.offset, + mergeFeatureSettings( + presetInfos.attr, { - ...capabilitiesPresets.attr, - rename: { - normalize: camelize, - apply: getPropRenameApply(prop.name), + navigation: { + resolveRenameNewName: camelize, + resolveRenameEditText: getPropRenameApply(prop.name), }, }, - ], prop.loc), - ': (', - prop.value !== undefined ? `"${toUnicodeIfNeed(prop.value.content)}"` : 'true', - '),\n', + ), + prop.loc + ); + yield _ts(': ('); + yield _ts( + prop.value !== undefined + ? `"${needToUnicode(prop.value.content) ? toUnicode(prop.value.content) : prop.value.content}"` + : 'true' ); + yield _ts('),\n'); } } - codes.push( - '}', - hasScriptSetupSlots ? ['', 'template', startTagOffset + node.tag.length, capabilitiesPresets.diagnosticOnly] : '', - hasScriptSetupSlots ? `);\n` : `;\n` - ); + yield _ts('}'); + if (hasScriptSetupSlots) { + yield _ts(['', 'template', startTagOffset + node.tag.length, presetInfos.diagnosticOnly]); + yield _ts(`)`); + } + yield _ts(`;\n`); if (hasScriptSetupSlots) { return; @@ -1770,31 +1664,31 @@ export function generate( if (slotNameExpNode) { const varSlotExp = `__VLS_${elementIndex++}`; - codes.push(`var ${varSlotExp} = `); + yield _ts(`var ${varSlotExp} = `); if (typeof slotNameExpNode === 'string') { - codes.push(slotNameExpNode); + yield _ts(slotNameExpNode); } else { - codes.push( - ...createInterpolationCode( - slotNameExpNode.content, - slotNameExpNode, - undefined, undefined, - '(', - ')', - ), + yield* generateInterpolation( + slotNameExpNode.content, + slotNameExpNode, + undefined, undefined, + '(', + ')', ); } - codes.push(` as const;\n`); + yield _ts(` as const;\n`); slotExps.set(varSlotExp, { varName: varSlot, }); } else { const slotName = getSlotName(); - slots.set(slotName, { + slots.set(slotName?.[0] ?? 'default', { + name: slotName?.[0], + loc: slotName?.[1], + tagRange: [startTagOffset, startTagOffset + node.tag.length], varName: varSlot, - loc: [startTagOffset, startTagOffset + node.tag.length], nodeLoc: node.loc, }); } @@ -1803,11 +1697,13 @@ export function generate( for (const prop2 of node.props) { if (prop2.name === 'name' && prop2.type === CompilerDOM.NodeTypes.ATTRIBUTE && prop2.value) { if (prop2.value.content) { - return prop2.value.content; + return [ + prop2.value.content, + prop2.loc.start.offset + prop2.loc.source.indexOf(prop2.value.content, prop2.name.length), + ] as const; } } } - return 'default'; } function getSlotNameExpNode() { for (const prop2 of node.props) { @@ -1820,100 +1716,173 @@ export function generate( } } - function generateAutoImportCompletionCode() { + function* generateExtraAutoImport(): Generator<_CodeAndStack> { if (!tempVars.length) return; - codes.push('// @ts-ignore\n'); // #2304 - codes.push('['); + yield _ts('// @ts-ignore\n'); // #2304 + yield _ts('['); for (const _vars of tempVars) { for (const v of _vars) { - codes.push([v.text, 'template', v.offset, { completion: { additional: true } }]); - codes.push(','); + yield _ts([ + v.text, + 'template', + v.offset, + disableAllFeatures({ completion: { isAdditional: true }, }), + ]); + yield _ts(','); } } - codes.push('];\n'); + yield _ts('];\n'); tempVars.length = 0; } - // functional like + function* generateAttrValue(attrNode: CompilerDOM.TextNode, info: VueCodeInformation): Generator<_CodeAndStack> { + const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; + yield _ts(char); + let start = attrNode.loc.start.offset; + let end = attrNode.loc.end.offset; + let content = attrNode.loc.source; + if ( + (content.startsWith('"') && content.endsWith('"')) + || (content.startsWith("'") && content.endsWith("'")) + ) { + start++; + end--; + content = content.slice(1, -1); + } + if (needToUnicode(content)) { + yield _ts(['', 'template', start, info]); + yield _ts(toUnicode(content)); + yield _ts(['', 'template', end, disableAllFeatures({ __combineLastMappping: true })]); + } + else { + yield _ts([content, 'template', start, info]); + } + yield _ts(char); + } - function createFormatCode(mapCode: string, sourceOffset: number, formatWrapper: [string, string]): Code[] { - return [ - formatWrapper[0], - [mapCode, 'template', sourceOffset, { completion: true /* fix vue-autoinsert-parentheses not working */ }], - formatWrapper[1], - '\n', - ]; + function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator<_CodeAndStack> { + const parts = code.split('-'); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part !== '') { + yield _ts([ + i === 0 + ? part + : capitalize(part), + 'template', + offset, + i === 0 + ? info + : disableAllFeatures({ __combineLastMappping: true }), + ]); + } + offset += part.length + 1; + } } - function createObjectPropertyCode(a: Code, astHolder?: any): Code[] { - const aStr = typeof a === 'string' ? a : a[0]; - if (validTsVarReg.test(aStr)) { - return [a]; - } - else if (aStr.startsWith('[') && aStr.endsWith(']') && astHolder) { - const range = typeof a === 'object' ? a[2] : undefined; - const data = typeof a === 'object' ? a[3] : undefined; - return createInterpolationCode( - aStr, - astHolder, - range && typeof range === 'object' ? range[0] : range, - data, - '', - '', - ); + function* generateTsFormat(code: string, offset: number, formatWrapper: [string, string]): Generator<_CodeAndStack> { + yield _tsFormat(formatWrapper[0]); + yield _tsFormat([ + code, + 'template', + offset, + mergeFeatureSettings( + presetInfos.disabledAll, + { + format: true, + // autoInserts: true, // TODO: support vue-autoinsert-parentheses + }, + ), + ]); + yield _tsFormat(formatWrapper[1]); + yield _tsFormat('\n'); + } + + function* generateObjectProperty(code: string, offset: number, info: VueCodeInformation, astHolder?: any, shouldCamelize = false): Generator<_CodeAndStack> { + if (code.startsWith('[') && code.endsWith(']') && astHolder) { + yield* generateInterpolation(code, astHolder, offset, info, '', ''); + } + else if (shouldCamelize) { + if (validTsVarReg.test(camelize(code))) { + yield* generateCamelized(code, offset, info); + } + else { + yield _ts(['', 'template', offset, info]); + yield _ts('"'); + yield* generateCamelized(code, offset, disableAllFeatures({ __combineLastMappping: true })); + yield _ts('"'); + yield _ts(['', 'template', offset + code.length, disableAllFeatures({ __combineLastMappping: true })]); + } } else { - return createStringLiteralKeyCode(a); + if (validTsVarReg.test(code)) { + yield _ts([code, 'template', offset, info]); + } + else { + yield* generateStringLiteralKey(code, offset, info); + } } } - function createInterpolationCode( + function* generateInterpolation( _code: string, astHolder: any, start: number | undefined, - data: FileRangeCapabilities | (() => FileRangeCapabilities) | undefined, + data: VueCodeInformation | (() => VueCodeInformation) | undefined, prefix: string, suffix: string, - ): Code[] { + ): Generator<_CodeAndStack> { const code = prefix + _code + suffix; const ast = createTsAst(astHolder, code); - const codes: Code[] = []; - const vars = walkInterpolationFragment(ts, code, ast, (frag, fragOffset, isJustForErrorMapping) => { - if (fragOffset === undefined) { - codes.push(frag); + const vars: { + text: string, + isShorthand: boolean, + offset: number, + }[] = []; + for (let [section, offset, onlyError] of eachInterpolationSegment( + ts, + code, + ast, + localVars, + accessedGlobalVariables, + vueCompilerOptions, + vars, + )) { + if (offset === undefined) { + yield _ts(section); } else { - fragOffset -= prefix.length; + offset -= prefix.length; let addSuffix = ''; - const overLength = fragOffset + frag.length - _code.length; + const overLength = offset + section.length - _code.length; if (overLength > 0) { - addSuffix = frag.substring(frag.length - overLength); - frag = frag.substring(0, frag.length - overLength); + addSuffix = section.substring(section.length - overLength); + section = section.substring(0, section.length - overLength); } - if (fragOffset < 0) { - codes.push(frag.substring(0, -fragOffset)); - frag = frag.substring(-fragOffset); - fragOffset = 0; + if (offset < 0) { + yield _ts(section.substring(0, -offset)); + section = section.substring(-offset); + offset = 0; } if (start !== undefined && data !== undefined) { - codes.push([ - frag, + yield _ts([ + section, 'template', - start + fragOffset, - isJustForErrorMapping - ? capabilitiesPresets.diagnosticOnly + start + offset, + onlyError + ? presetInfos.diagnosticOnly : typeof data === 'function' ? data() : data, ]); } else { - codes.push(frag); + yield _ts(section); } - codes.push(addSuffix); + yield _ts(addSuffix); } - }, localVars, accessedGlobalVariables, vueCompilerOptions); + } if (start !== undefined) { for (const v of vars) { v.offset = start + v.offset - prefix.length; @@ -1922,72 +1891,77 @@ export function generate( tempVars.push(vars); } } - return codes; } - function createTsAst(astHolder: any, text: string) { - if (astHolder.__volar_ast_text !== text) { - astHolder.__volar_ast_text = text; - astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, ts.ScriptTarget.ESNext); + function* generatePropertyAccess(code: string, offset?: number, info?: VueCodeInformation, astHolder?: any): Generator<_CodeAndStack> { + if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(code)) { + yield _ts('.'); + yield _ts(offset !== undefined && info + ? [code, 'template', offset, info] + : code); + } + else if (code.startsWith('[') && code.endsWith(']')) { + yield* generateInterpolation(code, astHolder, offset, info, '', ''); + } + else { + yield _ts('['); + yield* generateStringLiteralKey(code, offset, info); + yield _ts(']'); } - return astHolder.__volar_ast as ts.SourceFile; } - function createPropertyAccessCode(a: Code, astHolder?: any): Code[] { - const aStr = typeof a === 'string' ? a : a[0]; - if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(aStr)) { - return ['.', a]; - } - else if (aStr.startsWith('[') && aStr.endsWith(']')) { - if (typeof a === 'string' || !astHolder) { - return [a]; - } - else { - return createInterpolationCode( - a[0], - astHolder, - typeof a[2] === 'number' ? a[2] : a[2][0], - a[3], - '', - '', - ); - } + function* generateStringLiteralKey(code: string, offset?: number, info?: VueCodeInformation): Generator<_CodeAndStack> { + if (offset === undefined || !info) { + yield _ts(`"${code}"`); } else { - return ['[', ...createStringLiteralKeyCode(a), ']']; + yield _ts(['', 'template', offset, info]); + yield _ts('"'); + yield _ts([code, 'template', offset, disableAllFeatures({ __combineLastMappping: true })]); + yield _ts('"'); + yield _ts(['', 'template', offset + code.length, disableAllFeatures({ __combineLastMappping: true })]); } } - function createStringLiteralKeyCode(a: Code): Code[] { - let codes: Code[] = ['"', a, '"']; - if (typeof a === 'object') { - const start = typeof a[2] === 'number' ? a[2] : a[2][0]; - const end = typeof a[2] === 'number' ? a[2] : a[2][1]; - codes = [ - ['', 'template', start, a[3]], - ...codes, - ['', 'template', end, a[3]], - ]; - } - return codes; + function createTsAst(astHolder: any, text: string) { + if (astHolder.__volar_ast_text !== text) { + astHolder.__volar_ast_text = text; + astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, ts.ScriptTarget.ESNext); + } + return astHolder.__volar_ast as ts.SourceFile; } -}; +} + +function getCanonicalComponentName(tagText: string) { + return validTsVarReg.test(tagText) + ? tagText + : capitalize(camelize(tagText.replace(colonReg, '-'))); +} + +function getPossibleOriginalComponentNames(tagText: string) { + return [...new Set([ + // order is important: https://github.com/vuejs/language-tools/issues/2010 + capitalize(camelize(tagText)), + camelize(tagText), + tagText, + ])]; +} -export function walkElementNodes(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode, cb: (node: CompilerDOM.ElementNode) => void) { +export function* eachElementNode(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode): Generator { if (node.type === CompilerDOM.NodeTypes.ROOT) { for (const child of node.children) { - walkElementNodes(child, cb); + yield* eachElementNode(child); } } else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { const patchForNode = getVForNode(node); if (patchForNode) { - walkElementNodes(patchForNode, cb); + yield* eachElementNode(patchForNode); } else { - cb(node); + yield node; for (const child of node.children) { - walkElementNodes(child, cb); + yield* eachElementNode(child); } } } @@ -1996,23 +1970,20 @@ export function walkElementNodes(node: CompilerDOM.RootNode | CompilerDOM.Templa for (let i = 0; i < node.branches.length; i++) { const branch = node.branches[i]; for (const childNode of branch.children) { - walkElementNodes(childNode, cb); + yield* eachElementNode(childNode); } } } else if (node.type === CompilerDOM.NodeTypes.FOR) { // v-for for (const child of node.children) { - walkElementNodes(child, cb); + yield* eachElementNode(child); } } } -function toUnicodeIfNeed(str: string) { - if (str.indexOf('\\') === -1 && str.indexOf('\n') === -1) { - return str; - } - return toUnicode(str); +function needToUnicode(str: string) { + return str.indexOf('\\') >= 0 || str.indexOf('\n') >= 0; } function toUnicode(str: string) { @@ -2030,15 +2001,11 @@ function camelizeComponentName(newName: string) { } function getTagRenameApply(oldName: string) { - return oldName === hyphenateTag(oldName) ? hyphenateTag : noEditApply; + return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; } function getPropRenameApply(oldName: string) { - return oldName === hyphenateAttr(oldName) ? hyphenateAttr : noEditApply; -} - -function noEditApply(n: string) { - return n; + return oldName === hyphenateAttr(oldName) ? hyphenateAttr : undefined; } function getModelValuePropName(node: CompilerDOM.ElementNode, vueVersion: number, vueCompilerOptions: VueCompilerOptions) { diff --git a/packages/language-core/src/generators/utils.ts b/packages/language-core/src/generators/utils.ts new file mode 100644 index 0000000000..3556496cbc --- /dev/null +++ b/packages/language-core/src/generators/utils.ts @@ -0,0 +1,55 @@ +import { Code, CodeAndStack, VueCodeInformation } from '../types'; + +export function withStack(code: Code): CodeAndStack { + return [code, getStack()]; +} + +// TODO: import from muggle-string +export function getStack() { + const stack = new Error().stack!; + let source = stack.split('\n')[3].trim(); + if (source.endsWith(')')) { + source = source.slice(source.lastIndexOf('(') + 1, -1); + } + else { + source = source.slice(source.lastIndexOf(' ') + 1); + } + return source; +} + +export function disableAllFeatures(override: Partial): VueCodeInformation { + return { + verification: false, + completion: false, + semantic: false, + navigation: false, + structure: false, + format: false, + ...override, + }; +} + +export function enableAllFeatures(override: Partial): VueCodeInformation { + return { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, + ...override, + }; +} + +export function mergeFeatureSettings(base: VueCodeInformation, ...others: Partial[]): VueCodeInformation { + const result: VueCodeInformation = { ...base }; + for (const info of others) { + for (const key in info) { + const value = info[key as keyof VueCodeInformation]; + if (value) { + result[key as keyof VueCodeInformation] = value as any; + } + } + } + return result; +} diff --git a/packages/language-core/src/index.ts b/packages/language-core/src/index.ts index 30594dbcbd..11f1e53f8d 100644 --- a/packages/language-core/src/index.ts +++ b/packages/language-core/src/index.ts @@ -13,5 +13,4 @@ export * from './utils/shared'; export { tsCodegen } from './plugins/vue-tsx'; export * from '@volar/language-core'; -export * from '@volar/source-map'; export type * as CompilerDOM from '@vue/compiler-dom'; diff --git a/packages/language-core/src/languageModule.ts b/packages/language-core/src/languageModule.ts index dcf477b78d..06ccc5f33f 100644 --- a/packages/language-core/src/languageModule.ts +++ b/packages/language-core/src/languageModule.ts @@ -1,4 +1,4 @@ -import type { Language } from '@volar/language-core'; +import type { LanguagePlugin } from '@volar/language-core'; import * as path from 'path-browserify'; import { getDefaultVueLanguagePlugins } from './plugins'; import { VueFile } from './virtualFile/vueFile'; @@ -38,7 +38,7 @@ export function createVueLanguage( compilerOptions: ts.CompilerOptions = {}, _vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -): Language { +): LanguagePlugin { const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); const plugins = getDefaultVueLanguagePlugins( @@ -68,48 +68,52 @@ export function createVueLanguage( } return { - createVirtualFile(fileName, snapshot, languageId) { - if ( - (languageId && allowLanguageIds.has(languageId)) - || (!languageId && vueCompilerOptions.extensions.some(ext => fileName.endsWith(ext))) - ) { - if (fileRegistry.has(fileName)) { - const reusedVueFile = fileRegistry.get(fileName)!; + createVirtualFile(id, languageId, snapshot) { + if (allowLanguageIds.has(languageId)) { + if (fileRegistry.has(id)) { + const reusedVueFile = fileRegistry.get(id)!; reusedVueFile.update(snapshot); return reusedVueFile; } - const vueFile = new VueFile(fileName, snapshot, vueCompilerOptions, plugins, ts, codegenStack); - fileRegistry.set(fileName, vueFile); + const vueFile = new VueFile(id, languageId, snapshot, vueCompilerOptions, plugins, ts, codegenStack); + fileRegistry.set(id, vueFile); return vueFile; } }, updateVirtualFile(sourceFile, snapshot) { sourceFile.update(snapshot); }, - resolveHost(host) { - const sharedTypesSnapshot = ts.ScriptSnapshot.fromString(sharedTypes.getTypesCode(vueCompilerOptions)); - const sharedTypesFileName = path.join(host.rootPath, sharedTypes.baseName); - return { - ...host, - resolveModuleName(moduleName, impliedNodeFormat) { - if (impliedNodeFormat === ts.ModuleKind.ESNext && vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) { - return `${moduleName}.js`; - } - return host.resolveModuleName?.(moduleName, impliedNodeFormat) ?? moduleName; - }, - getScriptFileNames() { - return [ - sharedTypesFileName, - ...host.getScriptFileNames(), - ]; - }, - getScriptSnapshot(fileName) { - if (fileName === sharedTypesFileName) { - return sharedTypesSnapshot; - } - return host.getScriptSnapshot(fileName); - }, - }; + typescript: { + resolveSourceFileName(tsFileName) { + const baseName = path.basename(tsFileName); + if (baseName.indexOf('.vue.') >= 0) { // .vue.ts .vue.d.ts .vue.js .vue.jsx .vue.tsx + return tsFileName.substring(0, tsFileName.lastIndexOf('.vue.') + '.vue'.length); + } + }, + resolveModuleName(moduleName, impliedNodeFormat) { + if (impliedNodeFormat === 99 satisfies ts.ModuleKind.ESNext && vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) { + return `${moduleName}.js`; + } + }, + resolveLanguageServiceHost(host) { + const sharedTypesSnapshot = ts.ScriptSnapshot.fromString(sharedTypes.getTypesCode(vueCompilerOptions)); + const sharedTypesFileName = path.join(host.getCurrentDirectory(), sharedTypes.baseName); + return { + ...host, + getScriptFileNames() { + return [ + sharedTypesFileName, + ...host.getScriptFileNames(), + ]; + }, + getScriptSnapshot(fileName) { + if (fileName === sharedTypesFileName) { + return sharedTypesSnapshot; + } + return host.getScriptSnapshot(fileName); + }, + }; + }, }, }; } @@ -122,7 +126,7 @@ export function createLanguages( compilerOptions: ts.CompilerOptions = {}, vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -): Language[] { +): LanguagePlugin[] { return [ createVueLanguage(ts, compilerOptions, vueCompilerOptions, codegenStack), ...vueCompilerOptions.experimentalAdditionalLanguageModules?.map(module => require(module)) ?? [], diff --git a/packages/language-core/src/plugins/file-md.ts b/packages/language-core/src/plugins/file-md.ts index b4d37b339b..d8a22f8c6c 100644 --- a/packages/language-core/src/plugins/file-md.ts +++ b/packages/language-core/src/plugins/file-md.ts @@ -1,4 +1,4 @@ -import { buildMappings, Segment, SourceMap, toString } from '@volar/source-map'; +import { buildMappings, Segment, SourceMap, toString } from '@volar/language-core'; import type { SFCBlock } from '@vue/compiler-sfc'; import { VueLanguagePlugin } from '../types'; import { parse } from '../utils/parseSfc'; @@ -74,8 +74,8 @@ const plugin: VueLanguagePlugin = () => { return sfc; function transformRange(block: SFCBlock) { - block.loc.start.offset = file2VueSourceMap.toSourceOffset(block.loc.start.offset)?.[0] ?? -1; - block.loc.end.offset = file2VueSourceMap.toSourceOffset(block.loc.end.offset)?.[0] ?? -1; + block.loc.start.offset = file2VueSourceMap.getSourceOffset(block.loc.start.offset)?.[0] ?? -1; + block.loc.end.offset = file2VueSourceMap.getSourceOffset(block.loc.end.offset)?.[0] ?? -1; } }; } diff --git a/packages/language-core/src/plugins/vue-sfc-customblocks.ts b/packages/language-core/src/plugins/vue-sfc-customblocks.ts index b2bdf2e79e..7ec8d1d5d0 100644 --- a/packages/language-core/src/plugins/vue-sfc-customblocks.ts +++ b/packages/language-core/src/plugins/vue-sfc-customblocks.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const customBlockReg = /^(.*)\.customBlock_([^_]+)_(\d+)\.([^.]+)$/; @@ -24,12 +24,11 @@ const plugin: VueLanguagePlugin = () => { const index = parseInt(match[3]); const customBlock = sfc.customBlocks[index]; - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ customBlock.content, customBlock.name, 0, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-scripts.ts b/packages/language-core/src/plugins/vue-sfc-scripts.ts index e50695bf7c..df45f8cb0e 100644 --- a/packages/language-core/src/plugins/vue-sfc-scripts.ts +++ b/packages/language-core/src/plugins/vue-sfc-scripts.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileKind } from '@volar/language-core'; +import { disableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const scriptFormatReg = /^(.*)\.script_format\.([^.]+)$/; @@ -26,18 +26,14 @@ const plugin: VueLanguagePlugin = () => { const scriptSetupMatch = embeddedFile.fileName.match(scriptSetupFormatReg); const script = scriptMatch ? sfc.script : scriptSetupMatch ? sfc.scriptSetup : undefined; if (script) { - embeddedFile.kind = FileKind.TextFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - diagnostic: false, - codeAction: false, - inlayHint: false, - }; embeddedFile.content.push([ script.content, script.name, 0, - {}, + disableAllFeatures({ + structure: true, + format: true, + }), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-styles.ts b/packages/language-core/src/plugins/vue-sfc-styles.ts index b750349a1d..b661a8d88f 100644 --- a/packages/language-core/src/plugins/vue-sfc-styles.ts +++ b/packages/language-core/src/plugins/vue-sfc-styles.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const styleReg = /^(.*)\.style_(\d+)\.([^.]+)$/; @@ -24,12 +24,11 @@ const plugin: VueLanguagePlugin = () => { const index = parseInt(match[2]); const style = sfc.styles[index]; - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ style.content, style.name, 0, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-sfc-template.ts b/packages/language-core/src/plugins/vue-sfc-template.ts index b21e8e8cd4..1b366f235a 100644 --- a/packages/language-core/src/plugins/vue-sfc-template.ts +++ b/packages/language-core/src/plugins/vue-sfc-template.ts @@ -1,4 +1,4 @@ -import { FileCapabilities, FileRangeCapabilities } from '@volar/language-core'; +import { enableAllFeatures } from '../generators/utils'; import { VueLanguagePlugin } from '../types'; const templateReg = /^(.*)\.template\.([^.]+)$/; @@ -19,12 +19,11 @@ const plugin: VueLanguagePlugin = () => { resolveEmbeddedFile(_fileName, sfc, embeddedFile) { const match = embeddedFile.fileName.match(templateReg); if (match && sfc.template) { - embeddedFile.capabilities = FileCapabilities.full; embeddedFile.content.push([ sfc.template.content, sfc.template.name, 0, - FileRangeCapabilities.full, + enableAllFeatures({}), ]); } }, diff --git a/packages/language-core/src/plugins/vue-tsx.ts b/packages/language-core/src/plugins/vue-tsx.ts index b3cb5e826b..0bc3a4d4e1 100644 --- a/packages/language-core/src/plugins/vue-tsx.ts +++ b/packages/language-core/src/plugins/vue-tsx.ts @@ -1,11 +1,11 @@ +import { CodeInformation, Mapping, Segment, StackNode, track } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; import { generate as generateScript } from '../generators/script'; import { generate as generateTemplate } from '../generators/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges'; -import { Sfc, VueLanguagePlugin } from '../types'; -import { FileCapabilities, FileKind } from '@volar/language-core'; -import * as muggle from 'muggle-string'; +import { Code, Sfc, VueLanguagePlugin } from '../types'; +import { enableAllFeatures } from '../generators/utils'; const templateFormatReg = /^\.template_format\.ts$/; const templateStyleCssReg = /^\.template_style\.css$/; @@ -43,41 +43,39 @@ const plugin: VueLanguagePlugin = (ctx) => { resolveEmbeddedFile(fileName, sfc, embeddedFile) { const _tsx = useTsx(fileName, sfc); + const lang = _tsx.lang(); const suffix = embeddedFile.fileName.replace(fileName, ''); - if (suffix === '.' + _tsx.lang()) { - embeddedFile.kind = FileKind.TypeScriptHostFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - foldingRange: false, - documentFormatting: false, - documentSymbol: false, + if (suffix === '.' + lang) { + embeddedFile.typescript = { + scriptKind: lang === 'js' ? ctx.modules.typescript.ScriptKind.JS + : lang === 'jsx' ? ctx.modules.typescript.ScriptKind.JSX + : lang === 'tsx' ? ctx.modules.typescript.ScriptKind.TSX + : ctx.modules.typescript.ScriptKind.TS }; const tsx = _tsx.generatedScript(); if (tsx) { - const [content, contentStacks] = ctx.codegenStack ? muggle.track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + const [content, contentStacks] = ctx.codegenStack ? track([...tsx.codes], [...tsx.codeStacks]) : [[...tsx.codes], [...tsx.codeStacks]]; + content.forEach(code => { + if (typeof code !== 'string') { + code[3].structure = false; + code[3].format = false; + } + }); embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; - embeddedFile.mirrorBehaviorMappings = [...tsx.mirrorBehaviorMappings]; + embeddedFile.linkedNavigationMappings = [...tsx.linkedCodeMappings]; } } else if (suffix.match(templateFormatReg)) { embeddedFile.parentFileName = fileName + '.template.' + sfc.template?.lang; - embeddedFile.kind = FileKind.TextFile; - embeddedFile.capabilities = { - ...FileCapabilities.full, - diagnostic: false, - foldingRange: false, - codeAction: false, - inlayHint: false, - }; const template = _tsx.generatedTemplate(); if (template) { const [content, contentStacks] = ctx.codegenStack - ? muggle.track([...template.formatCodes], [...template.formatCodeStacks]) - : [[...template.formatCodes], [...template.formatCodeStacks]]; + ? track([...template.formatCodes], template.formatCodeStacks.map(stack => ({ stack, length: 1 }))) + : [[...template.formatCodes], template.formatCodeStacks.map(stack => ({ stack, length: 1 }))]; embeddedFile.content = content; embeddedFile.contentStacks = contentStacks; } @@ -90,7 +88,7 @@ const plugin: VueLanguagePlugin = (ctx) => { cssVar.text, style.name, cssVar.offset, - {}, + enableAllFeatures({}), ]); embeddedFile.content.push(');\n'); } @@ -103,14 +101,11 @@ const plugin: VueLanguagePlugin = (ctx) => { const template = _tsx.generatedTemplate(); if (template) { const [content, contentStacks] = ctx.codegenStack - ? muggle.track([...template.cssCodes], [...template.cssCodeStacks]) - : [[...template.cssCodes], [...template.cssCodeStacks]]; - embeddedFile.content = content; + ? track([...template.cssCodes], template.cssCodeStacks.map(stack => ({ stack, length: 1 }))) + : [[...template.cssCodes], template.cssCodeStacks.map(stack => ({ stack, length: 1 }))]; + embeddedFile.content = content as Segment[]; embeddedFile.contentStacks = contentStacks; } - - // for color pickers support - embeddedFile.capabilities.documentSymbol = true; } }, }; @@ -174,7 +169,13 @@ function createTsx(fileName: string, _sfc: Sfc, { vueCompilerOptions, compilerOp if (!_sfc.template) return; - return generateTemplate( + const tsCodes: Code[] = []; + const tsFormatCodes: Code[] = []; + const inlineCssCodes: Code[] = []; + const tsCodegenStacks: string[] = []; + const tsFormatCodegenStacks: string[] = []; + const inlineCssCodegenStacks: string[] = []; + const codegen = generateTemplate( ts, compilerOptions, vueCompilerOptions, @@ -186,24 +187,89 @@ function createTsx(fileName: string, _sfc: Sfc, { vueCompilerOptions, compilerOp propsAssignName(), codegenStack, ); + + let current = codegen.next(); + + while (!current.done) { + const [type, code, stack] = current.value; + if (type === 'ts') { + tsCodes.push(code); + } + else if (type === 'tsFormat') { + tsFormatCodes.push(code); + } + else if (type === 'inlineCss') { + inlineCssCodes.push(code); + } + if (codegenStack) { + if (type === 'ts') { + tsCodegenStacks.push(stack); + } + else if (type === 'tsFormat') { + tsFormatCodegenStacks.push(stack); + } + else if (type === 'inlineCss') { + inlineCssCodegenStacks.push(stack); + } + } + current = codegen.next(); + } + + return { + ...current.value, + codes: tsCodes, + codeStacks: tsCodegenStacks, + formatCodes: tsFormatCodes, + formatCodeStacks: tsFormatCodegenStacks, + cssCodes: inlineCssCodes, + cssCodeStacks: inlineCssCodegenStacks, + }; }); const hasScriptSetupSlots = computed(() => !!scriptSetupRanges()?.slots.define); const slotsAssignName = computed(() => scriptSetupRanges()?.slots.name); const propsAssignName = computed(() => scriptSetupRanges()?.props.name); - const generatedScript = computed(() => generateScript( - ts, - fileName, - _sfc.script, - _sfc.scriptSetup, - _sfc.styles, - lang(), - scriptRanges(), - scriptSetupRanges(), - generatedTemplate(), - compilerOptions, - vueCompilerOptions, - codegenStack, - )); + const generatedScript = computed(() => { + const codes: Code[] = []; + const codeStacks: StackNode[] = []; + const linkedCodeMappings: Mapping[] = []; + const _template = generatedTemplate(); + let generatedLength = 0; + for (const [code, stack] of generateScript( + ts, + fileName, + _sfc.script, + _sfc.scriptSetup, + _sfc.styles, + lang(), + scriptRanges(), + scriptSetupRanges(), + _template ? { + tsCodes: _template.codes, + tsCodegenStacks: _template.codeStacks, + accessedGlobalVariables: _template.accessedGlobalVariables, + hasSlot: _template.hasSlot, + tagNames: new Set(_template.tagOffsetsMap.keys()), + } : undefined, + compilerOptions, + vueCompilerOptions, + () => generatedLength, + linkedCodeMappings, + codegenStack, + )) { + codes.push(code); + if (codegenStack) { + codeStacks.push({ stack, length: 1 }); + } + generatedLength += typeof code === 'string' + ? code.length + : code[0].length; + }; + return { + codes, + codeStacks, + linkedCodeMappings, + }; + }); return { scriptRanges, diff --git a/packages/language-core/src/types.ts b/packages/language-core/src/types.ts index 4096cadafd..54856bdf5d 100644 --- a/packages/language-core/src/types.ts +++ b/packages/language-core/src/types.ts @@ -2,6 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCParseResult } from '@vue/compiler-sfc'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { VueEmbeddedFile } from './virtualFile/embeddedFile'; +import type { CodeInformation, Segment } from '@volar/language-core'; export type { SFCParseResult } from '@vue/compiler-sfc'; @@ -10,6 +11,23 @@ export type RawVueCompilerOptions = Partial; + export interface VueCompilerOptions { target: number; lib: string; @@ -31,7 +49,6 @@ export interface VueCompilerOptions { withDefaults: string[]; }; plugins: VueLanguagePlugin[]; - hooks: string[]; // experimental experimentalDefinePropProposal: 'kevinEdition' | 'johnsonEdition' | false; diff --git a/packages/language-core/src/utils/parseSfc.ts b/packages/language-core/src/utils/parseSfc.ts index 1e2157b8e8..6294be73e4 100644 --- a/packages/language-core/src/utils/parseSfc.ts +++ b/packages/language-core/src/utils/parseSfc.ts @@ -10,26 +10,7 @@ export function parse(source: string): SFCParseResult { isNativeTag: () => true, // preserve all whitespaces isPreTag: () => true, - getTextMode: ({ tag, props }, parent) => { - if ( - (!parent && tag !== 'template') - || ( - tag === 'template' - && props.some( - p => - p.type === compiler.NodeTypes.ATTRIBUTE && - p.name === 'lang' && - p.value && - p.value.content && - p.value.content !== 'html' - ) - )) { - return compiler.TextModes.RAWTEXT; - } - else { - return compiler.TextModes.DATA; - } - }, + parseMode: "sfc", onError: e => { errors.push(e); }, @@ -54,7 +35,7 @@ export function parse(source: string): SFCParseResult { switch (node.tag) { case 'template': const templateBlock = (descriptor.template = createBlock(node, source) as SFCTemplateBlock); - templateBlock.ast = node; + templateBlock.ast = compiler.createRoot(node.children, source); break; case 'script': const scriptBlock = createBlock(node, source) as SFCScriptBlock; diff --git a/packages/language-core/src/utils/transform.ts b/packages/language-core/src/utils/transform.ts index 9465908195..5bed52f50f 100644 --- a/packages/language-core/src/utils/transform.ts +++ b/packages/language-core/src/utils/transform.ts @@ -2,21 +2,19 @@ import { isGloballyWhitelisted } from '@vue/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { VueCompilerOptions } from '../types'; -export function walkInterpolationFragment( +export function* eachInterpolationSegment( ts: typeof import('typescript/lib/tsserverlibrary'), code: string, ast: ts.SourceFile, - cb: (fragment: string, offset: number | undefined, isJustForErrorMapping?: boolean) => void, localVars: Map, identifiers: Set, vueOptions: VueCompilerOptions, -) { - - let ctxVars: { + ctxVars: { text: string, isShorthand: boolean, offset: number, - }[] = []; + }[] = [] +): Generator<[fragment: string, offset: number | undefined, isJustForErrorMapping?: boolean]> { const varCb = (id: ts.Identifier, isShorthand: boolean) => { if ( @@ -44,44 +42,44 @@ export function walkInterpolationFragment( if (ctxVars.length) { if (ctxVars[0].isShorthand) { - cb(code.substring(0, ctxVars[0].offset + ctxVars[0].text.length), 0); - cb(': ', undefined); + yield [code.substring(0, ctxVars[0].offset + ctxVars[0].text.length), 0]; + yield [': ', undefined]; } else { - cb(code.substring(0, ctxVars[0].offset), 0); + yield [code.substring(0, ctxVars[0].offset), 0]; } for (let i = 0; i < ctxVars.length - 1; i++) { // fix https://github.com/vuejs/language-tools/issues/1205 // fix https://github.com/vuejs/language-tools/issues/1264 - cb('', ctxVars[i + 1].offset, true); + yield ['', ctxVars[i + 1].offset, true]; if (vueOptions.experimentalUseElementAccessInTemplate) { const varStart = ctxVars[i].offset; const varEnd = ctxVars[i].offset + ctxVars[i].text.length; - cb('__VLS_ctx[', undefined); - cb('', varStart, true); - cb("'", undefined); - cb(code.substring(varStart, varEnd), varStart); - cb("'", undefined); - cb('', varEnd, true); - cb(']', undefined); + yield ['__VLS_ctx[', undefined]; + yield ['', varStart, true]; + yield ["'", undefined]; + yield [code.substring(varStart, varEnd), varStart]; + yield ["'", undefined]; + yield ['', varEnd, true]; + yield [']', undefined]; if (ctxVars[i + 1].isShorthand) { - cb(code.substring(varEnd, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), varEnd); - cb(': ', undefined); + yield [code.substring(varEnd, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), varEnd]; + yield [': ', undefined]; } else { - cb(code.substring(varEnd, ctxVars[i + 1].offset), varEnd); + yield [code.substring(varEnd, ctxVars[i + 1].offset), varEnd]; } } else { - cb('__VLS_ctx.', undefined); + yield ['__VLS_ctx.', undefined]; if (ctxVars[i + 1].isShorthand) { - cb(code.substring(ctxVars[i].offset, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), ctxVars[i].offset); - cb(': ', undefined); + yield [code.substring(ctxVars[i].offset, ctxVars[i + 1].offset + ctxVars[i + 1].text.length), ctxVars[i].offset]; + yield [': ', undefined]; } else { - cb(code.substring(ctxVars[i].offset, ctxVars[i + 1].offset), ctxVars[i].offset); + yield [code.substring(ctxVars[i].offset, ctxVars[i + 1].offset), ctxVars[i].offset]; } } } @@ -89,26 +87,24 @@ export function walkInterpolationFragment( if (vueOptions.experimentalUseElementAccessInTemplate) { const varStart = ctxVars[ctxVars.length - 1].offset; const varEnd = ctxVars[ctxVars.length - 1].offset + ctxVars[ctxVars.length - 1].text.length; - cb('__VLS_ctx[', undefined); - cb('', varStart, true); - cb("'", undefined); - cb(code.substring(varStart, varEnd), varStart); - cb("'", undefined); - cb('', varEnd, true); - cb(']', undefined); - cb(code.substring(varEnd), varEnd); + yield ['__VLS_ctx[', undefined]; + yield ['', varStart, true]; + yield ["'", undefined]; + yield [code.substring(varStart, varEnd), varStart]; + yield ["'", undefined]; + yield ['', varEnd, true]; + yield [']', undefined]; + yield [code.substring(varEnd), varEnd]; } else { - cb('', ctxVars[ctxVars.length - 1].offset, true); - cb('__VLS_ctx.', undefined); - cb(code.substring(ctxVars[ctxVars.length - 1].offset), ctxVars[ctxVars.length - 1].offset); + yield ['', ctxVars[ctxVars.length - 1].offset, true]; + yield ['__VLS_ctx.', undefined]; + yield [code.substring(ctxVars[ctxVars.length - 1].offset), ctxVars[ctxVars.length - 1].offset]; } } else { - cb(code, 0); + yield [code, 0]; } - - return ctxVars; } function walkIdentifiers( diff --git a/packages/language-core/src/utils/ts.ts b/packages/language-core/src/utils/ts.ts index 64b18fa817..6f751fb495 100644 --- a/packages/language-core/src/utils/ts.ts +++ b/packages/language-core/src/utils/ts.ts @@ -178,11 +178,6 @@ function getPartialVueCompilerOptions( result.plugins = plugins; } - if (rawOptions.hooks) { - result.hooks = rawOptions.hooks - .map(resolvePath) - .filter((hook): hook is NonNullable => !!hook); - } if (rawOptions.experimentalAdditionalLanguageModules) { result.experimentalAdditionalLanguageModules = rawOptions.experimentalAdditionalLanguageModules .map(resolvePath) @@ -268,7 +263,6 @@ export function resolveVueCompilerOptions(vueOptions: Partial[], @@ -50,7 +48,10 @@ export function computedFiles( for (const { file, snapshot, mappings, codegenStacks } of remain) { embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -66,7 +67,10 @@ export function computedFiles( const { file, snapshot, mappings, codegenStacks } = remain[i]; if (!file.parentFileName) { embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -78,7 +82,10 @@ export function computedFiles( const parent = findParentStructure(file.parentFileName, embeddedFiles); if (parent) { parent.embeddedFiles.push({ - ...file, + id: file.fileName, + languageId: resolveCommonLanguageId(file.fileName), + typescript: file.typescript, + linkedNavigationMappings: file.linkedNavigationMappings, snapshot, mappings, codegenStacks, @@ -89,12 +96,12 @@ export function computedFiles( } } } - function findParentStructure(fileName: string, current: VirtualFile[]): VirtualFile | undefined { + function findParentStructure(id: string, current: VirtualFile[]): VirtualFile | undefined { for (const child of current) { - if (child.fileName === fileName) { + if (child.id === id) { return child; } - let parent = findParentStructure(fileName, child.embeddedFiles); + let parent = findParentStructure(id, child.embeddedFiles); if (parent) { return parent; } @@ -128,7 +135,7 @@ function compiledPluginFiles( for (const embeddedFileName of embeddedFileNames) { if (!embeddedFiles[embeddedFileName]) { embeddedFiles[embeddedFileName] = computed(() => { - const [content, stacks] = codegenStack ? muggle.track([]) : [[], []]; + const [content, stacks] = codegenStack ? track([]) : [[], []]; const file = new VueEmbeddedFile(embeddedFileName, content, stacks); for (const plugin of plugins) { if (!plugin.resolveEmbeddedFile) { @@ -174,27 +181,37 @@ function compiledPluginFiles( return computed(() => { return files().map(_file => { + const { file, snapshot } = _file(); const mappings = buildMappings(file.content); + let lastValidMapping: typeof mappings[number]; + for (const mapping of mappings) { if (mapping.source !== undefined) { const block = nameToBlock()[mapping.source]; if (block) { - mapping.sourceRange = [ - mapping.sourceRange[0] + block.startTagEnd, - mapping.sourceRange[1] + block.startTagEnd, - ]; + mapping.sourceOffsets = mapping.sourceOffsets.map(offset => offset + block.startTagEnd); } else { // ignore } mapping.source = undefined; } + if (mapping.data.__combineLastMappping) { + lastValidMapping!.sourceOffsets.push(...mapping.sourceOffsets); + lastValidMapping!.generatedOffsets.push(...mapping.generatedOffsets); + lastValidMapping!.lengths.push(...mapping.lengths); + continue; + } + else { + lastValidMapping = mapping; + } } + return { file, snapshot, - mappings, + mappings: mappings.filter(mapping => !mapping.data.__combineLastMappping), codegenStacks: buildStacks(file.content, file.contentStacks), }; }); diff --git a/packages/language-core/src/virtualFile/computedMappings.ts b/packages/language-core/src/virtualFile/computedMappings.ts index 490c063841..a4661aec48 100644 --- a/packages/language-core/src/virtualFile/computedMappings.ts +++ b/packages/language-core/src/virtualFile/computedMappings.ts @@ -1,16 +1,15 @@ -import { FileRangeCapabilities } from '@volar/language-core'; -import { Mapping, Segment } from '@volar/source-map'; -import * as muggle from 'muggle-string'; -import type * as ts from 'typescript/lib/tsserverlibrary'; -import { Sfc } from '../types'; +import { Mapping, Segment, replaceSourceRange } from '@volar/language-core'; import { computed } from 'computeds'; +import type * as ts from 'typescript/lib/tsserverlibrary'; +import { enableAllFeatures } from '../generators/utils'; +import { Sfc, VueCodeInformation } from '../types'; export function computedMappings( snapshot: () => ts.IScriptSnapshot, sfc: Sfc ) { return computed(() => { - const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, FileRangeCapabilities.full]]; + const str: Segment[] = [[snapshot().getText(0, snapshot().getLength()), undefined, 0, enableAllFeatures({})]]; for (const block of [ sfc.script, sfc.scriptSetup, @@ -19,26 +18,20 @@ export function computedMappings( ...sfc.customBlocks, ]) { if (block) { - muggle.replaceSourceRange( - str, undefined, block.startTagEnd, block.endTagStart, - [ - block.content, - undefined, - block.startTagEnd, - {}, - ], - ); + replaceSourceRange(str, undefined, block.startTagEnd, block.endTagStart, '\n\n'); } } - return str.map>((m) => { - const text = m[0]; - const start = m[2] as number; - const end = start + text.length; - return { - sourceRange: [start, end], - generatedRange: [start, end], - data: m[3] as FileRangeCapabilities, - }; - }); + return str + .filter(s => typeof s !== 'string') + .map>((m) => { + const text = m[0]; + const start = m[2] as number; + return { + sourceOffsets: [start], + generatedOffsets: [start], + lengths: [text.length], + data: m[3] as VueCodeInformation, + }; + }); }); } diff --git a/packages/language-core/src/virtualFile/embeddedFile.ts b/packages/language-core/src/virtualFile/embeddedFile.ts index 001ec2e871..f5e9fa7b27 100644 --- a/packages/language-core/src/virtualFile/embeddedFile.ts +++ b/packages/language-core/src/virtualFile/embeddedFile.ts @@ -1,16 +1,15 @@ -import { FileCapabilities, FileKind, FileRangeCapabilities, MirrorBehaviorCapabilities } from '@volar/language-core'; -import { Mapping, Segment, StackNode } from '@volar/source-map'; +import { Mapping, StackNode, VirtualFile } from '@volar/language-core'; +import { Code } from '../types'; export class VueEmbeddedFile { public parentFileName?: string; - public kind = FileKind.TextFile; - public capabilities: FileCapabilities = {}; - public mirrorBehaviorMappings: Mapping<[MirrorBehaviorCapabilities, MirrorBehaviorCapabilities]>[] = []; + public typescript: VirtualFile['typescript']; + public linkedNavigationMappings: Mapping[] = []; constructor( public fileName: string, - public content: Segment[], + public content: Code[], public contentStacks: StackNode[], ) { } } diff --git a/packages/language-core/src/virtualFile/vueFile.ts b/packages/language-core/src/virtualFile/vueFile.ts index c0c0ab48fb..b60c944ade 100644 --- a/packages/language-core/src/virtualFile/vueFile.ts +++ b/packages/language-core/src/virtualFile/vueFile.ts @@ -1,5 +1,4 @@ -import { FileCapabilities, FileKind, VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; -import { Stack } from '@volar/source-map'; +import { Stack, VirtualFile, forEachEmbeddedFile } from '@volar/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; import { VueCompilerOptions, VueLanguagePlugin } from '../types'; import { computedFiles } from './computedFiles'; @@ -18,27 +17,23 @@ export class VueFile implements VirtualFile { // computeds - getVueSfc = computedVueSfc(this.plugins, this.fileName, () => this._snapshot()); - sfc = computedSfc(this.ts, this.plugins, this.fileName, () => this._snapshot(), this.getVueSfc); + getVueSfc = computedVueSfc(this.plugins, this.id, () => this._snapshot()); + sfc = computedSfc(this.ts, this.plugins, this.id, () => this._snapshot(), this.getVueSfc); getMappings = computedMappings(() => this._snapshot(), this.sfc); - getEmbeddedFiles = computedFiles(this.plugins, this.fileName, this.sfc, this.codegenStack); + getEmbeddedFiles = computedFiles(this.plugins, this.id, this.sfc, this.codegenStack); // others - capabilities = FileCapabilities.full; - kind = FileKind.TextFile; codegenStacks: Stack[] = []; get embeddedFiles() { return this.getEmbeddedFiles(); } - get mainScriptName() { - let res: string = ''; - forEachEmbeddedFile(this, file => { - if (file.kind === FileKind.TypeScriptHostFile && file.fileName.replace(this.fileName, '').match(jsxReg)) { - res = file.fileName; + get mainTsFile() { + for (const file of forEachEmbeddedFile(this)) { + if (file.typescript && file.id.substring(this.id.length).match(jsxReg)) { + return file; } - }); - return res; + } } get snapshot() { return this._snapshot(); @@ -48,7 +43,8 @@ export class VueFile implements VirtualFile { } constructor( - public fileName: string, + public id: string, + public languageId: string, public initSnapshot: ts.IScriptSnapshot, public vueCompilerOptions: VueCompilerOptions, public plugins: ReturnType[], diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index 7de2260710..5bd6c6c4a9 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -17,7 +17,7 @@ "@vue/language-core": "1.8.26" }, "dependencies": { - "@volar/source-map": "~1.11.1", - "volar-service-pug": "0.0.17" + "@volar/source-map": "2.0.0-alpha.0", + "volar-service-pug": "0.0.18" } } diff --git a/packages/language-plugin-pug/src/index.ts b/packages/language-plugin-pug/src/index.ts index d6e078a922..a96af4647c 100644 --- a/packages/language-plugin-pug/src/index.ts +++ b/packages/language-plugin-pug/src/index.ts @@ -38,7 +38,7 @@ const plugin: VueLanguagePlugin = ({ modules }) => { get(target, prop) { if (prop === 'offset') { const htmlOffset = target.offset; - for (const mapped of map.toSourceOffsets(htmlOffset)) { + for (const mapped of map.getSourceOffsets(htmlOffset)) { return mapped[0]; } return -1; diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 7e1b8aa360..27ba49da1d 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -16,9 +16,9 @@ "directory": "packages/language-server" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/language-server": "~1.11.1", - "@volar/typescript": "~1.11.1", + "@volar/language-core": "2.0.0-alpha.0", + "@volar/language-server": "2.0.0-alpha.0", + "@volar/typescript": "2.0.0-alpha.0", "@vue/language-core": "1.8.26", "@vue/language-service": "1.8.26", "vscode-languageserver-protocol": "^3.17.5", diff --git a/packages/language-server/src/languageServerPlugin.ts b/packages/language-server/src/languageServerPlugin.ts index 3cba8789e4..6aa8e8dd71 100644 --- a/packages/language-server/src/languageServerPlugin.ts +++ b/packages/language-server/src/languageServerPlugin.ts @@ -1,30 +1,30 @@ -import * as embedded from '@volar/language-core'; -import { LanguageServerPlugin, Connection } from '@volar/language-server'; -import * as vue from '@vue/language-service'; +import { Connection, ServerProject, TypeScriptServerPlugin } from '@volar/language-server'; +import { createSys } from '@volar/typescript'; import * as vue2 from '@vue/language-core'; +import { VueCompilerOptions } from '@vue/language-core'; import * as nameCasing from '@vue/language-service'; -import { DetectNameCasingRequest, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest, GetComponentMeta, GetDragAndDragImportEditsRequest } from './protocol'; -import { VueServerInitializationOptions } from './types'; +import * as vue from '@vue/language-service'; import type * as ts from 'typescript/lib/tsserverlibrary'; import * as componentMeta from 'vue-component-meta/out/base'; -import { VueCompilerOptions } from '@vue/language-core'; -import { createSys } from '@volar/typescript'; +import { DetectNameCasingRequest, GetComponentMeta, GetConvertAttrCasingEditsRequest, GetConvertTagCasingEditsRequest, ParseSFCRequest } from './protocol'; +import { VueServerInitializationOptions } from './types'; export function createServerPlugin(connection: Connection) { - const plugin: LanguageServerPlugin = (initOptions: VueServerInitializationOptions, modules): ReturnType => { + const plugin: TypeScriptServerPlugin = ({ initializationOptions, modules }): ReturnType => { if (!modules.typescript) { console.warn('No typescript found, vue-language-server will not work.'); return {}; } + const options: VueServerInitializationOptions = initializationOptions; const ts = modules.typescript; const vueFileExtensions: string[] = ['vue']; - const hostToVueOptions = new WeakMap(); + const envToVueOptions = new WeakMap(); - if (initOptions.additionalExtensions) { - for (const additionalExtension of initOptions.additionalExtensions) { + if (options.additionalExtensions) { + for (const additionalExtension of options.additionalExtensions) { vueFileExtensions.push(additionalExtension); } } @@ -32,40 +32,35 @@ export function createServerPlugin(connection: Connection) { return { extraFileExtensions: vueFileExtensions.map(ext => ({ extension: ext, isMixedContent: true, scriptKind: ts.ScriptKind.Deferred })), watchFileExtensions: ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts', 'jsx', 'tsx', 'json', ...vueFileExtensions], - async resolveConfig(config, ctx) { + async resolveConfig(config, env, info) { const vueOptions = await getVueCompilerOptions(); - if (ctx) { - hostToVueOptions.set(ctx.host, vue.resolveVueCompilerOptions(vueOptions)); + if (env) { + envToVueOptions.set(env, vue.resolveVueCompilerOptions(vueOptions)); } - return vue.resolveConfig( - ts, - config, - ctx?.host.getCompilationSettings() ?? {}, - vueOptions, - initOptions.codegenStack, - ); + config.languages = vue.resolveLanguages(ts, config.languages ?? {}, info?.parsedCommandLine.options ?? {}, vueOptions, options.codegenStack); + config.services = vue.resolveServices(ts, config.services ?? {}, vueOptions); - async function getVueCompilerOptions() { + return config; - const ts = modules.typescript; + async function getVueCompilerOptions() { let vueOptions: Partial = {}; - if (ts && ctx) { - const sys = createSys(ts, ctx.env); + if (env && info) { + const sys = createSys(ts, env, env.uriToFileName(env.workspaceFolder.uri.toString())); let sysVersion: number | undefined; let newSysVersion = await sys.sync(); while (sysVersion !== newSysVersion) { sysVersion = newSysVersion; - if (typeof ctx?.project.tsConfig === 'string' && ts) { - vueOptions = vue2.createParsedCommandLine(ts, sys, ctx.project.tsConfig).vueOptions; + if (info.configFileName) { + vueOptions = vue2.createParsedCommandLine(ts, sys, info.configFileName).vueOptions; } - else if (typeof ctx?.project.tsConfig === 'object' && ts) { - vueOptions = vue2.createParsedCommandLineByJson(ts, sys, ctx.host.rootPath, ctx.project.tsConfig).vueOptions; + else { + vueOptions = vue2.createParsedCommandLineByJson(ts, sys, env.uriToFileName(env.workspaceFolder.uri.toString()), info.parsedCommandLine.options).vueOptions; } newSysVersion = await sys.sync(); } @@ -80,7 +75,7 @@ export function createServerPlugin(connection: Connection) { return vueOptions; } }, - onInitialized(getService, env) { + onInitialized(projects) { connection.onRequest(ParseSFCRequest.type, params => { return vue2.parse(params); @@ -89,57 +84,52 @@ export function createServerPlugin(connection: Connection) { connection.onRequest(DetectNameCasingRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return nameCasing.detect(ts, languageService.context, params.textDocument.uri, hostToVueOptions.get(languageService.context.rawHost)!); + return nameCasing.detect(ts, languageService.context, params.textDocument.uri, envToVueOptions.get(languageService.context.env)!); } }); connection.onRequest(GetConvertTagCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - return nameCasing.convertTagName(ts, languageService.context, params.textDocument.uri, params.casing, hostToVueOptions.get(languageService.context.rawHost)!); - } - }); - - connection.onRequest(GetDragAndDragImportEditsRequest.type, async params => { - const languageService = await getService(params.uri); - if (languageService) { - return nameCasing.getDragImportEdits(ts, languageService.context, params.uri, params.importUri, params.casing); + return nameCasing.convertTagName(ts, languageService.context, params.textDocument.uri, params.casing, envToVueOptions.get(languageService.context.env)!); } }); connection.onRequest(GetConvertAttrCasingEditsRequest.type, async params => { const languageService = await getService(params.textDocument.uri); if (languageService) { - const vueOptions = hostToVueOptions.get(languageService.context.host); + const vueOptions = envToVueOptions.get(languageService.context.env); if (vueOptions) { - return nameCasing.convertAttrName(ts, languageService.context, params.textDocument.uri, params.casing, hostToVueOptions.get(languageService.context.rawHost)!); + return nameCasing.convertAttrName(ts, languageService.context, params.textDocument.uri, params.casing, envToVueOptions.get(languageService.context.env)!); } } }); - const checkers = new WeakMap(); + const checkers = new WeakMap(); connection.onRequest(GetComponentMeta.type, async params => { - const languageService = await getService(params.uri); - if (!languageService) - return; - - const host = languageService.context.rawHost; + const project = await projects.getProject(params.uri); + const langaugeService = project.getLanguageService(); - let checker = checkers.get(host); + let checker = checkers.get(project); if (!checker) { checker = componentMeta.baseCreate( ts, - host, - hostToVueOptions.get(host)!, + langaugeService.context.language.typescript!.configFileName, + langaugeService.context.language.typescript!.projectHost, + envToVueOptions.get(langaugeService.context.env)!, {}, - host.rootPath + '/tsconfig.json.global.vue', + langaugeService.context.language.typescript!.languageServiceHost.getCurrentDirectory() + '/tsconfig.json.global.vue', ); - checkers.set(host, checker); + checkers.set(project, checker); } - return checker.getComponentMeta(env.uriToFileName(params.uri)); + return checker?.getComponentMeta(langaugeService.context.env.uriToFileName(params.uri)); }); + + async function getService(uri: string) { + return (await projects.getProject(uri)).getLanguageService(); + } }, }; }; diff --git a/packages/language-server/src/nodeServer.ts b/packages/language-server/src/nodeServer.ts index f1b9409fdd..25654597a9 100644 --- a/packages/language-server/src/nodeServer.ts +++ b/packages/language-server/src/nodeServer.ts @@ -1,7 +1,7 @@ -import { createConnection, startLanguageServer } from '@volar/language-server/node'; +import { createConnection, startTypeScriptServer } from '@volar/language-server/node'; import { createServerPlugin } from './languageServerPlugin'; const connection = createConnection(); const plugin = createServerPlugin(connection); -startLanguageServer(connection, plugin); +startTypeScriptServer(connection, plugin); diff --git a/packages/language-server/src/protocol.ts b/packages/language-server/src/protocol.ts index e7638c67f8..a36d7c0752 100644 --- a/packages/language-server/src/protocol.ts +++ b/packages/language-server/src/protocol.ts @@ -31,21 +31,6 @@ export namespace GetConvertTagCasingEditsRequest { export const type = new vscode.RequestType('vue/convertTagNameCasing'); } -export namespace GetDragAndDragImportEditsRequest { - export type ParamsType = { - uri: string, - importUri: string, - casing: TagNameCasing, - }; - export type ResponseType = { - insertText: string; - insertTextFormat: vscode.InsertTextFormat; - additionalEdits: vscode.TextEdit[]; - } | null | undefined; - export type ErrorType = never; - export const type = new vscode.RequestType('vue/dragImportEdits'); -} - export namespace GetConvertAttrCasingEditsRequest { export type ParamsType = { textDocument: vscode.TextDocumentIdentifier, diff --git a/packages/language-server/src/webServer.ts b/packages/language-server/src/webServer.ts index d64a772dc3..35f1b53e35 100644 --- a/packages/language-server/src/webServer.ts +++ b/packages/language-server/src/webServer.ts @@ -1,7 +1,7 @@ -import { createConnection, startLanguageServer } from '@volar/language-server/browser'; +import { createConnection, startTypeScriptServer } from '@volar/language-server/browser'; import { createServerPlugin } from './languageServerPlugin'; const connection = createConnection(); const plugin = createServerPlugin(connection); -startLanguageServer(connection, plugin); +startTypeScriptServer(connection, plugin); diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 2622ae73cd..c8b631e77d 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -17,29 +17,29 @@ "update-html-data": "node ./scripts/update-html-data.js" }, "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/language-service": "~1.11.1", - "@volar/typescript": "~1.11.1", - "@vue/compiler-dom": "^3.3.0", + "@volar/language-core": "2.0.0-alpha.0", + "@volar/language-service": "2.0.0-alpha.0", + "@volar/typescript": "2.0.0-alpha.0", + "@vue/compiler-dom": "^3.4.0", "@vue/language-core": "1.8.26", "@vue/shared": "^3.3.0", "computeds": "^0.0.1", "path-browserify": "^1.0.1", - "volar-service-css": "0.0.17", - "volar-service-emmet": "0.0.17", - "volar-service-html": "0.0.17", - "volar-service-json": "0.0.17", - "volar-service-pug": "0.0.17", - "volar-service-pug-beautify": "0.0.17", - "volar-service-typescript": "0.0.17", - "volar-service-typescript-twoslash-queries": "0.0.17", + "volar-service-css": "0.0.18", + "volar-service-emmet": "0.0.18", + "volar-service-html": "0.0.18", + "volar-service-json": "0.0.18", + "volar-service-pug": "0.0.18", + "volar-service-pug-beautify": "0.0.18", + "volar-service-typescript": "0.0.18", + "volar-service-typescript-twoslash-queries": "0.0.18", "vscode-html-languageservice": "^5.1.0", "vscode-languageserver-textdocument": "^1.0.11" }, "devDependencies": { "@types/node": "latest", "@types/path-browserify": "latest", - "@volar/kit": "~1.11.1", + "@volar/kit": "2.0.0-alpha.0", "vscode-languageserver-protocol": "^3.17.5", "vscode-uri": "^3.0.8" } diff --git a/packages/language-service/src/helpers.ts b/packages/language-service/src/helpers.ts index 34cfe02890..8f845dd533 100644 --- a/packages/language-service/src/helpers.ts +++ b/packages/language-service/src/helpers.ts @@ -1,15 +1,16 @@ -import * as vue from '@vue/language-core'; -import type { CompilerDOM } from '@vue/language-core'; import * as embedded from '@volar/language-core'; -import { computed } from 'computeds'; +import type { CompilerDOM } from '@vue/language-core'; +import * as vue from '@vue/language-core'; import { sharedTypes } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; - +import { computed } from 'computeds'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import type { ServiceEnvironment } from './types'; export function getPropsByTag( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, tag: string, vueCompilerOptions: vue.VueCompilerOptions, @@ -17,7 +18,7 @@ export function getPropsByTag( ) { const checker = tsLs.getProgram()!.getTypeChecker(); - const components = getVariableType(ts, tsLs, sourceFile, '__VLS_components'); + const components = getVariableType(ts, tsLs, env, sourceFile, '__VLS_components'); if (!components) return []; @@ -83,13 +84,14 @@ export function getPropsByTag( export function getEventsOfTag( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, tag: string, vueCompilerOptions: vue.VueCompilerOptions, ) { const checker = tsLs.getProgram()!.getTypeChecker(); - const components = getVariableType(ts, tsLs, sourceFile, '__VLS_components'); + const components = getVariableType(ts, tsLs, env, sourceFile, '__VLS_components'); if (!components) return []; @@ -149,9 +151,10 @@ export function getEventsOfTag( export function getTemplateCtx( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, ) { - return getVariableType(ts, tsLs, sourceFile, '__VLS_ctx') + return getVariableType(ts, tsLs, env, sourceFile, '__VLS_ctx') ?.type ?.getProperties() .map(c => c.name); @@ -160,10 +163,11 @@ export function getTemplateCtx( export function getComponentNames( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, vueCompilerOptions: vue.VueCompilerOptions, ) { - return getVariableType(ts, tsLs, sourceFile, '__VLS_components') + return getVariableType(ts, tsLs, env, sourceFile, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) @@ -207,6 +211,7 @@ export function getElementAttrs( function getVariableType( ts: typeof import('typescript/lib/tsserverlibrary'), tsLs: ts.LanguageService, + env: ServiceEnvironment, sourceFile: embedded.VirtualFile, name: string, ) { @@ -215,16 +220,11 @@ function getVariableType( return; } - let file: embedded.VirtualFile | undefined; - let tsSourceFile: ts.SourceFile | undefined; + const file = sourceFile.mainTsFile; - embedded.forEachEmbeddedFile(sourceFile, embedded => { - if (embedded.fileName === sourceFile.mainScriptName) { - file = embedded; - } - }); + let tsSourceFile: ts.SourceFile | undefined; - if (file && (tsSourceFile = tsLs.getProgram()?.getSourceFile(file.fileName))) { + if (file && (tsSourceFile = tsLs.getProgram()?.getSourceFile(env.uriToFileName(file.id)))) { const node = searchVariableDeclarationNode(ts, tsSourceFile, name); const checker = tsLs.getProgram()?.getTypeChecker(); @@ -281,7 +281,7 @@ export function getTemplateTagsAndAttrs(sourceFile: embedded.VirtualFile): Tags const ast = sourceFile.sfc.template?.ast; const tags: Tags = new Map(); if (ast) { - vue.walkElementNodes(ast, node => { + for (const node of vue.eachElementNode(ast)) { if (!tags.has(node.tag)) { tags.set(node.tag, { offsets: [], attrs: new Map() }); @@ -323,7 +323,7 @@ export function getTemplateTagsAndAttrs(sourceFile: embedded.VirtualFile): Tags tag.attrs.get(name)!.offsets.push(offset); } } - }); + } } return tags; }); diff --git a/packages/language-service/src/ideFeatures/dragImport.ts b/packages/language-service/src/ideFeatures/dragImport.ts deleted file mode 100644 index 1c4a9f1050..0000000000 --- a/packages/language-service/src/ideFeatures/dragImport.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ServiceContext } from '@volar/language-service'; -import { VueFile } from '@vue/language-core'; -import { camelize, capitalize, hyphenate } from '@vue/shared'; -import * as path from 'path-browserify'; -import type * as vscode from 'vscode-languageserver-protocol'; -import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file'; -import { TagNameCasing } from '../types'; - -export function getDragImportEdits( - ts: typeof import('typescript/lib/tsserverlibrary'), - ctx: ServiceContext, - uri: string, - importUri: string, - casing: TagNameCasing -): { - insertText: string; - insertTextFormat: vscode.InsertTextFormat; - additionalEdits: vscode.TextEdit[]; -} | undefined { - - let baseName = importUri.substring(importUri.lastIndexOf('/') + 1); - baseName = baseName.substring(0, baseName.lastIndexOf('.')); - - const newName = capitalize(camelize(baseName)); - const document = ctx!.getTextDocument(uri)!; - const [vueFile] = ctx!.documents.getVirtualFileByUri(document.uri) as [VueFile, any]; - const { sfc } = vueFile; - const script = sfc.scriptSetup ?? sfc.script; - - if (!sfc.template || !script) - return; - - const lastImportNode = getLastImportNode(ts, script.ast); - const edits: vscode.TextEdit[] = [ - { - range: lastImportNode ? { - start: document.positionAt(script.startTagEnd + lastImportNode.end), - end: document.positionAt(script.startTagEnd + lastImportNode.end), - } : { - start: document.positionAt(script.startTagEnd), - end: document.positionAt(script.startTagEnd), - }, - newText: `\nimport ${newName} from './${path.relative(path.dirname(uri), importUri) || importUri.substring(importUri.lastIndexOf('/') + 1)}'`, - }, - ]; - - if (sfc.script) { - const edit = createAddComponentToOptionEdit(ts, sfc.script.ast, newName); - if (edit) { - edits.push({ - range: { - start: document.positionAt(sfc.script.startTagEnd + edit.range.start), - end: document.positionAt(sfc.script.startTagEnd + edit.range.end), - }, - newText: edit.newText, - }); - } - } - - return { - insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, - insertTextFormat: 2 satisfies typeof vscode.InsertTextFormat.Snippet, - additionalEdits: edits, - }; -} diff --git a/packages/language-service/src/ideFeatures/nameCasing.ts b/packages/language-service/src/ideFeatures/nameCasing.ts index d64264b907..fe75e59757 100644 --- a/packages/language-service/src/ideFeatures/nameCasing.ts +++ b/packages/language-service/src/ideFeatures/nameCasing.ts @@ -7,13 +7,13 @@ import { AttrNameCasing, TagNameCasing } from '../types'; export async function convertTagName( ts: typeof import('typescript/lib/tsserverlibrary'), - context: ServiceContext, + context: ServiceContext, uri: string, casing: TagNameCasing, vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -21,11 +21,11 @@ export async function convertTagName( if (!desc.template) return; - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); for (const [tagName, { offsets }] of tags) { @@ -56,7 +56,7 @@ export async function convertAttrName( vueCompilerOptions: VueCompilerOptions, ) { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) return; @@ -64,17 +64,17 @@ export async function convertAttrName( if (!desc.template) return; - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); const template = desc.template; - const document = context.documents.getDocumentByFileName(rootFile.snapshot, rootFile.fileName); + const document = context.documents.get(rootFile.id, rootFile.languageId, rootFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = getComponentNames(ts, languageService, rootFile, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, rootFile, vueCompilerOptions); const tags = getTemplateTagsAndAttrs(rootFile); for (const [tagName, { attrs }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); if (componentName) { - const props = getPropsByTag(ts, languageService, rootFile, componentName, vueCompilerOptions); + const props = getPropsByTag(ts, languageService, context.env, rootFile, componentName, vueCompilerOptions); for (const [attrName, { offsets }] of attrs) { const propName = props.find(prop => prop === attrName || hyphenateAttr(prop) === attrName); if (propName) { @@ -128,7 +128,7 @@ export function detect( attr: AttrNameCasing[], } { - const rootFile = context.documents.getSourceByUri(uri)?.root; + const rootFile = context.language.files.getSourceFile(uri)?.virtualFile?.[0]; if (!(rootFile instanceof VueFile)) { return { tag: [], @@ -136,7 +136,7 @@ export function detect( }; } - const languageService = context.inject('typescript/languageService'); + const languageService = context.inject('typescript/languageService'); return { tag: getTagNameCase(rootFile), @@ -169,7 +169,7 @@ export function detect( } function getTagNameCase(file: VirtualFile): TagNameCasing[] { - const components = getComponentNames(ts, languageService, file, vueCompilerOptions); + const components = getComponentNames(ts, languageService, context.env, file, vueCompilerOptions); const tagNames = getTemplateTagsAndAttrs(file); const result: TagNameCasing[] = []; diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index 4626d9053b..75191f5bb7 100644 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -1,7 +1,6 @@ export * from '@volar/language-service'; export * from '@vue/language-core'; export * from './ideFeatures/nameCasing'; -export * from './ideFeatures/dragImport'; export * from './languageService'; export { TagNameCasing, AttrNameCasing } from './types'; export { Provide } from './plugins/vue'; diff --git a/packages/language-service/src/languageService.ts b/packages/language-service/src/languageService.ts index 5b5a92a1da..89cfb291e4 100644 --- a/packages/language-service/src/languageService.ts +++ b/packages/language-service/src/languageService.ts @@ -1,5 +1,5 @@ -import { Config, Service, ServiceContext } from '@volar/language-service'; -import { VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; +import { ServicePlugin } from '@volar/language-service'; +import { LanguagePlugin, VueFile, createLanguages, hyphenateTag, resolveVueCompilerOptions, scriptRanges } from '@vue/language-core'; import { capitalize } from '@vue/shared'; import type * as ts from 'typescript/lib/tsserverlibrary'; import type { Data } from 'volar-service-typescript/out/features/completions/basic'; @@ -9,265 +9,281 @@ import { getNameCasing } from './ideFeatures/nameCasing'; import { TagNameCasing, VueCompilerOptions } from './types'; // volar services -import * as CssService from 'volar-service-css'; -import * as EmmetService from 'volar-service-emmet'; -import * as HtmlService from 'volar-service-html'; -import * as JsonService from 'volar-service-json'; -import * as PugService from 'volar-service-pug'; -import * as PugFormatService from 'volar-service-pug-beautify'; -import * as TsService from 'volar-service-typescript'; -import * as TsTqService from 'volar-service-typescript-twoslash-queries'; +import { create as createCssService } from 'volar-service-css'; +import { create as createEmmetService } from 'volar-service-emmet'; +import { create as createHtmlService } from 'volar-service-html'; +import { create as createJsonService } from 'volar-service-json'; +import { create as createPugService } from 'volar-service-pug'; +import { create as createPugFormatService } from 'volar-service-pug-beautify'; +import { create as createTsService, Provide as TSProvide } from 'volar-service-typescript'; +import { create as createTsTqService } from 'volar-service-typescript-twoslash-queries'; // our services -import * as VueService from './plugins/vue'; -import * as AutoDotValueService from './plugins/vue-autoinsert-dotvalue'; -import * as AutoWrapParenthesesService from './plugins/vue-autoinsert-parentheses'; -import * as AutoAddSpaceService from './plugins/vue-autoinsert-space'; -import * as ReferencesCodeLensService from './plugins/vue-codelens-references'; -import * as DirectiveCommentsService from './plugins/vue-directive-comments'; -import * as ExtractComponentService from './plugins/vue-extract-file'; -import * as VueTemplateLanguageService from './plugins/vue-template'; -import * as ToggleVBindService from './plugins/vue-toggle-v-bind-codeaction'; -import * as VueTqService from './plugins/vue-twoslash-queries'; -import * as VisualizeHiddenCallbackParamService from './plugins/vue-visualize-hidden-callback-param'; +import { create as createVueService } from './plugins/vue'; +import { create as createDocumentDropService } from './plugins/vue-document-drop'; +import { create as createAutoDotValueService } from './plugins/vue-autoinsert-dotvalue'; +import { create as createAutoWrapParenthesesService } from './plugins/vue-autoinsert-parentheses'; +import { create as createAutoAddSpaceService } from './plugins/vue-autoinsert-space'; +import { create as createReferencesCodeLensService } from './plugins/vue-codelens-references'; +import { create as createDirectiveCommentsService } from './plugins/vue-directive-comments'; +import { create as createVueExtractFileService, createAddComponentToOptionEdit } from './plugins/vue-extract-file'; +import { create as createVueTemplateLanguageService } from './plugins/vue-template'; +import { create as createToggleVBindService } from './plugins/vue-toggle-v-bind-codeaction'; +import { create as createVueTqService } from './plugins/vue-twoslash-queries'; +import { create as createVisualizeHiddenCallbackParamService } from './plugins/vue-visualize-hidden-callback-param'; export interface Settings { - json?: Parameters[0]; + json?: Parameters[0]; } -export function resolveConfig( +export function resolveLanguages( ts: typeof import('typescript/lib/tsserverlibrary'), - config: Config, + languages: Record = {}, compilerOptions: ts.CompilerOptions = {}, - vueCompilerOptions: Partial = {}, + _vueCompilerOptions: Partial = {}, codegenStack: boolean = false, -) { - - const resolvedVueCompilerOptions = resolveVueCompilerOptions(vueCompilerOptions); - const vueLanguageModules = createLanguages(ts, compilerOptions, resolvedVueCompilerOptions, codegenStack); +): Record { - config.languages = Object.assign({}, vueLanguageModules, config.languages); - config.services = resolvePlugins(config.services, resolvedVueCompilerOptions); + const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); + const vueLanguageModules = createLanguages(ts, compilerOptions, vueCompilerOptions, codegenStack); - return config; + return { + ...languages, + ...vueLanguageModules.reduce((obj, module, i) => { + obj['vue_' + i] = module; + return obj; + }, {} as Record), + }; } -function resolvePlugins( - services: Config['services'], - vueCompilerOptions: VueCompilerOptions, +export function resolveServices( + ts: typeof import('typescript/lib/tsserverlibrary'), + services: Record = {}, + _vueCompilerOptions: Partial = {}, ) { - const originalTsPlugin: Service = services?.typescript ?? TsService.create(); + const vueCompilerOptions = resolveVueCompilerOptions(_vueCompilerOptions); + const tsService: ServicePlugin = services?.typescript ?? createTsService(ts); services ??= {}; - services.typescript = (ctx: ServiceContext | undefined, modules): ReturnType => { - - const base = typeof originalTsPlugin === 'function' ? originalTsPlugin(ctx, modules) : originalTsPlugin; - - if (!ctx || !modules?.typescript) - return base; - - const ts = modules.typescript; - - return { - ...base, - async provideCompletionItems(document, position, context, item) { - const result = await base.provideCompletionItems?.(document, position, context, item); - if (result) { - - // filter __VLS_ - result.items = result.items.filter(item => - item.label.indexOf('__VLS_') === -1 - && (!item.labelDetails?.description || item.labelDetails.description.indexOf('__VLS_') === -1) - ); - - // handle component auto-import patch - let casing: Awaited> | undefined; - - for (const [_, map] of ctx.documents.getMapsByVirtualFileUri(document.uri)) { - const virtualFile = ctx.documents.getSourceByUri(map.sourceFileDocument.uri)?.root; - if (virtualFile instanceof VueFile) { - const isAutoImport = !!map.toSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.autoImportOnly); - if (isAutoImport) { - const source = ctx.documents.getVirtualFileByUri(document.uri)[1]; - for (const item of result.items) { - item.data.__isComponentAutoImport = true; - } + services.typescript = { + ...tsService, + create(ctx) { + const base = tsService.create(ctx); + return { + ...base, + async provideCompletionItems(document, position, context, item) { + const result = await base.provideCompletionItems?.(document, position, context, item); + if (result) { + + // filter __VLS_ + result.items = result.items.filter(item => + item.label.indexOf('__VLS_') === -1 + && (!item.labelDetails?.description || item.labelDetails.description.indexOf('__VLS_') === -1) + ); + + // handle component auto-import patch + let casing: Awaited> | undefined; + + const [virtualFile, sourceFile] = ctx.language.files.getVirtualFile(document.uri); + + if (virtualFile && sourceFile) { + + for (const map of ctx.documents.getMaps(virtualFile)) { + + const sourceVirtualFile = ctx.language.files.getSourceFile(map.sourceFileDocument.uri)?.virtualFile?.[0]; + + if (sourceVirtualFile instanceof VueFile) { + + const isAutoImport = !!map.getSourcePosition(position, data => typeof data.completion === 'object' && !!data.completion.onlyImport); + if (isAutoImport) { - // fix #2458 - if (source) { - casing ??= await getNameCasing(ts, ctx, ctx.env.fileNameToUri(source.fileName), vueCompilerOptions); - if (casing.tag === TagNameCasing.Kebab) { for (const item of result.items) { - item.filterText = hyphenateTag(item.filterText ?? item.label); + item.data.__isComponentAutoImport = true; + } + + // fix #2458 + casing ??= await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); + + if (casing.tag === TagNameCasing.Kebab) { + for (const item of result.items) { + item.filterText = hyphenateTag(item.filterText ?? item.label); + } } } } } } } - } - return result; - }, - async resolveCompletionItem(item, token) { + return result; + }, + async resolveCompletionItem(item, token) { - item = await base.resolveCompletionItem?.(item, token) ?? item; + item = await base.resolveCompletionItem?.(item, token) ?? item; - const itemData = item.data as { uri?: string; } | undefined; + const itemData = item.data as { uri?: string; } | undefined; - let newName: string | undefined; + let newName: string | undefined; - if (itemData?.uri && item.additionalTextEdits) { - patchAdditionalTextEdits(itemData.uri, item.additionalTextEdits); - } + if (itemData?.uri && item.additionalTextEdits) { + patchAdditionalTextEdits(itemData.uri, item.additionalTextEdits); + } - for (const ext of vueCompilerOptions.extensions) { - const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue - if ( - itemData?.uri - && item.textEdit?.newText.endsWith(suffix) - && item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0 - && (await ctx.env.getConfiguration?.('vue.complete.normalizeComponentImportName') ?? true) - ) { - newName = item.textEdit.newText.slice(0, -suffix.length); - newName = newName[0].toUpperCase() + newName.substring(1); - if (newName === 'Index') { - const tsItem = (item.data as Data).originalItem; - if (tsItem.source) { - const dirs = tsItem.source.split('/'); - if (dirs.length >= 3) { - newName = dirs[dirs.length - 2]; - newName = newName[0].toUpperCase() + newName.substring(1); + for (const ext of vueCompilerOptions.extensions) { + const suffix = capitalize(ext.substring('.'.length)); // .vue -> Vue + if ( + itemData?.uri + && item.textEdit?.newText.endsWith(suffix) + && item.additionalTextEdits?.length === 1 && item.additionalTextEdits[0].newText.indexOf('import ' + item.textEdit.newText + ' from ') >= 0 + && (await ctx.env.getConfiguration?.('vue.complete.normalizeComponentImportName') ?? true) + ) { + newName = item.textEdit.newText.slice(0, -suffix.length); + newName = newName[0].toUpperCase() + newName.substring(1); + if (newName === 'Index') { + const tsItem = (item.data as Data).originalItem; + if (tsItem.source) { + const dirs = tsItem.source.split('/'); + if (dirs.length >= 3) { + newName = dirs[dirs.length - 2]; + newName = newName[0].toUpperCase() + newName.substring(1); + } } } - } - item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace( - 'import ' + item.textEdit.newText + ' from ', - 'import ' + newName + ' from ', - ); - item.textEdit.newText = newName; - const source = ctx.documents.getVirtualFileByUri(itemData.uri)[1]; - if (source) { - const casing = await getNameCasing(ts, ctx, ctx.env.fileNameToUri(source.fileName), vueCompilerOptions); - if (casing.tag === TagNameCasing.Kebab) { - item.textEdit.newText = hyphenateTag(item.textEdit.newText); + item.additionalTextEdits[0].newText = item.additionalTextEdits[0].newText.replace( + 'import ' + item.textEdit.newText + ' from ', + 'import ' + newName + ' from ', + ); + item.textEdit.newText = newName; + const [_, sourceFile] = ctx.language.files.getVirtualFile(itemData.uri); + if (sourceFile) { + const casing = await getNameCasing(ts, ctx, sourceFile.id, vueCompilerOptions); + if (casing.tag === TagNameCasing.Kebab) { + item.textEdit.newText = hyphenateTag(item.textEdit.newText); + } } } + else if (item.textEdit?.newText && new RegExp(`import \\w*${suffix}\\$1 from [\\S\\s]*`).test(item.textEdit.newText)) { + // https://github.com/vuejs/language-tools/issues/2286 + item.textEdit.newText = item.textEdit.newText.replace(`${suffix}$1`, '$1'); + } } - else if (item.textEdit?.newText && new RegExp(`import \\w*${suffix}\\$1 from [\\S\\s]*`).test(item.textEdit.newText)) { - // https://github.com/vuejs/language-tools/issues/2286 - item.textEdit.newText = item.textEdit.newText.replace(`${suffix}$1`, '$1'); - } - } - const data: Data = item.data; - if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { - const fileName = ctx.env.uriToFileName(itemData.uri); - const langaugeService = ctx.inject('typescript/languageService'); - const [virtualFile] = ctx.virtualFiles.getVirtualFile(fileName); - const ast = langaugeService.getProgram()?.getSourceFile(fileName); - const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined; - if (virtualFile && ast && exportDefault) { - const componentName = newName ?? item.textEdit.newText; - const optionEdit = ExtractComponentService.createAddComponentToOptionEdit(ts, ast, componentName); - if (optionEdit) { - const textDoc = ctx.documents.getDocumentByFileName(virtualFile.snapshot, virtualFile.fileName); - item.additionalTextEdits.push({ - range: { - start: textDoc.positionAt(optionEdit.range.start), - end: textDoc.positionAt(optionEdit.range.end), - }, - newText: optionEdit.newText, - }); + const data: Data = item.data; + if (item.data?.__isComponentAutoImport && data && item.additionalTextEdits?.length && item.textEdit && itemData?.uri) { + const fileName = ctx.env.uriToFileName(itemData.uri); + const langaugeService = ctx.inject('typescript/languageService'); + const [virtualFile] = ctx.language.files.getVirtualFile(fileName); + const ast = langaugeService.getProgram()?.getSourceFile(fileName); + const exportDefault = ast ? scriptRanges.parseScriptRanges(ts, ast, false, true).exportDefault : undefined; + if (virtualFile && ast && exportDefault) { + const componentName = newName ?? item.textEdit.newText; + const optionEdit = createAddComponentToOptionEdit(ts, ast, componentName); + if (optionEdit) { + const textDoc = ctx.documents.get(virtualFile.id, virtualFile.languageId, virtualFile.snapshot); + item.additionalTextEdits.push({ + range: { + start: textDoc.positionAt(optionEdit.range.start), + end: textDoc.positionAt(optionEdit.range.end), + }, + newText: optionEdit.newText, + }); + } } } - } - return item; - }, - async provideCodeActions(document, range, context, token) { - const result = await base.provideCodeActions?.(document, range, context, token); - return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1); - }, - async resolveCodeAction(item, token) { + return item; + }, + async provideCodeActions(document, range, context, token) { + const result = await base.provideCodeActions?.(document, range, context, token); + return result?.filter(codeAction => codeAction.title.indexOf('__VLS_') === -1); + }, + async resolveCodeAction(item, token) { - const result = await base.resolveCodeAction?.(item, token) ?? item; + const result = await base.resolveCodeAction?.(item, token) ?? item; - if (result?.edit?.changes) { - for (const uri in result.edit.changes) { - const edits = result.edit.changes[uri]; - if (edits) { - patchAdditionalTextEdits(uri, edits); + if (result?.edit?.changes) { + for (const uri in result.edit.changes) { + const edits = result.edit.changes[uri]; + if (edits) { + patchAdditionalTextEdits(uri, edits); + } } } - } - if (result?.edit?.documentChanges) { - for (const documentChange of result.edit.documentChanges) { - if ('textDocument' in documentChange) { - patchAdditionalTextEdits(documentChange.textDocument.uri, documentChange.edits); + if (result?.edit?.documentChanges) { + for (const documentChange of result.edit.documentChanges) { + if ('textDocument' in documentChange) { + patchAdditionalTextEdits(documentChange.textDocument.uri, documentChange.edits); + } } } - } - return result; + return result; + }, + async provideSemanticDiagnostics(document, token) { + const result = await base.provideSemanticDiagnostics?.(document, token); + return result?.map(diagnostic => { + if ( + diagnostic.source === 'ts' + && diagnostic.code === 2578 /* Unused '@ts-expect-error' directive. */ + && document.getText(diagnostic.range) === '// @ts-expect-error __VLS_TS_EXPECT_ERROR' + ) { + diagnostic.source = 'vue'; + diagnostic.code = 'ts-2578'; + diagnostic.message = diagnostic.message.replace(/@ts-expect-error/g, '@vue-expect-error'); + } + return diagnostic; + }); + }, + }; + }, + }; + services.html ??= createVueTemplateLanguageService( + ts, + createHtmlService(), + { + getScanner: (htmlService, document): html.Scanner | undefined => { + return htmlService.provide['html/languageService']().createScanner(document.getText()); }, - async provideSemanticDiagnostics(document, token) { - const result = await base.provideSemanticDiagnostics?.(document, token); - return result?.map(diagnostic => { - if ( - diagnostic.source === 'ts' - && diagnostic.code === 2578 /* Unused '@ts-expect-error' directive. */ - && document.getText(diagnostic.range) === '// @ts-expect-error __VLS_TS_EXPECT_ERROR' - ) { - diagnostic.source = 'vue'; - diagnostic.code = 'ts-2578'; - diagnostic.message = diagnostic.message.replace(/@ts-expect-error/g, '@vue-expect-error'); - } - return diagnostic; - }); + updateCustomData(htmlService, extraData) { + htmlService.provide['html/updateCustomData'](extraData); }, - }; - }; - services.html ??= VueTemplateLanguageService.create({ - baseService: HtmlService.create(), - getScanner: (htmlService, document): html.Scanner | undefined => { - return htmlService.provide['html/languageService']().createScanner(document.getText()); - }, - updateCustomData(htmlService, extraData) { - htmlService.provide['html/updateCustomData'](extraData); - }, - isSupportedDocument: (document) => document.languageId === 'html', - vueCompilerOptions, - }); - services.pug ??= VueTemplateLanguageService.create({ - baseService: PugService.create(), - getScanner: (pugService, document): html.Scanner | undefined => { - const pugDocument = pugService.provide['pug/pugDocument'](document); - if (pugDocument) { - return pugService.provide['pug/languageService']().createScanner(pugDocument); - } - }, - updateCustomData(pugService, extraData) { - pugService.provide['pug/updateCustomData'](extraData); - }, - isSupportedDocument: (document) => document.languageId === 'jade', - vueCompilerOptions, - }); - services.vue ??= VueService.create(); - services.css ??= CssService.create(); - services['pug-beautify'] ??= PugFormatService.create(); - services.json ??= JsonService.create(); - services['typescript/twoslash-queries'] ??= TsTqService.create(); - services['vue/referencesCodeLens'] ??= ReferencesCodeLensService.create(); - services['vue/autoInsertDotValue'] ??= AutoDotValueService.create(); - services['vue/twoslash-queries'] ??= VueTqService.create(); - services['vue/autoInsertParentheses'] ??= AutoWrapParenthesesService.create(); - services['vue/autoInsertSpaces'] ??= AutoAddSpaceService.create(); - services['vue/visualizeHiddenCallbackParam'] ??= VisualizeHiddenCallbackParamService.create(); - services['vue/directiveComments'] ??= DirectiveCommentsService.create(); - services['vue/extractComponent'] ??= ExtractComponentService.create(); - services['vue/toggleVBind'] ??= ToggleVBindService.create(); - services.emmet ??= EmmetService.create(); + isSupportedDocument: (document) => document.languageId === 'html', + vueCompilerOptions, + } + ); + services.pug ??= createVueTemplateLanguageService( + ts, + createPugService(), + { + getScanner: (pugService, document): html.Scanner | undefined => { + const pugDocument = pugService.provide['pug/pugDocument'](document); + if (pugDocument) { + return pugService.provide['pug/languageService']().createScanner(pugDocument); + } + }, + updateCustomData(pugService, extraData) { + pugService.provide['pug/updateCustomData'](extraData); + }, + isSupportedDocument: (document) => document.languageId === 'jade', + vueCompilerOptions, + } + ); + services.vue ??= createVueService(); + services.css ??= createCssService(); + services['pug-beautify'] ??= createPugFormatService(); + services.json ??= createJsonService(); + services['typescript/twoslash-queries'] ??= createTsTqService(); + services['vue/referencesCodeLens'] ??= createReferencesCodeLensService(); + services['vue/documentDrop'] ??= createDocumentDropService(ts); + services['vue/autoInsertDotValue'] ??= createAutoDotValueService(ts); + services['vue/twoslash-queries'] ??= createVueTqService(ts); + services['vue/autoInsertParentheses'] ??= createAutoWrapParenthesesService(ts); + services['vue/autoInsertSpaces'] ??= createAutoAddSpaceService(); + services['vue/visualizeHiddenCallbackParam'] ??= createVisualizeHiddenCallbackParamService(); + services['vue/directiveComments'] ??= createDirectiveCommentsService(); + services['vue/extractComponent'] ??= createVueExtractFileService(ts); + services['vue/toggleVBind'] ??= createToggleVBindService(ts); + services.emmet ??= createEmmetService(); return services; } diff --git a/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts index 336863dff1..a682839a31 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-dotvalue.ts @@ -1,83 +1,79 @@ -import { AutoInsertionContext, Service, ServiceContext } from '@volar/language-service'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { hyphenateAttr } from '@vue/language-core'; import type * as ts from 'typescript/lib/tsserverlibrary'; +import { Provide } from 'volar-service-typescript'; import type * as vscode from 'vscode-languageserver-protocol'; import type { TextDocument } from 'vscode-languageserver-textdocument'; -const plugin: Service = (context: ServiceContext | undefined, modules) => { - - if (!modules?.typescript) - return {}; - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, position, lastChange) { - async provideAutoInsertionEdit(document, position, insertContext) { + if (!isTsDocument(document)) + return; - if (!isTsDocument(document)) - return; + if (!isCharacterTyping(document, lastChange)) + return; - if (!isCharacterTyping(document, insertContext)) - return; + const enabled = await context.env.getConfiguration?.('vue.autoInsert.dotValue') ?? true; + if (!enabled) + return; - const enabled = await context!.env.getConfiguration?.('vue.autoInsert.dotValue') ?? true; - if (!enabled) - return; + const program = context.inject('typescript/languageService').getProgram(); + if (!program) + return; - const program = context!.inject('typescript/languageService').getProgram(); - if (!program) - return; + const sourceFile = program.getSourceFile(context.env.uriToFileName(document.uri)); + if (!sourceFile) + return; - const sourceFile = program.getSourceFile(context!.env.uriToFileName(document.uri)); - if (!sourceFile) - return; + if (isBlacklistNode(ts, sourceFile, document.offsetAt(position), false)) + return; - if (isBlacklistNode(ts, sourceFile, document.offsetAt(position), false)) - return; + const node = findPositionIdentifier(sourceFile, sourceFile, document.offsetAt(position)); + if (!node) + return; - const node = findPositionIdentifier(sourceFile, sourceFile, document.offsetAt(position)); - if (!node) - return; + const token = context.inject('typescript/languageServiceHost').getCancellationToken?.(); + if (token) { + context.inject('typescript/languageService').getQuickInfoAtPosition(context.env.uriToFileName(document.uri), node.end); + if (token?.isCancellationRequested()) { + return; // check cancel here because type checker do not use cancel token + } + } - const token = context!.inject('typescript/languageServiceHost').getCancellationToken?.(); - if (token) { - context!.inject('typescript/languageService').getQuickInfoAtPosition(context!.env.uriToFileName(document.uri), node.end); - if (token?.isCancellationRequested()) { - return; // check cancel here because type checker do not use cancel token - } - } + const checker = program.getTypeChecker(); + const type = checker.getTypeAtLocation(node); + const props = type.getProperties(); - const checker = program.getTypeChecker(); - const type = checker.getTypeAtLocation(node); - const props = type.getProperties(); + if (props.some(prop => prop.name === 'value')) { + return '${1:.value}'; + } - if (props.some(prop => prop.name === 'value')) { - return '${1:.value}'; - } + function findPositionIdentifier(sourceFile: ts.SourceFile, node: ts.Node, offset: number) { - function findPositionIdentifier(sourceFile: ts.SourceFile, node: ts.Node, offset: number) { + let result: ts.Node | undefined; - let result: ts.Node | undefined; + node.forEachChild(child => { + if (!result) { + if (child.end === offset && ts.isIdentifier(child)) { + result = child; + } + else if (child.end >= offset && child.getStart(sourceFile) < offset) { + result = findPositionIdentifier(sourceFile, child, offset); + } + } + }); - node.forEachChild(child => { - if (!result) { - if (child.end === offset && ts.isIdentifier(child)) { - result = child; - } - else if (child.end >= offset && child.getStart(sourceFile) < offset) { - result = findPositionIdentifier(sourceFile, child, offset); - } + return result; } - }); - - return result; - } + }, + }; }, }; -}; - -export const create = () => plugin; +} function isTsDocument(document: TextDocument) { return document.languageId === 'javascript' || @@ -88,13 +84,13 @@ function isTsDocument(document: TextDocument) { const charReg = /\w/; -export function isCharacterTyping(document: TextDocument, options: AutoInsertionContext) { +export function isCharacterTyping(document: TextDocument, lastChange: { range: vscode.Range; text: string; }) { - const lastCharacter = options.lastChange.text[options.lastChange.text.length - 1]; - const rangeStart = options.lastChange.range.start; + const lastCharacter = lastChange.text[lastChange.text.length - 1]; + const rangeStart = lastChange.range.start; const position: vscode.Position = { line: rangeStart.line, - character: rangeStart.character + options.lastChange.text.length, + character: rangeStart.character + lastChange.text.length, }; const nextCharacter = document.getText({ start: position, @@ -104,7 +100,7 @@ export function isCharacterTyping(document: TextDocument, options: AutoInsertion if (lastCharacter === undefined) { // delete text return false; } - if (options.lastChange.text.indexOf('\n') >= 0) { // multi-line change + if (lastChange.text.indexOf('\n') >= 0) { // multi-line change return false; } diff --git a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts index fba00b5f71..09363d0cd9 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-parentheses.ts @@ -1,79 +1,71 @@ -import { Service } from '@volar/language-service'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; import { isCharacterTyping } from './vue-autoinsert-dotvalue'; -const plugin: Service = (context, modules) => { - - if (!context) { - return {}; - } - - if (!modules?.typescript) { - return {}; - } - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, position, lastChange) { - async provideAutoInsertionEdit(document, position, options_2) { - - const enabled = await context.env.getConfiguration?.('vue.autoInsert.parentheses') ?? false; - if (!enabled) - return; + const enabled = await context.env.getConfiguration?.('vue.autoInsert.parentheses') ?? false; + if (!enabled) + return; - if (!isCharacterTyping(document, options_2)) - return; + if (!isCharacterTyping(document, lastChange)) + return; - const [virtualFile] = context.documents.getVirtualFileByUri(document.uri); - if (!virtualFile?.fileName.endsWith('.template_format.ts')) - return; + const [virtualFile] = context.language.files.getVirtualFile(document.uri); + if (!virtualFile?.id.endsWith('.template_format.ts')) + return; - const offset = document.offsetAt(position); + const offset = document.offsetAt(position); - for (const mappedRange of virtualFile.mappings) { - if (mappedRange.generatedRange[1] === offset) { - const text = document.getText().substring(mappedRange.generatedRange[0], mappedRange.generatedRange[1]); - const ast = ts.createSourceFile(virtualFile.fileName, text, ts.ScriptTarget.Latest); - if (ast.statements.length === 1) { - const statement = ast.statements[0]; - if ( - ts.isExpressionStatement(statement) - && ( - ( - ts.isAsExpression(statement.expression) - && ts.isTypeReferenceNode(statement.expression.type) - && ts.isIdentifier(statement.expression.type.typeName) - && statement.expression.type.typeName.text - ) - || ( - ts.isBinaryExpression(statement.expression) - && statement.expression.right.getText(ast) - && statement.expression.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword - ) - || ( - ts.isTypeOfExpression(statement.expression) - && statement.expression.expression.getText(ast) - ) - ) - ) { - // https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar - const escapedText = text - .replaceAll('\\', '\\\\') - .replaceAll('$', '\\$') - .replaceAll('}', '\\}'); - return { - range: { - start: document.positionAt(mappedRange.generatedRange[0]), - end: document.positionAt(mappedRange.generatedRange[1]), - }, - newText: '(' + escapedText + '$0' + ')', - }; + for (const mappedRange of virtualFile.mappings) { + const generatedCodeEnd = mappedRange.generatedOffsets[mappedRange.generatedOffsets.length - 1] + + mappedRange.lengths[mappedRange.lengths.length - 1]; + if (generatedCodeEnd === offset) { + const text = document.getText().substring(mappedRange.generatedOffsets[0], generatedCodeEnd); + const ast = ts.createSourceFile(context.env.uriToFileName(virtualFile.id), text, ts.ScriptTarget.Latest); + if (ast.statements.length === 1) { + const statement = ast.statements[0]; + if ( + ts.isExpressionStatement(statement) + && ( + ( + ts.isAsExpression(statement.expression) + && ts.isTypeReferenceNode(statement.expression.type) + && ts.isIdentifier(statement.expression.type.typeName) + && statement.expression.type.typeName.text + ) + || ( + ts.isBinaryExpression(statement.expression) + && statement.expression.right.getText(ast) + && statement.expression.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword + ) + || ( + ts.isTypeOfExpression(statement.expression) + && statement.expression.expression.getText(ast) + ) + ) + ) { + // https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar + const escapedText = text + .replaceAll('\\', '\\\\') + .replaceAll('$', '\\$') + .replaceAll('}', '\\}'); + return { + range: { + start: document.positionAt(mappedRange.generatedOffsets[0]), + end: document.positionAt(generatedCodeEnd), + }, + newText: '(' + escapedText + '$0' + ')', + }; + } + } } } - } - } + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue-autoinsert-space.ts b/packages/language-service/src/plugins/vue-autoinsert-space.ts index 5b71baa6bc..cfe0a097bd 100644 --- a/packages/language-service/src/plugins/vue-autoinsert-space.ts +++ b/packages/language-service/src/plugins/vue-autoinsert-space.ts @@ -1,38 +1,35 @@ -import { Service } from '@volar/language-service'; - -const plugin: Service = (context): ReturnType => { - - if (!context) - return {}; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +export function create(): ServicePlugin { return { - - async provideAutoInsertionEdit(document, _, { lastChange }) { - - if (document.languageId === 'html' || document.languageId === 'jade') { - - const enabled = await context.env.getConfiguration?.('vue.autoInsert.bracketSpacing') ?? true; - if (!enabled) - return; - - if ( - lastChange.text === '{}' - && document.getText({ - start: { line: lastChange.range.start.line, character: lastChange.range.start.character - 1 }, - end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 3 } - }) === '{{}}' - ) { - return { - newText: ` $0 `, - range: { - start: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 }, - end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 } - }, - }; - } - } + create(context): ServicePluginInstance { + return { + async provideAutoInsertionEdit(document, _, lastChange) { + + if (document.languageId === 'html' || document.languageId === 'jade') { + + const enabled = await context.env.getConfiguration?.('vue.autoInsert.bracketSpacing') ?? true; + if (!enabled) + return; + + if ( + lastChange.text === '{}' + && document.getText({ + start: { line: lastChange.range.start.line, character: lastChange.range.start.character - 1 }, + end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 3 } + }) === '{{}}' + ) { + return { + newText: ` $0 `, + range: { + start: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 }, + end: { line: lastChange.range.start.line, character: lastChange.range.start.character + 1 } + }, + }; + } + } + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue-codelens-references.ts b/packages/language-service/src/plugins/vue-codelens-references.ts index 4badb080b4..38f8f4370d 100644 --- a/packages/language-service/src/plugins/vue-codelens-references.ts +++ b/packages/language-service/src/plugins/vue-codelens-references.ts @@ -1,70 +1,79 @@ -import { Service } from '@volar/language-service'; -import { VueFile } from '@vue/language-core'; +import { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import { SourceFile, VirtualFile, VueCodeInformation, VueFile } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; -export const create = function (): Service { +export function create(): ServicePlugin { + return { + create(context): ServicePluginInstance { + return { + provideReferencesCodeLensRanges(document) { - return (context): ReturnType => { + return worker(document.uri, async virtualFile => { - if (!context) - return {}; + const result: vscode.Range[] = []; - return { + for (const map of context.documents.getMaps(virtualFile) ?? []) { + for (const mapping of map.map.mappings) { - provideReferencesCodeLensRanges(document) { + if (!(mapping.data as VueCodeInformation).__referencesCodeLens) + continue; - return worker(document.uri, async () => { - - const result: vscode.Range[] = []; - - for (const [_, map] of context.documents.getMapsBySourceFileUri(document.uri)?.maps ?? []) { - for (const mapping of map.map.mappings) { - - if (!mapping.data.referencesCodeLens) - continue; + result.push({ + start: document.positionAt(mapping.generatedOffsets[0]), + end: document.positionAt( + mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + + mapping.lengths[mapping.lengths.length - 1] + ), + }); + } + } - result.push({ - start: document.positionAt(mapping.sourceRange[0]), - end: document.positionAt(mapping.sourceRange[1]), - }); + return result; + }); + }, + + async resolveReferencesCodeLensLocations(document, range, references) { + + const [virtualFile, sourceFile] = context.language.files.getVirtualFile(document.uri); + if (virtualFile && sourceFile?.virtualFile?.[0] instanceof VueFile) { + const vueFile = sourceFile.virtualFile[0]; + const blocks = [ + vueFile.sfc.script, + vueFile.sfc.scriptSetup, + vueFile.sfc.template, + ...vueFile.sfc.styles, + ...vueFile.sfc.customBlocks, + ]; + for (const map of context.documents.getMaps(virtualFile)) { + const sourceOffset = map.map.getSourceOffset(document.offsetAt(range.start)); + if (sourceOffset !== undefined) { + const sourceBlock = blocks.find(block => block && sourceOffset[0] >= block.startTagEnd && sourceOffset[0] <= block.endTagStart); + const sourceDocument = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); + references = references.filter(reference => + reference.uri !== sourceDocument.uri // different file + || sourceBlock !== blocks.find(block => + block + && sourceDocument.offsetAt(reference.range.start) >= block.startTagEnd + && sourceDocument.offsetAt(reference.range.end) <= block.endTagStart + ) // different block + ); + break; + } } } - return result; - }); - }, - - async resolveReferencesCodeLensLocations(document, range, references) { - - await worker(document.uri, async (vueFile) => { - - const document = context.documents.getDocumentByFileName(vueFile.snapshot, vueFile.fileName); - const offset = document.offsetAt(range.start); - const blocks = [ - vueFile.sfc.script, - vueFile.sfc.scriptSetup, - vueFile.sfc.template, - ...vueFile.sfc.styles, - ...vueFile.sfc.customBlocks, - ]; - const sourceBlock = blocks.find(block => block && offset >= block.startTagEnd && offset <= block.endTagStart); - references = references.filter(reference => - reference.uri !== document.uri // different file - || sourceBlock !== blocks.find(block => block && document.offsetAt(reference.range.start) >= block.startTagEnd && document.offsetAt(reference.range.end) <= block.endTagStart) // different block - ); - }); - - return references; - }, - }; + return references; + }, + }; - function worker(uri: string, callback: (vueSourceFile: VueFile) => T) { + function worker(uri: string, callback: (vueFile: VirtualFile, sourceFile: SourceFile) => T) { - const [virtualFile] = context!.documents.getVirtualFileByUri(uri); - if (!(virtualFile instanceof VueFile)) - return; + const [virtualFile, sourceFile] = context.language.files.getVirtualFile(uri); + if (!(sourceFile?.virtualFile?.[0] instanceof VueFile) || !sourceFile) + return; - return callback(virtualFile); - } + return callback(virtualFile, sourceFile); + } + }, }; -}; +} diff --git a/packages/language-service/src/plugins/vue-directive-comments.ts b/packages/language-service/src/plugins/vue-directive-comments.ts index ceda0d2028..1e0ecd01ac 100644 --- a/packages/language-service/src/plugins/vue-directive-comments.ts +++ b/packages/language-service/src/plugins/vue-directive-comments.ts @@ -1,4 +1,4 @@ -import { CompletionItem, Service } from '@volar/language-service'; +import { CompletionItem, ServicePlugin, ServicePluginInstance } from '@volar/language-service'; const cmds = [ 'vue-ignore', @@ -8,58 +8,57 @@ const cmds = [ const directiveCommentReg = //g; -const plugin: Service = (context: ServiceContext | undefined, modules) => { - - if (!context || !modules?.typescript) - return {}; - - const ts = modules.typescript; - +export function create(ts: typeof import('typescript/lib/tsserverlibrary')): ServicePlugin { return { + create(context): ServicePluginInstance { + return { + provideInlayHints(document, range) { + return worker(document.uri, (vueFile) => { - provideInlayHints(document, range) { - return worker(document.uri, (vueFile) => { + const hoverOffsets: [vscode.Position, number][] = []; + const inlayHints: vscode.InlayHint[] = []; + const languageService = context.inject('typescript/languageService'); - const hoverOffsets: [vscode.Position, number][] = []; - const inlayHints: vscode.InlayHint[] = []; - const languageService = context.inject('typescript/languageService'); - - for (const pointer of document.getText(range).matchAll(twoslashReg)) { - const offset = pointer.index! + pointer[0].indexOf('^?') + document.offsetAt(range.start); - const position = document.positionAt(offset); - hoverOffsets.push([position, document.offsetAt({ - line: position.line - 1, - character: position.character, - })]); - } + for (const pointer of document.getText(range).matchAll(twoslashReg)) { + const offset = pointer.index! + pointer[0].indexOf('^?') + document.offsetAt(range.start); + const position = document.positionAt(offset); + hoverOffsets.push([position, document.offsetAt({ + line: position.line - 1, + character: position.character, + })]); + } - forEachEmbeddedFile(vueFile, (embedded) => { - if (embedded.kind === FileKind.TypeScriptHostFile) { - for (const [_, map] of context.documents.getMapsByVirtualFileName(embedded.fileName)) { - for (const [pointerPosition, hoverOffset] of hoverOffsets) { - for (const [tsOffset, mapping] of map.map.toGeneratedOffsets(hoverOffset)) { - if (mapping.data.hover) { - const quickInfo = languageService.getQuickInfoAtPosition(embedded.fileName, tsOffset); - if (quickInfo) { - inlayHints.push({ - position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, - label: ts.displayPartsToString(quickInfo.displayParts), - paddingLeft: true, - paddingRight: false, - }); + for (const virtualFile of forEachEmbeddedFile(vueFile)) { + if (virtualFile.typescript) { + for (const map of context.documents.getMaps(virtualFile)) { + for (const [pointerPosition, hoverOffset] of hoverOffsets) { + for (const [tsOffset, mapping] of map.map.getGeneratedOffsets(hoverOffset)) { + if (vue.isHoverEnabled(mapping.data)) { + const quickInfo = languageService.getQuickInfoAtPosition(context.env.uriToFileName(virtualFile.id), tsOffset); + if (quickInfo) { + inlayHints.push({ + position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, + label: ts.displayPartsToString(quickInfo.displayParts), + paddingLeft: true, + paddingRight: false, + }); + } + break; + } } - break; } } } } - } - }); - return inlayHints; - }); - }, - }; - - function worker(uri: string, callback: (vueSourceFile: vue.VueFile) => T) { + return inlayHints; + }); + }, + }; - const [virtualFile] = context!.documents.getVirtualFileByUri(uri); - if (!(virtualFile instanceof vue.VueFile)) - return; + function worker(uri: string, callback: (vueSourceFile: vue.VueFile) => T) { - return callback(virtualFile); - } -}; + const [virtualFile] = context.language.files.getVirtualFile(uri); + if (!(virtualFile instanceof vue.VueFile)) + return; -export const create = () => plugin; + return callback(virtualFile); + } + }, + }; +} diff --git a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts index 1aca2da3e4..e58468c86a 100644 --- a/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts +++ b/packages/language-service/src/plugins/vue-visualize-hidden-callback-param.ts @@ -1,59 +1,54 @@ -import { Service } from '@volar/language-service'; +import { ServicePluginInstance } from '@volar/language-service'; import type * as vscode from 'vscode-languageserver-protocol'; +import type { ServicePlugin, VueCodeInformation } from '../types'; -const plugin: Service = (context) => { - - if (!context) - return {}; - +export function create(): ServicePlugin { return { - - async provideInlayHints(document, range) { - - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const [file] = context.documents.getVirtualFileByUri(document.uri); - if (file) { - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - for (const mapping of file.mappings) { - - const hint: { - setting: string; - label: string; - tooltip: string; - paddingRight?: boolean; - paddingLeft?: boolean; - } | undefined = (mapping.data as any).__hint; - - if ( - mapping.generatedRange[0] >= start - && mapping.generatedRange[1] <= end - && hint - ) { - - 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(mapping.generatedRange[0]), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - tooltip: { - kind: 'markdown', - value: hint.tooltip, - }, - }); + create(context): ServicePluginInstance { + return { + async provideInlayHints(document, range) { + + const settings: Record = {}; + const result: vscode.InlayHint[] = []; + const [vitualFile] = context.language.files.getVirtualFile(document.uri); + + if (vitualFile) { + + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + + for (const mapping of vitualFile.mappings) { + + const hint = (mapping.data as VueCodeInformation).__hint; + + if ( + mapping.generatedOffsets[0] >= start + && mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1] <= end + && hint + ) { + + 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(mapping.generatedOffsets[0]), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + tooltip: { + kind: 'markdown', + value: hint.tooltip, + }, + }); + } + } } - } - } - return result; + return result; + }, + }; }, }; -}; - -export const create = () => plugin; +} diff --git a/packages/language-service/src/plugins/vue.ts b/packages/language-service/src/plugins/vue.ts index a9b14dd2c9..79f4c79d2e 100644 --- a/packages/language-service/src/plugins/vue.ts +++ b/packages/language-service/src/plugins/vue.ts @@ -1,9 +1,10 @@ -import type { Service, ServiceContext } from '@volar/language-service'; +import type { ServicePlugin, ServicePluginInstance } from '@volar/language-service'; +import * as vue from '@vue/language-core'; +import { create as createHtmlService } from 'volar-service-html'; +import type { Provide as TSProvide } from 'volar-service-typescript'; import * as html from 'vscode-html-languageservice'; import type * as vscode from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import createHtmlPlugin from 'volar-service-html'; -import * as vue from '@vue/language-core'; import { loadLanguageBlocks } from './data'; let sfcDataProvider: html.IHTMLDataProvider | undefined; @@ -12,186 +13,195 @@ export interface Provide { 'vue/vueFile': (document: TextDocument) => vue.VueFile | undefined; } -export const create = (): Service => (context: ServiceContext | undefined, modules): ReturnType> => { +export function create(): ServicePlugin { + return { + create(context): ServicePluginInstance { - const htmlPlugin = createHtmlPlugin({ languageId: 'vue', useCustomDataProviders: false })(context, modules); + const htmlPlugin = createHtmlService({ languageId: 'vue', useCustomDataProviders: false }).create(context); - if (!context) - return htmlPlugin as any; + if (!context) + return htmlPlugin as any; - sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); + sfcDataProvider ??= html.newHTMLDataProvider('vue', loadLanguageBlocks(context.env.locale ?? 'en')); - htmlPlugin.provide['html/languageService']().setDataProviders(false, [sfcDataProvider]); + htmlPlugin.provide['html/languageService']().setDataProviders(false, [sfcDataProvider]); - return { + return { - ...htmlPlugin, + ...htmlPlugin, - provide: { - 'vue/vueFile': document => { - return worker(document, (vueFile) => { - return vueFile; - }); - }, - }, + provide: { + 'vue/vueFile': document => { + return worker(document, (vueFile) => { + return vueFile; + }); + }, + }, + + provideSemanticDiagnostics(document) { + return worker(document, (vueSourceFile) => { + + if (!vueSourceFile.mainTsFile) { + return; + } + + const result: vscode.Diagnostic[] = []; + const sfc = vueSourceFile.sfc; + const program = context.inject('typescript/languageService').getProgram(); + const tsFileName = context.env.uriToFileName(vueSourceFile.mainTsFile.id); + + if (program && !program.getSourceFile(tsFileName)) { + for (const script of [sfc.script, sfc.scriptSetup]) { + + if (!script || script.content === '') + continue; + + const error: vscode.Diagnostic = { + range: { + start: document.positionAt(script.start), + end: document.positionAt(script.startTagEnd), + }, + message: `Virtual script ${JSON.stringify(tsFileName)} not found, may missing diff --git a/test-workspace/language-service/reference/v-bind-shorthand/entry.vue b/test-workspace/language-service/reference/v-bind-shorthand/entry.vue new file mode 100644 index 0000000000..46adbd67dc --- /dev/null +++ b/test-workspace/language-service/reference/v-bind-shorthand/entry.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/test-workspace/tsc/vue3/v-bind-shorthand/Comp.vue b/test-workspace/tsc/vue3/v-bind-shorthand/Comp.vue new file mode 100644 index 0000000000..51b389e379 --- /dev/null +++ b/test-workspace/tsc/vue3/v-bind-shorthand/Comp.vue @@ -0,0 +1,6 @@ + diff --git a/test-workspace/tsc/vue3/v-bind-shorthand/main.vue b/test-workspace/tsc/vue3/v-bind-shorthand/main.vue new file mode 100644 index 0000000000..e526decb05 --- /dev/null +++ b/test-workspace/tsc/vue3/v-bind-shorthand/main.vue @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e1dc6131f5..3cbcb17ecb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,6 @@ { "path": "./packages/language-plugin-pug/tsconfig.json" }, { "path": "./packages/typescript-plugin/tsconfig.json" }, { "path": "./packages/tsc/tsconfig.json" }, - { "path": "./packages/tsc-eslint-hook/tsconfig.json" }, { "path": "./packages/component-meta/tsconfig.json" }, ], } \ No newline at end of file