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-service/index.ts b/packages/language-service/index.ts index aa1a7856b2..bbdfb44909 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -15,20 +15,21 @@ 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' { @@ -55,21 +56,22 @@ export function createVueLanguageServicePlugins( createTypeScriptDocCommentTemplatePlugin(ts), createTypescriptSemanticTokensPlugin(getTsPluginClient), createTypeScriptSyntacticPlugin(ts), - createVueAutoAddSpacePlugin(), + createVueAutoSpacePlugin(), createVueAutoDotValuePlugin(ts, getTsPluginClient), createVueCompilerDomErrorsPlugin(), createVueComponentSemanticTokensPlugin(getTsPluginClient), createVueDocumentDropPlugin(ts, getTsPluginClient), - createVueDocumentLinksPlugin(), createVueDirectiveCommentsPlugin(), createVueExtractFilePlugin(ts, getTsPluginClient), createVueGlobalTypesErrorPlugin(), createVueInlayHintsPlugin(ts), createVueMissingPropsHintsPlugin(getTsPluginClient), + createVueScopedClassLinksPlugin(), createVueSfcPlugin(), createVueSuggestDefineAssignmentPlugin(), createVueTemplatePlugin('html', getTsPluginClient), createVueTemplatePlugin('pug', getTsPluginClient), + createVueTemplateRefLinksPlugin(), createVueTwoslashQueriesPlugin(getTsPluginClient), createEmmetPlugin({ mappedLanguages: { diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index a9ae2246be..0df678bd71 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -1,5 +1,5 @@ import type { LanguageServicePlugin, TextDocument, VirtualCode } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; +import { isRenameEnabled, VueVirtualCode } 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'; @@ -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; }, @@ -83,11 +85,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..298a3c67e7 100644 --- a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts @@ -9,7 +9,7 @@ export function create( ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { - name: 'typescript-highlights', + name: 'typescript-semantic-tokens', capabilities: { semanticTokensProvider: { legend: { diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 56416b93bc..da79c6c3cd 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -21,6 +21,7 @@ export function create( create(context) { const tsPluginClient = getTsPluginClient?.(context); let currentReq = 0; + return { async provideAutoInsertSnippet(document, selection, change) { // selection must at end of change @@ -67,32 +68,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..c62d2f783c 100644 --- a/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts +++ b/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts @@ -21,10 +21,6 @@ export function create(): LanguageServicePlugin { 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)) { 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..d4110da8d2 100644 --- a/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts @@ -9,7 +9,7 @@ export function create( ) => import('@vue/typescript-plugin/lib/requests').Requests | undefined, ): LanguageServicePlugin { return { - name: 'vue-component-highlights', + name: 'vue-component-semantic-tokens', capabilities: { semanticTokensProvider: { legend: { 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..fcac7a576c 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -235,7 +235,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 e72c2ac26a..2ac72006b6 100644 --- a/packages/language-service/lib/plugins/vue-global-types-error.ts +++ b/packages/language-service/lib/plugins/vue-global-types-error.ts @@ -2,7 +2,7 @@ import type { DiagnosticSeverity, LanguageServicePlugin } from '@volar/language- export function create(): LanguageServicePlugin { return { - name: 'vue-compiler-dom-errors', + name: 'vue-global-types-error', capabilities: { diagnosticProvider: { interFileDependencies: false, diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 4302f18052..19c5b63ecc 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -5,7 +5,7 @@ import { URI } from 'vscode-uri'; export function create(ts: typeof import('typescript')): LanguageServicePlugin { return { - name: 'vue-inlay-hints', + name: 'vue-inlayhints', capabilities: { inlayHintProvider: {}, }, @@ -16,7 +16,12 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { 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)) { + if (!sourceScript?.generated || virtualCode?.id !== 'main') { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -26,15 +31,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 +48,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 +67,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); 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..72a784e209 100644 --- a/packages/language-service/lib/plugins/vue-missing-props-hints.ts +++ b/packages/language-service/lib/plugins/vue-missing-props-hints.ts @@ -42,12 +42,11 @@ export function create( 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) { + if (!sourceScript?.generated) { return; } - const root = sourceScript?.generated?.root; + const root = sourceScript.generated.root; if (!(root instanceof VueVirtualCode)) { return; } @@ -58,7 +57,7 @@ export function create( } const result: InlayHint[] = []; - const casing = await checkCasing(context, decoded[0]); + const casing = await checkCasing(context, decoded![0]); const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; const componentProps: Record = {}; 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..669d8c531a --- /dev/null +++ b/packages/language-service/lib/plugins/vue-scoped-class-links.ts @@ -0,0 +1,77 @@ +import type { LanguageServicePlugin } from '@volar/language-service'; +import { tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { URI } from 'vscode-uri'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-scoped-class-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') { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + + 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(decoded![0], '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-template-ref-links.ts b/packages/language-service/lib/plugins/vue-template-ref-links.ts new file mode 100644 index 0000000000..f2354ea43d --- /dev/null +++ b/packages/language-service/lib/plugins/vue-template-ref-links.ts @@ -0,0 +1,72 @@ +import type { LanguageServicePlugin } from '@volar/language-service'; +import { tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { URI } from 'vscode-uri'; + +export function create(): LanguageServicePlugin { + return { + name: 'vue-template-ref-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 !== 'scriptsetup_raw') { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + + 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(decoded![0], '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..089c9c472d 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -110,7 +110,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 +123,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 +139,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; } @@ -162,52 +166,38 @@ export function create( 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; - - if (root instanceof VueVirtualCode) { - // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver - baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token); + if (!sourceScript?.generated) { + return; + } - sync = (await provideHtmlData(sourceScript!.id, root)).sync; - currentVersion = await sync(); + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; } - let htmlComplete = await baseServiceInstance.provideCompletionItems?.( - document, - position, - completionContext, - token, - ); + // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver + baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token); + + let sync = (await provideHtmlData(sourceScript.id, root)).sync; + let currentVersion: number | undefined; + let completionList: CompletionList | null | undefined; + while (currentVersion !== (currentVersion = await sync?.())) { - htmlComplete = await baseServiceInstance.provideCompletionItems?.( + completionList = await baseServiceInstance.provideCompletionItems?.( document, position, completionContext, token, ); } - if (!htmlComplete) { - return; - } - 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 (completionList) { + transformCompletionList(completionList, document); + return completionList; } - - return htmlComplete; }, provideHover(document, position, token) { @@ -478,7 +468,7 @@ export function create( }; } - function afterHtmlCompletion(completionList: CompletionList, document: TextDocument) { + function transformCompletionList(completionList: CompletionList, document: TextDocument) { addDirectiveModifiers(); function addDirectiveModifiers() {