From d6f251976c58de615230eb75aa7c2e7b93568137 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Wed, 27 Mar 2024 23:35:03 +0800 Subject: [PATCH] fix(language-core): generate each interpolation into separate virtual code (#4165) --- .../language-core/lib/generators/inlineCss.ts | 10 +- .../language-core/lib/generators/template.ts | 290 +++++++----------- packages/language-core/lib/plugins.ts | 2 + .../lib/plugins/vue-sfc-styles.ts | 56 +++- .../lib/plugins/vue-template-inline-css.ts | 15 +- .../lib/plugins/vue-template-inline-ts.ts | 190 ++++++++++++ packages/language-core/lib/plugins/vue-tsx.ts | 57 +--- .../lib/virtualFile/computedFiles.ts | 11 +- .../lib/plugins/vue-autoinsert-parentheses.ts | 2 +- .../tests/format/2105.spec.ts | 4 - .../format/deep-interpolation-indent.spec.ts | 30 ++ 11 files changed, 389 insertions(+), 278 deletions(-) create mode 100644 packages/language-core/lib/plugins/vue-template-inline-ts.ts create mode 100644 packages/language-service/tests/format/deep-interpolation-indent.spec.ts diff --git a/packages/language-core/lib/generators/inlineCss.ts b/packages/language-core/lib/generators/inlineCss.ts index 57b6bc7d0..19aef52e3 100644 --- a/packages/language-core/lib/generators/inlineCss.ts +++ b/packages/language-core/lib/generators/inlineCss.ts @@ -3,6 +3,11 @@ import { forEachElementNode } from './template'; import { enableAllFeatures } from './utils'; import type { Code } from '../types'; +const codeFeatures = enableAllFeatures({ + format: false, + structure: false, +}); + export function* generate(templateAst: NonNullable): Generator { for (const node of forEachElementNode(templateAst)) { for (const prop of node.props) { @@ -24,10 +29,7 @@ export function* generate(templateAst: NonNullable): Gener content, 'template', prop.arg.loc.start.offset + start, - enableAllFeatures({ - format: false, - structure: false, - }), + codeFeatures, ]; yield ` }\n`; } diff --git a/packages/language-core/lib/generators/template.ts b/packages/language-core/lib/generators/template.ts index abd4d6393..d8b389c63 100644 --- a/packages/language-core/lib/generators/template.ts +++ b/packages/language-core/lib/generators/template.ts @@ -36,15 +36,6 @@ const presetInfos = { slotNameExport: disableAllFeatures({ semantic: { shouldHighlight: () => false }, verification: true, navigation: true, /* __navigationCodeLens: true */ }), refAttr: disableAllFeatures({ navigation: true }), }; -const formatBrackets = { - normal: ['`${', '}`;'] as [string, string], - // fix https://github.com/vuejs/language-tools/issues/3572 - params: ['(', ') => {};'] as [string, string], - // fix https://github.com/vuejs/language-tools/issues/1210 - // fix https://github.com/vuejs/language-tools/issues/2305 - curly: ['0 +', '+ 0;'] as [string, string], - event: ['() => ', ';'] as [string, string], -}; const validTsVarReg = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; const colonReg = /:/g; // @ts-ignore @@ -63,11 +54,6 @@ const transformContext: CompilerDOM.TransformContext = { expressionPlugins: ['typescript'], }; -export type _CodeAndStack = [ - codeType: 'ts' | 'tsFormat', - ...codeAndStack: CodeAndStack, -]; - export function* generate( ts: typeof import('typescript'), compilerOptions: ts.CompilerOptions, @@ -109,11 +95,8 @@ export function* generate( return code; }; const _ts = codegenStack - ? (code: Code): _CodeAndStack => ['ts', processDirectiveComment(code), getStack()] - : (code: Code): _CodeAndStack => ['ts', processDirectiveComment(code), '']; - const _tsFormat = codegenStack - ? (code: Code): _CodeAndStack => ['tsFormat', code, getStack()] - : (code: Code): _CodeAndStack => ['tsFormat', code, '']; + ? (code: Code): CodeAndStack => [processDirectiveComment(code), getStack()] + : (code: Code): CodeAndStack => [processDirectiveComment(code), '']; const nativeTags = new Set(vueCompilerOptions.nativeTags); const slots = new Map { + function* generateExpectErrorComment(): Generator { if (expectErrorToken && expectedErrorNode) { const token = expectErrorToken; @@ -247,7 +230,7 @@ export function* generate( expectedErrorNode = undefined; } - function* generateCanonicalComponentName(tagText: string, offset: number, info: VueCodeInformation): Generator<_CodeAndStack> { + function* generateCanonicalComponentName(tagText: string, offset: number, info: VueCodeInformation): Generator { if (validTsVarReg.test(tagText)) { yield _ts([tagText, 'template', offset, info]); } @@ -260,7 +243,7 @@ export function* generate( } } - function* generateSlotsType(): Generator<_CodeAndStack> { + function* generateSlotsType(): Generator { for (const [exp, slot] of slotExps) { hasSlot = true; yield _ts(`Partial, (_: typeof ${slot.varName}) => any>> &\n`); @@ -286,7 +269,7 @@ export function* generate( yield _ts(`}`); } - function* generateStyleScopedClasses(): Generator<_CodeAndStack> { + function* generateStyleScopedClasses(): Generator { yield _ts(`if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {\n`); for (const { className, offset } of scopedClasses) { yield _ts(`__VLS_styleScopedClasses[`); @@ -303,7 +286,7 @@ export function* generate( yield _ts('}\n'); } - function* generatePreResolveComponents(): Generator<_CodeAndStack> { + function* generatePreResolveComponents(): Generator { yield _ts(`let __VLS_resolvedLocalAndGlobalComponents!: {}\n`); @@ -401,7 +384,7 @@ export function* generate( parentEl: CompilerDOM.ElementNode | undefined, prevNode: CompilerDOM.TemplateChildNode | undefined, componentCtxVar: string | undefined, - ): Generator<_CodeAndStack> { + ): Generator { yield* generateExpectErrorComment(); @@ -454,21 +437,7 @@ export function* generate( } else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) { // {{ ... }} - - let content = node.content.loc.source; - let start = node.content.loc.start.offset; - let leftCharacter: string; - let rightCharacter: string; - - // fix https://github.com/vuejs/language-tools/issues/1787 - while ((leftCharacter = template.content.substring(start - 1, start)).trim() === '' && leftCharacter.length) { - start--; - content = leftCharacter + content; - } - while ((rightCharacter = template.content.substring(start + content.length, start + content.length + 1)).trim() === '' && rightCharacter.length) { - content = content + rightCharacter; - } - + const [content, start] = parseInterpolationNode(node, template.content); yield* generateInterpolation( content, node.content.loc, @@ -477,15 +446,6 @@ export function* generate( '(', ');\n', ); - const lines = content.split('\n'); - yield* generateTsFormat( - content, - start, - lines.length <= 1 ? formatBrackets.curly : [ - lines[0].trim() === '' ? '(' : formatBrackets.curly[0], - lines[lines.length - 1].trim() === '' ? ');' : formatBrackets.curly[1], - ], - ); } else if (node.type === CompilerDOM.NodeTypes.IF) { // v-if / v-else-if / v-else @@ -500,7 +460,7 @@ export function* generate( } } - function* generateVIf(node: CompilerDOM.IfNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { + function* generateVIf(node: CompilerDOM.IfNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { let originalBlockConditionsLength = blockConditions.length; @@ -537,12 +497,6 @@ export function* generate( ) ); addedBlockCondition = true; - - yield* generateTsFormat( - branch.condition.content, - branch.condition.loc.start.offset, - formatBrackets.normal, - ); } yield _ts(` {\n`); @@ -565,17 +519,15 @@ export function* generate( blockConditions.length = originalBlockConditionsLength; } - function* generateVFor(node: CompilerDOM.ForNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { - - const { source, value, key, index } = node.parseResult; - const leftExpressionRange = value ? { start: (value ?? key ?? index).loc.start.offset, end: (index ?? key ?? value).loc.end.offset } : undefined; - const leftExpressionText = leftExpressionRange ? node.loc.source.substring(leftExpressionRange.start - node.loc.start.offset, leftExpressionRange.end - node.loc.start.offset) : undefined; + function* generateVFor(node: CompilerDOM.ForNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { + const { source } = node.parseResult; + const { leftExpressionRange, leftExpressionText } = parseVForNode(node); const forBlockVars: string[] = []; yield _ts(`for (const [`); if (leftExpressionRange && leftExpressionText) { - const collectAst = createTsAst(node.parseResult, `const [${leftExpressionText}]`); + const collectAst = createTsAst(ts, node.parseResult, `const [${leftExpressionText}]`); collectVars(ts, collectAst, collectAst, forBlockVars); for (const varName of forBlockVars) { @@ -583,7 +535,6 @@ export function* generate( } yield _ts([leftExpressionText, 'template', leftExpressionRange.start, presetInfos.all]); - yield* generateTsFormat(leftExpressionText, leftExpressionRange.start, formatBrackets.normal); } yield _ts(`] of __VLS_getVForSourceType`); if (source.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { @@ -608,12 +559,6 @@ export function* generate( yield* generateExtraAutoImport(); yield _ts('}\n'); - - yield* generateTsFormat( - source.content, - source.loc.start.offset, - formatBrackets.normal, - ); } for (const varName of forBlockVars) { @@ -621,7 +566,7 @@ export function* generate( } } - function* generateElement(node: CompilerDOM.ElementNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator<_CodeAndStack> { + function* generateElement(node: CompilerDOM.ElementNode, parentEl: CompilerDOM.ElementNode | undefined, componentCtxVar: string | undefined): Generator { yield _ts(`{\n`); @@ -808,14 +753,6 @@ export function* generate( ')', ); yield _ts(';\n'); - const fb = formatBrackets.normal; - if (fb) { - yield* generateTsFormat( - failedExp.loc.source, - failedExp.loc.start.offset, - fb, - ); - } } const vScope = props.find(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && (prop.name === 'scope' || prop.name === 'data')); @@ -870,13 +807,7 @@ export function* generate( let hasProps = false; if (slotDir?.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - yield* generateTsFormat( - slotDir.exp.content, - slotDir.exp.loc.start.offset, - formatBrackets.params, - ); - - const slotAst = createTsAst(slotDir, `(${slotDir.exp.content}) => {}`); + const slotAst = createTsAst(ts, slotDir, `(${slotDir.exp.content}) => {}`); collectVars(ts, slotAst, slotAst, slotBlockVars); hasProps = true; if (slotDir.exp.content.indexOf(':') === -1) { @@ -986,7 +917,7 @@ export function* generate( yield _ts(`}\n`); } - function* generateEvents(node: CompilerDOM.ElementNode, componentVar: string, componentInstanceVar: string, eventsVar: string, used: () => void): Generator<_CodeAndStack> { + function* generateEvents(node: CompilerDOM.ElementNode, componentVar: string, componentInstanceVar: string, eventsVar: string, used: () => void): Generator { for (const prop of node.props) { if ( @@ -1091,44 +1022,19 @@ export function* generate( ')}', ); yield _ts(';\n'); - yield* generateTsFormat( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ); } } } - function* appendExpressionNode(prop: CompilerDOM.DirectiveNode): Generator<_CodeAndStack> { + function* appendExpressionNode(prop: CompilerDOM.DirectiveNode): Generator { if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { - - const ast = createTsAst(prop.exp, prop.exp.content); - let isCompoundExpression = true; - - if (ast.statements.length === 1) { - ts.forEachChild(ast, child_1 => { - if (ts.isExpressionStatement(child_1)) { - ts.forEachChild(child_1, child_2 => { - if (ts.isArrowFunction(child_2)) { - isCompoundExpression = false; - } - else if (ts.isIdentifier(child_2)) { - isCompoundExpression = false; - } - }); - } - else if (ts.isFunctionDeclaration(child_1)) { - isCompoundExpression = false; - } - }); - } - let prefix = '('; let suffix = ')'; let isFirstMapping = true; - if (isCompoundExpression) { + const ast = createTsAst(ts, prop.exp, prop.exp.content); + const _isCompoundExpression = isCompoundExpression(ts, ast); + if (_isCompoundExpression) { yield _ts('$event => {\n'); localVars.set('$event', (localVars.get('$event') ?? 0) + 1); @@ -1145,7 +1051,7 @@ export function* generate( prop.exp.loc, prop.exp.loc.start.offset, () => { - if (isCompoundExpression && isFirstMapping) { + if (_isCompoundExpression && isFirstMapping) { isFirstMapping = false; return presetInfos.allWithHiddenParam; } @@ -1155,26 +1061,20 @@ export function* generate( suffix, ); - if (isCompoundExpression) { + if (_isCompoundExpression) { localVars.set('$event', localVars.get('$event')! - 1); yield _ts(';\n'); yield* generateExtraAutoImport(); yield _ts('}\n'); } - - yield* generateTsFormat( - prop.exp.content, - prop.exp.loc.start.offset, - isCompoundExpression ? formatBrackets.event : formatBrackets.normal, - ); } else { yield _ts(`() => {}`); } } - function* generateProps(node: CompilerDOM.ElementNode, props: CompilerDOM.ElementNode['props'], mode: 'normal' | 'extraReferences', propsFailedExps?: CompilerDOM.SimpleExpressionNode[]): Generator<_CodeAndStack> { + function* generateProps(node: CompilerDOM.ElementNode, props: CompilerDOM.ElementNode['props'], mode: 'normal' | 'extraReferences', propsFailedExps?: CompilerDOM.SimpleExpressionNode[]): Generator { let styleAttrNum = 0; let classAttrNum = 0; @@ -1283,14 +1183,6 @@ export function* generate( '(', ')', ); - - if (mode === 'normal') { - yield* generateTsFormat( - prop.exp.loc.source, - prop.exp.loc.start.offset, - formatBrackets.normal, - ); - } } else { const propVariableName = camelize(prop.exp.loc.source); @@ -1405,13 +1297,6 @@ export function* generate( ); yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); yield _ts(', '); - if (mode === 'normal') { - yield* generateTsFormat( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ); - } } else { // comment this line to avoid affecting comments in prop expressions @@ -1420,7 +1305,7 @@ export function* generate( } } - function* generateDirectives(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { + function* generateDirectives(node: CompilerDOM.ElementNode): Generator { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.DIRECTIVE @@ -1443,11 +1328,6 @@ export function* generate( ')', ); yield _ts(';\n'); - yield* generateTsFormat( - prop.arg.content, - prop.arg.loc.start.offset, - formatBrackets.normal, - ); } yield _ts(['', 'template', prop.loc.start.offset, presetInfos.diagnosticOnly]); @@ -1483,11 +1363,6 @@ export function* generate( ')', ); yield _ts(['', 'template', prop.exp.loc.end.offset, presetInfos.diagnosticOnly]); - yield* generateTsFormat( - prop.exp.content, - prop.exp.loc.start.offset, - formatBrackets.normal, - ); } else { yield _ts('undefined'); @@ -1499,7 +1374,7 @@ export function* generate( } } - function* generateReferencesForElements(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { + function* generateReferencesForElements(node: CompilerDOM.ElementNode): Generator { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.ATTRIBUTE @@ -1520,7 +1395,7 @@ export function* generate( } } - function* generateReferencesForScopedCssClasses(node: CompilerDOM.ElementNode): Generator<_CodeAndStack> { + function* generateReferencesForScopedCssClasses(node: CompilerDOM.ElementNode): Generator { for (const prop of node.props) { if ( prop.type === CompilerDOM.NodeTypes.ATTRIBUTE @@ -1561,7 +1436,7 @@ export function* generate( } } - function* generateSlot(node: CompilerDOM.ElementNode, startTagOffset: number): Generator<_CodeAndStack> { + function* generateSlot(node: CompilerDOM.ElementNode, startTagOffset: number): Generator { const varSlot = `__VLS_${elementIndex++}`; const slotNameExpNode = getSlotNameExpNode(); @@ -1722,7 +1597,7 @@ export function* generate( } } - function* generateExtraAutoImport(): Generator<_CodeAndStack> { + function* generateExtraAutoImport(): Generator { if (!tempVars.length) { return; @@ -1745,7 +1620,7 @@ export function* generate( tempVars.length = 0; } - function* generateAttrValue(attrNode: CompilerDOM.TextNode, info: VueCodeInformation): Generator<_CodeAndStack> { + function* generateAttrValue(attrNode: CompilerDOM.TextNode, info: VueCodeInformation): Generator { const char = attrNode.loc.source.startsWith("'") ? "'" : '"'; yield _ts(char); let start = attrNode.loc.start.offset; @@ -1770,7 +1645,7 @@ export function* generate( yield _ts(char); } - function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator<_CodeAndStack> { + function* generateCamelized(code: string, offset: number, info: VueCodeInformation): Generator { const parts = code.split('-'); for (let i = 0; i < parts.length; i++) { const part = parts[i]; @@ -1790,25 +1665,7 @@ export function* generate( } } - function* generateTsFormat(code: string, offset: number, formatWrapper: [string, string]): Generator<_CodeAndStack> { - yield _tsFormat(formatWrapper[0]); - yield _tsFormat([ - code, - 'template', - offset, - mergeFeatureSettings( - presetInfos.disabledAll, - { - format: true, - // autoInserts: true, // TODO: support vue-autoinsert-parentheses - }, - ), - ]); - yield _tsFormat(formatWrapper[1]); - yield _tsFormat('\n'); - } - - function* generateObjectProperty(code: string, offset: number, info: VueCodeInformation, astHolder?: any, shouldCamelize = false): Generator<_CodeAndStack> { + function* generateObjectProperty(code: string, offset: number, info: VueCodeInformation, astHolder?: any, shouldCamelize = false): Generator { if (code.startsWith('[') && code.endsWith(']') && astHolder) { yield* generateInterpolation(code, astHolder, offset, info, '', ''); } @@ -1841,9 +1698,9 @@ export function* generate( data: VueCodeInformation | (() => VueCodeInformation) | undefined, prefix: string, suffix: string, - ): Generator<_CodeAndStack> { + ): Generator { const code = prefix + _code + suffix; - const ast = createTsAst(astHolder, code); + const ast = createTsAst(ts, astHolder, code); const vars: { text: string, isShorthand: boolean, @@ -1900,7 +1757,7 @@ export function* generate( } } - function* generatePropertyAccess(code: string, offset?: number, info?: VueCodeInformation, astHolder?: any): Generator<_CodeAndStack> { + function* generatePropertyAccess(code: string, offset?: number, info?: VueCodeInformation, astHolder?: any): Generator { if (!compilerOptions.noPropertyAccessFromIndexSignature && validTsVarReg.test(code)) { yield _ts('.'); yield _ts(offset !== undefined && info @@ -1917,7 +1774,7 @@ export function* generate( } } - function* generateStringLiteralKey(code: string, offset?: number, info?: VueCodeInformation): Generator<_CodeAndStack> { + function* generateStringLiteralKey(code: string, offset?: number, info?: VueCodeInformation): Generator { if (offset === undefined || !info) { yield _ts(`"${code}"`); } @@ -1929,14 +1786,77 @@ export function* generate( yield _ts(['', 'template', offset + code.length, disableAllFeatures({ __combineLastMapping: true })]); } } +} - function createTsAst(astHolder: any, text: string) { - if (astHolder.__volar_ast_text !== text) { - astHolder.__volar_ast_text = text; - astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, 99 satisfies ts.ScriptTarget.ESNext); - } - return astHolder.__volar_ast as ts.SourceFile; +export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) { + if (astHolder.__volar_ast_text !== text) { + astHolder.__volar_ast_text = text; + astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, 99 satisfies ts.ScriptTarget.ESNext); } + return astHolder.__volar_ast as ts.SourceFile; +} + +export function isCompoundExpression(ts: typeof import('typescript'), ast: ts.SourceFile,) { + let result = true; + if (ast.statements.length === 1) { + ts.forEachChild(ast, child_1 => { + if (ts.isExpressionStatement(child_1)) { + ts.forEachChild(child_1, child_2 => { + if (ts.isArrowFunction(child_2)) { + result = false; + } + else if (ts.isIdentifier(child_2)) { + result = false; + } + }); + } + else if (ts.isFunctionDeclaration(child_1)) { + result = false; + } + }); + } + return result; +} + +export function parseInterpolationNode(node: CompilerDOM.InterpolationNode, template: string) { + let content = node.content.loc.source; + let start = node.content.loc.start.offset; + let leftCharacter: string; + let rightCharacter: string; + + // fix https://github.com/vuejs/language-tools/issues/1787 + while ((leftCharacter = template.substring(start - 1, start)).trim() === '' && leftCharacter.length) { + start--; + content = leftCharacter + content; + } + while ((rightCharacter = template.substring(start + content.length, start + content.length + 1)).trim() === '' && rightCharacter.length) { + content = content + rightCharacter; + } + + return [ + content, + start, + ] as const; +} + +export function parseVForNode(node: CompilerDOM.ForNode) { + const { value, key, index } = node.parseResult; + const leftExpressionRange = (value || key || index) + ? { + start: (value ?? key ?? index)!.loc.start.offset, + end: (index ?? key ?? value)!.loc.end.offset, + } + : undefined; + const leftExpressionText = leftExpressionRange + ? node.loc.source.substring( + leftExpressionRange.start - node.loc.start.offset, + leftExpressionRange.end - node.loc.start.offset + ) + : undefined; + return { + leftExpressionRange, + leftExpressionText, + }; } function getCanonicalComponentName(tagText: string) { diff --git a/packages/language-core/lib/plugins.ts b/packages/language-core/lib/plugins.ts index ceef6cca7..0f9e145a7 100644 --- a/packages/language-core/lib/plugins.ts +++ b/packages/language-core/lib/plugins.ts @@ -7,6 +7,7 @@ import useVueSfcStyles from './plugins/vue-sfc-styles'; import useVueSfcTemplate from './plugins/vue-sfc-template'; import useVueTemplateHtmlPlugin from './plugins/vue-template-html'; import useVueTemplateInlineCssPlugin from './plugins/vue-template-inline-css'; +import useVueTemplateInlineTsPlugin from './plugins/vue-template-inline-ts'; import useVueTsx from './plugins/vue-tsx'; import { pluginVersion, type VueLanguagePlugin } from './types'; @@ -18,6 +19,7 @@ export function getDefaultVueLanguagePlugins(pluginContext: Parameters { version: 2, getEmbeddedCodes(_fileName, sfc) { - return sfc.styles.map((style, i) => ({ - id: 'style_' + i, - lang: style.lang, - })); + const result: { + id: string; + lang: string; + }[] = []; + for (let i = 0; i < sfc.styles.length; i++) { + const style = sfc.styles[i]; + if (style) { + result.push({ + id: 'style_' + i, + lang: style.lang, + }); + if (style.cssVars.length) { + result.push({ + id: 'style_' + i + '_inline_ts', + lang: 'ts', + }); + } + } + } + return result; }, resolveEmbeddedCode(_fileName, sfc, embeddedFile) { if (embeddedFile.id.startsWith('style_')) { - const index = parseInt(embeddedFile.id.slice('style_'.length)); + const index = parseInt(embeddedFile.id.split('_')[1]); const style = sfc.styles[index]; - - embeddedFile.content.push([ - style.content, - style.name, - 0, - enableAllFeatures({}), - ]); + if (embeddedFile.id.endsWith('_inline_ts')) { + embeddedFile.parentCodeId = 'style_' + index; + for (const cssVar of style.cssVars) { + embeddedFile.content.push( + '(', + [ + cssVar.text, + style.name, + cssVar.offset, + enableAllFeatures({}), + ], + ');\n', + ); + } + } + else { + embeddedFile.content.push([ + style.content, + style.name, + 0, + enableAllFeatures({}), + ]); + } } }, }; 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 91238bfde..58f31a1e8 100644 --- a/packages/language-core/lib/plugins/vue-template-inline-css.ts +++ b/packages/language-core/lib/plugins/vue-template-inline-css.ts @@ -8,19 +8,18 @@ const plugin: VueLanguagePlugin = () => { version: 2, getEmbeddedCodes(_fileName, sfc) { - if (sfc.template?.ast) { - return [{ id: 'template_inline_css', lang: 'css' }]; + if (!sfc.template?.ast) { + return []; } - return []; + return [{ id: 'template_inline_css', lang: 'css' }]; }, resolveEmbeddedCode(_fileName, sfc, embeddedFile) { - if (embeddedFile.id === 'template_inline_css') { - embeddedFile.parentCodeId = 'template'; - if (sfc.template?.ast) { - embeddedFile.content.push(...generateInlineCss(sfc.template.ast)); - } + if (embeddedFile.id !== 'template_inline_css' || !sfc.template?.ast) { + return; } + embeddedFile.parentCodeId = 'template'; + embeddedFile.content.push(...generateInlineCss(sfc.template.ast)); }, }; }; diff --git a/packages/language-core/lib/plugins/vue-template-inline-ts.ts b/packages/language-core/lib/plugins/vue-template-inline-ts.ts new file mode 100644 index 000000000..6dd91652f --- /dev/null +++ b/packages/language-core/lib/plugins/vue-template-inline-ts.ts @@ -0,0 +1,190 @@ +import { createTsAst, isCompoundExpression, parseVForNode, parseInterpolationNode } from '../generators/template'; +import { disableAllFeatures } from '../generators/utils'; +import type { Code, Sfc, VueLanguagePlugin } from '../types'; +import * as CompilerDOM from '@vue/compiler-dom'; + +const codeFeatures = disableAllFeatures({ + format: true, + // autoInserts: true, // TODO: support vue-autoinsert-parentheses +}); +const formatBrackets = { + normal: ['`${', '}`;'] as [string, string], + if: ['if (', ') { }'] as [string, string], + for: ['for (', ') { }'] as [string, string], + // fix https://github.com/vuejs/language-tools/issues/3572 + params: ['(', ') => {};'] as [string, string], + // fix https://github.com/vuejs/language-tools/issues/1210 + // fix https://github.com/vuejs/language-tools/issues/2305 + curly: ['0 +', '+ 0;'] as [string, string], + event: ['() => ', ';'] as [string, string], +}; + +const plugin: VueLanguagePlugin = ctx => { + + const parseds = new WeakMap>(); + + return { + + version: 2, + + getEmbeddedCodes(_fileName, sfc) { + if (!sfc.template?.ast) { + return []; + } + const parsed = parse(sfc); + parseds.set(sfc, parsed); + const result: { + id: string; + lang: string; + }[] = []; + for (const [id] of parsed) { + result.push({ id, lang: 'ts' }); + } + return result; + }, + + resolveEmbeddedCode(_fileName, sfc, embeddedFile) { + // access template content to watch change + (() => sfc.template?.content)(); + + const parsed = parseds.get(sfc); + if (parsed) { + const codes = parsed.get(embeddedFile.id); + if (codes) { + embeddedFile.content.push(...codes); + embeddedFile.parentCodeId = 'template'; + } + } + }, + }; + + function parse(sfc: Sfc) { + const data = new Map(); + if (!sfc.template?.ast) { + return data; + } + const templateContent = sfc.template.content; + let i = 0; + sfc.template.ast.children.forEach(visit); + return data; + + function visit(node: CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode) { + if (node.type === CompilerDOM.NodeTypes.ELEMENT) { + for (const prop of node.props) { + if (prop.type !== CompilerDOM.NodeTypes.DIRECTIVE) { + continue; + } + const isShorthand = prop.arg?.loc.start.offset === prop.exp?.loc.start.offset; // vue 3.4+ + if (isShorthand) { + continue; + } + if (prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic) { + addFormatCodes( + prop.arg.content, + prop.arg.loc.start.offset, + formatBrackets.normal, + ); + } + if ( + prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.exp.constType !== CompilerDOM.ConstantTypes.CAN_STRINGIFY // style='z-index: 2' will compile to {'z-index':'2'} + ) { + if (prop.name === 'on') { + const ast = createTsAst(ctx.modules.typescript, prop.exp, prop.exp.content); + addFormatCodes( + prop.exp.content, + prop.exp.loc.start.offset, + isCompoundExpression(ctx.modules.typescript, ast) + ? formatBrackets.event + : formatBrackets.normal, + ); + } + else { + addFormatCodes( + prop.exp.content, + prop.exp.loc.start.offset, + formatBrackets.normal, + ); + } + } + } + for (const child of node.children) { + visit(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.IF) { + for (let i = 0; i < node.branches.length; i++) { + const branch = node.branches[i]; + if (branch.condition?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + addFormatCodes( + branch.condition.content, + branch.condition.loc.start.offset, + formatBrackets.if, + ); + } + + for (const childNode of branch.children) { + visit(childNode); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.FOR) { + const { leftExpressionRange, leftExpressionText } = parseVForNode(node); + const { source } = node.parseResult; + if (leftExpressionRange && leftExpressionText && source.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) { + const start = leftExpressionRange.start; + const end = source.loc.start.offset + source.content.length; + addFormatCodes( + templateContent.substring(start, end), + start, + formatBrackets.for, + ); + } + for (const child of node.children) { + visit(child); + } + } + else if (node.type === CompilerDOM.NodeTypes.TEXT_CALL) { + // {{ var }} + visit(node.content); + } + else if (node.type === CompilerDOM.NodeTypes.COMPOUND_EXPRESSION) { + // {{ ... }} {{ ... }} + for (const childNode of node.children) { + if (typeof childNode === 'object') { + visit(childNode); + } + } + } + else if (node.type === CompilerDOM.NodeTypes.INTERPOLATION) { + // {{ ... }} + const [content, start] = parseInterpolationNode(node, templateContent); + const lines = content.split('\n'); + addFormatCodes( + content, + start, + lines.length <= 1 ? formatBrackets.curly : [ + lines[0].trim() === '' ? '(' : formatBrackets.curly[0], + lines[lines.length - 1].trim() === '' ? ');' : formatBrackets.curly[1], + ], + ); + } + } + + function addFormatCodes(code: string, offset: number, wrapper: [string, string]) { + const id = 'template_inline_ts_' + i++; + data.set(id, [ + wrapper[0], + [ + code, + 'template', + offset, + codeFeatures, + ], + wrapper[1], + ]); + } + } +}; + +export default plugin; diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index ad215befb..c3b71a54c 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -2,7 +2,6 @@ import { Mapping, StackNode, track } from '@volar/language-core'; import { computed, computedSet } from 'computeds'; import { generate as generateScript } from '../generators/script'; import { generate as generateTemplate } from '../generators/template'; -import { enableAllFeatures } from '../generators/utils'; import { parseScriptRanges } from '../parsers/scriptRanges'; import { parseScriptSetupRanges } from '../parsers/scriptSetupRanges'; import type { Code, Sfc, VueLanguagePlugin } from '../types'; @@ -21,21 +20,14 @@ const plugin: VueLanguagePlugin = ctx => { ], getEmbeddedCodes(fileName, sfc) { - const tsx = useTsx(fileName, sfc); const files: { id: string; lang: string; }[] = []; - if (['js', 'ts', 'jsx', 'tsx'].includes(tsx.lang())) { files.push({ id: 'script_' + tsx.lang(), lang: tsx.lang() }); } - - if (sfc.template) { - files.push({ id: 'template_format', lang: 'ts' }); - } - return files; }, @@ -58,33 +50,6 @@ const plugin: VueLanguagePlugin = ctx => { embeddedFile.linkedCodeMappings = [...tsx.linkedCodeMappings]; } } - else if (embeddedFile.id === 'template_format') { - - embeddedFile.parentCodeId = 'template'; - - const template = _tsx.generatedTemplate(); - if (template) { - const [content, contentStacks] = ctx.codegenStack - ? track([...template.formatCodes], template.formatCodeStacks.map(stack => ({ stack, length: 1 }))) - : [[...template.formatCodes], template.formatCodeStacks.map(stack => ({ stack, length: 1 }))]; - embeddedFile.content = content; - embeddedFile.contentStacks = contentStacks; - } - - for (const style of sfc.styles) { - embeddedFile.content.push('\n\n'); - for (const cssVar of style.cssVars) { - embeddedFile.content.push('('); - embeddedFile.content.push([ - cssVar.text, - style.name, - cssVar.offset, - enableAllFeatures({}), - ]); - embeddedFile.content.push(');\n'); - } - } - } }, }; @@ -153,10 +118,7 @@ function createTsx( } const tsCodes: Code[] = []; - const tsFormatCodes: Code[] = []; const tsCodegenStacks: string[] = []; - const tsFormatCodegenStacks: string[] = []; - const inlineCssCodegenStacks: string[] = []; const codegen = generateTemplate( ts, ctx.compilerOptions, @@ -173,20 +135,10 @@ function createTsx( let current = codegen.next(); while (!current.done) { - const [type, code, stack] = current.value; - if (type === 'ts') { - tsCodes.push(code); - } - else if (type === 'tsFormat') { - tsFormatCodes.push(code); - } + const [code, stack] = current.value; + tsCodes.push(code); if (ctx.codegenStack) { - if (type === 'ts') { - tsCodegenStacks.push(stack); - } - else if (type === 'tsFormat') { - tsFormatCodegenStacks.push(stack); - } + tsCodegenStacks.push(stack); } current = codegen.next(); } @@ -195,9 +147,6 @@ function createTsx( ...current.value, codes: tsCodes, codeStacks: tsCodegenStacks, - formatCodes: tsFormatCodes, - formatCodeStacks: tsFormatCodegenStacks, - cssCodeStacks: inlineCssCodegenStacks, }; }); const hasScriptSetupSlots = computed(() => !!scriptSetupRanges()?.slots.define); diff --git a/packages/language-core/lib/virtualFile/computedFiles.ts b/packages/language-core/lib/virtualFile/computedFiles.ts index 212c50b78..7bdb97da6 100644 --- a/packages/language-core/lib/virtualFile/computedFiles.ts +++ b/packages/language-core/lib/virtualFile/computedFiles.ts @@ -46,16 +46,7 @@ export function computedFiles( } } - for (const { file, snapshot, mappings, codegenStacks } of remain) { - embeddedCodes.push({ - id: file.id, - languageId: resolveCommonLanguageId(`/dummy.${file.lang}`), - linkedCodeMappings: file.linkedCodeMappings, - snapshot, - mappings, - codegenStacks, - embeddedCodes: [], - }); + for (const { file } of remain) { console.error('Unable to resolve embedded: ' + file.parentCodeId + ' -> ' + file.id); } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-parentheses.ts b/packages/language-service/lib/plugins/vue-autoinsert-parentheses.ts index ef22e680a..9c87fa8ee 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-parentheses.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-parentheses.ts @@ -20,7 +20,7 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { const decoded = context.decodeEmbeddedDocumentUri(document.uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); - if (virtualCode?.id !== 'template_format') { + if (!virtualCode?.id.startsWith('template_inline_ts_')) { return; } diff --git a/packages/language-service/tests/format/2105.spec.ts b/packages/language-service/tests/format/2105.spec.ts index 94b1b7beb..798e08a4c 100644 --- a/packages/language-service/tests/format/2105.spec.ts +++ b/packages/language-service/tests/format/2105.spec.ts @@ -4,8 +4,6 @@ defineFormatTest({ title: '#' + __filename.split('.')[0], languageId: 'vue', input: ` - - `.trim(), output: ` - -