From ee1e1314dcc7021a19a11daa815cd564146d94c6 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Wed, 12 Nov 2025 04:03:30 +0800 Subject: [PATCH 1/2] fix: replace interpolations with spaces in template document --- packages/language-core/index.ts | 1 + .../lib/codegen/template/index.ts | 39 +---------- .../lib/plugins/vue-sfc-template.ts | 24 ++++++- .../lib/plugins/vue-template-inline-css.ts | 2 +- .../lib/utils/forEachTemplateNode.ts | 69 +++++++++++++++++++ 5 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 packages/language-core/lib/utils/forEachTemplateNode.ts diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index 54dffda72f..993c326573 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -6,6 +6,7 @@ export * from './lib/parsers/scriptSetupRanges'; export * from './lib/plugins'; export * from './lib/types'; export * from './lib/utils/collectBindings'; +export * from './lib/utils/forEachTemplateNode'; export * from './lib/utils/parseSfc'; export * from './lib/utils/shared'; export * from './lib/virtualFile/vueFile'; diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index ffa43f2820..ad2e926bf6 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -1,4 +1,3 @@ -import * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import type { Code, Sfc, VueCompilerOptions } from '../../types'; import { codeFeatures } from '../codeFeatures'; @@ -7,7 +6,7 @@ import { wrapWith } from '../utils/wrapWith'; import { createTemplateCodegenContext, type TemplateCodegenContext } from './context'; import { generateObjectProperty } from './objectProperty'; import { generateStyleScopedClassReferences } from './styleScopedClasses'; -import { generateTemplateChild, getVForNode } from './templateChild'; +import { generateTemplateChild } from './templateChild'; export interface TemplateCodegenOptions { ts: typeof ts; @@ -203,39 +202,3 @@ function* generateRootEl( yield endOfLine; return `__VLS_RootEl`; } - -export function* forEachElementNode( - node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode, -): Generator { - if (node.type === CompilerDOM.NodeTypes.ROOT) { - for (const child of node.children) { - yield* forEachElementNode(child); - } - } - else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { - const patchForNode = getVForNode(node); - if (patchForNode) { - yield* forEachElementNode(patchForNode); - } - else { - yield node; - for (const child of node.children) { - yield* forEachElementNode(child); - } - } - } - else if (node.type === CompilerDOM.NodeTypes.IF) { - // v-if / v-else-if / v-else - for (const branch of node.branches) { - for (const childNode of branch.children) { - yield* forEachElementNode(childNode); - } - } - } - else if (node.type === CompilerDOM.NodeTypes.FOR) { - // v-for - for (const child of node.children) { - yield* forEachElementNode(child); - } - } -} diff --git a/packages/language-core/lib/plugins/vue-sfc-template.ts b/packages/language-core/lib/plugins/vue-sfc-template.ts index eb4c9bcd8f..6e9619e3af 100644 --- a/packages/language-core/lib/plugins/vue-sfc-template.ts +++ b/packages/language-core/lib/plugins/vue-sfc-template.ts @@ -1,4 +1,6 @@ +import type * as CompilerDOM from '@vue/compiler-dom'; import type { VueLanguagePlugin } from '../types'; +import { forEachInterpolationNode } from '../utils/forEachTemplateNode'; import { allCodeFeatures } from './shared'; const plugin: VueLanguagePlugin = () => { @@ -17,10 +19,28 @@ const plugin: VueLanguagePlugin = () => { resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id === 'template' && sfc.template?.lang === 'html') { + const locs: CompilerDOM.SourceLocation[] = []; + if (sfc.template.ast) { + for (const node of forEachInterpolationNode(sfc.template.ast)) { + locs.push(node.loc); + } + } + + // replace interpolations with spaces + let offset = 0; + for (const loc of locs) { + embeddedFile.content.push([ + sfc.template.content.slice(offset, loc.start.offset + 2), + sfc.template.name, + offset, + allCodeFeatures, + ], ' '.repeat(loc.source.length - 4)); + offset = loc.end.offset - 2; + } embeddedFile.content.push([ - sfc.template.content, + sfc.template.content.slice(offset), sfc.template.name, - 0, + offset, allCodeFeatures, ]); } diff --git a/packages/language-core/lib/plugins/vue-template-inline-css.ts b/packages/language-core/lib/plugins/vue-template-inline-css.ts index 46fe7aac0b..1f6a218814 100644 --- a/packages/language-core/lib/plugins/vue-template-inline-css.ts +++ b/packages/language-core/lib/plugins/vue-template-inline-css.ts @@ -1,6 +1,6 @@ import * as CompilerDOM from '@vue/compiler-dom'; -import { forEachElementNode } from '../codegen/template'; import type { Code, VueLanguagePlugin } from '../types'; +import { forEachElementNode } from '../utils/forEachTemplateNode'; import { allCodeFeatures } from './shared'; const codeFeatures = { diff --git a/packages/language-core/lib/utils/forEachTemplateNode.ts b/packages/language-core/lib/utils/forEachTemplateNode.ts new file mode 100644 index 0000000000..9b127b8e05 --- /dev/null +++ b/packages/language-core/lib/utils/forEachTemplateNode.ts @@ -0,0 +1,69 @@ +import * as CompilerDOM from '@vue/compiler-dom'; + +export function* forEachElementNode( + node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode, +): Generator { + if (node.type === CompilerDOM.NodeTypes.ROOT) { + for (const child of node.children) { + yield* forEachElementNode(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { + yield node; + for (const child of node.children) { + yield* forEachElementNode(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.IF) { + for (const branch of node.branches) { + for (const childNode of branch.children) { + yield* forEachElementNode(childNode); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.FOR) { + for (const child of node.children) { + yield* forEachElementNode(child); + } + } +} + +export function* forEachInterpolationNode( + node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode, +): Generator { + if (node.type === CompilerDOM.NodeTypes.ROOT) { + for (const child of node.children) { + yield* forEachInterpolationNode(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.ELEMENT) { + for (const child of node.children) { + yield* forEachInterpolationNode(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) { + yield* forEachInterpolationNode(node.content); + } + else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) { + for (const child of node.children) { + if (typeof child === 'object') { + yield* forEachInterpolationNode(child); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) { + yield node; + } + else if (node.type === CompilerDOM.NodeTypes.IF) { + for (const branch of node.branches) { + for (const childNode of branch.children) { + yield* forEachInterpolationNode(childNode); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.FOR) { + for (const child of node.children) { + yield* forEachInterpolationNode(child); + } + } +} From e4ecce56d9e6019043a8693d2f8bf304b578790c Mon Sep 17 00:00:00 2001 From: KazariEX Date: Wed, 12 Nov 2025 04:23:19 +0800 Subject: [PATCH 2/2] fix: replace during the provision of auto insert snippet --- .../lib/plugins/vue-sfc-template.ts | 24 +--------- .../lib/plugins/vue-template.ts | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/language-core/lib/plugins/vue-sfc-template.ts b/packages/language-core/lib/plugins/vue-sfc-template.ts index 6e9619e3af..eb4c9bcd8f 100644 --- a/packages/language-core/lib/plugins/vue-sfc-template.ts +++ b/packages/language-core/lib/plugins/vue-sfc-template.ts @@ -1,6 +1,4 @@ -import type * as CompilerDOM from '@vue/compiler-dom'; import type { VueLanguagePlugin } from '../types'; -import { forEachInterpolationNode } from '../utils/forEachTemplateNode'; import { allCodeFeatures } from './shared'; const plugin: VueLanguagePlugin = () => { @@ -19,28 +17,10 @@ const plugin: VueLanguagePlugin = () => { resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id === 'template' && sfc.template?.lang === 'html') { - const locs: CompilerDOM.SourceLocation[] = []; - if (sfc.template.ast) { - for (const node of forEachInterpolationNode(sfc.template.ast)) { - locs.push(node.loc); - } - } - - // replace interpolations with spaces - let offset = 0; - for (const loc of locs) { - embeddedFile.content.push([ - sfc.template.content.slice(offset, loc.start.offset + 2), - sfc.template.name, - offset, - allCodeFeatures, - ], ' '.repeat(loc.source.length - 4)); - offset = loc.end.offset - 2; - } embeddedFile.content.push([ - sfc.template.content.slice(offset), + sfc.template.content, sfc.template.name, - offset, + 0, allCodeFeatures, ]); } diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 0784333f74..c419994da1 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -6,7 +6,14 @@ import type { LanguageServicePlugin, TextDocument, } from '@volar/language-service'; -import { hyphenateAttr, hyphenateTag, tsCodegen, type VueVirtualCode } from '@vue/language-core'; +import type * as CompilerDOM from '@vue/compiler-dom'; +import { + forEachInterpolationNode, + 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'; @@ -157,6 +164,43 @@ export function create( disposable?.dispose(); }, + provideAutoInsertSnippet(document, position, autoInsertContext, token) { + if (document.languageId !== languageId) { + return; + } + const info = resolveEmbeddedCode(context, document.uri); + if (info?.code.id !== 'template') { + return; + } + + return baseServiceInstance.provideAutoInsertSnippet?.( + { + ...document, + getText: () => { + if (info.root.sfc.template?.ast) { + const locs: CompilerDOM.SourceLocation[] = []; + for (const node of forEachInterpolationNode(info.root.sfc.template.ast)) { + locs.push(node.loc); + } + let text = ''; + let offset = 0; + for (const loc of locs) { + text += info.root.sfc.template.content.slice(offset, loc.start.offset + 2); + text += ' '.repeat(loc.source.length - 4); + offset = loc.end.offset - 2; + } + text += info.root.sfc.template.content.slice(offset); + return text; + } + return document.getText(); + }, + }, + position, + autoInsertContext, + token, + ); + }, + async provideCompletionItems(document, position, completionContext, token) { if (document.languageId !== languageId) { return;