diff --git a/.github/workflows/extension-build.yml b/.github/workflows/extension-build.yml new file mode 100644 index 0000000000..993380f68d --- /dev/null +++ b/.github/workflows/extension-build.yml @@ -0,0 +1,31 @@ +name: extension + +on: + push: + branches: + - 'master' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm --filter ./extensions/vscode run pack + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: Extension + path: ./extensions/vscode/volar-*.vsix diff --git a/.github/workflows/release.yml b/.github/workflows/extension-release.yml similarity index 97% rename from .github/workflows/release.yml rename to .github/workflows/extension-release.yml index 01e60841a7..6102269aa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/extension-release.yml @@ -1,4 +1,4 @@ -name: release +name: extension on: workflow_dispatch: diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 66ce7606d9..8ab6a806bb 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -1,4 +1,4 @@ -name: Publish Any Commit +name: publish-any-commit on: [push, pull_request] jobs: @@ -6,17 +6,14 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - run: | - npm install -g corepack@latest - corepack enable + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 - cache: "pnpm" + cache: pnpm - name: Install dependencies run: pnpm install diff --git a/CHANGELOG.md b/CHANGELOG.md index ae673de21f..9fc0f3547c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ > [Join the Insiders Program](https://github.com/vuejs/language-tools/wiki/Get-Insiders-Edition) for more exclusive features and updates. +## 2.2.4 official, 2.2.5 insiders (2025-02-22) + +### Features + +- feat(language-service): map sfc compiler errors outside the template inner content (#5045) - Thanks to @KazariEX! +- feat(language-core): introduce options to control type inference of `$attrs`, `$el`, `$refs` and `$slots` (#5135) - Thanks to @KazariEX! +- feat(language-core): enhance single root nodes collection (#4819) - Thanks to @KazariEX! + +### Bug Fixes + +- fix(language-core): move `generateSfcBlockSection` to the end to fix missing comma errors (#5184) - Thanks to @zhiyuanzmj! +- fix(language-core): handle edge case of default slot name mismatch - Thanks to @KazariEX! +- fix(language-core): combine dollar variable keys from the upper level interface - Thanks to @KazariEX! +- fix(language-core): hoist the variables that may cause `TS4081` (#5192) - Thanks to @KazariEX! +- fix(language-core): adjust regex match for `@vue-generic` to improve offset calculation (#5193) - Thanks to @Gehbt! +- fix(language-core): correct codegen of native element refs - Thanks to @KazariEX! +- fix(language-core): ignore latex block content (#5151) - Thanks to @KazariEX! +- fix(language-core): do not emit `undefined` for model with default value (#5198) - Thanks to @RylanBueckert-Broadsign! +- fix(language-service): typescript-semantic renaming first in style blocks (#4685) - Thanks to @KazariEX! +- fix(typescript-plugin): prevent removed components from appearing in the completion list - Thanks to @KazariEX! + +### Other Changes + +- refactor(language-core): drop invalid `v-scope` implemention - Thanks to @KazariEX! +- refactor(language-core): improve type declaration of `v-for` - Thanks to @KazariEX! +- test: enable `declaration` to track more errors - Thanks to @KazariEX! +- refactor(language-core): remove semantic highlight of style module names - Thanks to @KazariEX! +- chore(language-core): add docs for `@vue-expect-error` support (#5176) - Thanks to @machty! +- ci: upload extension as artifact for each commit - Thanks to @KazariEX! + ## 2.2.2 official, 2.2.3 insiders (2025-02-15) ### Features diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index b9e0b0e421..13e5f94fba 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "volar", - "version": "2.2.2", + "version": "2.2.4", "repository": { "type": "git", "url": "https://github.com/vuejs/language-tools.git", @@ -567,9 +567,9 @@ "@types/vscode": "^1.82.0", "@volar/vscode": "~2.4.11", "@vscode/vsce": "^3.2.1", - "@vue/language-core": "2.2.2", - "@vue/language-server": "2.2.2", - "@vue/typescript-plugin": "2.2.2", + "@vue/language-core": "2.2.4", + "@vue/language-server": "2.2.4", + "@vue/typescript-plugin": "2.2.4", "esbuild": "^0.25.0", "esbuild-visualizer": "^0.7.0", "reactive-vscode": "^0.2.9", diff --git a/insiders.json b/insiders.json index 697771f184..336237e694 100644 --- a/insiders.json +++ b/insiders.json @@ -1,6 +1,22 @@ { - "latest": "2.2.1", + "latest": "2.2.5", "versions": [ + { + "version": "2.2.5", + "date": "2025-02-22", + "downloads": { + "GitHub": "https://github.com/volarjs/insiders/releases/tag/v2.2.5", + "AFDIAN": "https://afdian.com/p/708861e2f1c611ef9fc552540025c377" + } + }, + { + "version": "2.2.3", + "date": "2025-02-15", + "downloads": { + "GitHub": "https://github.com/volarjs/insiders/releases/tag/v2.2.3", + "AFDIAN": "https://afdian.com/p/6fa17e40ebc111ef849e52540025c377" + } + }, { "version": "2.2.1", "date": "2024-12-24", diff --git a/lerna.json b/lerna.json index 83f3407f0c..4344415445 100644 --- a/lerna.json +++ b/lerna.json @@ -6,5 +6,5 @@ "packages/*", "test-workspace" ], - "version": "2.2.2" + "version": "2.2.4" } diff --git a/package.json b/package.json index 0187fafb40..ff76d56067 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@10.4.1", "scripts": { "build": "tsc -b", "watch": "pnpm run build && pnpm run \"/^watch:.*/\"", @@ -28,5 +28,13 @@ "@typescript-eslint/eslint-plugin": "^8.19.0", "typescript": "^5.7.2", "vitest": "^2.1.8" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@tsslint/core", + "@vscode/vsce-sign", + "esbuild", + "keytar" + ] } } diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 7e61eac0e1..a944fd7a03 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -719,6 +719,7 @@ function readVueComponentDefaultProps( default?: string; required?: boolean; }> = {}; + const { sfc } = root; scriptSetupWorker(); scriptWorker(); @@ -727,12 +728,12 @@ function readVueComponentDefaultProps( function scriptSetupWorker() { - const ast = root._sfc.scriptSetup?.ast; + const ast = sfc.scriptSetup?.ast; if (!ast) { return; } - const codegen = vue.tsCodegen.get(root._sfc); + const codegen = vue.tsCodegen.get(sfc); const scriptSetupRanges = codegen?.getScriptSetupRanges(); if (scriptSetupRanges?.withDefaults?.argNode) { @@ -785,13 +786,14 @@ function readVueComponentDefaultProps( function scriptWorker() { - const sfc = root._sfc; + const ast = sfc.script?.ast; + if (!ast) { + return; + } - if (sfc.script) { - const scriptResult = readTsComponentDefaultProps(sfc.script.ast, 'default', printer, ts); - for (const [key, value] of Object.entries(scriptResult)) { - result[key] = value; - } + const scriptResult = readTsComponentDefaultProps(ast, 'default', printer, ts); + for (const [key, value] of Object.entries(scriptResult)) { + result[key] = value; } } } diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index e8771fffd1..508cdb99c7 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -1,6 +1,6 @@ { "name": "vue-component-meta", - "version": "2.2.2", + "version": "2.2.4", "license": "MIT", "files": [ "**/*.js", @@ -14,9 +14,9 @@ }, "dependencies": { "@volar/typescript": "~2.4.11", - "@vue/language-core": "2.2.2", + "@vue/language-core": "2.2.4", "path-browserify": "^1.0.1", - "vue-component-type-helpers": "2.2.2" + "vue-component-type-helpers": "2.2.4" }, "peerDependencies": { "typescript": "*" diff --git a/packages/component-type-helpers/package.json b/packages/component-type-helpers/package.json index 354d5b5912..21a6177dca 100644 --- a/packages/component-type-helpers/package.json +++ b/packages/component-type-helpers/package.json @@ -1,6 +1,6 @@ { "name": "vue-component-type-helpers", - "version": "2.2.2", + "version": "2.2.4", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-core/lib/codegen/globalTypes.ts b/packages/language-core/lib/codegen/globalTypes.ts index 5c6b22a285..0a996c24df 100644 --- a/packages/language-core/lib/codegen/globalTypes.ts +++ b/packages/language-core/lib/codegen/globalTypes.ts @@ -45,11 +45,7 @@ export function generateGlobalTypes({ const __VLS_unref: typeof import('${lib}').unref; const __VLS_placeholder: any; - const __VLS_nativeElements = { - ...{} as SVGElementTagNameMap, - ...{} as HTMLElementTagNameMap, - }; - + type __VLS_NativeElements = __VLS_SpreadMerge; type __VLS_IntrinsicElements = ${( target >= 3.3 ? `import('${lib}/jsx-runtime').JSX.IntrinsicElements;` @@ -68,7 +64,7 @@ export function generateGlobalTypes({ type __VLS_GlobalDirectives = import('${lib}').GlobalDirectives; type __VLS_IsAny = 0 extends 1 & T ? true : false; type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; - type __VLS_unknownDirective = (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void; + type __VLS_SpreadMerge = Omit & B; type __VLS_WithComponent = N1 extends keyof LocalComponents ? N1 extends N0 ? Pick : { [K in N0]: LocalComponents[N1] } : N2 extends keyof LocalComponents ? N2 extends N0 ? Pick : { [K in N0]: LocalComponents[N2] } : @@ -131,19 +127,12 @@ export function generateGlobalTypes({ }; type __VLS_UseTemplateRef = Readonly>; - function __VLS_getVForSourceType(source: number): [number, number][]; - function __VLS_getVForSourceType(source: string): [string, number][]; - function __VLS_getVForSourceType(source: T): [ - item: T[number], - index: number, - ][]; - function __VLS_getVForSourceType }>(source: T): [ - item: T extends { [Symbol.iterator](): Iterator } ? T1 : never, - index: number, - ][]; - // #3845 - function __VLS_getVForSourceType }>(source: T): [ - item: number | (Exclude extends { [Symbol.iterator](): Iterator } ? T1 : never), + function __VLS_getVForSourceType>(source: T): [ + item: T extends number ? number + : T extends string ? string + : T extends any[] ? T[number] + : T extends Iterable ? T1 + : any, index: number, ][]; function __VLS_getVForSourceType(source: T): [ @@ -159,22 +148,25 @@ export function generateGlobalTypes({ ? NonNullable : T extends (...args: any) => any ? T - : __VLS_unknownDirective; - function __VLS_withScope(ctx: T, scope: K): ctx is T & K; + : (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void; function __VLS_makeOptional(t: T): { [K in keyof T]?: T[K] }; function __VLS_asFunctionalComponent any ? InstanceType : unknown>(t: T, instance?: K): T extends new (...args: any) => any - ? (props: ${fnPropsType}, ctx?: any) => __VLS_Element & { __ctx?: { - attrs?: any, - slots?: K extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : any, - emit?: K extends { $emit: infer Emit } ? Emit : any - } & { props?: ${fnPropsType}; expose?(exposed: K): void; } } + ? (props: ${fnPropsType}, ctx?: any) => __VLS_Element & { + __ctx?: { + attrs?: any; + slots?: K extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : any; + emit?: K extends { $emit: infer Emit } ? Emit : any; + expose?(exposed: K): void; + props?: ${fnPropsType}; + } + } : T extends () => any ? (props: {}, ctx?: any) => ReturnType : T extends (...args: any) => any ? T : (_: {}${checkUnknownProps ? '' : ' & Record'}, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {}${checkUnknownProps ? '' : ' & Record'} } }; - function __VLS_asFunctionalElement(tag: T, endTag?: T): (_: T${checkUnknownComponents ? '' : ' & Record'}) => void; function __VLS_functionalComponentArgsRest any>(t: T): 2 extends Parameters['length'] ? [any] : []; - function __VLS_normalizeSlot(s: S): S extends () => infer R ? (props: {}) => R : S; + function __VLS_asFunctionalElement(tag: T, endTag?: T): (attrs: T${checkUnknownComponents ? '' : ' & Record'}) => void; + function __VLS_asFunctionalSlot(slot: S): (props: NonNullable extends (props: infer P) => any ? P : {}) => void; function __VLS_tryAsConstant(t: T): T; } `; diff --git a/packages/language-core/lib/codegen/script/component.ts b/packages/language-core/lib/codegen/script/component.ts index 83910fbf19..20a6d42858 100644 --- a/packages/language-core/lib/codegen/script/component.ts +++ b/packages/language-core/lib/codegen/script/component.ts @@ -37,16 +37,24 @@ export function* generateComponent( } yield* generatePropsOption(options, ctx, scriptSetup, scriptSetupRanges, !!emitOptionCodes.length, true); } - if (options.sfc.script && options.scriptRanges?.exportDefault?.args) { - const { args } = options.scriptRanges.exportDefault; - yield generateSfcBlockSection(options.sfc.script, args.start + 1, args.end - 1, codeFeatures.all); - } - if (options.vueCompilerOptions.target >= 3.5 && options.templateCodegen?.templateRefs.size) { + if ( + options.vueCompilerOptions.target >= 3.5 + && options.vueCompilerOptions.inferComponentDollarRefs + && options.templateCodegen?.templateRefs.size + ) { yield `__typeRefs: {} as __VLS_TemplateRefs,${newLine}`; } - if (options.vueCompilerOptions.target >= 3.5 && options.templateCodegen?.singleRootElType) { + if ( + options.vueCompilerOptions.target >= 3.5 + && options.vueCompilerOptions.inferComponentDollarEl + && options.templateCodegen?.singleRootElTypes.length + ) { yield `__typeEl: {} as __VLS_RootEl,${newLine}`; } + if (options.sfc.script && options.scriptRanges?.exportDefault?.args) { + const { args } = options.scriptRanges.exportDefault; + yield generateSfcBlockSection(options.sfc.script, args.start + 1, args.end - 1, codeFeatures.all); + } yield `})`; } diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index fac4679d7a..fd38b1fc86 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -12,7 +12,6 @@ import { generateComponentSelf } from './componentSelf'; import { createScriptCodegenContext, ScriptCodegenContext } from './context'; import { generateScriptSetup, generateScriptSetupImports } from './scriptSetup'; import { generateSrc } from './src'; -import { generateStyleModulesType } from './styleModulesType'; import { generateTemplate } from './template'; export interface ScriptCodegenOptions { @@ -54,14 +53,16 @@ export function* generateScript(options: ScriptCodegenOptions): Generator`; } - yield `>(${newLine}` + yield `(${newLine}` + ` __VLS_props: NonNullable>['props'],${newLine}` + ` __VLS_ctx?: ${ctx.localTypes.PrettifyLocal}>, 'attrs' | 'emit' | 'slots'>>,${newLine}` // use __VLS_Prettify for less dts code + ` __VLS_expose?: NonNullable>['expose'],${newLine}` @@ -167,16 +171,18 @@ function* generateSetupFunction( ]); } } - for (const { callExp } of scriptSetupRanges.useAttrs) { - setupCodeModifies.push([ - [`(`], - callExp.start, - callExp.start - ], [ - [` as typeof __VLS_special.$attrs)`], - callExp.end, - callExp.end - ]); + if (options.vueCompilerOptions.inferTemplateDollarAttrs) { + for (const { callExp } of scriptSetupRanges.useAttrs) { + setupCodeModifies.push([ + [`(`], + callExp.start, + callExp.start + ], [ + [` as typeof __VLS_dollars.$attrs)`], + callExp.end, + callExp.end + ]); + } } for (const { callExp, exp, arg } of scriptSetupRanges.useCssModule) { setupCodeModifies.push([ @@ -200,22 +206,24 @@ function* generateSetupFunction( ]); if (arg) { setupCodeModifies.push([ - [`(__VLS_placeholder)`], + [`__VLS_placeholder`], arg.start, arg.end ]); } } - for (const { callExp } of scriptSetupRanges.useSlots) { - setupCodeModifies.push([ - [`(`], - callExp.start, - callExp.start - ], [ - [` as typeof __VLS_special.$slots)`], - callExp.end, - callExp.end - ]); + if (options.vueCompilerOptions.inferTemplateDollarSlots) { + for (const { callExp } of scriptSetupRanges.useSlots) { + setupCodeModifies.push([ + [`(`], + callExp.start, + callExp.start + ], [ + [` as typeof __VLS_dollars.$slots)`], + callExp.end, + callExp.end + ]); + } } const isTs = options.lang !== 'js' && options.lang !== 'jsx'; for (const { callExp, exp, arg } of scriptSetupRanges.useTemplateRef) { @@ -254,7 +262,7 @@ function* generateSetupFunction( } if (arg) { setupCodeModifies.push([ - [`(__VLS_placeholder)`], + [`__VLS_placeholder`], arg.start, arg.end ]); @@ -294,7 +302,14 @@ function* generateSetupFunction( yield* generateComponentSelf(options, ctx, templateCodegenCtx); if (syntax) { - if (!options.vueCompilerOptions.skipTemplateCodegen && (options.templateCodegen?.hasSlot || scriptSetupRanges.defineSlots)) { + if ( + !options.vueCompilerOptions.skipTemplateCodegen + && ( + scriptSetupRanges.defineSlots + || options.templateCodegen?.slots.length + || options.templateCodegen?.dynamicSlots.length + ) + ) { yield `const __VLS_component = `; yield* generateComponent(options, ctx, scriptSetup, scriptSetupRanges); yield endOfLine; @@ -521,7 +536,7 @@ function* generateModelEmit( const [propName, localName] = getPropAndLocalName(scriptSetup, defineModel); yield `'update:${propName}': [value: `; yield* generateDefinePropType(scriptSetup, propName, localName, defineModel); - if (!defineModel.required) { + if (!defineModel.required && defineModel.defaultValue === undefined) { yield ` | undefined`; } yield `]${endOfLine}`; diff --git a/packages/language-core/lib/codegen/script/src.ts b/packages/language-core/lib/codegen/script/src.ts index a6d1989ebc..55cdbab735 100644 --- a/packages/language-core/lib/codegen/script/src.ts +++ b/packages/language-core/lib/codegen/script/src.ts @@ -1,54 +1,51 @@ -import type { Code, Sfc } from '../../types'; +import type { Code, SfcBlockAttr } from '../../types'; import { codeFeatures } from '../codeFeatures'; -import { endOfLine } from '../utils'; +import { endOfLine, generateSfcBlockAttrValue } from '../utils'; -export function* generateSrc( - script: NonNullable, - src: string -): Generator { - if (src.endsWith('.d.ts')) { - src = src.slice(0, -'.d.ts'.length); +export function* generateSrc(src: SfcBlockAttr): Generator { + if (src === true) { + return; } - else if (src.endsWith('.ts')) { - src = src.slice(0, -'.ts'.length); + let { text } = src; + + if (text.endsWith('.d.ts')) { + text = text.slice(0, -'.d.ts'.length); + } + else if (text.endsWith('.ts')) { + text = text.slice(0, -'.ts'.length); } - else if (src.endsWith('.tsx')) { - src = src.slice(0, -'.tsx'.length) + '.jsx'; + else if (text.endsWith('.tsx')) { + text = text.slice(0, -'.tsx'.length) + '.jsx'; } - if (!src.endsWith('.js') && !src.endsWith('.jsx')) { - src = src + '.js'; + if (!text.endsWith('.js') && !text.endsWith('.jsx')) { + text = text + '.js'; } yield `export * from `; - yield [ - `'${src}'`, - 'script', - script.srcOffset - 1, - { - ...codeFeatures.all, - 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')) { - newName = newName + '.d.ts'; - } - else if (script?.src?.endsWith('.ts')) { - newName = newName + '.ts'; - } - else if (script?.src?.endsWith('.tsx')) { - newName = newName + '.tsx'; - } - return newName; - }, + yield* generateSfcBlockAttrValue(src, text, { + ...codeFeatures.all, + navigation: text === src.text + ? true + : { + shouldRename: () => false, + resolveRenameEditText(newName) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { + newName = newName.split('.').slice(0, -1).join('.'); + } + if (src?.text.endsWith('.d.ts')) { + newName = newName + '.d.ts'; + } + else if (src?.text.endsWith('.ts')) { + newName = newName + '.ts'; + } + else if (src?.text.endsWith('.tsx')) { + newName = newName + '.tsx'; + } + return newName; }, - }, - ]; + }, + }); yield endOfLine; - yield `export { default } from '${src}'${endOfLine}`; + yield `export { default } from '${text}'${endOfLine}`; } diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index c9f8d2223d..56bc80aafb 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -1,7 +1,9 @@ import type { Code } from '../../types'; import { hyphenateTag } from '../../utils/shared'; import { codeFeatures } from '../codeFeatures'; -import { TemplateCodegenContext, createTemplateCodegenContext } from '../template/context'; +import { generateStyleModules } from '../style/modules'; +import { generateStyleScopedClasses } from '../style/scopedClasses'; +import { type TemplateCodegenContext, createTemplateCodegenContext } from '../template/context'; import { generateInterpolation } from '../template/interpolation'; import { generateStyleScopedClassReferences } from '../template/styleScopedClasses'; import { endOfLine, newLine } from '../utils'; @@ -115,6 +117,7 @@ function* generateTemplateBody( ): Generator { yield* generateStyleScopedClasses(options, templateCodegenCtx); yield* generateStyleScopedClassReferences(templateCodegenCtx, true); + yield* generateStyleModules(options); yield* generateCssVars(options, templateCodegenCtx); if (options.templateCodegen) { @@ -133,71 +136,6 @@ function* generateTemplateBody( } } -function* generateStyleScopedClasses( - options: ScriptCodegenOptions, - ctx: TemplateCodegenContext -): Generator { - const firstClasses = new Set(); - yield `type __VLS_StyleScopedClasses = {}`; - for (let i = 0; i < options.sfc.styles.length; i++) { - const style = options.sfc.styles[i]; - const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses; - if (option === 'always' || (option === 'scoped' && style.scoped)) { - for (const className of style.classNames) { - if (firstClasses.has(className.text)) { - ctx.scopedClasses.push({ - source: 'style_' + i, - className: className.text.slice(1), - offset: className.offset + 1 - }); - continue; - } - firstClasses.add(className.text); - yield* generateCssClassProperty( - i, - className.text, - className.offset, - 'boolean', - true - ); - } - } - } - yield endOfLine; -} - -export function* generateCssClassProperty( - styleIndex: number, - classNameWithDot: string, - offset: number, - propertyType: string, - optional: boolean -): Generator { - yield `${newLine} & { `; - yield [ - '', - 'style_' + styleIndex, - offset, - codeFeatures.navigation, - ]; - yield `'`; - yield [ - classNameWithDot.slice(1), - 'style_' + styleIndex, - offset + 1, - codeFeatures.navigation, - ]; - yield `'`; - yield [ - '', - 'style_' + styleIndex, - offset + classNameWithDot.length, - codeFeatures.navigationWithoutRename, - ]; - yield `${optional ? '?' : ''}: ${propertyType}`; - yield ` }`; -} - function* generateCssVars(options: ScriptCodegenOptions, ctx: TemplateCodegenContext): Generator { if (!options.sfc.styles.length) { return; diff --git a/packages/language-core/lib/codegen/style/classProperty.ts b/packages/language-core/lib/codegen/style/classProperty.ts new file mode 100644 index 0000000000..b1fe9ea2ce --- /dev/null +++ b/packages/language-core/lib/codegen/style/classProperty.ts @@ -0,0 +1,34 @@ +import type { Code } from '../../types'; +import { codeFeatures } from '../codeFeatures'; +import { newLine } from '../utils'; + +export function* generateClassProperty( + styleIndex: number, + classNameWithDot: string, + offset: number, + propertyType: string +): Generator { + yield `${newLine} & { `; + yield [ + '', + 'style_' + styleIndex, + offset, + codeFeatures.navigation, + ]; + yield `'`; + yield [ + classNameWithDot.slice(1), + 'style_' + styleIndex, + offset + 1, + codeFeatures.navigation, + ]; + yield `'`; + yield [ + '', + 'style_' + styleIndex, + offset + classNameWithDot.length, + codeFeatures.navigation, + ]; + yield `: ${propertyType}`; + yield ` }`; +} diff --git a/packages/language-core/lib/codegen/script/styleModulesType.ts b/packages/language-core/lib/codegen/style/modules.ts similarity index 55% rename from packages/language-core/lib/codegen/script/styleModulesType.ts rename to packages/language-core/lib/codegen/style/modules.ts index f501fdf490..c2c343c3d4 100644 --- a/packages/language-core/lib/codegen/script/styleModulesType.ts +++ b/packages/language-core/lib/codegen/style/modules.ts @@ -1,13 +1,11 @@ import type { Code } from '../../types'; import { codeFeatures } from '../codeFeatures'; +import type { ScriptCodegenOptions } from '../script'; import { endOfLine, newLine } from '../utils'; -import type { ScriptCodegenContext } from './context'; -import type { ScriptCodegenOptions } from './index'; -import { generateCssClassProperty } from './template'; +import { generateClassProperty } from './classProperty'; -export function* generateStyleModulesType( - options: ScriptCodegenOptions, - ctx: ScriptCodegenContext +export function* generateStyleModules( + options: ScriptCodegenOptions ): Generator { const styles = options.sfc.styles.map((style, i) => [style, i] as const).filter(([style]) => style.module); if (!styles.length && !options.scriptSetupRanges?.useCssModule.length) { @@ -15,29 +13,28 @@ export function* generateStyleModulesType( } yield `type __VLS_StyleModules = {${newLine}`; for (const [style, i] of styles) { - const { name, offset } = style.module!; - if (offset) { + if (style.module === true) { + yield `$style`; + } + else { + const { text, offset } = style.module!; yield [ - name, + text, 'main', - offset + 1, - codeFeatures.all + offset, + codeFeatures.withoutHighlight ]; } - else { - yield name; - } - yield `: Record & ${ctx.localTypes.PrettifyLocal}<{}`; + yield `: Record & __VLS_PrettifyGlobal<{}`; for (const className of style.classNames) { - yield* generateCssClassProperty( + yield* generateClassProperty( i, className.text, className.offset, - 'string', - false + 'string' ); } yield `>${endOfLine}`; } yield `}${endOfLine}`; -} \ No newline at end of file +} diff --git a/packages/language-core/lib/codegen/style/scopedClasses.ts b/packages/language-core/lib/codegen/style/scopedClasses.ts new file mode 100644 index 0000000000..455ee25781 --- /dev/null +++ b/packages/language-core/lib/codegen/style/scopedClasses.ts @@ -0,0 +1,41 @@ +import type { Code } from '../../types'; +import type { ScriptCodegenOptions } from '../script'; +import type { TemplateCodegenContext } from '../template/context'; +import { endOfLine } from '../utils'; +import { generateClassProperty } from './classProperty'; + +export function* generateStyleScopedClasses( + options: ScriptCodegenOptions, + ctx: TemplateCodegenContext +): Generator { + const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses; + const styles = options.sfc.styles + .map((style, i) => [style, i] as const) + .filter(([style]) => option === 'always' || (option === 'scoped' && style.scoped)); + if (!styles.length) { + return; + } + + const firstClasses = new Set(); + yield `type __VLS_StyleScopedClasses = {}`; + for (const [style, i] of styles) { + for (const className of style.classNames) { + if (firstClasses.has(className.text)) { + ctx.scopedClasses.push({ + source: 'style_' + i, + className: className.text.slice(1), + offset: className.offset + 1 + }); + continue; + } + firstClasses.add(className.text); + yield* generateClassProperty( + i, + className.text, + className.offset, + 'boolean' + ); + } + } + yield endOfLine; +} diff --git a/packages/language-core/lib/codegen/template/context.ts b/packages/language-core/lib/codegen/template/context.ts index e9e7ac3c84..cad062b01b 100644 --- a/packages/language-core/lib/codegen/template/context.ts +++ b/packages/language-core/lib/codegen/template/context.ts @@ -7,6 +7,103 @@ import type { TemplateCodegenOptions } from './index'; export type TemplateCodegenContext = ReturnType; +/** + * Creates and returns a Context object used for generating type-checkable TS code + * from the template section of a .vue file. + * + * ## Implementation Notes for supporting `@vue-ignore`, `@vue-expect-error`, and `@vue-skip` directives. + * + * Vue language tooling supports a number of directives for suppressing diagnostics within + * Vue templates (https://github.com/vuejs/language-tools/pull/3215) + * + * Here is an overview for how support for how @vue-expect-error is implemented within this file + * (@vue-expect-error is the most complicated directive to support due to its behavior of raising + * a diagnostic when it is annotating a piece of code that doesn't actually have any errors/warning/diagnostics). + * + * Given .vue code: + * + * ```vue + * + * + * + * ``` + * + * The above code should raise two diagnostics: + * + * 1. Property 'error_unknownProp' does not exist on type [...] + * 2. Unused '@ts-expect-error' directive.ts(2578) -- this is the bottom `@vue-expect-error` directive + * that covers code that doesn't actually raise an error -- note that all `@vue-...` directives + * will ultimately translate into `@ts-...` diagnostics. + * + * The above code will produce the following type-checkable TS code (note: omitting asterisks + * to prevent VSCode syntax double-greying out double-commented code). + * + * ```ts + * ( __VLS_ctx.knownProp1 ); + * ( __VLS_ctx.error_unknownProp ); // ERROR: Property 'error_unknownProp' does not exist on type [...] + * ( __VLS_ctx.knownProp2 ); + * // @vue-expect-error start + * ( __VLS_ctx.suppressed_error_unknownProp ); + * // @ts-expect-error __VLS_TS_EXPECT_ERROR + * ; + * // @vue-expect-error end of INTERPOLATION + * ( __VLS_ctx.knownProp3 ); + * // @vue-expect-error start + * ( __VLS_ctx.knownProp4_will_trigger_unused_expect_error ); + * // @ts-expect-error __VLS_TS_EXPECT_ERROR + * ; + * // @vue-expect-error end of INTERPOLATION + * ``` + * + * In the generated code, there are actually 3 diagnostic errors that'll be raised in the first + * pass on this generated code (but through cleverness described below, not all of them will be + * propagated back to the original .vue file): + * + * 1. Property 'error_unknownProp' does not exist on type [...] + * 2. Unused '@ts-expect-error' directive.ts(2578) from the 1st `@ts-expect-error __VLS_TS_EXPECT_ERROR` + * 3. Unused '@ts-expect-error' directive.ts(2578) from the 2nd `@ts-expect-error __VLS_TS_EXPECT_ERROR` + * + * Be sure to pay careful attention to the mixture of `@vue-expect-error` and `@ts-expect-error`; + * Within the TS file, the only "real" directives recognized by TS are going to be prefixed with `@ts-`; + * any `@vue-` prefixed directives in the comments are only for debugging purposes. + * + * As mentioned above, there are 3 diagnostics errors that'll be generated for the above code, but + * only 2 should be propagated back to the original .vue file. + * + * (The reason we structure things this way is somewhat complicated, but in short it allows us + * to lean on TS as much as possible to generate actual `unused @ts-expect-error directive` errors + * while covering a number of edge cases.) + * + * So, we need a way to dynamically decide whether each of the `@ts-expect-error __VLS_TS_EXPECT_ERROR` + * directives should be reported as an unused directive or not. + * + * To do this, we'll make use of the `shouldReport` callback that'll optionally be provided to the + * `verification` property of the `CodeInformation` object attached to the mapping between source .vue + * and generated .ts code. The `verification` property determines whether "verification" (which includes + * semantic diagnostics) should be performed on the generated .ts code, and `shouldReport`, if provided, + * can be used to determine whether a given diagnostic should be reported back "upwards" to the original + * .vue file or not. + * + * See the comments in the code below for how and where we use this hook to keep track of whether + * an error/diagnostic was encountered for a region of code covered by a `@vue-expect-error` directive, + * and additionally how we use that to determine whether to propagate diagnostics back upward. + */ export function createTemplateCodegenContext(options: Pick) { let ignoredError = false; let expectErrorToken: { @@ -22,12 +119,18 @@ export function createTemplateCodegenContext(options: Pick(); const localVars = new Map(); - const specialVars = new Set(); + const dollarVars = new Set(); const accessExternalVariables = new Map>(); const slots: { name: string; @@ -67,7 +171,10 @@ export function createTemplateCodegenContext(options: Pick(); - const templateRefs = new Map(); + const templateRefs = new Map(); return { codeFeatures: new Proxy(codeFeatures, { @@ -79,14 +186,13 @@ export function createTemplateCodegenContext(options: Pick(), accessExternalVariable(name: string, offset?: number) { let arr = accessExternalVariables.get(name); if (!arr) { @@ -117,6 +223,24 @@ export function createTemplateCodegenContext(options: Pick { return `__VLS_${variableId++}`; }, + getHoistVariable: (originalVar: string) => { + let name = hoistVars.get(originalVar); + if (name === undefined) { + hoistVars.set(originalVar, name = `__VLS_${variableId++}`); + } + return name; + }, + generateHoistVariables: function* () { + // trick to avoid TS 4081 (#5186) + if (hoistVars.size) { + yield `// @ts-ignore${newLine}`; + yield `var `; + for (const [originalVar, hoistVar] of hoistVars) { + yield `${hoistVar} = ${originalVar}, `; + } + yield endOfLine; + } + }, ignoreError: function* (): Generator { if (!ignoredError) { ignoredError = true; @@ -140,6 +264,9 @@ export function createTemplateCodegenContext(options: Pick token.errors === 0, }, }, diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index 2d8b94a677..38aba2e045 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -14,14 +14,15 @@ import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generatePropertyAccess } from './propertyAccess'; import { collectStyleScopedClassReferences } from './styleScopedClasses'; -import { generateVSlot } from './vSlot'; +import { generateImplicitDefaultSlot, generateVSlot } from './vSlot'; const colonReg = /:/g; export function* generateComponent( options: TemplateCodegenOptions, ctx: TemplateCodegenContext, - node: CompilerDOM.ElementNode + node: CompilerDOM.ElementNode, + isVForChild: boolean ): Generator { const tagOffsets = [node.loc.start.offset + options.template.content.slice(node.loc.start.offset).indexOf(node.tag)]; if (!node.isSelfClosing && options.template.lang === 'html') { @@ -33,16 +34,14 @@ export function* generateComponent( const failedPropExps: FailedPropExpression[] = []; const possibleOriginalNames = getPossibleOriginalComponentNames(node.tag, true); const matchImportName = possibleOriginalNames.find(name => options.scriptSetupImportComponentNames.has(name)); - const var_originalComponent = matchImportName ?? ctx.getInternalVariable(); - const var_functionalComponent = ctx.getInternalVariable(); - const var_componentInstance = ctx.getInternalVariable(); - const var_componentEmit = ctx.getInternalVariable(); - const var_componentEvents = ctx.getInternalVariable(); - const var_defineComponentCtx = ctx.getInternalVariable(); + const componentOriginalVar = matchImportName ?? ctx.getInternalVariable(); + const componentFunctionalVar = ctx.getInternalVariable(); + const componentVNodeVar = ctx.getInternalVariable(); + const componentCtxVar = ctx.getInternalVariable(); const isComponentTag = node.tag.toLowerCase() === 'component'; ctx.currentComponent = { - ctxVar: var_defineComponentCtx, + ctxVar: componentCtxVar, used: false }; @@ -84,14 +83,13 @@ export function* generateComponent( } if (matchImportName) { - // hover, renaming / find references support - yield `// @ts-ignore${newLine}`; // #2304 - yield `/** @type { [`; + // navigation support + yield `/** @type {[`; for (const tagOffset of tagOffsets) { yield `typeof `; - if (var_originalComponent === node.tag) { + if (componentOriginalVar === node.tag) { yield [ - var_originalComponent, + componentOriginalVar, 'template', tagOffset, ctx.codeFeatures.withoutHighlightAndCompletion, @@ -113,10 +111,10 @@ export function* generateComponent( } yield `, `; } - yield `] } */${endOfLine}`; + yield `]} */${endOfLine}`; } else if (dynamicTagInfo) { - yield `const ${var_originalComponent} = (`; + yield `const ${componentOriginalVar} = (`; yield* generateInterpolation( options, ctx, @@ -125,8 +123,8 @@ export function* generateComponent( dynamicTagInfo.tag, dynamicTagInfo.offsets[0], dynamicTagInfo.astHolder, - '(', - ')' + `(`, + `)` ); if (dynamicTagInfo.offsets[1] !== undefined) { yield `,`; @@ -138,14 +136,14 @@ export function* generateComponent( dynamicTagInfo.tag, dynamicTagInfo.offsets[1], dynamicTagInfo.astHolder, - '(', - ')' + `(`, + `)` ); } yield `)${endOfLine}`; } else if (!isComponentTag) { - yield `const ${var_originalComponent} = ({} as __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', __VLS_LocalComponents, `; + yield `const ${componentOriginalVar} = ({} as __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', __VLS_LocalComponents, `; if (options.selfComponentName && possibleOriginalNames.includes(options.selfComponentName)) { yield `typeof __VLS_self & (new () => { ` + getSlotsPropertyName(options.vueCompilerOptions.target) @@ -167,8 +165,8 @@ export function* generateComponent( const camelizedTag = camelize(node.tag); if (variableNameRegex.test(camelizedTag)) { - // renaming / find references support - yield `/** @type { [`; + // navigation support + yield `/** @type {[`; for (const tagOffset of tagOffsets) { for (const shouldCapitalize of (node.tag[0] === node.tag[0].toUpperCase() ? [false] : [true, false])) { const expectName = shouldCapitalize ? capitalize(camelizedTag) : camelizedTag; @@ -186,7 +184,7 @@ export function* generateComponent( yield `, `; } } - yield `] } */${endOfLine}`; + yield `]} */${endOfLine}`; // auto import support if (options.edited) { yield `// @ts-ignore${newLine}`; // #2304 @@ -205,11 +203,11 @@ export function* generateComponent( } } else { - yield `const ${var_originalComponent} = {} as any${endOfLine}`; + yield `const ${componentOriginalVar} = {} as any${endOfLine}`; } yield `// @ts-ignore${newLine}`; - yield `const ${var_functionalComponent} = __VLS_asFunctionalComponent(${var_originalComponent}, new ${var_originalComponent}({${newLine}`; + yield `const ${componentFunctionalVar} = __VLS_asFunctionalComponent(${componentOriginalVar}, new ${componentOriginalVar}({${newLine}`; yield* generateElementProps( options, ctx, @@ -227,13 +225,14 @@ export function* generateComponent( ctx.resolveCodeFeatures({ verification: { shouldReport(_source, code) { + // https://typescript.tv/errors/#ts6133 return String(code) !== '6133'; }, } }), - var_componentInstance + componentVNodeVar ); - yield ` = ${var_functionalComponent}`; + yield ` = ${componentFunctionalVar}`; yield* generateComponentGeneric(ctx); yield `(`; yield* wrapWith( @@ -252,57 +251,57 @@ export function* generateComponent( ), `}` ); - yield `, ...__VLS_functionalComponentArgsRest(${var_functionalComponent}))${endOfLine}`; + yield `, ...__VLS_functionalComponentArgsRest(${componentFunctionalVar}))${endOfLine}`; yield* generateFailedPropExps(options, ctx, failedPropExps); + yield* generateElementEvents(options, ctx, node, componentFunctionalVar, componentVNodeVar, componentCtxVar); + yield* generateElementDirectives(options, ctx, node); - const [refName, offset] = yield* generateVScope(options, ctx, node, props); - const isRootNode = node === ctx.singleRootNode; + const [refName, offset] = yield* generateElementReference(options, ctx, node); + const tag = hyphenateTag(node.tag); + const isRootNode = ctx.singleRootNodes.has(node) && !options.vueCompilerOptions.fallthroughComponentNames.includes(tag); if (refName || isRootNode) { - const varName = ctx.getInternalVariable(); + const componentInstanceVar = ctx.getInternalVariable(); ctx.currentComponent.used = true; - yield `var ${varName} = {} as (Parameters>[0] | null)`; - if (node.codegenNode?.type === CompilerDOM.NodeTypes.VNODE_CALL - && node.codegenNode.props?.type === CompilerDOM.NodeTypes.JS_OBJECT_EXPRESSION - && node.codegenNode.props.properties.some(({ key }) => key.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && key.content === 'ref_for') - ) { + yield `var ${componentInstanceVar} = {} as (Parameters>[0] | null)`; + if (isVForChild) { yield `[]`; } yield `${endOfLine}`; - if (refName) { - ctx.templateRefs.set(refName, [varName, offset!]); + if (refName && offset) { + ctx.templateRefs.set(refName, { + typeExp: `typeof ${ctx.getHoistVariable(componentInstanceVar)}`, + offset + }); } if (isRootNode) { - ctx.singleRootElType = `NonNullable['$el']`; + ctx.singleRootElTypes.push(`NonNullable['$el']`); } } - const usedComponentEventsVar = yield* generateElementEvents(options, ctx, node, var_functionalComponent, var_componentInstance, var_componentEvents); - if (usedComponentEventsVar) { - ctx.currentComponent.used = true; - yield `let ${var_componentEmit}!: typeof ${var_defineComponentCtx}.emit${endOfLine}`; - yield `let ${var_componentEvents}!: __VLS_NormalizeEmits${endOfLine}`; - } - if (hasVBindAttrs(options, ctx, node)) { const attrsVar = ctx.getInternalVariable(); + yield `let ${attrsVar}!: Parameters[0]${endOfLine}`; ctx.inheritedAttrVars.add(attrsVar); - yield `let ${attrsVar}!: Parameters[0];\n`; } + collectStyleScopedClassReferences(options, ctx, node); + const slotDir = node.props.find(p => p.type === CompilerDOM.NodeTypes.DIRECTIVE && p.name === 'slot') as CompilerDOM.DirectiveNode; if (slotDir) { yield* generateVSlot(options, ctx, node, slotDir); } else { - yield* generateElementChildren(options, ctx, node, true); + // #932: reference for default slot + yield* generateImplicitDefaultSlot(ctx, node); + yield* generateElementChildren(options, ctx, node); } if (ctx.currentComponent.used) { - yield `var ${var_defineComponentCtx}!: __VLS_PickFunctionalComponentCtx${endOfLine}`; + yield `var ${componentCtxVar}!: __VLS_PickFunctionalComponentCtx${endOfLine}`; } } @@ -356,23 +355,29 @@ export function* generateElement( yield `)${endOfLine}`; yield* generateFailedPropExps(options, ctx, failedPropExps); + yield* generateElementDirectives(options, ctx, node); - const [refName, offset] = yield* generateVScope(options, ctx, node, node.props); - if (refName) { - let refValue = `__VLS_nativeElements['${node.tag}']`; + const [refName, offset] = yield* generateElementReference(options, ctx, node); + if (refName && offset) { + let typeExp = `__VLS_NativeElements['${node.tag}']`; if (isVForChild) { - refValue = `[${refValue}]`; + typeExp += `[]`; } - ctx.templateRefs.set(refName, [refValue, offset!]); + ctx.templateRefs.set(refName, { + typeExp, + offset + }); } - if (ctx.singleRootNode === node) { - ctx.singleRootElType = `typeof __VLS_nativeElements['${node.tag}']`; + if (ctx.singleRootNodes.has(node)) { + ctx.singleRootElTypes.push(`__VLS_NativeElements['${node.tag}']`); } if (hasVBindAttrs(options, ctx, node)) { ctx.inheritedAttrVars.add(`__VLS_intrinsicElements.${node.tag}`); } + collectStyleScopedClassReferences(options, ctx, node); + yield* generateElementChildren(options, ctx, node); } @@ -397,45 +402,6 @@ function* generateFailedPropExps( } } -function* generateVScope( - options: TemplateCodegenOptions, - ctx: TemplateCodegenContext, - node: CompilerDOM.ElementNode, - props: (CompilerDOM.AttributeNode | CompilerDOM.DirectiveNode)[] -): Generator { - const vScope = props.find(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && (prop.name === 'scope' || prop.name === 'data')); - let inScope = false; - let originalConditionsNum = ctx.blockConditions.length; - - if (vScope?.type === CompilerDOM.NodeTypes.DIRECTIVE && vScope.exp) { - - const scopeVar = ctx.getInternalVariable(); - const condition = `__VLS_withScope(__VLS_ctx, ${scopeVar})`; - - yield `const ${scopeVar} = `; - yield [ - vScope.exp.loc.source, - 'template', - vScope.exp.loc.start.offset, - ctx.codeFeatures.all, - ]; - yield endOfLine; - yield `if (${condition}) {${newLine}`; - ctx.blockConditions.push(condition); - inScope = true; - } - - yield* generateElementDirectives(options, ctx, node); - const [refName, offset] = yield* generateReferencesForElements(options, ctx, node); // - collectStyleScopedClassReferences(options, ctx, node); - - if (inScope) { - yield `}${newLine}`; - ctx.blockConditions.length = originalConditionsNum; - } - return [refName, offset]; -} - function getCanonicalComponentName(tagText: string) { return variableNameRegex.test(tagText) ? tagText @@ -491,7 +457,7 @@ function* generateComponentGeneric( ctx.lastGenericComment = undefined; } -function* generateReferencesForElements( +function* generateElementReference( options: TemplateCodegenOptions, ctx: TemplateCodegenContext, node: CompilerDOM.ElementNode @@ -504,8 +470,8 @@ function* generateReferencesForElements( ) { const [content, startOffset] = normalizeAttributeValue(prop.value); - yield `// @ts-ignore navigation for \`const ${content} = ref()\`${newLine}`; - yield `/** @type { typeof __VLS_ctx`; + // navigation support for `const foo = ref()` + yield `/** @type {typeof __VLS_ctx`; yield* generatePropertyAccess( options, ctx, @@ -514,7 +480,7 @@ function* generateReferencesForElements( ctx.codeFeatures.navigation, prop.value.loc ); - yield ` } */${endOfLine}`; + yield `} */${endOfLine}`; if (variableNameRegex.test(content) && !options.templateRefNames.has(content)) { ctx.accessExternalVariable(content, startOffset); @@ -532,7 +498,7 @@ function hasVBindAttrs( node: CompilerDOM.ElementNode ) { return options.vueCompilerOptions.fallthroughAttributes && ( - node === ctx.singleRootNode || + (options.inheritAttrs && ctx.singleRootNodes.has(node)) || node.props.some(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'bind' diff --git a/packages/language-core/lib/codegen/template/elementChildren.ts b/packages/language-core/lib/codegen/template/elementChildren.ts index f49265e5c7..65de04b9ce 100644 --- a/packages/language-core/lib/codegen/template/elementChildren.ts +++ b/packages/language-core/lib/codegen/template/elementChildren.ts @@ -1,6 +1,5 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type { Code } from '../../types'; -import { endOfLine, wrapWith } from '../utils'; import type { TemplateCodegenContext } from './context'; import type { TemplateCodegenOptions } from './index'; import { generateTemplateChild } from './templateChild'; @@ -8,8 +7,7 @@ import { generateTemplateChild } from './templateChild'; export function* generateElementChildren( options: TemplateCodegenOptions, ctx: TemplateCodegenContext, - node: CompilerDOM.ElementNode, - isDefaultSlot: boolean = false + node: CompilerDOM.ElementNode ): Generator { yield* ctx.resetDirectiveComments('end of element children start'); let prev: CompilerDOM.TemplateChildNode | undefined; @@ -18,22 +16,4 @@ export function* generateElementChildren( prev = childNode; } yield* ctx.generateAutoImportCompletion(); - - // fix https://github.com/vuejs/language-tools/issues/932 - if ( - ctx.currentComponent - && isDefaultSlot - && node.children.length - && node.tagType === CompilerDOM.ElementTypes.COMPONENT - ) { - ctx.currentComponent.used = true; - yield `${ctx.currentComponent.ctxVar}.slots!.`; - yield* wrapWith( - node.children[0].loc.start.offset, - node.children[node.children.length - 1].loc.end.offset, - ctx.codeFeatures.navigation, - `default` - ); - yield endOfLine; - } } diff --git a/packages/language-core/lib/codegen/template/elementDirectives.ts b/packages/language-core/lib/codegen/template/elementDirectives.ts index e861c8220c..94630b05b3 100644 --- a/packages/language-core/lib/codegen/template/elementDirectives.ts +++ b/packages/language-core/lib/codegen/template/elementDirectives.ts @@ -7,6 +7,7 @@ import { endOfLine, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import { generateStringLiteralKey } from '../utils/stringLiteralKey'; import type { TemplateCodegenContext } from './context'; +import { generatePropExp } from './elementProps'; import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generateObjectProperty } from './objectProperty'; @@ -176,21 +177,12 @@ function* generateValue( `value` ); yield `: `; - yield* wrapWith( - exp.loc.start.offset, - exp.loc.end.offset, - ctx.codeFeatures.verification, - ...generateInterpolation( - options, - ctx, - 'template', - ctx.codeFeatures.all, - exp.content, - exp.loc.start.offset, - exp.loc, - `(`, - `)` - ) + yield* generatePropExp( + options, + ctx, + prop, + exp, + ctx.codeFeatures.all ); } diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts index e6ec49a56a..97ce5f20ac 100644 --- a/packages/language-core/lib/codegen/template/elementEvents.ts +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -12,11 +12,12 @@ export function* generateElementEvents( options: TemplateCodegenOptions, ctx: TemplateCodegenContext, node: CompilerDOM.ElementNode, - componentVar: string, - componentInstanceVar: string, - eventsVar: string -): Generator { - let usedComponentEventsVar = false; + componentFunctionalVar: string, + componentVNodeVar: string, + componentCtxVar: string +): Generator { + let emitVar: string | undefined; + let eventsVar: string | undefined; let propsVar: string | undefined; for (const prop of node.props) { if ( @@ -26,10 +27,14 @@ export function* generateElementEvents( && !prop.arg.loc.source.startsWith('[') && !prop.arg.loc.source.endsWith(']') ) { - usedComponentEventsVar = true; - if (!propsVar) { + ctx.currentComponent!.used = true; + if (!emitVar) { + emitVar = ctx.getInternalVariable(); + eventsVar = ctx.getInternalVariable(); propsVar = ctx.getInternalVariable(); - yield `let ${propsVar}!: __VLS_FunctionalComponentProps${endOfLine}`; + yield `let ${emitVar}!: typeof ${componentCtxVar}.emit${endOfLine}`; + yield `let ${eventsVar}!: __VLS_NormalizeEmits${endOfLine}`; + yield `let ${propsVar}!: __VLS_FunctionalComponentProps${endOfLine}`; } let source = prop.arg.loc.source; let start = prop.arg.loc.start.offset; @@ -48,7 +53,6 @@ export function* generateElementEvents( yield `}${endOfLine}`; } } - return usedComponentEventsVar; } export function* generateEventArg( @@ -94,8 +98,8 @@ export function* generateEventExpression( prop: CompilerDOM.DirectiveNode ): Generator { if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - let prefix = '('; - let suffix = ')'; + let prefix = `(`; + let suffix = `)`; let isFirstMapping = true; const ast = createTsAst(options.ts, prop.exp, prop.exp.content); @@ -104,10 +108,10 @@ export function* generateEventExpression( yield `(...[$event]) => {${newLine}`; ctx.addLocalVariable('$event'); - prefix = ''; - suffix = ''; + prefix = ``; + suffix = ``; for (const blockCondition of ctx.blockConditions) { - prefix += `if (!(${blockCondition})) return${endOfLine}`; + prefix += `if (!${blockCondition}) return${endOfLine}`; } } diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 01ed50a01c..de35e27dc3 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -61,14 +61,14 @@ export function* generateElementProps( && prop.arg.loc.source.startsWith('[') && prop.arg.loc.source.endsWith(']') ) { - failedPropExps?.push({ node: prop.arg, prefix: '(', suffix: ')' }); - failedPropExps?.push({ node: prop.exp, prefix: '() => {', suffix: '}' }); + failedPropExps?.push({ node: prop.arg, prefix: `(`, suffix: `)` }); + failedPropExps?.push({ node: prop.exp, prefix: `() => {`, suffix: `}` }); } else if ( !prop.arg && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION ) { - failedPropExps?.push({ node: prop.exp, prefix: '(', suffix: ')' }); + failedPropExps?.push({ node: prop.exp, prefix: `(`, suffix: `)` }); } } } @@ -98,7 +98,7 @@ export function* generateElementProps( || options.vueCompilerOptions.dataAttributes.some(pattern => minimatch(propName!, pattern)) ) { if (prop.exp && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY) { - failedPropExps?.push({ node: prop.exp, prefix: '(', suffix: ')' }); + failedPropExps?.push({ node: prop.exp, prefix: `(`, suffix: `)` }); } continue; } @@ -139,7 +139,7 @@ export function* generateElementProps( propName ) ), - `: (`, + `: `, ...generatePropExp( options, ctx, @@ -147,8 +147,7 @@ export function* generateElementProps( prop.exp, ctx.codeFeatures.all, enableCodeFeatures - ), - `)` + ) ); if (enableCodeFeatures) { yield* codes; @@ -215,13 +214,12 @@ export function* generateElementProps( (prop.loc as any).name_1 ??= {}, shouldCamelize ), - `: (`, + `: `, ...( prop.value ? generateAttrValue(prop.value, ctx.codeFeatures.withoutNavigation) : [`true`] - ), - `)` + ) ); if (enableCodeFeatures) { yield* codes; @@ -278,7 +276,7 @@ export function* generatePropExp( prop: CompilerDOM.DirectiveNode, exp: CompilerDOM.SimpleExpressionNode | undefined, features: VueCodeInformation, - enableCodeFeatures: boolean + enableCodeFeatures: boolean = true ): Generator { const isShorthand = prop.arg?.loc.start.offset === prop.exp?.loc.start.offset; @@ -298,8 +296,8 @@ export function* generatePropExp( exp.loc.source, exp.loc.start.offset, exp.loc, - '(', - ')' + `(`, + `)` ); } else { @@ -389,6 +387,8 @@ function getPropsCodeInfo( }, verification: strictPropsCheck || { shouldReport(_source, code) { + // https://typescript.tv/errors/#ts2353 + // https://typescript.tv/errors/#ts2561 if (String(code) === '2353' || String(code) === '2561') { return false; } diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index e329204109..8776db6c17 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -34,17 +34,29 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator`; for (const { expVar, propsVar } of ctx.dynamicSlots) { - ctx.hasSlot = true; yield `${newLine}& { [K in NonNullable]?: (props: typeof ${propsVar}) => any }`; } for (const slot of ctx.slots) { yield `${newLine}& { `; - ctx.hasSlot = true; if (slot.name && slot.offset !== undefined) { yield* generateObjectProperty( options, @@ -114,7 +123,7 @@ function* generateInheritedAttrs( if (ctx.bindingAttrLocs.length) { yield `[`; for (const loc of ctx.bindingAttrLocs) { - yield `__VLS_special.`; + yield `__VLS_dollars.`; yield [ loc.source, 'template', @@ -133,7 +142,7 @@ function* generateTemplateRefs( ctx: TemplateCodegenContext ): Generator { yield `type __VLS_TemplateRefs = {${newLine}`; - for (const [name, [varName, offset]] of ctx.templateRefs) { + for (const [name, { typeExp, offset }] of ctx.templateRefs) { yield* generateObjectProperty( options, ctx, @@ -141,7 +150,7 @@ function* generateTemplateRefs( offset, ctx.codeFeatures.navigationAndCompletion ); - yield `: typeof ${varName},${newLine}`; + yield `: ${typeExp},${newLine}`; } yield `}${endOfLine}`; return `__VLS_TemplateRefs`; @@ -151,7 +160,14 @@ function* generateRootEl( ctx: TemplateCodegenContext ): Generator { yield `type __VLS_RootEl = `; - yield ctx.singleRootElType ?? `any`; + if (ctx.singleRootElTypes.length && !ctx.singleRootNodes.has(null)) { + for (const type of ctx.singleRootElTypes) { + yield `${newLine}| ${type}`; + } + } + else { + yield `any`; + } yield endOfLine; return `__VLS_RootEl`; } diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index f305d90d63..86fee0b6be 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -134,7 +134,7 @@ function* forEachInterpolationSegment( const curVar = ctxVars[i]; const nextVar = ctxVars[i + 1]; - yield* generateVar(code, ctx.specialVars, destructuredPropNames, templateRefNames, curVar); + yield* generateVar(code, ctx.dollarVars, destructuredPropNames, templateRefNames, curVar); if (nextVar.isShorthand) { yield [code.slice(curVar.offset + curVar.text.length, nextVar.offset + nextVar.text.length), curVar.offset + curVar.text.length]; @@ -146,7 +146,7 @@ function* forEachInterpolationSegment( } const lastVar = ctxVars.at(-1)!; - yield* generateVar(code, ctx.specialVars, destructuredPropNames, templateRefNames, lastVar); + yield* generateVar(code, ctx.dollarVars, destructuredPropNames, templateRefNames, lastVar); if (lastVar.offset + lastVar.text.length < code.length) { yield [code.slice(lastVar.offset + lastVar.text.length), lastVar.offset + lastVar.text.length, 'endText']; } @@ -158,7 +158,7 @@ function* forEachInterpolationSegment( function* generateVar( code: string, - specialVars: Set, + dollarVars: Set, destructuredPropNames: Set | undefined, templateRefNames: Set | undefined, curVar: CtxVar @@ -175,8 +175,8 @@ function* generateVar( yield [`)`, undefined]; } else { - if (specialVars.has(curVar.text)) { - yield [`__VLS_special.`, undefined]; + if (dollarVars.has(curVar.text)) { + yield [`__VLS_dollars.`, undefined]; } else if (!isDestructuredProp) { yield [`__VLS_ctx.`, undefined]; diff --git a/packages/language-core/lib/codegen/template/slotOutlet.ts b/packages/language-core/lib/codegen/template/slotOutlet.ts index 3d42e0353b..e4dfe97936 100644 --- a/packages/language-core/lib/codegen/template/slotOutlet.ts +++ b/packages/language-core/lib/codegen/template/slotOutlet.ts @@ -15,6 +15,7 @@ export function* generateSlotOutlet( node: CompilerDOM.SlotOutletNode ): Generator { const startTagOffset = node.loc.start.offset + options.template.content.slice(node.loc.start.offset).indexOf(node.tag); + const startTagEndOffset = startTagOffset + node.tag.length; const propsVar = ctx.getInternalVariable(); const nameProp = node.props.find(prop => { if (prop.type === CompilerDOM.NodeTypes.ATTRIBUTE) { @@ -30,7 +31,7 @@ export function* generateSlotOutlet( }); if (options.hasDefineSlots) { - yield `__VLS_normalizeSlot(`; + yield `__VLS_asFunctionalSlot(`; if (nameProp) { let codes: Generator | Code[]; if (nameProp.type === CompilerDOM.NodeTypes.ATTRIBUTE && nameProp.value) { @@ -58,8 +59,7 @@ export function* generateSlotOutlet( ctx, nameProp, nameProp.exp, - ctx.codeFeatures.all, - true + ctx.codeFeatures.all ), `]` ]; @@ -78,16 +78,23 @@ export function* generateSlotOutlet( } else { yield* wrapWith( - node.loc.start.offset, - node.loc.end.offset, + startTagOffset, + startTagEndOffset, ctx.codeFeatures.verification, - `${options.slotsAssignName ?? '__VLS_slots'}['default']` + `${options.slotsAssignName ?? '__VLS_slots'}[`, + ...wrapWith( + startTagOffset, + startTagEndOffset, + ctx.codeFeatures.verification, + `'default'` + ), + `]` ); } - yield `)?.(`; + yield `)(`; yield* wrapWith( startTagOffset, - startTagOffset + node.tag.length, + startTagEndOffset, ctx.codeFeatures.verification, `{${newLine}`, ...generateElementProps( @@ -123,7 +130,7 @@ export function* generateSlotOutlet( offset: nameProp.loc.start.offset + nameProp.loc.source.indexOf(nameProp.value.content, nameProp.name.length), tagRange: [startTagOffset, startTagOffset + node.tag.length], nodeLoc: node.loc, - propsVar, + propsVar: ctx.getHoistVariable(propsVar), }); } else if ( @@ -147,16 +154,16 @@ export function* generateSlotOutlet( ); yield `)${endOfLine}`; ctx.dynamicSlots.push({ - expVar, - propsVar, + expVar: ctx.getHoistVariable(expVar), + propsVar: ctx.getHoistVariable(propsVar), }); } else { ctx.slots.push({ name: 'default', - tagRange: [startTagOffset, startTagOffset + node.tag.length], + tagRange: [startTagOffset, startTagEndOffset], nodeLoc: node.loc, - propsVar, + propsVar: ctx.getHoistVariable(propsVar), }); } } diff --git a/packages/language-core/lib/codegen/template/styleScopedClasses.ts b/packages/language-core/lib/codegen/template/styleScopedClasses.ts index 43f2a4ce9b..17d8f55135 100644 --- a/packages/language-core/lib/codegen/template/styleScopedClasses.ts +++ b/packages/language-core/lib/codegen/template/styleScopedClasses.ts @@ -10,22 +10,18 @@ export function* generateStyleScopedClassReferences( ctx: TemplateCodegenContext, withDot = false ): Generator { - if (!ctx.emptyClassOffsets.length && !ctx.scopedClasses.length) { - return; - } - - yield `[`; for (const offset of ctx.emptyClassOffsets) { - yield `'`; + yield `/** @type {__VLS_StyleScopedClasses['`; yield [ '', 'template', offset, ctx.codeFeatures.additionalCompletion, ]; - yield `', `; + yield `']} */${endOfLine}`; } for (const { source, className, offset } of ctx.scopedClasses) { + yield `/** @type {__VLS_StyleScopedClasses[`; yield [ '', source, @@ -41,11 +37,10 @@ export function* generateStyleScopedClassReferences( '', source, offset + className.length, - ctx.codeFeatures.navigationWithoutRename, + ctx.codeFeatures.navigation, ]; - yield `, `; + yield `]} */${endOfLine}`; } - yield `] as (keyof __VLS_StyleScopedClasses)[]${endOfLine}`; function* escapeString(source: string, className: string, offset: number, escapeTargets: string[]): Generator { let count = 0; diff --git a/packages/language-core/lib/codegen/template/templateChild.ts b/packages/language-core/lib/codegen/template/templateChild.ts index 922c33e6cc..8d3bb6518e 100644 --- a/packages/language-core/lib/codegen/template/templateChild.ts +++ b/packages/language-core/lib/codegen/template/templateChild.ts @@ -1,5 +1,6 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type { Code } from '../../types'; +import { hyphenateTag } from '../../utils/shared'; import { endOfLine, newLine } from '../utils'; import type { TemplateCodegenContext } from './context'; import { generateComponent, generateElement } from './element'; @@ -10,6 +11,8 @@ import { generateVFor } from './vFor'; import { generateVIf } from './vIf'; import { generateVSlot } from './vSlot'; +const commentDirectiveRegex = /^$/; + // @ts-ignore const transformContext: CompilerDOM.TransformContext = { onError: () => { }, @@ -34,41 +37,46 @@ export function* generateTemplateChild( isVForChild: boolean = false ): Generator { if (prevNode?.type === CompilerDOM.NodeTypes.COMMENT) { - const commentText = prevNode.content.trim().split(' ')[0]; - if (/^@vue-skip\b[\s\S]*/.test(commentText)) { - yield `// @vue-skip${newLine}`; - return; - } - else if (/^@vue-ignore\b[\s\S]*/.test(commentText)) { - yield* ctx.ignoreError(); - } - else if (/^@vue-expect-error\b[\s\S]*/.test(commentText)) { - yield* ctx.expectError(prevNode); - } - else { - const match = prevNode.loc.source.match(/^$/); if (match) { - const { content } = match.groups ?? {}; + const { content } = match.groups!; addFormatCodes( content, - node.loc.start.offset + match[0].indexOf(content), + node.loc.start.offset + node.loc.source.indexOf('{') + 1, formatBrackets.generic ); } diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index cfd908e3cb..af2b9e766b 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -69,19 +69,21 @@ export default plugin; function createTsx( fileName: string, - _sfc: Sfc, + sfc: Sfc, ctx: Parameters[0], appendGlobalTypes: boolean ) { const ts = ctx.modules.typescript; + const getLang = computed(() => { - return !_sfc.script && !_sfc.scriptSetup ? 'ts' - : _sfc.scriptSetup && _sfc.scriptSetup.lang !== 'js' ? _sfc.scriptSetup.lang - : _sfc.script && _sfc.script.lang !== 'js' ? _sfc.script.lang + return !sfc.script && !sfc.scriptSetup ? 'ts' + : sfc.scriptSetup && sfc.scriptSetup.lang !== 'js' ? sfc.scriptSetup.lang + : sfc.script && sfc.script.lang !== 'js' ? sfc.script.lang : 'js'; }); + const getResolvedOptions = computed(() => { - const options = parseVueCompilerOptions(_sfc.comments); + const options = parseVueCompilerOptions(sfc.comments); if (options) { const resolver = new CompilerOptionsResolver(); resolver.addConfig(options, path.dirname(fileName)); @@ -89,33 +91,37 @@ function createTsx( } return ctx.vueCompilerOptions; }); + const getScriptRanges = computed(() => - _sfc.script - ? parseScriptRanges(ts, _sfc.script.ast, !!_sfc.scriptSetup, false) + sfc.script + ? parseScriptRanges(ts, sfc.script.ast, !!sfc.scriptSetup, false) : undefined ); + const getScriptSetupRanges = computed(() => - _sfc.scriptSetup - ? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, getResolvedOptions()) + sfc.scriptSetup + ? parseScriptSetupRanges(ts, sfc.scriptSetup.ast, getResolvedOptions()) : undefined ); + const getSetupBindingNames = computedSet( computed(() => { const newNames = new Set(); const bindings = getScriptSetupRanges()?.bindings; - if (_sfc.scriptSetup && bindings) { + if (sfc.scriptSetup && bindings) { for (const { range } of bindings) { - newNames.add(_sfc.scriptSetup.content.slice(range.start, range.end)); + newNames.add(sfc.scriptSetup.content.slice(range.start, range.end)); } } return newNames; }) ); + const getSetupImportComponentNames = computedSet( computed(() => { const newNames = new Set(); const bindings = getScriptSetupRanges()?.bindings; - if (_sfc.scriptSetup && bindings) { + if (sfc.scriptSetup && bindings) { for (const { range, moduleName, isDefaultImport, isNamespace } of bindings) { if ( moduleName @@ -123,13 +129,14 @@ function createTsx( && !isNamespace && ctx.vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext)) ) { - newNames.add(_sfc.scriptSetup.content.slice(range.start, range.end)); + newNames.add(sfc.scriptSetup.content.slice(range.start, range.end)); } } } return newNames; }) ); + const getSetupDestructuredPropNames = computedSet( computed(() => { const newNames = new Set(getScriptSetupRanges()?.defineProps?.destructured?.keys()); @@ -140,6 +147,7 @@ function createTsx( return newNames; }) ); + const getSetupTemplateRefNames = computedSet( computed(() => { const newNames = new Set( @@ -150,29 +158,34 @@ function createTsx( return newNames; }) ); + const setupHasDefineSlots = computed(() => !!getScriptSetupRanges()?.defineSlots); + const getSetupSlotsAssignName = computed(() => getScriptSetupRanges()?.defineSlots?.name); + const getSetupPropsAssignName = computed(() => getScriptSetupRanges()?.defineProps?.name); + const getSetupInheritAttrs = computed(() => { const value = getScriptSetupRanges()?.defineOptions?.inheritAttrs ?? getScriptRanges()?.exportDefault?.inheritAttrsOption; return value !== 'false'; }); + const getComponentSelfName = computed(() => { const { exportDefault } = getScriptRanges() ?? {}; - if (_sfc.script && exportDefault?.nameOption) { + if (sfc.script && exportDefault?.nameOption) { const { nameOption } = exportDefault; - return _sfc.script.content.slice(nameOption.start + 1, nameOption.end - 1); + return sfc.script.content.slice(nameOption.start + 1, nameOption.end - 1); } const { defineOptions } = getScriptSetupRanges() ?? {}; - if (_sfc.scriptSetup && defineOptions?.name) { + if (sfc.scriptSetup && defineOptions?.name) { return defineOptions.name; } const baseName = path.basename(fileName); return capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.')))); }); - const getGeneratedTemplate = computed(() => { - if (getResolvedOptions().skipTemplateCodegen || !_sfc.template) { + const getGeneratedTemplate = computed(() => { + if (getResolvedOptions().skipTemplateCodegen || !sfc.template) { return; } @@ -181,7 +194,7 @@ function createTsx( ts, compilerOptions: ctx.compilerOptions, vueCompilerOptions: getResolvedOptions(), - template: _sfc.template, + template: sfc.template, edited: getResolvedOptions().__test || (fileEditTimes.get(fileName) ?? 0) >= 2, scriptSetupBindingNames: getSetupBindingNames(), scriptSetupImportComponentNames: getSetupImportComponentNames(), @@ -195,7 +208,6 @@ function createTsx( }); let current = codegen.next(); - while (!current.done) { const code = current.value; codes.push(code); @@ -207,15 +219,17 @@ function createTsx( codes, }; }); + const getGeneratedScript = computed(() => { - const codes: Code[] = []; const linkedCodeMappings: Mapping[] = []; let generatedLength = 0; + + const codes: Code[] = []; const codegen = generateScript({ ts, compilerOptions: ctx.compilerOptions, vueCompilerOptions: getResolvedOptions(), - sfc: _sfc, + sfc: sfc, edited: getResolvedOptions().__test || (fileEditTimes.get(fileName) ?? 0) >= 2, fileName, lang: getLang(), @@ -231,7 +245,6 @@ function createTsx( fileEditTimes.set(fileName, (fileEditTimes.get(fileName) ?? 0) + 1); let current = codegen.next(); - while (!current.done) { const code = current.value; codes.push(code); diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 523eda9fb2..accbab70be 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -16,8 +16,7 @@ export type RawVueCompilerOptions = Partial; @@ -33,8 +32,15 @@ export interface VueCompilerOptions { checkUnknownEvents: boolean; checkUnknownDirectives: boolean; checkUnknownComponents: boolean; + inferComponentDollarEl: boolean; + inferComponentDollarRefs: boolean; + inferTemplateDollarAttrs: boolean; + inferTemplateDollarEl: boolean; + inferTemplateDollarRefs: boolean; + inferTemplateDollarSlots: boolean; skipTemplateCodegen: boolean; fallthroughAttributes: boolean; + fallthroughComponentNames: string[]; dataAttributes: string[]; htmlAttributes: string[]; optionsWrapper: [string, string] | []; @@ -107,6 +113,12 @@ export interface SfcBlock { attrs: Record; } +export type SfcBlockAttr = true | { + text: string; + offset: number; + quotes: boolean; +}; + export interface Sfc { content: string; comments: string[]; @@ -116,22 +128,17 @@ export interface Sfc { warnings: CompilerDOM.CompilerError[]; } | undefined; script: (SfcBlock & { - src: string | undefined; - srcOffset: number; + src: SfcBlockAttr | undefined; ast: ts.SourceFile; }) | undefined; scriptSetup: SfcBlock & { // https://github.com/vuejs/rfcs/discussions/436 - generic: string | undefined; - genericOffset: number; + generic: SfcBlockAttr | undefined; ast: ts.SourceFile; } | undefined; styles: readonly (SfcBlock & { scoped: boolean; - module?: { - name: string; - offset?: number; - }; + module?: SfcBlockAttr | undefined; cssVars: { text: string; offset: number; @@ -147,11 +154,16 @@ export interface Sfc { } declare module '@vue/compiler-sfc' { + interface SFCBlock { + __src?: SfcBlockAttr; + } + + interface SFCScriptBlock { + __generic?: SfcBlockAttr; + } + interface SFCStyleBlock { - __module?: { - name: string; - offset?: number; - }; + __module?: SfcBlockAttr; } } diff --git a/packages/language-core/lib/utils/parseSfc.ts b/packages/language-core/lib/utils/parseSfc.ts index 2bcbd3f616..e4e99df55c 100644 --- a/packages/language-core/lib/utils/parseSfc.ts +++ b/packages/language-core/lib/utils/parseSfc.ts @@ -1,5 +1,5 @@ import type { ElementNode, SourceLocation } from '@vue/compiler-dom'; -import * as compiler from '@vue/compiler-dom'; +import * as CompilerDOM from '@vue/compiler-dom'; import type { CompilerError, SFCBlock, SFCDescriptor, SFCParseResult, SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from '@vue/compiler-sfc'; declare module '@vue/compiler-sfc' { @@ -11,7 +11,7 @@ declare module '@vue/compiler-sfc' { export function parse(source: string): SFCParseResult { const errors: CompilerError[] = []; - const ast = compiler.parse(source, { + const ast = CompilerDOM.parse(source, { // there are no components at SFC parsing level isNativeTag: () => true, // preserve all whitespaces @@ -36,11 +36,11 @@ export function parse(source: string): SFCParseResult { shouldForceReload: () => false, }; ast.children.forEach(node => { - if (node.type === compiler.NodeTypes.COMMENT) { + if (node.type === CompilerDOM.NodeTypes.COMMENT) { descriptor.comments.push(node.content); return; } - else if (node.type !== compiler.NodeTypes.ELEMENT) { + else if (node.type !== CompilerDOM.NodeTypes.ELEMENT) { return; } switch (node.tag) { @@ -101,41 +101,64 @@ function createBlock(node: ElementNode, source: string) { end }; const attrs: Record = {}; - const block: SFCBlock - & Pick - & Pick = { + const block: SFCBlock = { type, content, loc, attrs }; node.props.forEach(p => { - if (p.type === compiler.NodeTypes.ATTRIBUTE) { + if (p.type === CompilerDOM.NodeTypes.ATTRIBUTE) { attrs[p.name] = p.value ? p.value.content || true : true; if (p.name === 'lang') { - block.lang = p.value && p.value.content; + block.lang = p.value?.content; } else if (p.name === 'src') { - block.src = p.value && p.value.content; + block.__src = parseAttr(p, node); } - else if (type === 'style') { + else if (isScriptBlock(block)) { + if (p.name === 'setup' || p.name === 'vapor') { + block.setup = attrs[p.name]; + } + else if (p.name === 'generic') { + block.__generic = parseAttr(p, node); + } + } + else if (isStyleBlock(block)) { if (p.name === 'scoped') { block.scoped = true; } else if (p.name === 'module') { - block.__module = { - name: p.value?.content ?? '$style', - offset: p.value?.content ? p.value?.loc.start.offset - node.loc.start.offset : undefined - }; + block.__module = parseAttr(p, node); } } - else if ( - type === 'script' - && (p.name === 'setup' || p.name === 'vapor') - ) { - block.setup = attrs[p.name]; - } } }); return block; } + +function isScriptBlock(block: SFCBlock): block is SFCScriptBlock { + return block.type === 'script'; +} + +function isStyleBlock(block: SFCBlock): block is SFCStyleBlock { + return block.type === 'style'; +} + +function parseAttr(p: CompilerDOM.AttributeNode, node: CompilerDOM.ElementNode) { + if (!p.value) { + return true; + } + const text = p.value.content; + const source = p.value.loc.source; + let offset = p.value.loc.start.offset - node.loc.start.offset; + const quotes = source.startsWith('"') || source.startsWith("'"); + if (quotes) { + offset++; + } + return { + text, + offset, + quotes, + }; +} diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index d9d4877167..2fe99e763c 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -4,6 +4,7 @@ import type * as ts from 'typescript'; import { generateGlobalTypes, getGlobalTypesFileName } from '../codegen/globalTypes'; import { getAllExtensions } from '../languagePlugin'; import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types'; +import { hyphenateTag } from './shared'; export type ParsedCommandLine = ts.ParsedCommandLine & { vueOptions: VueCompilerOptions; @@ -219,6 +220,10 @@ export class CompilerOptionsResolver { ...defaults.composables, ...this.options.composables, }, + fallthroughComponentNames: [ + ...defaults.fallthroughComponentNames, + ...this.options.fallthroughComponentNames ?? [] + ].map(hyphenateTag), // https://github.com/vuejs/vue-next/blob/master/packages/compiler-dom/src/transforms/vModel.ts#L49-L51 // https://vuejs.org/guide/essentials/forms.html#form-input-bindings experimentalModelPropName: Object.fromEntries(Object.entries( @@ -266,8 +271,20 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla checkUnknownEvents: strictTemplates, checkUnknownDirectives: strictTemplates, checkUnknownComponents: strictTemplates, + inferComponentDollarEl: false, + inferComponentDollarRefs: false, + inferTemplateDollarAttrs: false, + inferTemplateDollarEl: false, + inferTemplateDollarRefs: false, + inferTemplateDollarSlots: false, skipTemplateCodegen: false, fallthroughAttributes: false, + fallthroughComponentNames: [ + 'Transition', + 'KeepAlive', + 'Teleport', + 'Suspense', + ], dataAttributes: [], htmlAttributes: ['aria-*'], optionsWrapper: target >= 2.7 diff --git a/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts b/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts index 77555633d8..b40dc3ac54 100644 --- a/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts +++ b/packages/language-core/lib/virtualFile/computedEmbeddedCodes.ts @@ -201,29 +201,19 @@ function computedPluginEmbeddedCodes( ]; })); const newMappings: typeof mappings = []; - let lastValidMapping: typeof mappings[number] | undefined; for (let i = 0; i < mappings.length; i++) { const mapping = mappings[i]; - if (mapping.data.__combineOffsetMapping !== undefined) { - const offsetMapping = mappings[i - mapping.data.__combineOffsetMapping]; + if (mapping.data.__combineOffset !== undefined) { + const offsetMapping = mappings[i - mapping.data.__combineOffset]; if (typeof offsetMapping === 'string' || !offsetMapping) { - throw new Error('Invalid offset mapping, mappings: ' + mappings.length + ', i: ' + i + ', offset: ' + mapping.data.__combineOffsetMapping); + throw new Error('Invalid offset mapping, mappings: ' + mappings.length + ', i: ' + i + ', offset: ' + mapping.data.__combineOffset); } offsetMapping.sourceOffsets.push(...mapping.sourceOffsets); offsetMapping.generatedOffsets.push(...mapping.generatedOffsets); offsetMapping.lengths.push(...mapping.lengths); continue; } - else if (mapping.data.__combineLastMapping) { - lastValidMapping!.sourceOffsets.push(...mapping.sourceOffsets); - lastValidMapping!.generatedOffsets.push(...mapping.generatedOffsets); - lastValidMapping!.lengths.push(...mapping.lengths); - continue; - } - else { - lastValidMapping = mapping; - } newMappings.push(mapping); } diff --git a/packages/language-core/lib/virtualFile/computedSfc.ts b/packages/language-core/lib/virtualFile/computedSfc.ts index 1e2027dbbb..fffbabc33b 100644 --- a/packages/language-core/lib/virtualFile/computedSfc.ts +++ b/packages/language-core/lib/virtualFile/computedSfc.ts @@ -2,7 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCBlock, SFCParseResult } from '@vue/compiler-sfc'; import { computed, pauseTracking, resumeTracking } from 'alien-signals'; import type * as ts from 'typescript'; -import type { Sfc, SfcBlock, VueLanguagePluginReturn } from '../types'; +import type { Sfc, SfcBlock, SfcBlockAttr, VueLanguagePluginReturn } from '../types'; import { parseCssClassNames } from '../utils/parseCssClassNames'; import { parseCssVars } from '../utils/parseCssVars'; import { computedArray } from '../utils/signals'; @@ -52,12 +52,8 @@ export function computedSfc( 'js', computed(() => getParseResult()?.descriptor.script ?? undefined), (block, base): NonNullable => { - const src = computed(() => block().src); - const srcOffset = computed(() => { - const _src = src(); - return _src ? getUntrackedSnapshot().getText(0, base.startTagEnd).lastIndexOf(_src) - base.startTagEnd : -1; - }); - const ast = computed(() => { + const getSrc = computedAttrValue('__src', base, block); + const getAst = computed(() => { for (const plugin of plugins) { const ast = plugin.compileSFCScript?.(base.lang, base.content); if (ast) { @@ -67,9 +63,8 @@ export function computedSfc( return ts.createSourceFile(fileName + '.' + base.lang, '', 99 satisfies ts.ScriptTarget.Latest); }); return mergeObject(base, { - get src() { return src(); }, - get srcOffset() { return srcOffset(); }, - get ast() { return ast(); }, + get src() { return getSrc(); }, + get ast() { return getAst(); }, }); } ); @@ -78,15 +73,8 @@ export function computedSfc( 'js', computed(() => getParseResult()?.descriptor.scriptSetup ?? undefined), (block, base): NonNullable => { - const generic = computed(() => { - const _block = block(); - return typeof _block.attrs.generic === 'string' ? _block.attrs.generic : undefined; - }); - const genericOffset = computed(() => { - const _generic = generic(); - return _generic !== undefined ? getUntrackedSnapshot().getText(0, base.startTagEnd).lastIndexOf(_generic) - base.startTagEnd : -1; - }); - const ast = computed(() => { + const getGeneric = computedAttrValue('__generic', base, block); + const getAst = computed(() => { for (const plugin of plugins) { const ast = plugin.compileSFCScript?.(base.lang, base.content); if (ast) { @@ -96,9 +84,8 @@ export function computedSfc( return ts.createSourceFile(fileName + '.' + base.lang, '', 99 satisfies ts.ScriptTarget.Latest); }); return mergeObject(base, { - get generic() { return generic(); }, - get genericOffset() { return genericOffset(); }, - get ast() { return ast(); }, + get generic() { return getGeneric(); }, + get ast() { return getAst(); }, }); } ); @@ -127,13 +114,7 @@ export function computedSfc( computed(() => getParseResult()?.descriptor.styles ?? []), (getBlock, i) => { const base = computedSfcBlock('style_' + i, 'css', getBlock); - const getModule = computed(() => { - const { __module } = getBlock(); - return __module ? { - name: __module.name, - offset: __module.offset ? base.start + __module.offset : undefined - } : undefined; - }); + const getModule = computedAttrValue('__module', base, getBlock); const getScoped = computed(() => !!getBlock().scoped); const getCssVars = computed(() => [...parseCssVars(base.content)]); const getClassNames = computed(() => [...parseCssClassNames(base.content)]); @@ -309,6 +290,23 @@ export function computedSfc( get end() { return getEnd(); }, }; } + + function computedAttrValue( + key: keyof T & string, + base: ReturnType, + getBlock: () => T + ) { + return computed(() => { + const val = getBlock()[key] as SfcBlockAttr | undefined; + if (typeof val === 'object') { + return { + ...val, + offset: base.start + val.offset, + }; + } + return val; + }); + } } function mergeObject(a: T, b: K): T & K { diff --git a/packages/language-core/lib/virtualFile/vueFile.ts b/packages/language-core/lib/virtualFile/vueFile.ts index af6633abc8..200403b995 100644 --- a/packages/language-core/lib/virtualFile/vueFile.ts +++ b/packages/language-core/lib/virtualFile/vueFile.ts @@ -17,9 +17,10 @@ export class VueVirtualCode implements VirtualCode { // computeds - _vueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, this._snapshot); - _sfc = computedSfc(this.ts, this.plugins, this.fileName, this._snapshot, this._vueSfc); - _mappings = computed(() => { + private _vueSfc = computedVueSfc(this.plugins, this.fileName, this.languageId, this._snapshot); + private _sfc = computedSfc(this.ts, this.plugins, this.fileName, this._snapshot, this._vueSfc); + private _embeddedCodes = computedEmbeddedCodes(this.plugins, this.fileName, this._sfc); + private _mappings = computed(() => { const snapshot = this._snapshot(); return [{ sourceOffsets: [0], @@ -28,16 +29,21 @@ export class VueVirtualCode implements VirtualCode { data: allCodeFeatures, }]; }); - _embeddedCodes = computedEmbeddedCodes(this.plugins, this.fileName, this._sfc); // others - get embeddedCodes() { - return this._embeddedCodes(); - } get snapshot() { return this._snapshot(); } + get vueSfc() { + return this._vueSfc(); + } + get sfc() { + return this._sfc; + } + get embeddedCodes() { + return this._embeddedCodes(); + } get mappings() { return this._mappings(); } diff --git a/packages/language-core/package.json b/packages/language-core/package.json index 5116034662..00f7ce374e 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-core", - "version": "2.2.2", + "version": "2.2.4", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-core/schemas/vue-tsconfig.schema.json b/packages/language-core/schemas/vue-tsconfig.schema.json index ed60b2c699..4bd341cd37 100644 --- a/packages/language-core/schemas/vue-tsconfig.schema.json +++ b/packages/language-core/schemas/vue-tsconfig.schema.json @@ -65,6 +65,36 @@ "default": false, "markdownDescription": "Check unknown components. If not set, uses the 'strictTemplates' value." }, + "inferComponentDollarEl": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$el` type on the component instance." + }, + "inferComponentDollarRefs": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$refs` type on the component instance." + }, + "inferTemplateDollarAttrs": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$attrs` type in the template and the return type of `useAttrs`." + }, + "inferTemplateDollarEl": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$el` type in the template." + }, + "inferTemplateDollarRefs": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$refs` type in the template." + }, + "inferTemplateDollarSlots": { + "type": "boolean", + "default": false, + "markdownDescription": "Infer `$slots` type in the template and the return type of `useSlots`." + }, "skipTemplateCodegen": { "type": "boolean", "default": false, @@ -75,6 +105,16 @@ "default": false, "markdownDescription": "Enable to support typed fallthrough attributes. Please note that enabling may significantly slow down type checking." }, + "fallthroughComponentNames": { + "type": "array", + "default": [ + "Transition", + "KeepAlive", + "Teleport", + "Suspense" + ], + "markdownDescription": "Component names that will be transparent when collecting single root child nodes and fallthrough attributes." + }, "dataAttributes": { "type": "array", "default": [ ], @@ -85,11 +125,6 @@ "default": [ "aria-*" ], "markdownDescription": "A glob matcher array that should always be recognizing as HTML Attributes rather than Component props. Attribute name will never convert to camelize case." }, - "plugins": { - "type": "array", - "default": [ ], - "markdownDescription": "Plugins to be used in the SFC compiler." - }, "optionsWrapper": { "type": "array", "default": [ @@ -119,6 +154,11 @@ "useTemplateRef": [ "useTemplateRef", "templateRef" ] } }, + "plugins": { + "type": "array", + "default": [ ], + "markdownDescription": "Plugins to be used in the SFC compiler." + }, "experimentalResolveStyleCssClasses": { "enum": [ "scoped", diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index b6c91c6a82..f285fbdd33 100644 --- a/packages/language-plugin-pug/package.json +++ b/packages/language-plugin-pug/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-plugin-pug", - "version": "2.2.2", + "version": "2.2.4", "license": "MIT", "files": [ "**/*.js", @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/node": "^22.10.4", - "@vue/language-core": "2.2.2" + "@vue/language-core": "2.2.4" }, "dependencies": { "@volar/source-map": "~2.4.11", diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 20220c9b70..11c55f57d0 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-server", - "version": "2.2.2", + "version": "2.2.4", "license": "MIT", "files": [ "**/*.js", @@ -19,9 +19,9 @@ "@volar/language-core": "~2.4.11", "@volar/language-server": "~2.4.11", "@volar/test-utils": "~2.4.11", - "@vue/language-core": "2.2.2", - "@vue/language-service": "2.2.2", - "@vue/typescript-plugin": "2.2.2", + "@vue/language-core": "2.2.4", + "@vue/language-service": "2.2.4", + "@vue/typescript-plugin": "2.2.4", "vscode-languageserver-protocol": "^3.17.5", "vscode-uri": "^3.0.8" } diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index 09cc6cf7c4..1a81801da8 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -104,12 +104,12 @@ describe('Renaming', async () => { "newText": "bar", "range": { "end": { - "character": 8, - "line": 7, + "character": 28, + "line": 2, }, "start": { - "character": 5, - "line": 7, + "character": 25, + "line": 2, }, }, }, @@ -117,12 +117,12 @@ describe('Renaming', async () => { "newText": "bar", "range": { "end": { - "character": 28, - "line": 2, + "character": 8, + "line": 7, }, "start": { - "character": 25, - "line": 2, + "character": 5, + "line": 7, }, }, }, @@ -743,12 +743,12 @@ describe('Renaming', async () => { "newText": "stylus", "range": { "end": { - "character": 23, - "line": 15, + "character": 22, + "line": 8, }, "start": { - "character": 19, - "line": 15, + "character": 18, + "line": 8, }, }, }, @@ -756,12 +756,12 @@ describe('Renaming', async () => { "newText": "stylus", "range": { "end": { - "character": 22, - "line": 8, + "character": 23, + "line": 15, }, "start": { - "character": 18, - "line": 8, + "character": 19, + "line": 15, }, }, }, diff --git a/packages/language-service/lib/ideFeatures/nameCasing.ts b/packages/language-service/lib/ideFeatures/nameCasing.ts index 5dd123585c..3ebb4d57dc 100644 --- a/packages/language-service/lib/ideFeatures/nameCasing.ts +++ b/packages/language-service/lib/ideFeatures/nameCasing.ts @@ -24,7 +24,7 @@ export async function convertTagName( return; } - const { template } = root._sfc; + const { template } = root.sfc; if (!template) { return; } @@ -71,7 +71,7 @@ export async function convertAttrName( return; } - const { template } = root._sfc; + const { template } = root.sfc; if (!template) { return; } @@ -172,8 +172,8 @@ export async function detect( const result = new Set(); - if (file._sfc.template?.ast) { - for (const element of vue.forEachElementNode(file._sfc.template.ast)) { + if (file.sfc.template?.ast) { + for (const element of vue.forEachElementNode(file.sfc.template.ast)) { if (element.tagType === 1 satisfies CompilerDOM.ElementTypes) { if (element.tag !== hyphenateTag(element.tag)) { // TagName @@ -208,7 +208,7 @@ function getTemplateTagsAndAttrs(sourceFile: VirtualCode): Tags { if (!(sourceFile instanceof vue.VueVirtualCode)) { return; } - const ast = sourceFile._sfc.template?.ast; + const ast = sourceFile.sfc.template?.ast; const tags: Tags = new Map(); if (ast) { for (const node of vue.forEachElementNode(ast)) { diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index de1dcaa276..4376e281d9 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -1,5 +1,9 @@ -import type { LanguageServicePlugin } from '@volar/language-service'; -import { create as baseCreate } from 'volar-service-css'; +import type { LanguageServicePlugin, VirtualCode } from '@volar/language-service'; +import { VueVirtualCode } from '@vue/language-core'; +import { create as baseCreate, type Provide } from 'volar-service-css'; +import * as css from 'vscode-css-languageservice'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; export function create(): LanguageServicePlugin { const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); @@ -7,6 +11,11 @@ export function create(): LanguageServicePlugin { ...base, create(context) { const baseInstance = base.create(context); + const { + 'css/languageService': getCssLs, + 'css/stylesheet': getStylesheet, + } = baseInstance.provide as Provide; + return { ...baseInstance, async provideDiagnostics(document, token) { @@ -18,7 +27,73 @@ export function create(): LanguageServicePlugin { } return diagnostics; }, + /** + * If the editing position is within the virtual code and navigation is enabled, + * skip the CSS renaming feature. + */ + provideRenameRange(document, position) { + do { + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!sourceScript?.generated || !virtualCode?.id.startsWith('style_')) { + break; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + break; + } + + const block = root.sfc.styles.find(style => style.name === decoded![1]); + if (!block) { + break; + } + + let script: VirtualCode | undefined; + for (const [key, value] of sourceScript.generated.embeddedCodes) { + if (key.startsWith('script_')) { + script = value; + break; + } + } + if (!script) { + break; + } + + const offset = document.offsetAt(position) + block.startTagEnd; + for (const { sourceOffsets, lengths, data } of script.mappings) { + if ( + !sourceOffsets.length + || !data.navigation + || typeof data.navigation === 'object' && !data.navigation.shouldRename + ) { + continue; + } + + const start = sourceOffsets[0]; + const end = sourceOffsets.at(-1)! + lengths.at(-1)!; + + if (offset >= start && offset <= end) { + return; + } + } + } while (0); + + return worker(document, (stylesheet, cssLs) => { + return cssLs.prepareRename(document, position, stylesheet); + }); + } }; + + function worker(document: TextDocument, callback: (stylesheet: css.Stylesheet, cssLs: css.LanguageService) => T) { + const cssLs = getCssLs(document); + if (!cssLs) { + return; + } + return callback(getStylesheet(document, cssLs), cssLs); + } }, }; } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index eb08119bd5..3b3b6794cf 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -60,10 +60,8 @@ export function create( return; } - const blocks = [ - root._sfc.script, - root._sfc.scriptSetup, - ].filter(block => !!block); + const { sfc } = root; + const blocks = [sfc.script, sfc.scriptSetup].filter(block => !!block); if (!blocks.length) { return; } diff --git a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts index 092d31feeb..07e9182619 100644 --- a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts +++ b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts @@ -36,8 +36,9 @@ export function create(): LanguageServicePlugin { return; } - const codegen = tsCodegen.get(root._sfc); - const scriptSetup = root._sfc.scriptSetup; + const { sfc } = root; + const codegen = tsCodegen.get(sfc); + const scriptSetup = sfc.scriptSetup; const scriptSetupRanges = codegen?.getScriptSetupRanges(); if (!scriptSetup || !scriptSetupRanges) { return; diff --git a/packages/language-service/lib/plugins/vue-directive-comments.ts b/packages/language-service/lib/plugins/vue-directive-comments.ts index ee144e4f48..29c93363cd 100644 --- a/packages/language-service/lib/plugins/vue-directive-comments.ts +++ b/packages/language-service/lib/plugins/vue-directive-comments.ts @@ -10,6 +10,10 @@ const cmds = [ const directiveCommentReg = / + - - diff --git a/test-workspace/tsc/passedFixtures/vue3/#4788/main.vue b/test-workspace/tsc/passedFixtures/vue3/#4788/main.vue deleted file mode 100644 index fb7c084754..0000000000 --- a/test-workspace/tsc/passedFixtures/vue3/#4788/main.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/useAttrs/main.vue b/test-workspace/tsc/passedFixtures/vue3/attrs/main.vue similarity index 87% rename from test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/useAttrs/main.vue rename to test-workspace/tsc/passedFixtures/vue3/attrs/main.vue index 3bad154af3..63a42d4a88 100644 --- a/test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/useAttrs/main.vue +++ b/test-workspace/tsc/passedFixtures/vue3/attrs/main.vue @@ -1,3 +1,5 @@ + + diff --git a/test-workspace/tsc/passedFixtures/vue3/rootEl/base.vue b/test-workspace/tsc/passedFixtures/vue3/rootEl/base.vue index dee0efe5f9..83ed66f279 100644 --- a/test-workspace/tsc/passedFixtures/vue3/rootEl/base.vue +++ b/test-workspace/tsc/passedFixtures/vue3/rootEl/base.vue @@ -1,3 +1,6 @@ + + + diff --git a/test-workspace/tsc/passedFixtures/vue3/rootEl/child.vue b/test-workspace/tsc/passedFixtures/vue3/rootEl/child.vue index ba23fc8016..684caa091c 100644 --- a/test-workspace/tsc/passedFixtures/vue3/rootEl/child.vue +++ b/test-workspace/tsc/passedFixtures/vue3/rootEl/child.vue @@ -1,10 +1,16 @@ + + + diff --git a/test-workspace/tsc/passedFixtures/vue3/rootEl/main.vue b/test-workspace/tsc/passedFixtures/vue3/rootEl/main.vue index a43fc7742b..68262319ab 100644 --- a/test-workspace/tsc/passedFixtures/vue3/rootEl/main.vue +++ b/test-workspace/tsc/passedFixtures/vue3/rootEl/main.vue @@ -1,3 +1,5 @@ + + diff --git a/test-workspace/tsc/passedFixtures/vue3/templateRef_missingImport/main.vue b/test-workspace/tsc/passedFixtures/vue3/templateRef/missing-import.vue similarity index 100% rename from test-workspace/tsc/passedFixtures/vue3/templateRef_missingImport/main.vue rename to test-workspace/tsc/passedFixtures/vue3/templateRef/missing-import.vue diff --git a/test-workspace/tsc/passedFixtures/vue3/templateRef/template-ref.vue b/test-workspace/tsc/passedFixtures/vue3/templateRef/template-refs.vue similarity index 71% rename from test-workspace/tsc/passedFixtures/vue3/templateRef/template-ref.vue rename to test-workspace/tsc/passedFixtures/vue3/templateRef/template-refs.vue index c40f9dc438..0da7ff0c0f 100644 --- a/test-workspace/tsc/passedFixtures/vue3/templateRef/template-ref.vue +++ b/test-workspace/tsc/passedFixtures/vue3/templateRef/template-refs.vue @@ -1,6 +1,9 @@ + +