diff --git a/CHANGELOG.md b/CHANGELOG.md index 9644756046..bddba05d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 3.0.4 (2025-07-25) + +### Features + +- feat(language-service): check casing when dropping component into template - Thanks to @KazariEX! +- feat(language-service): native completion experience for slot names (#5552) - Thanks to @KazariEX! + +### Bug Fixes + +- fix(language-core): avoid clearing global types path when local compiler options is present - Thanks to @KazariEX! +- fix(language-core): do not evaluate `skipTemplateCodegen` when exposing `$slots` - Thanks to @KazariEX! +- fix(language-service): correct kind and order of component completion items - Thanks to @KazariEX! +- fix(component-meta): filter events out of props (#5547) - Thanks to @Akryum! + +### Other Changes + +- refactor(language-core): allow configuring `checkUnknownEvents` and `checkUnknownComponents` in sfc (#5537) - Thanks to @KazariEX! +- chore(language-service): add restart server hint to global types warning - Thanks to @KazariEX! + ## 3.0.3 (2025-07-18) ### Bug Fixes diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 7de1a28ba6..02609994fe 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "volar", - "version": "3.0.3", + "version": "3.0.4", "repository": { "type": "git", "url": "https://github.com/vuejs/language-tools.git", @@ -466,8 +466,8 @@ "@volar/vscode": "2.4.20", "@vscode/vsce": "^3.2.1", "@vue/compiler-sfc": "^3.5.0", - "@vue/language-server": "3.0.3", - "@vue/typescript-plugin": "3.0.3", + "@vue/language-server": "3.0.4", + "@vue/typescript-plugin": "3.0.4", "reactive-vscode": "^0.2.9", "rolldown": "1.0.0-beta.8", "semver": "^7.5.4", diff --git a/lerna.json b/lerna.json index 8954f700b2..95378e03fc 100644 --- a/lerna.json +++ b/lerna.json @@ -6,5 +6,5 @@ "packages/**", "test-workspace" ], - "version": "3.0.3" + "version": "3.0.4" } diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 7a420d244a..8797527636 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -240,7 +240,7 @@ interface ComponentMeta { let _slots: ReturnType | undefined; let _exposed: ReturnType | undefined; - return { + const meta = { get type() { return _type ?? (_type = getType()); }, @@ -258,6 +258,8 @@ interface ComponentMeta { }, }; + return meta; + function getType() { const $type = symbolProperties.find(prop => prop.escapedName === 'type'); @@ -278,6 +280,10 @@ interface ComponentMeta { const type = typeChecker.getTypeOfSymbolAtLocation($props, symbolNode); const properties = type.getProperties(); + const eventProps = new Set( + meta.events.map(event => `on${event.name.charAt(0).toUpperCase()}${event.name.slice(1)}`), + ); + result = properties .map(prop => { const { @@ -286,7 +292,7 @@ interface ComponentMeta { return resolveNestedProperties(prop); }) - .filter(prop => !vnodeEventRegex.test(prop.name)); + .filter(prop => !vnodeEventRegex.test(prop.name) && !eventProps.has(prop.name)); } // fill global diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index b3285a3a87..504a2f739c 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -1,6 +1,6 @@ { "name": "vue-component-meta", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", @@ -14,7 +14,7 @@ }, "dependencies": { "@volar/typescript": "2.4.20", - "@vue/language-core": "3.0.3", + "@vue/language-core": "3.0.4", "path-browserify": "^1.0.1" }, "peerDependencies": { diff --git a/packages/component-meta/tests/index.spec.ts b/packages/component-meta/tests/index.spec.ts index 4aa7ba3012..8d6a6e3349 100644 --- a/packages/component-meta/tests/index.spec.ts +++ b/packages/component-meta/tests/index.spec.ts @@ -1168,6 +1168,21 @@ const worker = (checker: ComponentMetaChecker, withTsconfig: boolean) => `); }); + test('component with both props and events', () => { + const componentPath = path.resolve(__dirname, '../../../test-workspace/component-meta/#5546/main.vue'); + const meta = checker.getComponentMeta(componentPath); + + expect(meta.type).toEqual(TypeMeta.Class); + + // Nothing special about this prop + expect(meta.props.find(prop => prop.name === 'title')).toBeDefined(); + // Event + expect(meta.props.find(prop => prop.name === 'onClose')).toBeUndefined(); + expect(meta.events.find(event => event.name === 'close')).toBeDefined(); + // Prop that starts with `on` + expect(meta.props.find(prop => prop.name === 'onCompleted')).toBeDefined(); + }); + test('non-component', () => { const componentPath = path.resolve( __dirname, diff --git a/packages/component-type-helpers/package.json b/packages/component-type-helpers/package.json index d20dbd84f8..a12841c5ce 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": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index d3a6d53844..a09efc0f4b 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -1,10 +1,10 @@ export * from './lib/codegen/globalTypes'; export * from './lib/codegen/template'; -export * from './lib/codegen/utils'; export * from './lib/languagePlugin'; export * from './lib/parsers/scriptSetupRanges'; export * from './lib/plugins'; export * from './lib/types'; +export * from './lib/utils/collectBindings'; export * from './lib/utils/parseSfc'; export * from './lib/utils/shared'; export * from './lib/utils/ts'; diff --git a/packages/language-core/lib/codegen/codeFeatures.ts b/packages/language-core/lib/codegen/codeFeatures.ts index 0baa2d992c..c7143649c7 100644 --- a/packages/language-core/lib/codegen/codeFeatures.ts +++ b/packages/language-core/lib/codegen/codeFeatures.ts @@ -72,6 +72,26 @@ const raw = { navigation: true, completion: true, }, + doNotReportTs2339AndTs2551: { + verification: { + // https://typescript.tv/errors/#ts2339 + // https://typescript.tv/errors/#ts2551 + shouldReport: (_source, code) => String(code) !== '2339' && String(code) !== '2551', + }, + }, + doNotReportTs2353AndTs2561: { + verification: { + // https://typescript.tv/errors/#ts2353 + // https://typescript.tv/errors/#ts2561 + shouldReport: (_source, code) => String(code) !== '2353' && String(code) !== '2561', + }, + }, + doNotReportTs6133: { + verification: { + // https://typescript.tv/errors/#ts6133 + shouldReport: (_source, code) => String(code) !== '6133', + }, + }, } satisfies Record; export const codeFeatures = raw as { diff --git a/packages/language-core/lib/codegen/globalTypes.ts b/packages/language-core/lib/codegen/globalTypes.ts index c03bd3bf49..5851bb1899 100644 --- a/packages/language-core/lib/codegen/globalTypes.ts +++ b/packages/language-core/lib/codegen/globalTypes.ts @@ -1,29 +1,17 @@ import type { VueCompilerOptions } from '../types'; import { getSlotsPropertyName } from '../utils/shared'; -export function getGlobalTypesFileName({ - lib, - target, - checkUnknownProps, - checkUnknownEvents, - checkUnknownComponents, -}: VueCompilerOptions) { +export function getGlobalTypesFileName(options: VueCompilerOptions) { return [ - lib, - target, - checkUnknownProps, - checkUnknownEvents, - checkUnknownComponents, + options.lib, + options.target, + options.checkUnknownProps, ].map(v => (typeof v === 'boolean' ? Number(v) : v)).join('_') + '.d.ts'; } -export function generateGlobalTypes({ - lib, - target, - checkUnknownProps, - checkUnknownEvents, - checkUnknownComponents, -}: VueCompilerOptions) { +export function generateGlobalTypes(options: VueCompilerOptions) { + const { lib, target, checkUnknownProps } = options; + const fnPropsType = `(T extends { $props: infer Props } ? Props : {})${ checkUnknownProps ? '' : ' & Record' }`; @@ -69,7 +57,7 @@ export function generateGlobalTypes({ N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } : N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } : N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } : - ${checkUnknownComponents ? '{}' : '{ [K in N0]: unknown }'}; + {}; type __VLS_FunctionalComponentCtx = __VLS_PickNotAny<'__ctx' extends keyof __VLS_PickNotAny ? K extends { __ctx?: infer Ctx } ? NonNullable : never : any , T extends (props: any, ctx: infer Ctx) => any ? Ctx : any @@ -80,12 +68,12 @@ export function generateGlobalTypes({ : {}; type __VLS_FunctionalComponent = (props: ${fnPropsType}, ctx?: any) => __VLS_Element & { __ctx?: { - attrs?: any, - slots?: T extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : Record, - emit?: T extends { $emit: infer Emit } ? Emit : {}, - props?: ${fnPropsType}, - expose?: (exposed: T) => void, - } + attrs?: any; + slots?: T extends { ${getSlotsPropertyName(target)}: infer Slots } ? Slots : Record; + emit?: T extends { $emit: infer Emit } ? Emit : {}; + props?: ${fnPropsType}; + expose?: (exposed: T) => void; + }; }; type __VLS_IsFunction = K extends keyof T ? __VLS_IsAny extends false @@ -94,15 +82,19 @@ export function generateGlobalTypes({ : true : false : false; - type __VLS_NormalizeComponentEvent = ( - __VLS_IsFunction extends true - ? Props - : __VLS_IsFunction extends true - ? { [K in onEvent]?: Emits[Event] } - : __VLS_IsFunction extends true - ? { [K in onEvent]?: Emits[CamelizedEvent] } - : Props - )${checkUnknownEvents ? '' : ' & Record'}; + type __VLS_NormalizeComponentEvent< + Props, + Emits, + onEvent extends keyof Props, + Event extends keyof Emits, + CamelizedEvent extends keyof Emits, + > = __VLS_IsFunction extends true + ? Props + : __VLS_IsFunction extends true + ? { [K in onEvent]?: Emits[Event] } + : __VLS_IsFunction extends true + ? { [K in onEvent]?: Emits[CamelizedEvent] } + : Props; // fix https://github.com/vuejs/language-tools/issues/926 type __VLS_UnionToIntersection = (U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never; type __VLS_OverloadUnionInner = U & T extends (...args: infer A) => infer R @@ -174,7 +166,7 @@ export function generateGlobalTypes({ : __VLS_FunctionalComponent<{}>; function __VLS_functionalComponentArgsRest any>(t: T): 2 extends Parameters['length'] ? [any] : []; function __VLS_asFunctionalElement(tag: T, endTag?: T): (attrs: T${ - checkUnknownComponents ? '' : ' & Record' + checkUnknownProps ? '' : ' & Record' }) => void; function __VLS_asFunctionalSlot(slot: S): S extends () => infer R ? (props: {}) => R : NonNullable; function __VLS_tryAsConstant(t: T): T; diff --git a/packages/language-core/lib/codegen/script/scriptSetup.ts b/packages/language-core/lib/codegen/script/scriptSetup.ts index 63588e136b..83af4ed7ff 100644 --- a/packages/language-core/lib/codegen/script/scriptSetup.ts +++ b/packages/language-core/lib/codegen/script/scriptSetup.ts @@ -313,12 +313,9 @@ function* generateSetupFunction( if (syntax) { if ( - !options.vueCompilerOptions.skipTemplateCodegen - && ( - scriptSetupRanges.defineSlots - || options.templateCodegen?.slots.length - || options.templateCodegen?.dynamicSlots.length - ) + scriptSetupRanges.defineSlots + || options.templateCodegen?.slots.length + || options.templateCodegen?.dynamicSlots.length ) { yield `const __VLS_component = `; yield* generateComponent(options, ctx, scriptSetup, scriptSetupRanges); diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index f5254adbe2..55e28e1b0e 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -152,7 +152,12 @@ export function* generateComponent( yield* generateCanonicalComponentName( node.tag, tagOffsets[0], - ctx.codeFeatures.withoutHighlightAndCompletionAndNavigation, + ctx.resolveCodeFeatures({ + ...codeFeatures.semanticWithoutHighlight, + ...options.vueCompilerOptions.checkUnknownComponents + ? codeFeatures.verification + : codeFeatures.doNotReportTs2339AndTs2551, + }), ); yield `${endOfLine}`; @@ -207,14 +212,7 @@ export function* generateComponent( yield* wrapWith( node.loc.start.offset, node.loc.end.offset, - ctx.resolveCodeFeatures({ - verification: { - shouldReport(_source, code) { - // https://typescript.tv/errors/#ts6133 - return String(code) !== '6133'; - }, - }, - }), + ctx.codeFeatures.doNotReportTs6133, componentVNodeVar, ); yield ` = ${componentFunctionalVar}`; diff --git a/packages/language-core/lib/codegen/template/elementEvents.ts b/packages/language-core/lib/codegen/template/elementEvents.ts index 95cef6fadd..132fbe65a7 100644 --- a/packages/language-core/lib/codegen/template/elementEvents.ts +++ b/packages/language-core/lib/codegen/template/elementEvents.ts @@ -2,6 +2,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import type * as ts from 'typescript'; import type { Code, VueCodeInformation } from '../../types'; +import { codeFeatures } from '../codeFeatures'; import { combineLastMapping, createTsAst, endOfLine, identifierRegex, newLine } from '../utils'; import { generateCamelized } from '../utils/camelized'; import { wrapWith } from '../utils/wrapWith'; @@ -61,12 +62,12 @@ export function* generateElementEvents( yield `const ${ctx.getInternalVariable()}: __VLS_NormalizeComponentEvent = (${newLine}`; if (prop.name === 'on') { yield `{ `; - yield* generateEventArg(ctx, source, start!, emitPrefix.slice(0, -1), ctx.codeFeatures.navigation); + yield* generateEventArg(options, ctx, source, start!, emitPrefix.slice(0, -1), ctx.codeFeatures.navigation); yield `: {} as any } as typeof ${emitsVar},${newLine}`; } yield `{ `; if (prop.name === 'on') { - yield* generateEventArg(ctx, source, start!, propPrefix.slice(0, -1)); + yield* generateEventArg(options, ctx, source, start!, propPrefix.slice(0, -1)); yield `: `; yield* generateEventExpression(options, ctx, prop); } @@ -80,15 +81,21 @@ export function* generateElementEvents( } export function* generateEventArg( + options: TemplateCodegenOptions, ctx: TemplateCodegenContext, name: string, start: number, directive = 'on', - features: VueCodeInformation = { - ...ctx.codeFeatures.withoutHighlightAndCompletion, - ...ctx.codeFeatures.navigationWithoutRename, - }, + features?: VueCodeInformation, ): Generator { + features ??= ctx.resolveCodeFeatures({ + ...codeFeatures.semanticWithoutHighlight, + ...codeFeatures.navigationWithoutRename, + ...options.vueCompilerOptions.checkUnknownEvents + ? codeFeatures.verification + : codeFeatures.doNotReportTs2353AndTs2561, + }); + if (directive.length) { name = capitalize(name); } diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 62dec6978d..57dc63eb40 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -46,7 +46,7 @@ export function* generateElementProps( ) { if (!isComponent) { yield `...{ `; - yield* generateEventArg(ctx, prop.arg.loc.source, prop.arg.loc.start.offset); + yield* generateEventArg(options, ctx, prop.arg.loc.source, prop.arg.loc.start.offset); yield `: `; yield* generateEventExpression(options, ctx, prop); yield `},`; @@ -378,16 +378,9 @@ function getPropsCodeInfo( ): VueCodeInformation { return ctx.resolveCodeFeatures({ ...codeFeatures.withoutHighlightAndCompletion, - verification: strictPropsCheck || { - shouldReport(_source, code) { - // https://typescript.tv/errors/#ts2353 - // https://typescript.tv/errors/#ts2561 - if (String(code) === '2353' || String(code) === '2561') { - return false; - } - return true; - }, - }, + ...strictPropsCheck + ? codeFeatures.verification + : codeFeatures.doNotReportTs2353AndTs2561, }); } diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index 064bb0b2d0..210f31834d 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -1,9 +1,10 @@ import { isGloballyAllowed, makeMap } from '@vue/shared'; import type * as ts from 'typescript'; import type { Code, VueCodeInformation } from '../../types'; +import { collectBindingNames } from '../../utils/collectBindings'; import { getNodeText, getStartEnd } from '../../utils/shared'; import type { ScriptCodegenOptions } from '../script'; -import { collectBindingNames, createTsAst, identifierRegex } from '../utils'; +import { createTsAst, identifierRegex } from '../utils'; import type { TemplateCodegenContext } from './context'; import type { TemplateCodegenOptions } from './index'; diff --git a/packages/language-core/lib/codegen/template/vFor.ts b/packages/language-core/lib/codegen/template/vFor.ts index 794602f42a..fce675dbda 100644 --- a/packages/language-core/lib/codegen/template/vFor.ts +++ b/packages/language-core/lib/codegen/template/vFor.ts @@ -1,6 +1,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type { Code } from '../../types'; -import { collectBindingNames, createTsAst, endOfLine, newLine } from '../utils'; +import { collectBindingNames } from '../../utils/collectBindings'; +import { createTsAst, endOfLine, newLine } from '../utils'; import type { TemplateCodegenContext } from './context'; import { generateElementChildren } from './elementChildren'; import type { TemplateCodegenOptions } from './index'; diff --git a/packages/language-core/lib/codegen/template/vSlot.ts b/packages/language-core/lib/codegen/template/vSlot.ts index 7651ed0190..4a3fa82a64 100644 --- a/packages/language-core/lib/codegen/template/vSlot.ts +++ b/packages/language-core/lib/codegen/template/vSlot.ts @@ -1,8 +1,9 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import type { Code } from '../../types'; +import { collectBindingNames } from '../../utils/collectBindings'; import { getStartEnd } from '../../utils/shared'; -import { collectBindingNames, createTsAst, endOfLine, newLine } from '../utils'; +import { createTsAst, endOfLine, newLine } from '../utils'; import { wrapWith } from '../utils/wrapWith'; import type { TemplateCodegenContext } from './context'; import { generateElementChildren } from './elementChildren'; diff --git a/packages/language-core/lib/codegen/utils/index.ts b/packages/language-core/lib/codegen/utils/index.ts index 92b80bc630..6f7ab63253 100644 --- a/packages/language-core/lib/codegen/utils/index.ts +++ b/packages/language-core/lib/codegen/utils/index.ts @@ -1,53 +1,12 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import type { Code, SfcBlock, VueCodeInformation } from '../../types'; -import { getNodeText } from '../../utils/shared'; export const newLine = `\n`; export const endOfLine = `;${newLine}`; export const combineLastMapping: VueCodeInformation = { __combineOffset: 1 }; export const identifierRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; -export function collectBindingNames( - ts: typeof import('typescript'), - node: ts.Node, - ast: ts.SourceFile, -) { - return collectIdentifiers(ts, node).map(({ id }) => getNodeText(ts, id, ast)); -} - -export function collectIdentifiers( - ts: typeof import('typescript'), - node: ts.Node, - results: { - id: ts.Identifier; - isRest: boolean; - initializer: ts.Expression | undefined; - }[] = [], - isRest = false, - initializer: ts.Expression | undefined = undefined, -) { - if (ts.isIdentifier(node)) { - results.push({ id: node, isRest, initializer }); - } - else if (ts.isObjectBindingPattern(node)) { - for (const el of node.elements) { - collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken, el.initializer); - } - } - else if (ts.isArrayBindingPattern(node)) { - for (const el of node.elements) { - if (ts.isBindingElement(el)) { - collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken); - } - } - } - else { - ts.forEachChild(node, node => collectIdentifiers(ts, node, results, false)); - } - return results; -} - export function normalizeAttributeValue(node: CompilerDOM.TextNode): [string, number] { let offset = node.loc.start.offset; let content = node.loc.source; diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 5dabc257ed..886de3ff79 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript'; -import { collectIdentifiers } from '../codegen/utils'; import type { TextRange, VueCompilerOptions } from '../types'; +import { collectBindingIdentifiers, collectBindingRanges } from '../utils/collectBindings'; import { getNodeText, getStartEnd } from '../utils/shared'; const tsCheckReg = /^\/\/\s*@ts-(?:no)?check($|\s)/; @@ -236,7 +236,7 @@ export function parseScriptSetupRanges( }; if (ts.isVariableDeclaration(parent) && ts.isObjectBindingPattern(parent.name)) { defineProps.destructured = new Map(); - const identifiers = collectIdentifiers(ts, parent.name); + const identifiers = collectBindingIdentifiers(ts, parent.name); for (const { id, isRest, initializer } of identifiers) { const name = _getNodeText(id); if (isRest) { @@ -374,8 +374,8 @@ export function parseBindingRanges(ts: typeof import('typescript'), ast: ts.Sour ts.forEachChild(ast, node => { if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { - const vars = _findBindingVars(decl.name); - bindings.push(...vars.map(range => ({ range }))); + const ranges = collectBindingRanges(ts, decl.name, ast); + bindings.push(...ranges.map(range => ({ range }))); } } else if (ts.isFunctionDeclaration(node)) { @@ -445,47 +445,6 @@ export function parseBindingRanges(ts: typeof import('typescript'), ast: ts.Sour function _getNodeText(node: ts.Node) { return getNodeText(ts, node, ast); } - - function _findBindingVars(left: ts.BindingName) { - return findBindingVars(ts, left, ast); - } -} - -export function findBindingVars( - ts: typeof import('typescript'), - left: ts.BindingName, - ast: ts.SourceFile, -) { - const vars: TextRange[] = []; - worker(left); - return vars; - function worker(node: ts.Node) { - if (ts.isIdentifier(node)) { - vars.push(getStartEnd(ts, node, ast)); - } - // { ? } = ... - // [ ? ] = ... - else if (ts.isObjectBindingPattern(node) || ts.isArrayBindingPattern(node)) { - for (const property of node.elements) { - if (ts.isBindingElement(property)) { - worker(property.name); - } - } - } - // { foo: ? } = ... - else if (ts.isPropertyAssignment(node)) { - worker(node.initializer); - } - // { foo } = ... - else if (ts.isShorthandPropertyAssignment(node)) { - vars.push(getStartEnd(ts, node.name, ast)); - } - // { ...? } = ... - // [ ...? ] = ... - else if (ts.isSpreadAssignment(node) || ts.isSpreadElement(node)) { - worker(node.expression); - } - } } function getStatementRange( diff --git a/packages/language-core/lib/plugins/vue-root-tags.ts b/packages/language-core/lib/plugins/vue-root-tags.ts index 6a010b1d76..cee5cedd2e 100644 --- a/packages/language-core/lib/plugins/vue-root-tags.ts +++ b/packages/language-core/lib/plugins/vue-root-tags.ts @@ -18,9 +18,9 @@ const plugin: VueLanguagePlugin = () => { embeddedFile.content.push([sfc.content, undefined, 0, allCodeFeatures]); for ( const block of [ + sfc.template, sfc.script, sfc.scriptSetup, - sfc.template, ...sfc.styles, ...sfc.customBlocks, ] @@ -28,14 +28,7 @@ const plugin: VueLanguagePlugin = () => { if (!block) { continue; } - let content = block.content; - if (content.endsWith('\r\n')) { - content = content.slice(0, -2); - } - else if (content.endsWith('\n')) { - content = content.slice(0, -1); - } - const offset = content.lastIndexOf('\n') + 1; + const offset = block.content.lastIndexOf('\n', block.content.lastIndexOf('\n') - 1) + 1; // fix folding range end position failed to mapping replaceSourceRange( embeddedFile.content, diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index 6735c55380..8f8315ebc1 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -239,9 +239,10 @@ function createTsx( }); return { + getLang, getScriptRanges, getScriptSetupRanges, - getLang, + getSetupSlotsAssignName, getGeneratedScript, getGeneratedTemplate, }; diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 999625b4e9..7bffaac10a 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -26,7 +26,7 @@ export type Code = Segment; export interface VueCompilerOptions { target: number; lib: string; - globalTypesPath: (fileName: string) => string | undefined; + globalTypesPath: (fileName: string) => string | void; extensions: string[]; vitePressExtensions: string[]; petiteVueExtensions: string[]; diff --git a/packages/language-core/lib/utils/collectBindings.ts b/packages/language-core/lib/utils/collectBindings.ts new file mode 100644 index 0000000000..0aefcd6ce2 --- /dev/null +++ b/packages/language-core/lib/utils/collectBindings.ts @@ -0,0 +1,45 @@ +import type * as ts from 'typescript'; +import { getNodeText, getStartEnd } from './shared'; + +export function collectBindingNames( + ts: typeof import('typescript'), + node: ts.Node, + ast: ts.SourceFile, +) { + return collectBindingIdentifiers(ts, node).map(({ id }) => getNodeText(ts, id, ast)); +} + +export function collectBindingRanges( + ts: typeof import('typescript'), + node: ts.Node, + ast: ts.SourceFile, +) { + return collectBindingIdentifiers(ts, node).map(({ id }) => getStartEnd(ts, id, ast)); +} + +export function collectBindingIdentifiers( + ts: typeof import('typescript'), + node: ts.Node, + results: { + id: ts.Identifier; + isRest: boolean; + initializer: ts.Expression | undefined; + }[] = [], + isRest = false, + initializer: ts.Expression | undefined = undefined, +) { + if (ts.isIdentifier(node)) { + results.push({ id: node, isRest, initializer }); + } + else if (ts.isArrayBindingPattern(node) || ts.isObjectBindingPattern(node)) { + for (const el of node.elements) { + if (ts.isBindingElement(el)) { + collectBindingIdentifiers(ts, el.name, results, !!el.dotDotDotToken, el.initializer); + } + } + } + else { + ts.forEachChild(node, node => collectBindingIdentifiers(ts, node, results, false)); + } + return results; +} diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index aaec7af426..5c26a694fb 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -1,4 +1,4 @@ -import { camelize } from '@vue/shared'; +import { camelize, NOOP as noop } from '@vue/shared'; import { posix as path } from 'path-browserify'; import type * as ts from 'typescript'; import { generateGlobalTypes, getGlobalTypesFileName } from '../codegen/globalTypes'; @@ -232,30 +232,32 @@ export class CompilerOptionsResolver { ), }; - if (this.fileExists && this.globalTypesPath === undefined) { - const fileDirToGlobalTypesPath = new Map(); - resolvedOptions.globalTypesPath = fileName => { - const fileDir = path.dirname(fileName); - if (fileDirToGlobalTypesPath.has(fileDir)) { - return fileDirToGlobalTypesPath.get(fileDir); - } + if (resolvedOptions.globalTypesPath === noop) { + if (this.fileExists && this.globalTypesPath === undefined) { + const fileDirToGlobalTypesPath = new Map(); + resolvedOptions.globalTypesPath = fileName => { + const fileDir = path.dirname(fileName); + if (fileDirToGlobalTypesPath.has(fileDir)) { + return fileDirToGlobalTypesPath.get(fileDir); + } - const root = this.findNodeModulesRoot(fileDir, resolvedOptions.lib); - const result = root - ? path.join( - root, - 'node_modules', - '.vue-global-types', - getGlobalTypesFileName(resolvedOptions), - ) - : undefined; + const root = this.findNodeModulesRoot(fileDir, resolvedOptions.lib); + const result = root + ? path.join( + root, + 'node_modules', + '.vue-global-types', + getGlobalTypesFileName(resolvedOptions), + ) + : undefined; - fileDirToGlobalTypesPath.set(fileDir, result); - return result; - }; - } - else { - resolvedOptions.globalTypesPath = () => this.globalTypesPath; + fileDirToGlobalTypesPath.set(fileDir, result); + return result; + }; + } + else { + resolvedOptions.globalTypesPath = () => this.globalTypesPath; + } } return resolvedOptions; @@ -303,7 +305,7 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla return { target, lib, - globalTypesPath: () => undefined, + globalTypesPath: noop, extensions: ['.vue'], vitePressExtensions: [], petiteVueExtensions: [], diff --git a/packages/language-core/package.json b/packages/language-core/package.json index ddffb27f98..24a78ae47d 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-core", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", diff --git a/packages/language-plugin-pug/package.json b/packages/language-plugin-pug/package.json index 4e3faf336b..290f9f1b81 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": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", @@ -19,6 +19,6 @@ "devDependencies": { "@types/node": "^22.10.4", "@vue/compiler-dom": "^3.5.0", - "@vue/language-core": "3.0.3" + "@vue/language-core": "3.0.4" } } diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index 3b8d8e7a92..34b40ed8a9 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -117,6 +117,9 @@ connection.onInitialize(params => { getComponentProps(...args) { return sendTsServerRequest('_vue:getComponentProps', args); }, + getComponentSlots(...args) { + return sendTsServerRequest('_vue:getComponentSlots', args); + }, getElementAttrs(...args) { return sendTsServerRequest('_vue:getElementAttrs', args); }, @@ -202,7 +205,7 @@ connection.onInitialize(params => { language, server.languageServicePlugins, createLanguageServiceEnvironment(server, [...server.workspaceFolders.all]), - { vue: { compilerOptions: commonLine.vueOptions } }, + {}, ); } }); diff --git a/packages/language-server/lib/reactivityAnalyze.ts b/packages/language-server/lib/reactivityAnalyze.ts index a3b0001ed6..793c0930d1 100644 --- a/packages/language-server/lib/reactivityAnalyze.ts +++ b/packages/language-server/lib/reactivityAnalyze.ts @@ -1,4 +1,4 @@ -import { findBindingVars, hyphenateAttr, type TextRange } from '@vue/language-core'; +import { collectBindingRanges, hyphenateAttr, type TextRange } from '@vue/language-core'; import type * as ts from 'typescript'; const enum TrackKind { @@ -164,8 +164,8 @@ export function analyze( } function findSubscribers(refName: ts.BindingName, trackKinds: TrackKind[], visited = new Set()) { - return findBindingVars(ts, refName, sourceFile) - .map(binding => findSubscribersWorker(binding, trackKinds, visited)) + return collectBindingRanges(ts, refName, sourceFile) + .map(range => findSubscribersWorker(range, trackKinds, visited)) .flat(); } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 7ccd69686e..2c45282971 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-server", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", @@ -17,9 +17,9 @@ }, "dependencies": { "@volar/language-server": "2.4.20", - "@vue/language-core": "3.0.3", - "@vue/language-service": "3.0.3", - "@vue/typescript-plugin": "3.0.3", + "@vue/language-core": "3.0.4", + "@vue/language-service": "3.0.4", + "@vue/typescript-plugin": "3.0.4", "vscode-uri": "^3.0.8" }, "peerDependencies": { diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index aa1a7856b2..9c249e2828 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -4,9 +4,6 @@ export * from '@volar/language-service'; // for @vue/language-server usage export * from '@volar/language-service/lib/utils/featureWorkers'; -import type { VueCompilerOptions } from '@vue/language-core'; -import type * as ts from 'typescript'; - import { create as createEmmetPlugin } from 'volar-service-emmet'; import { create as createJsonPlugin } from 'volar-service-json'; import { create as createPugFormatPlugin } from 'volar-service-pug-beautify'; @@ -15,62 +12,52 @@ import { create as createTypeScriptSyntacticPlugin } from 'volar-service-typescr import { create as createCssPlugin } from './lib/plugins/css'; import { create as createTypescriptSemanticTokensPlugin } from './lib/plugins/typescript-semantic-tokens'; import { create as createVueAutoDotValuePlugin } from './lib/plugins/vue-autoinsert-dotvalue'; -import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoinsert-space'; +import { create as createVueAutoSpacePlugin } from './lib/plugins/vue-autoinsert-space'; import { create as createVueCompilerDomErrorsPlugin } from './lib/plugins/vue-compiler-dom-errors'; import { create as createVueComponentSemanticTokensPlugin } from './lib/plugins/vue-component-semantic-tokens'; import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments'; import { create as createVueDocumentDropPlugin } from './lib/plugins/vue-document-drop'; import { create as createVueDocumentHighlightsPlugin } from './lib/plugins/vue-document-highlights'; -import { create as createVueDocumentLinksPlugin } from './lib/plugins/vue-document-links'; import { create as createVueExtractFilePlugin } from './lib/plugins/vue-extract-file'; import { create as createVueGlobalTypesErrorPlugin } from './lib/plugins/vue-global-types-error'; import { create as createVueInlayHintsPlugin } from './lib/plugins/vue-inlayhints'; import { create as createVueMissingPropsHintsPlugin } from './lib/plugins/vue-missing-props-hints'; +import { create as createVueScopedClassLinksPlugin } from './lib/plugins/vue-scoped-class-links'; import { create as createVueSfcPlugin } from './lib/plugins/vue-sfc'; import { create as createVueSuggestDefineAssignmentPlugin } from './lib/plugins/vue-suggest-define-assignment'; import { create as createVueTemplatePlugin } from './lib/plugins/vue-template'; +import { create as createVueTemplateRefLinksPlugin } from './lib/plugins/vue-template-ref-links'; import { create as createVueTwoslashQueriesPlugin } from './lib/plugins/vue-twoslash-queries'; -declare module '@volar/language-service' { - export interface ProjectContext { - vue?: { - compilerOptions: VueCompilerOptions; - }; - } -} - export function createVueLanguageServicePlugins( ts: typeof import('typescript'), - tsPluginClient: - | import('@vue/typescript-plugin/lib/requests').Requests & { - getDocumentHighlights: (fileName: string, position: number) => Promise; - } - | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ) { - const getTsPluginClient = () => tsPluginClient; const plugins = [ createCssPlugin(), createJsonPlugin(), createPugFormatPlugin(), createTypeScriptDocCommentTemplatePlugin(ts), - createTypescriptSemanticTokensPlugin(getTsPluginClient), + createTypescriptSemanticTokensPlugin(tsPluginClient), createTypeScriptSyntacticPlugin(ts), - createVueAutoAddSpacePlugin(), - createVueAutoDotValuePlugin(ts, getTsPluginClient), + createVueAutoSpacePlugin(), + createVueAutoDotValuePlugin(ts, tsPluginClient), createVueCompilerDomErrorsPlugin(), - createVueComponentSemanticTokensPlugin(getTsPluginClient), - createVueDocumentDropPlugin(ts, getTsPluginClient), - createVueDocumentLinksPlugin(), + createVueComponentSemanticTokensPlugin(tsPluginClient), + createVueDocumentDropPlugin(ts, tsPluginClient), + createVueDocumentHighlightsPlugin(tsPluginClient), createVueDirectiveCommentsPlugin(), - createVueExtractFilePlugin(ts, getTsPluginClient), + createVueExtractFilePlugin(ts, tsPluginClient), createVueGlobalTypesErrorPlugin(), createVueInlayHintsPlugin(ts), - createVueMissingPropsHintsPlugin(getTsPluginClient), + createVueMissingPropsHintsPlugin(tsPluginClient), + createVueScopedClassLinksPlugin(), createVueSfcPlugin(), createVueSuggestDefineAssignmentPlugin(), - createVueTemplatePlugin('html', getTsPluginClient), - createVueTemplatePlugin('pug', getTsPluginClient), - createVueTwoslashQueriesPlugin(getTsPluginClient), + createVueTemplatePlugin('html', tsPluginClient), + createVueTemplatePlugin('pug', tsPluginClient), + createVueTemplateRefLinksPlugin(), + createVueTwoslashQueriesPlugin(tsPluginClient), createEmmetPlugin({ mappedLanguages: { 'vue-root-tags': 'html', @@ -78,8 +65,5 @@ export function createVueLanguageServicePlugins( }, }), ]; - if (tsPluginClient) { - plugins.push(createVueDocumentHighlightsPlugin(tsPluginClient.getDocumentHighlights)); - } return plugins; } diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index a9ae2246be..68b2102959 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -1,8 +1,8 @@ import type { LanguageServicePlugin, TextDocument, VirtualCode } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; +import { isRenameEnabled } from '@vue/language-core'; import { create as baseCreate, type Provide } from 'volar-service-css'; import type * as css from 'vscode-css-languageservice'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); @@ -20,9 +20,11 @@ export function create(): LanguageServicePlugin { async provideDiagnostics(document, token) { let diagnostics = await baseInstance.provideDiagnostics?.(document, token) ?? []; if (document.languageId === 'postcss') { - diagnostics = diagnostics.filter(diag => diag.code !== 'css-semicolonexpected'); - diagnostics = diagnostics.filter(diag => diag.code !== 'css-ruleorselectorexpected'); - diagnostics = diagnostics.filter(diag => diag.code !== 'unknownAtRules'); + diagnostics = diagnostics.filter(diag => + diag.code !== 'css-semicolonexpected' + && diag.code !== 'css-ruleorselectorexpected' + && diag.code !== 'unknownAtRules' + ); } return diagnostics; }, @@ -52,20 +54,13 @@ export function create(): LanguageServicePlugin { document: TextDocument, position: css.Position, ) { - 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_')) { + const info = getEmbeddedInfo(context, document, id => id.startsWith('style_')); + if (!info) { return false; } + const { sourceScript, virtualCode, root } = info; - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return false; - } - - const block = root.sfc.styles.find(style => style.name === decoded![1]); + const block = root.sfc.styles.find(style => style.name === virtualCode.id); if (!block) { return false; } @@ -83,11 +78,7 @@ export function create(): LanguageServicePlugin { 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 - ) { + if (!sourceOffsets.length || !isRenameEnabled(data)) { continue; } diff --git a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts index 20949218f6..58233dbe05 100644 --- a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts @@ -1,15 +1,12 @@ -import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; +import type { LanguageServicePlugin } from '@volar/language-service'; import { convertClassificationsToSemanticTokens } from 'volar-service-typescript/lib/semanticFeatures/semanticTokens'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create( - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { - name: 'typescript-highlights', + name: 'typescript-semantic-tokens', capabilities: { semanticTokensProvider: { legend: { @@ -39,22 +36,13 @@ export function create( }, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); - return { async provideDocumentSemanticTokens(document, range, legend) { - 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 !== 'main') { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'main'); + if (!info) { return; } + const { root } = info; const start = document.offsetAt(range.start); const end = document.offsetAt(range.end); diff --git a/packages/language-service/lib/plugins/utils.ts b/packages/language-service/lib/plugins/utils.ts index 0abc86981f..60ddd36d42 100644 --- a/packages/language-service/lib/plugins/utils.ts +++ b/packages/language-service/lib/plugins/utils.ts @@ -1,12 +1,56 @@ -import type { TextDocument } from '@volar/language-service'; +import { type LanguageServiceContext, type SourceScript, type TextDocument } from '@volar/language-service'; +import { VueVirtualCode } from '@vue/language-core'; +import { URI } from 'vscode-uri'; export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export function isTsDocument(document: TextDocument) { - return document.languageId === 'javascript' - || document.languageId === 'typescript' - || document.languageId === 'javascriptreact' - || document.languageId === 'typescriptreact'; +export function getEmbeddedInfo( + context: LanguageServiceContext, + document: TextDocument, + embeddedCodeId?: string | ((id: string) => boolean), + languageId?: string, +) { + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); + if (!decoded) { + return; + } + + if (embeddedCodeId) { + if (typeof embeddedCodeId === 'string') { + if (decoded[1] !== embeddedCodeId) { + return; + } + } + else if (!embeddedCodeId(decoded[1])) { + return; + } + } + + if (languageId && document.languageId !== languageId) { + return; + } + + const sourceScript = context.language.scripts.get(decoded[0]); + if (!sourceScript?.generated) { + return; + } + + const virtualCode = sourceScript.generated.embeddedCodes.get(decoded[1]); + if (!virtualCode) { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + + return { + sourceScript: sourceScript as Required>, + virtualCode, + root, + }; } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 56416b93bc..46f2ddc681 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -1,14 +1,11 @@ -import type { LanguageServiceContext, LanguageServicePlugin, TextDocument } from '@volar/language-service'; -import { hyphenateAttr, VueVirtualCode } from '@vue/language-core'; +import type { LanguageServicePlugin, TextDocument } from '@volar/language-service'; +import { hyphenateAttr } from '@vue/language-core'; import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; -import { isTsDocument, sleep } from './utils'; +import { getEmbeddedInfo, sleep } from './utils'; export function create( ts: typeof import('typescript'), - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-autoinsert-dotvalue', @@ -19,16 +16,17 @@ export function create( }, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); let currentReq = 0; + return { async provideAutoInsertSnippet(document, selection, change) { - // selection must at end of change - if (document.offsetAt(selection) !== change.rangeOffset + change.text.length) { + const info = getEmbeddedInfo(context, document, id => id.startsWith('script_')); + if (!info) { return; } - if (!isTsDocument(document)) { + // selection must at end of change + if (document.offsetAt(selection) !== change.rangeOffset + change.text.length) { return; } @@ -48,18 +46,7 @@ export function create( return; } - 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) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return; - } + const { sourceScript, virtualCode, root } = info; const { sfc } = root; const blocks = [sfc.script, sfc.scriptSetup].filter(block => !!block); @@ -67,32 +54,26 @@ export function create( return; } - let sourceCodeOffset = document.offsetAt(selection); - let mapped = false; - for (const [, map] of context.language.maps.forEach(virtualCode)) { - for (const [sourceOffset] of map.toSourceLocation(sourceCodeOffset)) { - sourceCodeOffset = sourceOffset; - mapped = true; - break; - } - if (mapped) { - break; - } + let sourceOffset: number | undefined; + const map = context.language.maps.get(virtualCode, sourceScript); + for (const [offset] of map.toSourceLocation(document.offsetAt(selection))) { + sourceOffset = offset; + break; } - if (!mapped) { + if (sourceOffset === undefined) { return; } for (const { ast, startTagEnd, endTagStart } of blocks) { - if (sourceCodeOffset < startTagEnd || sourceCodeOffset > endTagStart) { + if (sourceOffset < startTagEnd || sourceOffset > endTagStart) { continue; } - if (isBlacklistNode(ts, ast, sourceCodeOffset - startTagEnd, false)) { + if (isBlacklistNode(ts, ast, sourceOffset - startTagEnd, false)) { return; } } - const props = await tsPluginClient?.getPropertiesAtLocation(root.fileName, sourceCodeOffset) ?? []; + const props = await tsPluginClient?.getPropertiesAtLocation(root.fileName, sourceOffset) ?? []; if (props.some(prop => prop === 'value')) { return '${1:.value}'; } diff --git a/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts b/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts index 4621071b14..4721d69d32 100644 --- a/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts +++ b/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts @@ -1,6 +1,5 @@ -import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin, TextDocument } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -14,65 +13,42 @@ export function create(): LanguageServicePlugin { create(context) { return { provideDiagnostics(document) { - if (!isSupportedDocument(document)) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { return; } + const { root } = info; - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!virtualCode) { - return; - } - - const root = sourceScript?.generated?.root; - if (!(root instanceof VueVirtualCode)) { + const { template } = root.sfc; + if (!template) { return; } - const templateErrors: Diagnostic[] = []; - const { template } = root.sfc; + const diagnostics: Diagnostic[] = []; - if (template) { - for (const error of template.errors) { - onCompilerError(error, 1 satisfies typeof DiagnosticSeverity.Error); - } - - for (const warning of template.warnings) { - onCompilerError(warning, 2 satisfies typeof DiagnosticSeverity.Warning); - } - - function onCompilerError( - error: NonNullable['errors'][number], - severity: DiagnosticSeverity, - ) { - const templateHtmlRange = { - start: error.loc?.start.offset ?? 0, - end: error.loc?.end.offset ?? 0, - }; - let errorMessage = error.message; - - templateErrors.push({ + for ( + const [errors, severity] of [ + [template.errors, 1 satisfies typeof DiagnosticSeverity.Error], + [template.warnings, 2 satisfies typeof DiagnosticSeverity.Warning], + ] as const + ) { + for (const error of errors) { + diagnostics.push({ range: { - start: document.positionAt(templateHtmlRange.start), - end: document.positionAt(templateHtmlRange.end), + start: document.positionAt(error.loc?.start.offset ?? 0), + end: document.positionAt(error.loc?.end.offset ?? 0), }, severity, code: error.code, source: 'vue', - message: errorMessage, + message: error.message, }); } } - return templateErrors; + return diagnostics; }, }; }, }; - - function isSupportedDocument(document: TextDocument) { - return document.languageId === 'jade' || document.languageId === 'html'; - } } diff --git a/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts b/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts index 60de24a5a1..1b973c22db 100644 --- a/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts @@ -1,15 +1,13 @@ -import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from '@volar/language-service'; -import { forEachElementNode, hyphenateTag, VueVirtualCode } from '@vue/language-core'; +import type { LanguageServicePlugin, SemanticToken } from '@volar/language-service'; +import { forEachElementNode, hyphenateTag } from '@vue/language-core'; import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create( - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { - name: 'vue-component-highlights', + name: 'vue-component-semantic-tokens', capabilities: { semanticTokensProvider: { legend: { @@ -19,68 +17,34 @@ export function create( }, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); - return { async provideDocumentSemanticTokens(document, range, legend) { - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!sourceScript?.generated || virtualCode?.id !== 'template') { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { return; } + const { root } = info; const { template } = root.sfc; - if (!template) { + if (!template?.ast) { return; } - const result: SemanticToken[] = []; + const componentSpans: ts.TextSpan[] = []; + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); - const tokenTypes = legend.tokenTypes.indexOf('component'); - const componentSpans = await getComponentSpans(root.fileName, template, { - start: document.offsetAt(range.start), - length: document.offsetAt(range.end) - document.offsetAt(range.start), - }); - - for (const span of componentSpans) { - const position = document.positionAt(span.start); - result.push([ - position.line, - position.character, - span.length, - tokenTypes, - 0, - ]); - } - return result; - }, - }; + const validComponentNames = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const elements = new Set(await tsPluginClient?.getElementNames(root.fileName) ?? []); + const components = new Set([ + ...validComponentNames, + ...validComponentNames.map(hyphenateTag), + ]); - async function getComponentSpans( - fileName: string, - template: NonNullable, - spanTemplateRange: ts.TextSpan, - ) { - const result: ts.TextSpan[] = []; - const validComponentNames = await tsPluginClient?.getComponentNames(fileName) ?? []; - const elements = new Set(await tsPluginClient?.getElementNames(fileName) ?? []); - const components = new Set([ - ...validComponentNames, - ...validComponentNames.map(hyphenateTag), - ]); - - if (template.ast) { for (const node of forEachElementNode(template.ast)) { if ( - node.loc.end.offset <= spanTemplateRange.start - || node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length) + node.loc.end.offset <= start + || node.loc.start.offset >= end ) { continue; } @@ -89,21 +53,36 @@ export function create( if (template.lang === 'html') { start += '<'.length; } - result.push({ + componentSpans.push({ start, length: node.tag.length, }); if (template.lang === 'html' && !node.isSelfClosing) { - result.push({ + componentSpans.push({ start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag), length: node.tag.length, }); } } } - } - return result; - } + + const result: SemanticToken[] = []; + const tokenType = legend.tokenTypes.indexOf('component'); + + for (const span of componentSpans) { + const position = document.positionAt(span.start); + result.push([ + position.line, + position.character, + span.length, + tokenType, + 0, + ]); + } + + return result; + }, + }; }, }; } diff --git a/packages/language-service/lib/plugins/vue-document-drop.ts b/packages/language-service/lib/plugins/vue-document-drop.ts index cf677cecee..0382d31073 100644 --- a/packages/language-service/lib/plugins/vue-document-drop.ts +++ b/packages/language-service/lib/plugins/vue-document-drop.ts @@ -1,22 +1,16 @@ -import type { - InsertTextFormat, - LanguageServiceContext, - LanguageServicePlugin, - WorkspaceEdit, -} from '@volar/language-service'; -import { forEachEmbeddedCode, VueVirtualCode } from '@vue/language-core'; +import type { InsertTextFormat, LanguageServicePlugin, WorkspaceEdit } from '@volar/language-service'; +import { forEachEmbeddedCode } from '@vue/language-core'; import { camelize, capitalize, hyphenate } from '@vue/shared'; import { posix as path } from 'path-browserify'; import { getUserPreferences } from 'volar-service-typescript/lib/configs/getUserPreferences'; import { URI } from 'vscode-uri'; -import { TagNameCasing } from '../nameCasing'; +import { checkCasing, TagNameCasing } from '../nameCasing'; import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file'; +import { getEmbeddedInfo } from './utils'; export function create( ts: typeof import('typescript'), - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-document-drop', @@ -24,32 +18,13 @@ export function create( documentDropEditsProvider: true, }, create(context) { - if (!context.project.vue) { - return {}; - } - - let casing = TagNameCasing.Pascal as TagNameCasing; // TODO - - const tsPluginClient = getTsPluginClient?.(context); - const vueCompilerOptions = context.project.vue.compilerOptions; - return { async provideDocumentDropEdits(document, _position, dataTransfer) { - if (document.languageId !== 'html') { - return; - } - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'template', 'html'); + if (!info) { return; } + const { sourceScript, root } = info; let importUri: string | undefined; for (const [mimeType, item] of dataTransfer) { @@ -57,7 +32,7 @@ export function create( importUri = item.value as string; } } - if (!importUri || !vueCompilerOptions.extensions.some(ext => importUri.endsWith(ext))) { + if (!importUri || !root.vueCompilerOptions.extensions.some(ext => importUri.endsWith(ext))) { return; } @@ -67,21 +42,20 @@ export function create( return; } - let baseName = importUri.slice(importUri.lastIndexOf('/') + 1); - baseName = baseName.slice(0, baseName.lastIndexOf('.')); - const newName = capitalize(camelize(baseName)); + const casing = await checkCasing(context, sourceScript.id); + const baseName = path.basename(importUri); + const newName = capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.')))); const additionalEdit: WorkspaceEdit = {}; const code = [...forEachEmbeddedCode(root)].find(code => code.id === (sfc.scriptSetup ? 'scriptsetup_raw' : 'script_raw') )!; const lastImportNode = getLastImportNode(ts, script.ast); - const incomingFileName = context.project.typescript?.uriConverter.asFileName(URI.parse(importUri)) - ?? URI.parse(importUri).fsPath.replace(/\\/g, '/'); + const incomingFileName = URI.parse(importUri).fsPath.replace(/\\/g, '/'); let importPath: string | undefined; - const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root); + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(root); if (tsPluginClient && serviceScript) { const tsDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, serviceScript.code.id); const tsDocument = context.documents.get( @@ -141,7 +115,7 @@ export function create( } return { - insertText: `<${casing === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, + insertText: `<${casing.tag === TagNameCasing.Kebab ? hyphenate(newName) : newName}$0 />`, insertTextFormat: 2 satisfies typeof InsertTextFormat.Snippet, additionalEdit, }; diff --git a/packages/language-service/lib/plugins/vue-document-highlights.ts b/packages/language-service/lib/plugins/vue-document-highlights.ts index cae6000347..29efe66b2d 100644 --- a/packages/language-service/lib/plugins/vue-document-highlights.ts +++ b/packages/language-service/lib/plugins/vue-document-highlights.ts @@ -1,10 +1,8 @@ import type { DocumentHighlightKind, LanguageServicePlugin } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create( - getDocumentHighlights: (fileName: string, position: number) => Promise, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-document-highlights', @@ -14,20 +12,13 @@ export function create( create(context) { return { async provideDocumentHighlights(document, position) { - 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 !== 'main') { + const info = getEmbeddedInfo(context, document, 'main'); + if (!info) { return; } + const { root } = info; - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return; - } - - const result = await getDocumentHighlights(root.fileName, document.offsetAt(position)); + const result = await tsPluginClient?.getDocumentHighlights(root.fileName, document.offsetAt(position)); return result ?.filter(({ fileName }) => fileName === root.fileName) diff --git a/packages/language-service/lib/plugins/vue-document-links.ts b/packages/language-service/lib/plugins/vue-document-links.ts deleted file mode 100644 index 59aba65179..0000000000 --- a/packages/language-service/lib/plugins/vue-document-links.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { DocumentLink, LanguageServicePlugin } from '@volar/language-service'; -import { type Sfc, tsCodegen, VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; - -export function create(): LanguageServicePlugin { - return { - name: 'vue-document-links', - capabilities: { - documentLinkProvider: {}, - }, - create(context) { - return { - provideDocumentLinks(document) { - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!sourceScript?.generated || (virtualCode?.id !== 'template' && virtualCode?.id !== 'scriptsetup_raw')) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return; - } - - const { sfc } = root; - const codegen = tsCodegen.get(sfc); - const result: DocumentLink[] = []; - - if (virtualCode.id === 'template') { - const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; - const styleClasses = new Map(); - const option = root.vueCompilerOptions.resolveStyleClassNames; - - for (let i = 0; i < sfc.styles.length; i++) { - const style = sfc.styles[i]; - if (option === true || (option === 'scoped' && style.scoped)) { - for (const className of style.classNames) { - if (!styleClasses.has(className.text.slice(1))) { - styleClasses.set(className.text.slice(1), []); - } - styleClasses.get(className.text.slice(1))!.push({ - index: i, - style, - classOffset: className.offset, - }); - } - } - } - - for (const { className, offset } of scopedClasses) { - const styles = styleClasses.get(className); - if (styles) { - for (const style of styles) { - const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index); - const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + style.index); - if (!styleVirtualCode) { - continue; - } - const styleDocument = context.documents.get( - styleDocumentUri, - styleVirtualCode.languageId, - styleVirtualCode.snapshot, - ); - const start = styleDocument.positionAt(style.classOffset); - const end = styleDocument.positionAt(style.classOffset + className.length + 1); - result.push({ - range: { - start: document.positionAt(offset), - end: document.positionAt(offset + className.length), - }, - target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) - + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, - }); - } - } - } - } - else if (virtualCode.id === 'scriptsetup_raw') { - if (!sfc.scriptSetup) { - return; - } - - const templateVirtualCode = sourceScript.generated.embeddedCodes.get('template'); - if (!templateVirtualCode) { - return; - } - const templateDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'template'); - const templateDocument = context.documents.get( - templateDocumentUri, - templateVirtualCode.languageId, - templateVirtualCode.snapshot, - ); - - const templateRefs = codegen?.getGeneratedTemplate()?.templateRefs; - const useTemplateRefs = codegen?.getScriptSetupRanges()?.useTemplateRef ?? []; - - for (const { arg } of useTemplateRefs) { - if (!arg) { - continue; - } - - const name = sfc.scriptSetup.content.slice(arg.start + 1, arg.end - 1); - - for (const { offset } of templateRefs?.get(name) ?? []) { - const start = templateDocument.positionAt(offset); - const end = templateDocument.positionAt(offset + name.length); - - result.push({ - range: { - start: document.positionAt(arg.start + 1), - end: document.positionAt(arg.end - 1), - }, - target: templateDocumentUri - + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, - }); - } - } - } - - return result; - }, - }; - }, - }; -} diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index d54c7f4d97..64656a78f8 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -1,14 +1,9 @@ -import type { - CreateFile, - LanguageServiceContext, - LanguageServicePlugin, - TextDocumentEdit, - TextEdit, -} from '@volar/language-service'; +import type { CreateFile, LanguageServicePlugin, TextDocumentEdit, TextEdit } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; -import { type Sfc, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { type Sfc, tsCodegen } from '@vue/language-core'; import type * as ts from 'typescript'; import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; interface ActionData { uri: string; @@ -20,9 +15,7 @@ const unicodeReg = /\\u/g; export function create( ts: typeof import('typescript'), - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-extract-file', @@ -33,7 +26,6 @@ export function create( }, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); return { provideCodeActions(document, range, ctx) { if (ctx.only && !ctx.only.includes('refactor')) { @@ -46,18 +38,11 @@ export function create( return; } - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!sourceScript?.generated || virtualCode?.id !== 'template') { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { return; } + const { root } = info; const { sfc } = root; const script = sfc.scriptSetup ?? sfc.script; @@ -87,18 +72,11 @@ export function create( const { uri, range, newName } = codeAction.data as ActionData; const [startOffset, endOffset]: [number, number] = range; - const parsedUri = URI.parse(uri); - const decoded = context.decodeEmbeddedDocumentUri(parsedUri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!sourceScript?.generated || virtualCode?.id !== 'template') { - return codeAction; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, { uri } as any, 'template'); + if (!info) { return codeAction; } + const { sourceScript, virtualCode, root } = info; const { sfc } = root; const script = sfc.scriptSetup ?? sfc.script; @@ -112,16 +90,13 @@ export function create( } const toExtract = await tsPluginClient?.collectExtractProps(root.fileName, templateCodeRange) ?? []; - if (!toExtract) { - return codeAction; - } const templateInitialIndent = await context.env.getConfiguration!('vue.format.template.initialIndent') ?? true; const scriptInitialIndent = await context.env.getConfiguration!('vue.format.script.initialIndent') ?? false; - const document = context.documents.get(parsedUri, virtualCode.languageId, virtualCode.snapshot); + const document = context.documents.get(URI.parse(uri), virtualCode.languageId, virtualCode.snapshot); const sfcDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); const newUri = sfcDocument.uri.slice(0, sfcDocument.uri.lastIndexOf('/') + 1) + `${newName}.vue`; const lastImportNode = getLastImportNode(ts, script.ast); @@ -235,7 +210,7 @@ export function create( const props = toExtract.filter(p => !p.model); const models = toExtract.filter(p => p.model); if (props.length) { - lines.push(`defineProps<{ \n\t${props.map(p => `${p.name}: ${p.type};`).join('\n\t')}\n}>()`); + lines.push(`defineProps<{\n\t${props.map(p => `${p.name}: ${p.type};`).join('\n\t')}\n}>()`); } for (const model of models) { lines.push(`const ${model.name} = defineModel<${model.type}>('${model.name}', { required: true })`); diff --git a/packages/language-service/lib/plugins/vue-global-types-error.ts b/packages/language-service/lib/plugins/vue-global-types-error.ts index d9200d5a94..7bf78db634 100644 --- a/packages/language-service/lib/plugins/vue-global-types-error.ts +++ b/packages/language-service/lib/plugins/vue-global-types-error.ts @@ -1,10 +1,9 @@ import type { DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { - name: 'vue-compiler-dom-errors', + name: 'vue-global-types-error', capabilities: { diagnosticProvider: { interFileDependencies: false, @@ -14,21 +13,11 @@ export function create(): LanguageServicePlugin { create(context) { return { provideDiagnostics(document) { - if (document.languageId !== 'vue-root-tags') { - return; - } - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { return; } + const { root } = info; const { vueCompilerOptions } = root; const globalTypesPath = vueCompilerOptions.globalTypesPath(root.fileName); @@ -41,11 +30,19 @@ export function create(): LanguageServicePlugin { start: document.positionAt(0), end: document.positionAt(0), }, - severity: 1 satisfies typeof DiagnosticSeverity.Error, + severity: 2 satisfies typeof DiagnosticSeverity.Warning, code: 404, source: 'vue', - message: - `Write global types file failed. Please ensure that "node_modules" exists and "${vueCompilerOptions.lib}" is a direct dependency, or set "vueCompilerOptions.globalTypesPath" in "tsconfig.json" manually.`, + message: ` +Failed to write the global types file. Make sure that: + +1. "node_modules" directory exists. +2. "${vueCompilerOptions.lib}" is installed as a direct dependency. + +Alternatively, you can manually set "vueCompilerOptions.globalTypesPath" in your "tsconfig.json". + +If all dependencies are installed, try running the "vue.action.restartServer" command to restart Vue and TS servers. + `.trim(), }]; }, }; diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 4302f18052..02f5f4bf73 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -1,24 +1,22 @@ import type { InlayHint, InlayHintKind, LanguageServicePlugin } from '@volar/language-service'; -import { collectIdentifiers, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { collectBindingIdentifiers, tsCodegen } from '@vue/language-core'; import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create(ts: typeof import('typescript')): LanguageServicePlugin { return { - name: 'vue-inlay-hints', + name: 'vue-inlayhints', capabilities: { inlayHintProvider: {}, }, create(context) { return { async provideInlayHints(document, range) { - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!(virtualCode instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'main'); + if (!info) { return; } + const { root } = info; const settings: Record = {}; async function getSettingEnabled(key: string) { @@ -26,15 +24,16 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { } const result: InlayHint[] = []; + const { sfc } = root; - const codegen = tsCodegen.get(virtualCode.sfc); + const codegen = tsCodegen.get(sfc); const inlayHints = [ ...codegen?.getGeneratedTemplate()?.inlayHints ?? [], ...codegen?.getGeneratedScript()?.inlayHints ?? [], ]; const scriptSetupRanges = codegen?.getScriptSetupRanges(); - if (scriptSetupRanges?.defineProps?.destructured && virtualCode.sfc.scriptSetup?.ast) { + if (scriptSetupRanges?.defineProps?.destructured && sfc.scriptSetup?.ast) { const setting = 'vue.inlayHints.destructuredProps'; const enabled = await getSettingEnabled(setting); @@ -42,7 +41,7 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { for ( const [prop, isShorthand] of findDestructuredProps( ts, - virtualCode.sfc.scriptSetup.ast, + sfc.scriptSetup.ast, scriptSetupRanges.defineProps.destructured.keys(), ) ) { @@ -61,9 +60,9 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { } const blocks = [ - virtualCode.sfc.template, - virtualCode.sfc.script, - virtualCode.sfc.scriptSetup, + sfc.template, + sfc.script, + sfc.scriptSetup, ]; const start = document.offsetAt(range.start); const end = document.offsetAt(range.end); @@ -192,7 +191,7 @@ export function findDestructuredProps( && ts.isCallExpression(initializer) && initializer.expression.getText(ast) === 'defineProps'; - for (const { id } of collectIdentifiers(ts, name)) { + for (const { id } of collectBindingIdentifiers(ts, name)) { if (isDefineProps) { excludedIds.add(id); } @@ -209,7 +208,7 @@ export function findDestructuredProps( } for (const p of parameters) { - for (const { id } of collectIdentifiers(ts, p)) { + for (const { id } of collectBindingIdentifiers(ts, p)) { registerLocalBinding(id); } } diff --git a/packages/language-service/lib/plugins/vue-missing-props-hints.ts b/packages/language-service/lib/plugins/vue-missing-props-hints.ts index 79d758d0b7..e7fd707956 100644 --- a/packages/language-service/lib/plugins/vue-missing-props-hints.ts +++ b/packages/language-service/lib/plugins/vue-missing-props-hints.ts @@ -5,15 +5,13 @@ import type { LanguageServicePlugin, TextDocument, } from '@volar/language-service'; -import { hyphenateAttr, hyphenateTag, VueVirtualCode } from '@vue/language-core'; +import { hyphenateAttr, hyphenateTag } from '@vue/language-core'; import * as html from 'vscode-html-languageservice'; -import { URI } from 'vscode-uri'; import { AttrNameCasing, checkCasing } from '../nameCasing'; +import { getEmbeddedInfo } from './utils'; export function create( - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-missing-props-hints', @@ -21,44 +19,28 @@ export function create( inlayHintProvider: {}, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); let intrinsicElementNames: Set; return { async provideInlayHints(document, range, cancellationToken) { - if (!isSupportedDocument(document)) { - return; - } - - if (!context.project.vue) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { return; } + const { sourceScript, root } = info; const enabled = await context.env.getConfiguration?.('vue.inlayHints.missingProps') ?? false; if (!enabled) { return; } - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (!virtualCode) { - return; - } - - const root = sourceScript?.generated?.root; - if (!(root instanceof VueVirtualCode)) { - return; - } - const scanner = getScanner(context, document); if (!scanner) { return; } const result: InlayHint[] = []; - const casing = await checkCasing(context, decoded[0]); + const casing = await checkCasing(context, sourceScript.id); const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; const componentProps: Record = {}; @@ -127,7 +109,7 @@ export function create( attrText = attrText.slice('v-model:'.length); } else if (attrText === 'v-model') { - attrText = context.project.vue.compilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? + attrText = root.vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? } else if (attrText.startsWith('v-on:')) { attrText = 'on-' + hyphenateAttr(attrText.slice('v-on:'.length)); @@ -184,8 +166,4 @@ export function create( } } } - - function isSupportedDocument(document: TextDocument) { - return document.languageId === 'jade' || document.languageId === 'html'; - } } diff --git a/packages/language-service/lib/plugins/vue-scoped-class-links.ts b/packages/language-service/lib/plugins/vue-scoped-class-links.ts new file mode 100644 index 0000000000..d02d655df1 --- /dev/null +++ b/packages/language-service/lib/plugins/vue-scoped-class-links.ts @@ -0,0 +1,70 @@ +import type { LanguageServicePlugin } from '@volar/language-service'; +import { tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-scoped-class-links', + capabilities: { + documentLinkProvider: {}, + }, + create(context) { + return { + provideDocumentLinks(document) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { + return; + } + const { sourceScript, root } = info; + + const { sfc } = root; + const codegen = tsCodegen.get(sfc); + + const option = root.vueCompilerOptions.resolveStyleClassNames; + const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; + const styleClasses = new Map(); + + for (let i = 0; i < sfc.styles.length; i++) { + const style = sfc.styles[i]; + if (option !== true && !(option === 'scoped' && style.scoped)) { + continue; + } + + const styleDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, 'style_' + i); + const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + i); + if (!styleVirtualCode) { + continue; + } + const styleDocument = context.documents.get( + styleDocumentUri, + styleVirtualCode.languageId, + styleVirtualCode.snapshot, + ); + + for (const { text, offset } of style.classNames) { + const start = styleDocument.positionAt(offset); + const end = styleDocument.positionAt(offset + text.length); + const target = styleDocumentUri + + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`; + if (!styleClasses.has(text)) { + styleClasses.set(text, []); + } + styleClasses.get(text)!.push(target); + } + } + + return scopedClasses.flatMap(({ className, offset }) => { + const range = { + start: document.positionAt(offset), + end: document.positionAt(offset + className.length), + }; + return styleClasses.get('.' + className)?.map(target => ({ + range, + target, + })) ?? []; + }); + }, + }; + }, + }; +} diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index c902ce89b8..11584a39af 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -4,16 +4,14 @@ import type { Diagnostic, DiagnosticSeverity, DocumentSymbol, - LanguageServiceContext, LanguageServicePlugin, SymbolKind, - TextDocument, } from '@volar/language-service'; import { VueVirtualCode } from '@vue/language-core'; import { create as createHtmlService } from 'volar-service-html'; import * as html from 'vscode-html-languageservice'; -import { URI } from 'vscode-uri'; import { loadLanguageBlocks } from './data'; +import { getEmbeddedInfo } from './utils'; let sfcDataProvider: html.IHTMLDataProvider | undefined; @@ -26,27 +24,31 @@ export function create(): LanguageServicePlugin { return [sfcDataProvider]; }, async getFormattingOptions(document, options, context) { - return await worker(document, context, async root => { - const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; - const blockTypes = ['template', 'script', 'style']; + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return {}; + } + const { root } = info; + + const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; + const blockTypes = ['template', 'script', 'style']; - for (const customBlock of root.sfc.customBlocks) { - blockTypes.push(customBlock.type); - } + for (const customBlock of root.sfc.customBlocks) { + blockTypes.push(customBlock.type); + } - return { - ...options, - ...formatSettings, - wrapAttributes: await context.env.getConfiguration?.('vue.format.wrapAttributes') ?? 'auto', - unformatted: '', - contentUnformatted: blockTypes.join(','), - endWithNewline: options.insertFinalNewline - ? true - : options.trimFinalNewlines - ? false - : document.getText().endsWith('\n'), - }; - }) ?? {}; + return { + ...options, + ...formatSettings, + wrapAttributes: await context.env.getConfiguration?.('vue.format.wrapAttributes') ?? 'auto', + unformatted: '', + contentUnformatted: blockTypes.join(','), + endWithNewline: options.insertFinalNewline + ? true + : options.trimFinalNewlines + ? false + : document.getText().endsWith('\n'), + }; }, }); return { @@ -88,133 +90,141 @@ export function create(): LanguageServicePlugin { return options; }, - provideDiagnostics(document, token) { - return worker(document, context, async root => { - const { vueSfc, sfc } = root; - if (!vueSfc) { - return; - } + async provideDiagnostics(document, token) { + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return []; + } + const { root } = info; + + const { vueSfc, sfc } = root; + if (!vueSfc) { + return; + } - const originalResult = await htmlServiceInstance.provideDiagnostics?.(document, token); - const sfcErrors: Diagnostic[] = []; - const { template } = sfc; + const originalResult = await htmlServiceInstance.provideDiagnostics?.(document, token); + const sfcErrors: Diagnostic[] = []; + const { template } = sfc; - const { - startTagEnd = Infinity, - endTagStart = -Infinity, - } = template ?? {}; + const { + startTagEnd = Infinity, + endTagStart = -Infinity, + } = template ?? {}; - for (const error of vueSfc.errors) { - if ('code' in error) { - const start = error.loc?.start.offset ?? 0; - const end = error.loc?.end.offset ?? 0; - if (end < startTagEnd || start >= endTagStart) { - sfcErrors.push({ - range: { - start: document.positionAt(start), - end: document.positionAt(end), - }, - severity: 1 satisfies typeof DiagnosticSeverity.Error, - code: error.code, - source: 'vue', - message: error.message, - }); - } + for (const error of vueSfc.errors) { + if ('code' in error) { + const start = error.loc?.start.offset ?? 0; + const end = error.loc?.end.offset ?? 0; + if (end < startTagEnd || start >= endTagStart) { + sfcErrors.push({ + range: { + start: document.positionAt(start), + end: document.positionAt(end), + }, + severity: 1 satisfies typeof DiagnosticSeverity.Error, + code: error.code, + source: 'vue', + message: error.message, + }); } } + } - return [ - ...originalResult ?? [], - ...sfcErrors, - ]; - }); + return [ + ...originalResult ?? [], + ...sfcErrors, + ]; }, provideDocumentSymbols(document) { - return worker(document, context, root => { - const result: DocumentSymbol[] = []; - const { sfc } = root; + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return; + } + const { root } = info; - if (sfc.template) { - result.push({ - name: 'template', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.template.start), - end: document.positionAt(sfc.template.end), - }, - selectionRange: { - start: document.positionAt(sfc.template.start), - end: document.positionAt(sfc.template.startTagEnd), - }, - }); - } - if (sfc.script) { - result.push({ - name: 'script', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.script.start), - end: document.positionAt(sfc.script.end), - }, - selectionRange: { - start: document.positionAt(sfc.script.start), - end: document.positionAt(sfc.script.startTagEnd), - }, - }); - } - if (sfc.scriptSetup) { - result.push({ - name: 'script setup', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.scriptSetup.start), - end: document.positionAt(sfc.scriptSetup.end), - }, - selectionRange: { - start: document.positionAt(sfc.scriptSetup.start), - end: document.positionAt(sfc.scriptSetup.startTagEnd), - }, - }); - } - for (const style of sfc.styles) { - let name = 'style'; - if (style.scoped) { - name += ' scoped'; - } - if (style.module) { - name += ' module'; - } - result.push({ - name, - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(style.start), - end: document.positionAt(style.end), - }, - selectionRange: { - start: document.positionAt(style.start), - end: document.positionAt(style.startTagEnd), - }, - }); + const result: DocumentSymbol[] = []; + const { sfc } = root; + + if (sfc.template) { + result.push({ + name: 'template', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.end), + }, + selectionRange: { + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.startTagEnd), + }, + }); + } + if (sfc.script) { + result.push({ + name: 'script', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.end), + }, + selectionRange: { + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.startTagEnd), + }, + }); + } + if (sfc.scriptSetup) { + result.push({ + name: 'script setup', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.end), + }, + selectionRange: { + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.startTagEnd), + }, + }); + } + for (const style of sfc.styles) { + let name = 'style'; + if (style.scoped) { + name += ' scoped'; } - for (const customBlock of sfc.customBlocks) { - result.push({ - name: `${customBlock.type}`, - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(customBlock.start), - end: document.positionAt(customBlock.end), - }, - selectionRange: { - start: document.positionAt(customBlock.start), - end: document.positionAt(customBlock.startTagEnd), - }, - }); + if (style.module) { + name += ' module'; } + result.push({ + name, + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(style.start), + end: document.positionAt(style.end), + }, + selectionRange: { + start: document.positionAt(style.start), + end: document.positionAt(style.startTagEnd), + }, + }); + } + for (const customBlock of sfc.customBlocks) { + result.push({ + name: `${customBlock.type}`, + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(customBlock.start), + end: document.positionAt(customBlock.end), + }, + selectionRange: { + start: document.positionAt(customBlock.start), + end: document.positionAt(customBlock.startTagEnd), + }, + }); + } - return result; - }); + return result; }, async provideCompletionItems(document, position, context, token) { @@ -300,19 +310,6 @@ export function create(): LanguageServicePlugin { }; }, }; - - function worker(document: TextDocument, context: LanguageServiceContext, callback: (root: VueVirtualCode) => T) { - if (document.languageId !== 'vue-root-tags') { - return; - } - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const root = sourceScript?.generated?.root; - if (root instanceof VueVirtualCode) { - return callback(root); - } - } } function getStyleCompletionItem( diff --git a/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts b/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts index e3871ce2f7..e79b554bd5 100644 --- a/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts +++ b/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts @@ -1,7 +1,6 @@ import type { CompletionItem, CompletionItemKind, LanguageServicePlugin } from '@volar/language-service'; -import { type TextRange, tsCodegen, VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; -import { isTsDocument } from './utils'; +import { type TextRange, tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -13,28 +12,17 @@ export function create(): LanguageServicePlugin { return { isAdditionalCompletion: true, async provideCompletionItems(document) { - if (!isTsDocument(document)) { + const info = getEmbeddedInfo(context, document, id => id.startsWith('script_')); + if (!info) { return; } + const { virtualCode, root } = info; const enabled = await context.env.getConfiguration?.('vue.suggest.defineAssignment') ?? true; if (!enabled) { return; } - 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) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return; - } - const { sfc } = root; const codegen = tsCodegen.get(sfc); const scriptSetup = sfc.scriptSetup; diff --git a/packages/language-service/lib/plugins/vue-template-ref-links.ts b/packages/language-service/lib/plugins/vue-template-ref-links.ts new file mode 100644 index 0000000000..cd526386c3 --- /dev/null +++ b/packages/language-service/lib/plugins/vue-template-ref-links.ts @@ -0,0 +1,65 @@ +import type { LanguageServicePlugin } from '@volar/language-service'; +import { tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-template-ref-links', + capabilities: { + documentLinkProvider: {}, + }, + create(context) { + return { + provideDocumentLinks(document) { + const info = getEmbeddedInfo(context, document, 'scriptsetup_raw'); + if (!info) { + return; + } + const { sourceScript, root } = info; + + const { sfc } = root; + const codegen = tsCodegen.get(sfc); + + if (!sfc.scriptSetup) { + return; + } + + const templateVirtualCode = sourceScript.generated.embeddedCodes.get('template'); + if (!templateVirtualCode) { + return; + } + const templateDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, 'template'); + const templateDocument = context.documents.get( + templateDocumentUri, + templateVirtualCode.languageId, + templateVirtualCode.snapshot, + ); + + const templateRefs = codegen?.getGeneratedTemplate()?.templateRefs; + const useTemplateRefs = codegen?.getScriptSetupRanges()?.useTemplateRef ?? []; + + return useTemplateRefs.flatMap(({ arg }) => { + if (!arg) { + return []; + } + const name = sfc.scriptSetup!.content.slice(arg.start + 1, arg.end - 1); + const range = { + start: document.positionAt(arg.start + 1), + end: document.positionAt(arg.end - 1), + }; + + return templateRefs?.get(name)?.map(({ offset }) => { + const start = templateDocument.positionAt(offset); + const end = templateDocument.positionAt(offset + name.length); + return { + range, + target: templateDocumentUri + + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, + }; + }) ?? []; + }); + }, + }; + }, + }; +} diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 49c9e0e3f4..78a34cb2aa 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -3,11 +3,10 @@ import type { CompletionItemTag, CompletionList, Disposable, - LanguageServiceContext, LanguageServicePlugin, TextDocument, } from '@volar/language-service'; -import { hyphenateAttr, hyphenateTag, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { hyphenateAttr, hyphenateTag, tsCodegen, type VueVirtualCode } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import type { ComponentPropInfo } from '@vue/typescript-plugin/lib/requests/getComponentProps'; import { create as createHtmlService } from 'volar-service-html'; @@ -16,11 +15,7 @@ import * as html from 'vscode-html-languageservice'; import { URI, Utils } from 'vscode-uri'; import { AttrNameCasing, checkCasing, TagNameCasing } from '../nameCasing'; import { loadModelModifiersData, loadTemplateData } from './data'; - -type InternalItemId = - | 'componentEvent' - | 'componentProp' - | 'specialTag'; +import { getEmbeddedInfo } from './utils'; const specialTags = new Set([ 'slot', @@ -42,15 +37,11 @@ let modelData: html.HTMLDataV1; export function create( mode: 'html' | 'pug', - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { let customData: html.IHTMLDataProvider[] = []; let extraCustomData: html.IHTMLDataProvider[] = []; - let lastCompletionComponentNames = new Set(); - const cachedPropInfos = new Map(); const onDidChangeCustomDataListeners = new Set<() => void>(); const onDidChangeCustomData = (listener: () => void): Disposable => { onDidChangeCustomDataListeners.add(listener); @@ -62,6 +53,7 @@ export function create( }; const baseService = mode === 'pug' ? createPugService({ + useDefaultDataProvider: false, getCustomData() { return [ ...customData, @@ -72,6 +64,7 @@ export function create( }) : createHtmlService({ documentSelector: ['html', 'markdown'], + useDefaultDataProvider: false, getCustomData() { return [ ...customData, @@ -80,6 +73,8 @@ export function create( }, onDidChangeCustomData, }); + const htmlDataProvider = html.getDefaultHTMLDataProvider(); + const languageId = mode === 'pug' ? 'jade' : 'html'; return { name: `vue-template (${mode})`, @@ -94,7 +89,6 @@ export function create( hoverProvider: true, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); const baseServiceInstance = baseService.create(context); builtInData ??= loadTemplateData(context.env.locale ?? 'en'); @@ -110,7 +104,9 @@ export function create( const vModel = builtInData.globalAttributes?.find(x => x.name === 'v-model'); if (vOn) { - const markdown = (typeof vOn.description === 'string' ? vOn.description : vOn.description?.value) ?? ''; + const markdown = typeof vOn.description === 'object' + ? vOn.description.value + : vOn.description ?? ''; const modifiers = markdown .split('\n- ')[4] .split('\n').slice(2, -1); @@ -121,7 +117,9 @@ export function create( } } if (vBind) { - const markdown = (typeof vBind.description === 'string' ? vBind.description : vBind.description?.value) ?? ''; + const markdown = typeof vBind.description === 'object' + ? vBind.description.value + : vBind.description ?? ''; const modifiers = markdown .split('\n- ')[4] .split('\n').slice(2, -1); @@ -135,7 +133,7 @@ export function create( for (const modifier of modelData.globalAttributes ?? []) { const description = typeof modifier.description === 'object' ? modifier.description.value - : modifier.description; + : modifier.description ?? ''; const references = modifier.references?.map(ref => `[${ref.name}](${ref.url})`).join(' | '); vModelModifiers[modifier.name] = description + '\n\n' + references; } @@ -154,120 +152,256 @@ export function create( }, async provideCompletionItems(document, position, completionContext, token) { - if (!isSupportedDocument(document)) { + const info = getEmbeddedInfo(context, document, 'template', languageId); + if (!info) { return; } + const { sourceScript, root } = info; + + const { + result: completionList, + target, + info: { + components, + propMap, + }, + } = await runWithVueData( + sourceScript.id, + root, + () => + baseServiceInstance.provideCompletionItems!( + document, + position, + completionContext, + token, + ), + ); - if (!context.project.vue) { + if (!completionList) { return; } - let sync: (() => Promise) | undefined; - let currentVersion: number | undefined; - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const root = sourceScript?.generated?.root; + switch (target) { + case 'tag': { + completionList.items.forEach(transformTag); + break; + } + case 'attribute': { + addDirectiveModifiers(completionList, document); + completionList.items.forEach(transformAttribute); + break; + } + } - if (root instanceof VueVirtualCode) { - // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver - baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token); + updateExtraCustomData([]); + return completionList; - sync = (await provideHtmlData(sourceScript!.id, root)).sync; - currentVersion = await sync(); + function transformTag(item: html.CompletionItem) { + const tagName = capitalize(camelize(item.label)); + if (components?.includes(tagName)) { + item.kind = 6 satisfies typeof CompletionItemKind.Variable; + item.sortText = '\u0000' + (item.sortText ?? item.label); + } } - let htmlComplete = await baseServiceInstance.provideCompletionItems?.( - document, - position, - completionContext, - token, - ); - while (currentVersion !== (currentVersion = await sync?.())) { - htmlComplete = await baseServiceInstance.provideCompletionItems?.( - document, - position, - completionContext, - token, - ); - } - if (!htmlComplete) { - return; - } + function transformAttribute(item: html.CompletionItem) { + let prop = propMap.get(item.label); - if (sourceScript?.generated) { - const virtualCode = sourceScript.generated.embeddedCodes.get('template'); - if (virtualCode) { - const embeddedDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, virtualCode.id); - afterHtmlCompletion( - htmlComplete, - context.documents.get(embeddedDocumentUri, virtualCode.languageId, virtualCode.snapshot), - ); + if (prop) { + if (prop.info?.documentation) { + item.documentation = { + kind: 'markdown', + value: prop.info.documentation, + }; + } + if (prop.info?.deprecated) { + item.tags = [1 satisfies typeof CompletionItemTag.Deprecated]; + } + } + else { + let name = item.label; + for (const str of ['v-bind:', ':']) { + if (name.startsWith(str) && name !== str) { + name = name.slice(str.length); + break; + } + } + if (specialProps.has(name)) { + prop = { + name, + kind: 'prop', + isGlobal: true, + }; + } + } + + const tokens: string[] = []; + + if (prop) { + const { isEvent, propName } = getPropName(prop.name, prop.kind === 'event'); + + if (prop.kind === 'prop') { + if (!prop.isGlobal || specialProps.has(propName)) { + item.kind = 5 satisfies typeof CompletionItemKind.Field; + } + } + else if (isEvent) { + item.kind = 23 satisfies typeof CompletionItemKind.Event; + if (propName.startsWith('vue:')) { + tokens.push('\u0004'); + } + } + + if (!prop.isGlobal || specialProps.has(propName)) { + tokens.push('\u0000'); + + if (item.label.startsWith(':')) { + tokens.push('\u0001'); + } + else if (item.label.startsWith('@')) { + tokens.push('\u0002'); + } + else if (item.label.startsWith('v-bind:')) { + tokens.push('\u0003'); + } + else if (item.label.startsWith('v-model:')) { + tokens.push('\u0004'); + } + else if (item.label.startsWith('v-on:')) { + tokens.push('\u0005'); + } + else { + tokens.push('\u0000'); + } + + if (specialProps.has(propName)) { + tokens.push('\u0001'); + } + else { + tokens.push('\u0000'); + } + } + } + else if ( + item.label === 'v-if' + || item.label === 'v-else-if' + || item.label === 'v-else' + || item.label === 'v-for' + ) { + item.kind = 14 satisfies typeof CompletionItemKind.Keyword; + tokens.push('\u0003'); + } + else if (item.label.startsWith('v-')) { + item.kind = 3 satisfies typeof CompletionItemKind.Function; + tokens.push('\u0002'); + } + else { + tokens.push('\u0001'); } - } - return htmlComplete; + item.sortText = tokens.join('') + (item.sortText ?? item.label); + } }, provideHover(document, position, token) { - if (!isSupportedDocument(document)) { + const info = getEmbeddedInfo(context, document, 'template', languageId); + if (!info) { return; } if (context.decodeEmbeddedDocumentUri(URI.parse(document.uri))) { - updateExtraCustomData([]); + updateExtraCustomData([ + htmlDataProvider, + ]); } return baseServiceInstance.provideHover?.(document, position, token); }, }; - async function provideHtmlData(sourceDocumentUri: URI, vueCode: VueVirtualCode) { + async function runWithVueData(sourceDocumentUri: URI, root: VueVirtualCode, fn: () => T) { + // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver + await fn(); + + const { sync } = await provideHtmlData(sourceDocumentUri, root); + let lastSync = await sync(); + let result = await fn(); + while (lastSync.version !== (lastSync = await sync()).version) { + result = await fn(); + } + return { result, ...lastSync }; + } + + async function provideHtmlData(sourceDocumentUri: URI, root: VueVirtualCode) { await (initializing ??= initialize()); const casing = await checkCasing(context, sourceDocumentUri); - if (builtInData.tags) { - for (const tag of builtInData.tags) { - if (isItemKey(tag.name)) { - continue; - } - - if (specialTags.has(tag.name)) { - tag.name = generateItemKey('specialTag', tag.name, ''); - } - else if (casing.tag === TagNameCasing.Kebab) { - tag.name = hyphenateTag(tag.name); - } - else { - tag.name = camelize(capitalize(tag.name)); - } + for (const tag of builtInData.tags ?? []) { + if (specialTags.has(tag.name)) { + continue; + } + if (casing.tag === TagNameCasing.Kebab) { + tag.name = hyphenateTag(tag.name); + } + else { + tag.name = camelize(capitalize(tag.name)); } } - const promises: Promise[] = []; - const tagInfos = new Map[] = []; + const tagMap = new Map(); - - let version = 0; - let components: string[] | undefined; - - cachedPropInfos.clear(); + const propMap = new Map(); updateExtraCustomData([ + { + getId: () => htmlDataProvider.getId(), + isApplicable: () => true, + provideTags() { + target = 'tag'; + return htmlDataProvider.provideTags() + .filter(tag => !specialTags.has(tag.name)); + }, + provideAttributes(tag) { + target = 'attribute'; + const attrs = htmlDataProvider.provideAttributes(tag); + if (tag === 'slot') { + const nameAttr = attrs.find(attr => attr.name === 'name'); + if (nameAttr) { + nameAttr.valueSet = 'slot'; + } + } + return attrs; + }, + provideValues(tag, attr) { + target = 'value'; + return htmlDataProvider.provideValues(tag, attr); + }, + }, html.newHTMLDataProvider('vue-template-built-in', builtInData), { getId: () => 'vue-template', isApplicable: () => true, provideTags: () => { if (!components) { - promises.push((async () => { - components = (await tsPluginClient?.getComponentNames(vueCode.fileName) ?? []) + components = []; + tasks.push((async () => { + components = (await tsPluginClient?.getComponentNames(root.fileName) ?? []) .filter(name => name !== 'Transition' && name !== 'TransitionGroup' @@ -275,12 +409,10 @@ export function create( && name !== 'Suspense' && name !== 'Teleport' ); - lastCompletionComponentNames = new Set(components); version++; })()); - return []; } - const scriptSetupRanges = tsCodegen.get(vueCode.sfc)?.getScriptSetupRanges(); + const scriptSetupRanges = tsCodegen.get(root.sfc)?.getScriptSetupRanges(); const names = new Set(); const tags: html.ITagData[] = []; @@ -294,7 +426,7 @@ export function create( } for (const binding of scriptSetupRanges?.bindings ?? []) { - const name = vueCode.sfc.scriptSetup!.content.slice(binding.range.start, binding.range.end); + const name = root.sfc.scriptSetup!.content.slice(binding.range.start, binding.range.end); if (casing.tag === TagNameCasing.Kebab) { names.add(hyphenateTag(name)); } @@ -313,35 +445,42 @@ export function create( return tags; }, provideAttributes: tag => { - const tagInfo = tagInfos.get(tag); - + let tagInfo = tagMap.get(tag); if (!tagInfo) { - promises.push((async () => { - const attrs = await tsPluginClient?.getElementAttrs(vueCode.fileName, tag) ?? []; - const propInfos = await tsPluginClient?.getComponentProps(vueCode.fileName, tag) ?? []; - const events = await tsPluginClient?.getComponentEvents(vueCode.fileName, tag) ?? []; - const directives = await tsPluginClient?.getComponentDirectives(vueCode.fileName) ?? []; - tagInfos.set(tag, { - attrs, - propInfos: propInfos.filter(prop => !prop.name.startsWith('ref_')), - events, - directives, + tagInfo = { + attrs: [], + propInfos: [], + events: [], + directives: [], + }; + tagMap.set(tag, tagInfo); + tasks.push((async () => { + tagMap.set(tag, { + attrs: await tsPluginClient?.getElementAttrs(root.fileName, tag) ?? [], + propInfos: await tsPluginClient?.getComponentProps(root.fileName, tag) ?? [], + events: await tsPluginClient?.getComponentEvents(root.fileName, tag) ?? [], + directives: await tsPluginClient?.getComponentDirectives(root.fileName) ?? [], }); version++; })()); - return []; } const { attrs, propInfos, events, directives } = tagInfo; - for (const prop of propInfos) { + for (let i = 0; i < propInfos.length; i++) { + const prop = propInfos[i]; + if (prop.name.startsWith('ref_')) { + propInfos.splice(i--, 1); + continue; + } if (hyphenateTag(prop.name).startsWith('on-vnode-')) { - prop.name = 'onVue:' + prop.name.slice('onVnode'.length); + prop.name = 'onVue:' + prop.name['onVnode'.length].toLowerCase() + + prop.name.slice('onVnodeX'.length); } } const attributes: html.IAttributeData[] = []; - const propsSet = new Set(propInfos.map(prop => prop.name)); + const propNameSet = new Set(propInfos.map(prop => prop.name)); for ( const prop of [ @@ -349,76 +488,72 @@ export function create( ...attrs.map(attr => ({ name: attr })), ] ) { - const isGlobal = prop.isAttribute || !propsSet.has(prop.name); - const name = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name); - const isEvent = hyphenateAttr(name).startsWith('on-'); + const isGlobal = prop.isAttribute || !propNameSet.has(prop.name); + const propName = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name); + const isEvent = hyphenateAttr(propName).startsWith('on-'); if (isEvent) { - const propNameBase = name.startsWith('on-') - ? name.slice('on-'.length) - : (name['on'.length].toLowerCase() + name.slice('onX'.length)); - const propKey = generateItemKey('componentEvent', isGlobal ? '*' : tag, propNameBase); - - attributes.push( - { - name: 'v-on:' + propNameBase, - description: propKey, - }, - { - name: '@' + propNameBase, - description: propKey, - }, - ); + const eventName = casing.attr === AttrNameCasing.Camel + ? propName['on'.length].toLowerCase() + propName.slice('onX'.length) + : propName.slice('on-'.length); + + for ( + const name of [ + 'v-on:' + eventName, + '@' + eventName, + ] + ) { + attributes.push({ name }); + propMap.set(name, { + name: propName, + kind: 'event', + isGlobal, + info: prop, + }); + } } else { - const propName = name; const propInfo = propInfos.find(prop => { const name = casing.attr === AttrNameCasing.Camel ? prop.name : hyphenateAttr(prop.name); return name === propName; }); - const propKey = generateItemKey( - 'componentProp', - isGlobal ? '*' : tag, - propName, - propInfo?.deprecated, - ); - - if (propInfo) { - cachedPropInfos.set(propName, propInfo); - } - attributes.push( - { - name: propName, - description: propKey, + for ( + const name of [ + propName, + ':' + propName, + 'v-bind:' + propName, + ] + ) { + attributes.push({ + name, valueSet: prop.values?.some(value => typeof value === 'string') ? '__deferred__' : undefined, - }, - { - name: ':' + propName, - description: propKey, - }, - { - name: 'v-bind:' + propName, - description: propKey, - }, - ); + }); + propMap.set(name, { + name: propName, + kind: 'prop', + isGlobal, + info: propInfo, + }); + } } } for (const event of events) { - const name = casing.attr === AttrNameCasing.Camel ? event : hyphenateAttr(event); - const propKey = generateItemKey('componentEvent', tag, name); - - attributes.push( - { - name: 'v-on:' + name, - description: propKey, - }, - { - name: '@' + name, - description: propKey, - }, - ); + const eventName = casing.attr === AttrNameCasing.Camel ? event : hyphenateAttr(event); + + for ( + const name of [ + 'v-on:' + eventName, + '@' + eventName, + ] + ) { + attributes.push({ name }); + propMap.set(name, { + name: eventName, + kind: 'event', + }); + } } for (const directive of directives) { @@ -437,7 +572,7 @@ export function create( ] ) { if (prop.name.startsWith('onUpdate:')) { - const isGlobal = !propsSet.has(prop.name); + const isGlobal = !propNameSet.has(prop.name); models.push([isGlobal, prop.name.slice('onUpdate:'.length)]); } } @@ -449,262 +584,102 @@ export function create( for (const [isGlobal, model] of models) { const name = casing.attr === AttrNameCasing.Camel ? model : hyphenateAttr(model); - const propKey = generateItemKey('componentProp', isGlobal ? '*' : tag, name); - attributes.push({ - name: 'v-model:' + name, - description: propKey, + attributes.push({ name: 'v-model:' + name }); + propMap.set('v-model:' + name, { + name, + kind: 'prop', + isGlobal, }); if (model === 'modelValue') { - attributes.push({ - name: 'v-model', - description: propKey, + propMap.set('v-model', { + name, + kind: 'prop', + isGlobal, }); } } return attributes; }, - provideValues: () => [], + provideValues: (tag, attr) => { + if (!values) { + values = []; + tasks.push((async () => { + if (tag === 'slot' && attr === 'name') { + values = await tsPluginClient?.getComponentSlots(root.fileName) ?? []; + } + version++; + })()); + } + return values.map(value => ({ + name: value, + })); + }, }, ]); return { async sync() { - await Promise.all(promises); - return version; + await Promise.all(tasks); + return { + version, + target, + info: { + components, + propMap, + }, + }; }, }; } - function afterHtmlCompletion(completionList: CompletionList, document: TextDocument) { - addDirectiveModifiers(); - - function addDirectiveModifiers() { - const replacement = getReplacement(completionList, document); - if (!replacement?.text.includes('.')) { - return; - } - - const [text, ...modifiers] = replacement.text.split('.'); - const isVOn = text.startsWith('v-on:') || text.startsWith('@') && text.length > 1; - const isVBind = text.startsWith('v-bind:') || text.startsWith(':') && text.length > 1; - const isVModel = text.startsWith('v-model:') || text === 'v-model'; - const currentModifiers = isVOn - ? vOnModifiers - : isVBind - ? vBindModifiers - : isVModel - ? vModelModifiers - : undefined; - - if (!currentModifiers) { - return; - } - - for (const modifier in currentModifiers) { - if (modifiers.includes(modifier)) { - continue; - } - - const description = currentModifiers[modifier]; - const insertText = text + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier; - const newItem: html.CompletionItem = { - label: modifier, - filterText: insertText, - documentation: { - kind: 'markdown', - value: description, - }, - textEdit: { - range: replacement.textEdit.range, - newText: insertText, - }, - kind: 20 satisfies typeof CompletionItemKind.EnumMember, - }; - - completionList.items.push(newItem); - } + function addDirectiveModifiers(completionList: CompletionList, document: TextDocument) { + const replacement = getReplacement(completionList, document); + if (!replacement?.text.includes('.')) { + return; } - completionList.items = completionList.items.filter(item => !specialTags.has(parseLabel(item.label).name)); - - const htmlDocumentations = new Map(); - - for (const item of completionList.items) { - const documentation = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value; - if (documentation && !isItemKey(documentation)) { - htmlDocumentations.set(item.label, documentation); - } + const [text, ...modifiers] = replacement.text.split('.'); + const isVOn = text.startsWith('v-on:') || text.startsWith('@') && text.length > 1; + const isVBind = text.startsWith('v-bind:') || text.startsWith(':') && text.length > 1; + const isVModel = text.startsWith('v-model:') || text === 'v-model'; + const currentModifiers = isVOn + ? vOnModifiers + : isVBind + ? vBindModifiers + : isVModel + ? vModelModifiers + : undefined; + + if (!currentModifiers) { + return; } - for (const item of completionList.items) { - const parsedLabel = parseItemKey(item.label); - - if (parsedLabel) { - const name = parsedLabel.tag; - item.label = parsedLabel.leadingSlash ? '/' + name : name; - - const text = parsedLabel.leadingSlash ? `/${name}>` : name; - if (item.textEdit) { - item.textEdit.newText = text; - } - if (item.insertText) { - item.insertText = text; - } - if (item.sortText) { - item.sortText = text; - } - } - - const itemKey = typeof item.documentation === 'string' ? item.documentation : item.documentation?.value; - let parsedItem = itemKey ? parseItemKey(itemKey) : undefined; - let propInfo: ComponentPropInfo | undefined; - - if (parsedItem) { - const documentations: string[] = []; - - propInfo = cachedPropInfos.get(parsedItem.prop); - if (propInfo?.commentMarkdown) { - documentations.push(propInfo.commentMarkdown); - } - - let { isEvent, propName } = getPropName(parsedItem); - if (isEvent) { - // click -> onclick - propName = 'on' + propName; - } - if (htmlDocumentations.has(propName)) { - documentations.push(htmlDocumentations.get(propName)!); - } - - if (documentations.length) { - item.documentation = { - kind: 'markdown', - value: documentations.join('\n\n'), - }; - } - else { - item.documentation = undefined; - } - } - else { - let propName = item.label; - - for (const str of ['v-bind:', ':']) { - if (propName.startsWith(str) && propName !== str) { - propName = propName.slice(str.length); - break; - } - } - - // for special props without internal item key - if (specialProps.has(propName)) { - parsedItem = { - type: 'componentProp', - tag: '^', - prop: propName, - deprecated: false, - leadingSlash: false, - }; - } - - propInfo = cachedPropInfos.get(propName); - if (propInfo?.commentMarkdown) { - const originalDocumentation = typeof item.documentation === 'string' - ? item.documentation - : item.documentation?.value; - item.documentation = { - kind: 'markdown', - value: [ - propInfo.commentMarkdown, - originalDocumentation, - ].filter(str => !!str).join('\n\n'), - }; - } - } - - if (propInfo?.deprecated) { - item.tags = [1 satisfies typeof CompletionItemTag.Deprecated]; + for (const modifier in currentModifiers) { + if (modifiers.includes(modifier)) { + continue; } - const tokens: string[] = []; - - if ( - item.kind === 10 satisfies typeof CompletionItemKind.Property - && lastCompletionComponentNames.has(hyphenateTag(item.label)) - ) { - item.kind = 6 satisfies typeof CompletionItemKind.Variable; - tokens.push('\u0000'); - } - else if (parsedItem) { - const isComponent = parsedItem.tag !== '*'; - const { isEvent, propName } = getPropName(parsedItem); - - if (parsedItem.type === 'componentProp') { - if (isComponent || specialProps.has(propName)) { - item.kind = 5 satisfies typeof CompletionItemKind.Field; - } - } - else if (isEvent) { - item.kind = 23 satisfies typeof CompletionItemKind.Event; - if (propName.startsWith('vue:')) { - tokens.push('\u0004'); - } - } - - if (isComponent || specialProps.has(propName)) { - tokens.push('\u0000'); - - if (item.label.startsWith(':')) { - tokens.push('\u0001'); - } - else if (item.label.startsWith('@')) { - tokens.push('\u0002'); - } - else if (item.label.startsWith('v-bind:')) { - tokens.push('\u0003'); - } - else if (item.label.startsWith('v-model:')) { - tokens.push('\u0004'); - } - else if (item.label.startsWith('v-on:')) { - tokens.push('\u0005'); - } - else { - tokens.push('\u0000'); - } - - if (specialProps.has(propName)) { - tokens.push('\u0001'); - } - else { - tokens.push('\u0000'); - } - } - } - else if ( - item.label === 'v-if' - || item.label === 'v-else-if' - || item.label === 'v-else' - || item.label === 'v-for' - ) { - item.kind = 14 satisfies typeof CompletionItemKind.Keyword; - tokens.push('\u0003'); - } - else if (item.label.startsWith('v-')) { - item.kind = 3 satisfies typeof CompletionItemKind.Function; - tokens.push('\u0002'); - } - else { - tokens.push('\u0001'); - } + const description = currentModifiers[modifier]; + const insertText = text + modifiers.slice(0, -1).map(m => '.' + m).join('') + '.' + modifier; + const newItem: html.CompletionItem = { + label: modifier, + filterText: insertText, + documentation: { + kind: 'markdown', + value: description, + }, + textEdit: { + range: replacement.textEdit.range, + newText: insertText, + }, + kind: 20 satisfies typeof CompletionItemKind.EnumMember, + }; - item.sortText = tokens.join('') + (item.sortText ?? item.label); + completionList.items.push(newItem); } - - updateExtraCustomData([]); } async function initialize() { @@ -738,46 +713,6 @@ export function create( extraCustomData = extraData; onDidChangeCustomDataListeners.forEach(l => l()); } - - function isSupportedDocument(document: TextDocument) { - if (mode === 'pug') { - return document.languageId === 'jade'; - } - else { - return document.languageId === 'html'; - } - } -} - -function parseLabel(label: string) { - const leadingSlash = label.startsWith('/'); - const name = label.slice(leadingSlash ? 1 : 0); - return { - name, - leadingSlash, - }; -} - -function generateItemKey(type: InternalItemId, tag: string, prop: string, deprecated?: boolean) { - return `__VLS_data=${type},${tag},${prop},${Number(deprecated)}`; -} - -function isItemKey(key: string) { - return key.startsWith('__VLS_data='); -} - -function parseItemKey(key: string) { - const { leadingSlash, name } = parseLabel(key); - if (isItemKey(name)) { - const strs = name.slice('__VLS_data='.length).split(','); - return { - type: strs[0] as InternalItemId, - tag: strs[1], - prop: strs[2], - deprecated: strs[3] === '1', - leadingSlash, - }; - } } function getReplacement(list: html.CompletionList, doc: TextDocument) { @@ -793,14 +728,12 @@ function getReplacement(list: html.CompletionList, doc: TextDocument) { } function getPropName( - parsedItem: ReturnType & {}, + prop: string, + isEvent: boolean, ) { - const name = hyphenateAttr(parsedItem.prop); + const name = hyphenateAttr(prop); if (name.startsWith('on-')) { return { isEvent: true, propName: name.slice('on-'.length) }; } - else if (parsedItem.type === 'componentEvent') { - return { isEvent: true, propName: name }; - } - return { isEvent: false, propName: name }; + return { isEvent, propName: name }; } diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 16e7de4983..bac7e85c75 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,14 +1,11 @@ -import type { InlayHint, LanguageServiceContext, LanguageServicePlugin, Position } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import type { InlayHint, LanguageServicePlugin, Position } from '@volar/language-service'; +import { getEmbeddedInfo } from './utils'; const twoslashTemplateReg = //g; const twoslashScriptReg = /(?<=^|\n)\s*\/\/\s*\^\?/g; export function create( - getTsPluginClient?: ( - context: LanguageServiceContext, - ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, + tsPluginClient: import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { name: 'vue-twoslash-queries', @@ -16,24 +13,17 @@ export function create( inlayHintProvider: {}, }, create(context) { - const tsPluginClient = getTsPluginClient?.(context); return { async provideInlayHints(document, range) { - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if ( - !sourceScript?.generated - || (virtualCode?.id !== 'template' && !virtualCode?.id.startsWith('script_')) - ) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo( + context, + document, + id => id === 'template' || id.startsWith('script_'), + ); + if (!info) { return; } + const { sourceScript, virtualCode, root } = info; const hoverOffsets: [Position, number][] = []; const inlayHints: InlayHint[] = []; @@ -51,7 +41,7 @@ export function create( ]); } - const sourceDocument = context.documents.get(decoded![0], sourceScript.languageId, sourceScript.snapshot); + const sourceDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); for (const [pointerPosition, hoverOffset] of hoverOffsets) { const map = context.language.maps.get(virtualCode, sourceScript); for (const [sourceOffset] of map.toSourceLocation(hoverOffset)) { diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 8e16b30a46..990ea50d2a 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -1,6 +1,6 @@ { "name": "@vue/language-service", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "data", @@ -18,7 +18,7 @@ }, "dependencies": { "@volar/language-service": "2.4.20", - "@vue/language-core": "3.0.3", + "@vue/language-core": "3.0.4", "@vue/shared": "^3.5.0", "path-browserify": "^1.0.1", "volar-service-css": "0.0.65", @@ -37,7 +37,7 @@ "@volar/kit": "2.4.20", "@volar/typescript": "2.4.20", "@vue/compiler-dom": "^3.5.0", - "@vue/typescript-plugin": "3.0.3", + "@vue/typescript-plugin": "3.0.4", "vscode-css-languageservice": "^6.3.1" } } diff --git a/packages/tsc/package.json b/packages/tsc/package.json index 7bc2969071..4bb8ee5818 100644 --- a/packages/tsc/package.json +++ b/packages/tsc/package.json @@ -1,6 +1,6 @@ { "name": "vue-tsc", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "bin", @@ -21,7 +21,7 @@ }, "dependencies": { "@volar/typescript": "2.4.20", - "@vue/language-core": "3.0.3" + "@vue/language-core": "3.0.4" }, "devDependencies": { "@types/node": "^22.10.4" diff --git a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap index 26ac39b893..5a4791ef21 100644 --- a/packages/tsc/tests/__snapshots__/dts.spec.ts.snap +++ b/packages/tsc/tests/__snapshots__/dts.spec.ts.snap @@ -1,32 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`vue-tsc-dts > Input: #4577/main.vue, Output: #4577/main.vue.d.ts 1`] = ` -"export type BaseRow = { - value: string; -}; -declare const _default: (__VLS_props: NonNullable>["props"], __VLS_ctx?: __VLS_PrettifyLocal>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable>["expose"], __VLS_setup?: Promise<{ - props: __VLS_PrettifyLocal & Omit<{} & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, never> & { - nonGeneric: string; - rows: Row[]; - } & Partial<{}>> & import("vue").PublicProps; - expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; - attrs: any; - slots: { - default?: (props: { - row: Row; - }) => any; - }; - emit: {}; -}>) => import("vue").VNode & { - __ctx?: Awaited; -}; -export default _default; -type __VLS_PrettifyLocal = { - [K in keyof T as K]: T[K]; -} & {}; -" -`; - exports[`vue-tsc-dts > Input: empty-component/component.vue, Output: empty-component/component.vue.d.ts 1`] = ` "declare const _default: import("vue").DefineComponent2<{ setup(): void; diff --git a/packages/tsc/tests/dts.spec.ts b/packages/tsc/tests/dts.spec.ts index 7a67424d75..8815da600d 100644 --- a/packages/tsc/tests/dts.spec.ts +++ b/packages/tsc/tests/dts.spec.ts @@ -71,6 +71,9 @@ describe('vue-tsc-dts', () => { }); function readFilesRecursive(dir: string) { + if (path.relative(workspace, dir).startsWith('#')) { + return []; + } const result: string[] = []; for (const file of fs.readdirSync(dir)) { @@ -86,7 +89,6 @@ function readFilesRecursive(dir: string) { result.push(filepath); } } - return result; } diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 0f47d42696..a098844a1c 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -7,6 +7,7 @@ import { getComponentDirectives } from './lib/requests/getComponentDirectives'; import { getComponentEvents } from './lib/requests/getComponentEvents'; import { getComponentNames } from './lib/requests/getComponentNames'; import { getComponentProps } from './lib/requests/getComponentProps'; +import { getComponentSlots } from './lib/requests/getComponentSlots'; import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; @@ -14,7 +15,10 @@ import { getPropertiesAtLocation } from './lib/requests/getPropertiesAtLocation' import type { RequestContext } from './lib/requests/types'; const windowsPathReg = /\\/g; -const project2Service = new WeakMap(); +const project2Service = new WeakMap< + ts.server.Project, + [vue.Language, ts.LanguageServiceHost, ts.LanguageService] +>(); export = createLanguageServicePlugin( (ts, info) => { @@ -39,7 +43,6 @@ export = createLanguageServicePlugin( language, info.languageService, vueOptions, - fileName => fileName, ); // #3963 @@ -109,6 +112,16 @@ export = createLanguageServicePlugin( response: getPropertiesAtLocation.apply(getRequestContext(args[0]), args), }; }); + session.addProtocolHandler('_vue:getComponentDirectives', ({ arguments: args }) => { + return { + response: getComponentDirectives.apply(getRequestContext(args[0]), args), + }; + }); + session.addProtocolHandler('_vue:getComponentEvents', ({ arguments: args }) => { + return { + response: getComponentEvents.apply(getRequestContext(args[0]), args), + }; + }); session.addProtocolHandler('_vue:getComponentNames', ({ arguments: args }) => { return { response: getComponentNames.apply(getRequestContext(args[0]), args) ?? [], @@ -119,14 +132,9 @@ export = createLanguageServicePlugin( response: getComponentProps.apply(getRequestContext(args[0]), args), }; }); - session.addProtocolHandler('_vue:getComponentEvents', ({ arguments: args }) => { - return { - response: getComponentEvents.apply(getRequestContext(args[0]), args), - }; - }); - session.addProtocolHandler('_vue:getComponentDirectives', ({ arguments: args }) => { + session.addProtocolHandler('_vue:getComponentSlots', ({ arguments: args }) => { return { - response: getComponentDirectives.apply(getRequestContext(args[0]), args), + response: getComponentSlots.apply(getRequestContext(args[0]), args), }; }); session.addProtocolHandler('_vue:getElementAttrs', ({ arguments: args }) => { @@ -160,8 +168,6 @@ export = createLanguageServicePlugin( languageService: service[2], languageServiceHost: service[1], language: service[0], - isTsPlugin: true, - asScriptId: (fileName: string) => fileName, }; } }, diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 043215657a..e7710bcdc8 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -4,24 +4,23 @@ import type * as ts from 'typescript'; const windowsPathReg = /\\/g; -export function createVueLanguageServiceProxy( +export function createVueLanguageServiceProxy( ts: typeof import('typescript'), - language: Language, + language: Language, languageService: ts.LanguageService, vueOptions: VueCompilerOptions, - asScriptId: (fileName: string) => T, ) { const proxyCache = new Map(); const getProxyMethod = (target: ts.LanguageService, p: string | symbol): Function | undefined => { switch (p) { case 'getCompletionsAtPosition': - return getCompletionsAtPosition(ts, language, vueOptions, asScriptId, target[p]); + return getCompletionsAtPosition(ts, language, vueOptions, target[p]); case 'getCompletionEntryDetails': - return getCompletionEntryDetails(language, asScriptId, target[p]); + return getCompletionEntryDetails(language, target[p]); case 'getCodeFixesAtPosition': return getCodeFixesAtPosition(target[p]); case 'getDefinitionAndBoundSpan': - return getDefinitionAndBoundSpan(ts, language, languageService, vueOptions, asScriptId, target[p]); + return getDefinitionAndBoundSpan(ts, language, languageService, vueOptions, target[p]); } }; @@ -44,11 +43,10 @@ export function createVueLanguageServiceProxy( }); } -function getCompletionsAtPosition( +function getCompletionsAtPosition( ts: typeof import('typescript'), - language: Language, + language: Language, vueOptions: VueCompilerOptions, - asScriptId: (fileName: string) => T, getCompletionsAtPosition: ts.LanguageService['getCompletionsAtPosition'], ): ts.LanguageService['getCompletionsAtPosition'] { return (filePath, position, options, formattingSettings) => { @@ -63,7 +61,7 @@ function getCompletionsAtPosition( ); // filter global variables in template and styles - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(fileName); const root = sourceScript?.generated?.root; if (root instanceof VueVirtualCode) { const blocks = [ @@ -131,9 +129,8 @@ function getCompletionsAtPosition( }; } -function getCompletionEntryDetails( - language: Language, - asScriptId: (fileName: string) => T, +function getCompletionEntryDetails( + language: Language, getCompletionEntryDetails: ts.LanguageService['getCompletionEntryDetails'], ): ts.LanguageService['getCompletionEntryDetails'] { return (...args) => { @@ -158,7 +155,7 @@ function getCompletionEntryDetails( if (args[6]?.__isAutoImport) { // @ts-expect-error const { fileName } = args[6].__isAutoImport; - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(fileName); if (sourceScript?.generated?.root instanceof VueVirtualCode) { const sfc = sourceScript.generated.root.vueSfc; if (!sfc?.descriptor.script && !sfc?.descriptor.scriptSetup) { @@ -190,12 +187,11 @@ function getCodeFixesAtPosition( }; } -function getDefinitionAndBoundSpan( +function getDefinitionAndBoundSpan( ts: typeof import('typescript'), - language: Language, + language: Language, languageService: ts.LanguageService, vueOptions: VueCompilerOptions, - asScriptId: (fileName: string) => T, getDefinitionAndBoundSpan: ts.LanguageService['getDefinitionAndBoundSpan'], ): ts.LanguageService['getDefinitionAndBoundSpan'] { return (fileName, position) => { @@ -205,7 +201,7 @@ function getDefinitionAndBoundSpan( } const program = languageService.getProgram()!; - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(fileName); if (!sourceScript?.generated) { return result; } diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index e1f7b609b1..d272872947 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -6,9 +6,9 @@ export function collectExtractProps( fileName: string, templateCodeRange: [number, number], ) { - const { typescript: ts, languageService, language, isTsPlugin, asScriptId } = this; + const { typescript: ts, languageService, language } = this; - const sourceScript = language.scripts.get(asScriptId(fileName)); + const sourceScript = language.scripts.get(fileName); if (!sourceScript?.generated) { return; } @@ -41,7 +41,7 @@ export function collectExtractProps( for (const map of maps) { let mapped = false; for ( - const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? sourceScript.snapshot.getLength() : 0)) + const source of map.toSourceLocation(name.getEnd() - sourceScript.snapshot.getLength()) ) { if ( source[0] >= sfc.template!.startTagEnd + templateCodeRange[0] diff --git a/packages/typescript-plugin/lib/requests/getComponentDirectives.ts b/packages/typescript-plugin/lib/requests/getComponentDirectives.ts index c554da1209..c9b4dec522 100644 --- a/packages/typescript-plugin/lib/requests/getComponentDirectives.ts +++ b/packages/typescript-plugin/lib/requests/getComponentDirectives.ts @@ -15,8 +15,8 @@ export function getComponentDirectives( this: RequestContext, fileName: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getComponentEvents.ts b/packages/typescript-plugin/lib/requests/getComponentEvents.ts index ce602429f0..4e5cecf15f 100644 --- a/packages/typescript-plugin/lib/requests/getComponentEvents.ts +++ b/packages/typescript-plugin/lib/requests/getComponentEvents.ts @@ -7,8 +7,8 @@ export function getComponentEvents( fileName: string, tag: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getComponentNames.ts b/packages/typescript-plugin/lib/requests/getComponentNames.ts index 37e80b2de3..e80a7ac659 100644 --- a/packages/typescript-plugin/lib/requests/getComponentNames.ts +++ b/packages/typescript-plugin/lib/requests/getComponentNames.ts @@ -7,8 +7,8 @@ export function getComponentNames( this: RequestContext, fileName: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getComponentProps.ts b/packages/typescript-plugin/lib/requests/getComponentProps.ts index 56123ea006..f8c88b25e6 100644 --- a/packages/typescript-plugin/lib/requests/getComponentProps.ts +++ b/packages/typescript-plugin/lib/requests/getComponentProps.ts @@ -8,7 +8,7 @@ export interface ComponentPropInfo { required?: boolean; deprecated?: boolean; isAttribute?: boolean; - commentMarkdown?: string; + documentation?: string; values?: string[]; } @@ -17,8 +17,8 @@ export function getComponentProps( fileName: string, tag: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } @@ -69,9 +69,9 @@ export function getComponentProps( const name = prop.name; const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined; const { - content: commentMarkdown, + documentation, deprecated, - } = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()); + } = generateDocumentation(prop.getDocumentationComment(checker), prop.getJsDocTags()); const values: any[] = []; const type = checker.getTypeOfSymbol(prop); const subTypes: ts.Type[] | undefined = (type as any).types; @@ -106,19 +106,19 @@ export function getComponentProps( required, deprecated, isAttribute, - commentMarkdown, + documentation, values, }); } } -function generateCommentMarkdown(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JSDocTagInfo[]) { +function generateDocumentation(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JSDocTagInfo[]) { const parsedComment = _symbolDisplayPartsToMarkdown(parts); const parsedJsDoc = _jsDocTagInfoToMarkdown(jsDocTags); - const content = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n'); + const documentation = [parsedComment, parsedJsDoc].filter(str => !!str).join('\n\n'); const deprecated = jsDocTags.some(tag => tag.name === 'deprecated'); return { - content, + documentation, deprecated, }; } diff --git a/packages/typescript-plugin/lib/requests/getComponentSlots.ts b/packages/typescript-plugin/lib/requests/getComponentSlots.ts new file mode 100644 index 0000000000..b48828b164 --- /dev/null +++ b/packages/typescript-plugin/lib/requests/getComponentSlots.ts @@ -0,0 +1,28 @@ +import { tsCodegen, VueVirtualCode } from '@vue/language-core'; +import type { RequestContext } from './types'; +import { getVariableType } from './utils'; + +export function getComponentSlots( + this: RequestContext, + fileName: string, +) { + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); + if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { + return; + } + const vueCode = volarFile.generated.root; + + const codegen = tsCodegen.get(vueCode.sfc); + if (!codegen) { + return; + } + + const assignName = codegen.getSetupSlotsAssignName() ?? `__VLS_slots`; + const slots = getVariableType(ts, languageService, vueCode, assignName); + if (!slots) { + return []; + } + + return slots.type.getProperties().map(({ name }) => name); +} diff --git a/packages/typescript-plugin/lib/requests/getElementAttrs.ts b/packages/typescript-plugin/lib/requests/getElementAttrs.ts index 6c06fb785d..dee3f83373 100644 --- a/packages/typescript-plugin/lib/requests/getElementAttrs.ts +++ b/packages/typescript-plugin/lib/requests/getElementAttrs.ts @@ -7,8 +7,8 @@ export function getElementAttrs( fileName: string, tagName: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getElementNames.ts b/packages/typescript-plugin/lib/requests/getElementNames.ts index f10906b2d7..15ac70b8ee 100644 --- a/packages/typescript-plugin/lib/requests/getElementNames.ts +++ b/packages/typescript-plugin/lib/requests/getElementNames.ts @@ -7,8 +7,8 @@ export function getElementNames( this: RequestContext, fileName: string, ) { - const { typescript: ts, language, languageService, asScriptId } = this; - const volarFile = language.scripts.get(asScriptId(fileName)); + const { typescript: ts, language, languageService } = this; + const volarFile = language.scripts.get(fileName); if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { return; } diff --git a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts index efb1aea7a5..f6131f43d4 100644 --- a/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts +++ b/packages/typescript-plugin/lib/requests/getPropertiesAtLocation.ts @@ -9,10 +9,10 @@ export function getPropertiesAtLocation( fileName: string, position: number, ) { - const { languageService, language, typescript: ts, isTsPlugin, asScriptId } = this; + const { languageService, language, typescript: ts } = this; // mapping - const file = language.scripts.get(asScriptId(fileName)); + const file = language.scripts.get(fileName); if (file?.generated) { const virtualScript = file.generated.languagePlugin.typescript?.getServiceScript(file.generated.root); if (!virtualScript) { @@ -34,9 +34,7 @@ export function getPropertiesAtLocation( if (!mapped) { return; } - if (isTsPlugin) { - position += file.snapshot.getLength(); - } + position += file.snapshot.getLength(); } const program = languageService.getProgram()!; diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index ca107b1448..80e6f6a51e 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -6,12 +6,14 @@ export type Requests = { collectExtractProps: ToRequest; getImportPathForFile: ToRequest; getPropertiesAtLocation: ToRequest; + getComponentDirectives: ToRequest; + getComponentEvents: ToRequest; getComponentNames: ToRequest; getComponentProps: ToRequest; - getComponentEvents: ToRequest; - getComponentDirectives: ToRequest; + getComponentSlots: ToRequest; getElementAttrs: ToRequest; getElementNames: ToRequest; + getDocumentHighlights: ToRequest<(fileName: string, position: number) => ts.DocumentHighlights[]>; getEncodedSemanticClassifications: ToRequest<(fileName: string, span: ts.TextSpan) => ts.Classifications>; getQuickInfoAtPosition: ToRequest<(fileName: string, position: ts.LineAndCharacter) => string>; }; diff --git a/packages/typescript-plugin/lib/requests/types.ts b/packages/typescript-plugin/lib/requests/types.ts index 8c4a43a3f7..77ebce0657 100644 --- a/packages/typescript-plugin/lib/requests/types.ts +++ b/packages/typescript-plugin/lib/requests/types.ts @@ -1,11 +1,9 @@ import type { Language } from '@vue/language-core'; import type * as ts from 'typescript'; -export interface RequestContext { +export interface RequestContext { typescript: typeof ts; languageService: ts.LanguageService; languageServiceHost: ts.LanguageServiceHost; - language: Language; - isTsPlugin: boolean; - asScriptId: (fileName: string) => T; + language: Language; } diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index dd6fbe089e..3c402e5190 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vue/typescript-plugin", - "version": "3.0.3", + "version": "3.0.4", "license": "MIT", "files": [ "**/*.js", @@ -14,7 +14,7 @@ }, "dependencies": { "@volar/typescript": "2.4.20", - "@vue/language-core": "3.0.3", + "@vue/language-core": "3.0.4", "@vue/shared": "^3.5.0", "path-browserify": "^1.0.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b68a10e378..ef90bc2b68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,10 +60,10 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/language-server': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../../packages/language-server '@vue/typescript-plugin': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../../packages/typescript-plugin reactive-vscode: specifier: ^0.2.9 @@ -87,7 +87,7 @@ importers: specifier: 2.4.20 version: 2.4.20 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core path-browserify: specifier: ^1.0.1 @@ -170,7 +170,7 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core packages/language-server: @@ -179,13 +179,13 @@ importers: specifier: 2.4.20 version: 2.4.20 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core '@vue/language-service': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-service '@vue/typescript-plugin': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../typescript-plugin typescript: specifier: '*' @@ -207,7 +207,7 @@ importers: specifier: 2.4.20 version: 2.4.20 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core '@vue/shared': specifier: ^3.5.0 @@ -259,7 +259,7 @@ importers: specifier: ^3.5.0 version: 3.5.13 '@vue/typescript-plugin': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../typescript-plugin vscode-css-languageservice: specifier: ^6.3.1 @@ -271,7 +271,7 @@ importers: specifier: 2.4.20 version: 2.4.20 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core typescript: specifier: '>=5.0.0' @@ -287,7 +287,7 @@ importers: specifier: 2.4.20 version: 2.4.20 '@vue/language-core': - specifier: 3.0.3 + specifier: 3.0.4 version: link:../language-core '@vue/shared': specifier: ^3.5.0 @@ -312,7 +312,7 @@ importers: specifier: https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72 version: https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72(typescript@5.8.3) vue-component-type-helpers: - specifier: 3.0.3 + specifier: 3.0.4 version: link:../packages/component-type-helpers vue3.4: specifier: npm:vue@3.4.38 diff --git a/test-workspace/component-meta/#5546/main.vue b/test-workspace/component-meta/#5546/main.vue new file mode 100644 index 0000000000..d200323eff --- /dev/null +++ b/test-workspace/component-meta/#5546/main.vue @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/test-workspace/package.json b/test-workspace/package.json index 1791c1b3a9..2dc823ebb7 100644 --- a/test-workspace/package.json +++ b/test-workspace/package.json @@ -1,10 +1,10 @@ { "private": true, - "version": "3.0.3", + "version": "3.0.4", "devDependencies": { "typescript": "latest", "vue": "https://pkg.pr.new/vue@e1bc0eb02e22bc0c236e1471c11d96a368764b72", - "vue-component-type-helpers": "3.0.3", + "vue-component-type-helpers": "3.0.4", "vue3.4": "npm:vue@3.4.38" } }