Skip to content

Commit bee8ef5

Browse files
authored
feat(language-core): document links for classname within :class (#4642)
1 parent 9da831f commit bee8ef5

File tree

6 files changed

+221
-107
lines changed

6 files changed

+221
-107
lines changed

packages/language-core/lib/codegen/script/template.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { forEachInterpolationSegment } from '../template/interpolation';
77
import type { ScriptCodegenContext } from './context';
88
import { codeFeatures, type ScriptCodegenOptions } from './index';
99
import { generateInternalComponent } from './internalComponent';
10+
import { generateStyleScopedClasses } from '../template/styleScopedClasses';
1011

1112
export function* generateTemplate(
1213
options: ScriptCodegenOptions,
@@ -124,13 +125,23 @@ function* generateTemplateContext(
124125
yield `let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx${endOfLine}`; // for html completion, TS references...
125126

126127
/* Style Scoped */
128+
const firstClasses = new Set<string>();
127129
yield `/* Style Scoped */${newLine}`;
128-
yield `type __VLS_StyleScopedClasses = {}`;
130+
yield `let __VLS_styleScopedClasses!: {}`;
129131
for (let i = 0; i < options.sfc.styles.length; i++) {
130132
const style = options.sfc.styles[i];
131133
const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses;
132134
if (option === 'always' || (option === 'scoped' && style.scoped)) {
133135
for (const className of style.classNames) {
136+
if (firstClasses.has(className.text)) {
137+
templateCodegenCtx.scopedClasses.push({
138+
source: 'style_' + i,
139+
className: className.text.slice(1),
140+
offset: className.offset + 1
141+
});
142+
continue;
143+
}
144+
firstClasses.add(className.text);
134145
yield* generateCssClassProperty(
135146
i,
136147
className.text,
@@ -142,7 +153,7 @@ function* generateTemplateContext(
142153
}
143154
}
144155
yield endOfLine;
145-
yield `let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[]${endOfLine}`;
156+
yield* generateStyleScopedClasses(templateCodegenCtx, true);
146157
yield* generateCssVars(options, templateCodegenCtx);
147158

148159
if (options.templateCodegen) {
@@ -173,7 +184,7 @@ function* generateCssClassProperty(
173184
'',
174185
'style_' + styleIndex,
175186
offset,
176-
codeFeatures.navigationWithoutRename,
187+
codeFeatures.navigation,
177188
];
178189
yield `'`;
179190
yield [

packages/language-core/lib/codegen/template/context.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo
109109
const hasSlotElements = new Set<CompilerDOM.ElementNode>();;
110110
const blockConditions: string[] = [];
111111
const usedComponentCtxVars = new Set<string>();
112-
const scopedClasses: { className: string, offset: number; }[] = [];
112+
const scopedClasses: {
113+
source: string;
114+
className: string;
115+
offset: number;
116+
}[] = [];
113117
const emptyClassOffsets: number[] = [];
114118
const inlayHints: InlayHintInfo[] = [];
115119

packages/language-core/lib/codegen/template/element.ts

Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as CompilerDOM from '@vue/compiler-dom';
2+
import type * as ts from 'typescript';
23
import { camelize, capitalize } from '@vue/shared';
34
import type { Code, VueCodeInformation } from '../../types';
45
import { hyphenateTag } from '../../utils/shared';
@@ -397,7 +398,7 @@ function* generateVScope(
397398

398399
yield* generateElementDirectives(options, ctx, node);
399400
yield* generateReferencesForElements(options, ctx, node); // <el ref="foo" />
400-
yield* generateReferencesForScopedCssClasses(ctx, node);
401+
yield* generateReferencesForScopedCssClasses(options, ctx, node);
401402

402403
if (inScope) {
403404
yield `}${newLine}`;
@@ -575,6 +576,7 @@ function* generateReferencesForElements(
575576
}
576577

577578
function* generateReferencesForScopedCssClasses(
579+
options: TemplateCodegenOptions,
578580
ctx: TemplateCodegenContext,
579581
node: CompilerDOM.ElementNode
580582
): Generator<Code> {
@@ -586,28 +588,17 @@ function* generateReferencesForScopedCssClasses(
586588
) {
587589
let startOffset = prop.value.loc.start.offset;
588590
let content = prop.value.loc.source;
591+
let isWrapped = false;
589592
if (
590593
(content.startsWith(`'`) && content.endsWith(`'`))
591594
|| (content.startsWith(`"`) && content.endsWith(`"`))
592595
) {
593-
startOffset++;
594596
content = content.slice(1, -1);
597+
isWrapped = true;
595598
}
596599
if (content) {
597-
let currentClassName = '';
598-
for (const char of (content + ' ')) {
599-
if (char.trim() === '') {
600-
if (currentClassName !== '') {
601-
ctx.scopedClasses.push({ className: currentClassName, offset: startOffset });
602-
startOffset += currentClassName.length;
603-
currentClassName = '';
604-
}
605-
startOffset += char.length;
606-
}
607-
else {
608-
currentClassName += char;
609-
}
610-
}
600+
const classes = collectClasses(content, startOffset + (isWrapped ? 1 : 0));
601+
ctx.scopedClasses.push(...classes);
611602
}
612603
else {
613604
ctx.emptyClassOffsets.push(startOffset);
@@ -619,14 +610,84 @@ function* generateReferencesForScopedCssClasses(
619610
&& prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION
620611
&& prop.arg.content === 'class'
621612
) {
622-
yield `__VLS_styleScopedClasses = (`;
623-
yield [
624-
prop.exp.content,
625-
'template',
626-
prop.exp.loc.start.offset,
627-
ctx.codeFeatures.navigationAndCompletion,
628-
];
629-
yield `)${endOfLine}`;
613+
const content = '`${' + prop.exp.content + '}`';
614+
const startOffset = prop.exp.loc.start.offset - 3;
615+
616+
const { ts } = options;
617+
const ast = ts.createSourceFile('', content, 99 satisfies typeof ts.ScriptTarget.Latest);
618+
const literals: ts.StringLiteralLike[] = [];
619+
620+
ts.forEachChild(ast, node => {
621+
if (
622+
!ts.isExpressionStatement(node) ||
623+
!isTemplateExpression(node.expression)
624+
) {
625+
return;
626+
}
627+
628+
const expression = node.expression.templateSpans[0].expression;
629+
630+
if (ts.isStringLiteralLike(expression)) {
631+
literals.push(expression);
632+
}
633+
634+
if (ts.isArrayLiteralExpression(expression)) {
635+
walkArrayLiteral(expression);
636+
}
637+
638+
if (ts.isObjectLiteralExpression(expression)) {
639+
walkObjectLiteral(expression);
640+
}
641+
});
642+
643+
for (const literal of literals) {
644+
const classes = collectClasses(
645+
literal.text,
646+
literal.end - literal.text.length - 1 + startOffset
647+
);
648+
ctx.scopedClasses.push(...classes);
649+
}
650+
651+
function walkArrayLiteral(node: ts.ArrayLiteralExpression) {
652+
const { elements } = node;
653+
for (const element of elements) {
654+
if (ts.isStringLiteralLike(element)) {
655+
literals.push(element);
656+
}
657+
else if (ts.isObjectLiteralExpression(element)) {
658+
walkObjectLiteral(element);
659+
}
660+
}
661+
}
662+
663+
function walkObjectLiteral(node: ts.ObjectLiteralExpression) {
664+
const { properties } = node;
665+
for (const property of properties) {
666+
if (ts.isPropertyAssignment(property)) {
667+
const { name } = property;
668+
if (ts.isIdentifier(name)) {
669+
walkIdentifier(name);
670+
}
671+
else if (ts.isComputedPropertyName(name)) {
672+
const { expression } = name;
673+
if (ts.isStringLiteralLike(expression)) {
674+
literals.push(expression);
675+
}
676+
}
677+
}
678+
else if (ts.isShorthandPropertyAssignment(property)) {
679+
walkIdentifier(property.name);
680+
}
681+
}
682+
}
683+
684+
function walkIdentifier(node: ts.Identifier) {
685+
ctx.scopedClasses.push({
686+
source: 'template',
687+
className: node.text,
688+
offset: node.end - node.text.length + startOffset
689+
});
690+
}
630691
}
631692
}
632693
}
@@ -638,3 +699,37 @@ function camelizeComponentName(newName: string) {
638699
function getTagRenameApply(oldName: string) {
639700
return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined;
640701
}
702+
703+
function collectClasses(content: string, startOffset = 0) {
704+
const classes: {
705+
source: string;
706+
className: string;
707+
offset: number;
708+
}[] = [];
709+
710+
let currentClassName = '';
711+
let offset = 0;
712+
for (const char of (content + ' ')) {
713+
if (char.trim() === '') {
714+
if (currentClassName !== '') {
715+
classes.push({
716+
source: 'template',
717+
className: currentClassName,
718+
offset: offset + startOffset
719+
});
720+
offset += currentClassName.length;
721+
currentClassName = '';
722+
}
723+
offset += char.length;
724+
}
725+
else {
726+
currentClassName += char;
727+
}
728+
}
729+
return classes;
730+
}
731+
732+
// isTemplateExpression is missing in tsc
733+
function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression {
734+
return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression;
735+
}

packages/language-core/lib/codegen/template/index.ts

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TemplateCodegenContext, createTemplateCodegenContext } from './context'
66
import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element';
77
import { generateObjectProperty } from './objectProperty';
88
import { generateTemplateChild, getVForNode } from './templateChild';
9+
import { generateStyleScopedClasses } from './styleScopedClasses';
910

1011
export interface TemplateCodegenOptions {
1112
ts: typeof ts;
@@ -36,7 +37,7 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
3637
yield* generateTemplateChild(options, ctx, options.template.ast, undefined, undefined, undefined);
3738
}
3839

39-
yield* generateStyleScopedClasses();
40+
yield* generateStyleScopedClasses(ctx);
4041

4142
if (!options.hasDefineSlots) {
4243
yield `var __VLS_slots!:`;
@@ -89,42 +90,6 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
8990
yield endOfLine;
9091
}
9192

92-
function* generateStyleScopedClasses(): Generator<Code> {
93-
yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`;
94-
for (const offset of ctx.emptyClassOffsets) {
95-
yield `__VLS_styleScopedClasses['`;
96-
yield [
97-
'',
98-
'template',
99-
offset,
100-
ctx.codeFeatures.additionalCompletion,
101-
];
102-
yield `']${endOfLine}`;
103-
}
104-
for (const { className, offset } of ctx.scopedClasses) {
105-
yield `__VLS_styleScopedClasses[`;
106-
yield [
107-
'',
108-
'template',
109-
offset,
110-
ctx.codeFeatures.navigationWithoutRename,
111-
];
112-
yield `'`;
113-
114-
// fix https://github.com/vuejs/language-tools/issues/4537
115-
yield* escapeString(className, offset, ['\\', '\'']);
116-
yield `'`;
117-
yield [
118-
'',
119-
'template',
120-
offset + className.length,
121-
ctx.codeFeatures.navigationWithoutRename,
122-
];
123-
yield `]${endOfLine}`;
124-
}
125-
yield `}${newLine}`;
126-
}
127-
12893
function* generatePreResolveComponents(): Generator<Code> {
12994
yield `let __VLS_resolvedLocalAndGlobalComponents!: {}`;
13095
if (options.template.ast) {
@@ -144,45 +109,6 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
144109
}
145110
yield endOfLine;
146111
}
147-
148-
function* escapeString(className: string, offset: number, escapeTargets: string[]): Generator<Code> {
149-
let count = 0;
150-
151-
const currentEscapeTargets = [...escapeTargets];
152-
const firstEscapeTarget = currentEscapeTargets.shift()!;
153-
const splitted = className.split(firstEscapeTarget);
154-
155-
for (let i = 0; i < splitted.length; i++) {
156-
const part = splitted[i];
157-
const partLength = part.length;
158-
159-
if (escapeTargets.length > 0) {
160-
yield* escapeString(part, offset + count, [...currentEscapeTargets]);
161-
} else {
162-
yield [
163-
part,
164-
'template',
165-
offset + count,
166-
ctx.codeFeatures.navigationAndAdditionalCompletion,
167-
];
168-
}
169-
170-
if (i !== splitted.length - 1) {
171-
yield '\\';
172-
173-
yield [
174-
firstEscapeTarget,
175-
'template',
176-
offset + count + partLength,
177-
ctx.codeFeatures.navigationAndAdditionalCompletion,
178-
];
179-
180-
count += partLength + 1;
181-
} else {
182-
count += partLength;
183-
}
184-
}
185-
}
186112
}
187113

188114
export function* forEachElementNode(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode): Generator<CompilerDOM.ElementNode> {

0 commit comments

Comments
 (0)