Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language-core): document links for classname within :class #4642

Merged
merged 12 commits into from
Aug 25, 2024
17 changes: 14 additions & 3 deletions packages/language-core/lib/codegen/script/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { forEachInterpolationSegment } from '../template/interpolation';
import type { ScriptCodegenContext } from './context';
import { codeFeatures, type ScriptCodegenOptions } from './index';
import { generateInternalComponent } from './internalComponent';
import { generateStyleScopedClasses } from '../template/styleScopedClasses';

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

/* Style Scoped */
const firstClasses = new Set<string>();
yield `/* Style Scoped */${newLine}`;
yield `type __VLS_StyleScopedClasses = {}`;
yield `let __VLS_styleScopedClasses!: {}`;
for (let i = 0; i < options.sfc.styles.length; i++) {
const style = options.sfc.styles[i];
const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses;
if (option === 'always' || (option === 'scoped' && style.scoped)) {
for (const className of style.classNames) {
if (firstClasses.has(className.text)) {
templateCodegenCtx.scopedClasses.push({
source: 'style_' + i,
className: className.text.slice(1),
offset: className.offset + 1
});
continue;
}
firstClasses.add(className.text);
yield* generateCssClassProperty(
i,
className.text,
Expand All @@ -142,7 +153,7 @@ function* generateTemplateContext(
}
}
yield endOfLine;
yield `let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[]${endOfLine}`;
yield* generateStyleScopedClasses(templateCodegenCtx, true);
yield* generateCssVars(options, templateCodegenCtx);

if (options.templateCodegen) {
Expand Down Expand Up @@ -173,7 +184,7 @@ function* generateCssClassProperty(
'',
'style_' + styleIndex,
offset,
codeFeatures.navigationWithoutRename,
codeFeatures.navigation,
];
yield `'`;
yield [
Expand Down
6 changes: 5 additions & 1 deletion packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo
const hasSlotElements = new Set<CompilerDOM.ElementNode>();;
const blockConditions: string[] = [];
const usedComponentCtxVars = new Set<string>();
const scopedClasses: { className: string, offset: number; }[] = [];
const scopedClasses: {
source: string;
className: string;
offset: number;
}[] = [];
const emptyClassOffsets: number[] = [];
const inlayHints: InlayHintInfo[] = [];

Expand Down
143 changes: 119 additions & 24 deletions packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as CompilerDOM from '@vue/compiler-dom';
import type * as ts from 'typescript';
import { camelize, capitalize } from '@vue/shared';
import type { Code, VueCodeInformation } from '../../types';
import { hyphenateTag } from '../../utils/shared';
Expand Down Expand Up @@ -397,7 +398,7 @@ function* generateVScope(

yield* generateElementDirectives(options, ctx, node);
yield* generateReferencesForElements(options, ctx, node); // <el ref="foo" />
yield* generateReferencesForScopedCssClasses(ctx, node);
yield* generateReferencesForScopedCssClasses(options, ctx, node);

if (inScope) {
yield `}${newLine}`;
Expand Down Expand Up @@ -575,6 +576,7 @@ function* generateReferencesForElements(
}

function* generateReferencesForScopedCssClasses(
options: TemplateCodegenOptions,
ctx: TemplateCodegenContext,
node: CompilerDOM.ElementNode
): Generator<Code> {
Expand All @@ -586,28 +588,17 @@ function* generateReferencesForScopedCssClasses(
) {
let startOffset = prop.value.loc.start.offset;
let content = prop.value.loc.source;
let isWrapped = false;
if (
(content.startsWith(`'`) && content.endsWith(`'`))
|| (content.startsWith(`"`) && content.endsWith(`"`))
) {
startOffset++;
content = content.slice(1, -1);
isWrapped = true;
}
if (content) {
let currentClassName = '';
for (const char of (content + ' ')) {
if (char.trim() === '') {
if (currentClassName !== '') {
ctx.scopedClasses.push({ className: currentClassName, offset: startOffset });
startOffset += currentClassName.length;
currentClassName = '';
}
startOffset += char.length;
}
else {
currentClassName += char;
}
}
const classes = collectClasses(content, startOffset + (isWrapped ? 1 : 0));
ctx.scopedClasses.push(...classes);
}
else {
ctx.emptyClassOffsets.push(startOffset);
Expand All @@ -619,14 +610,84 @@ function* generateReferencesForScopedCssClasses(
&& prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION
&& prop.arg.content === 'class'
) {
yield `__VLS_styleScopedClasses = (`;
yield [
prop.exp.content,
'template',
prop.exp.loc.start.offset,
ctx.codeFeatures.navigationAndCompletion,
];
yield `)${endOfLine}`;
const content = '`${' + prop.exp.content + '}`';
const startOffset = prop.exp.loc.start.offset - 3;

const { ts } = options;
const ast = ts.createSourceFile('', content, 99 satisfies typeof ts.ScriptTarget.Latest);
const literals: ts.StringLiteralLike[] = [];

ts.forEachChild(ast, node => {
if (
!ts.isExpressionStatement(node) ||
!isTemplateExpression(node.expression)
) {
return;
}

const expression = node.expression.templateSpans[0].expression;

if (ts.isStringLiteralLike(expression)) {
literals.push(expression);
}

if (ts.isArrayLiteralExpression(expression)) {
walkArrayLiteral(expression);
}

if (ts.isObjectLiteralExpression(expression)) {
walkObjectLiteral(expression);
}
});

for (const literal of literals) {
const classes = collectClasses(
literal.text,
literal.end - literal.text.length - 1 + startOffset
);
ctx.scopedClasses.push(...classes);
}

function walkArrayLiteral(node: ts.ArrayLiteralExpression) {
const { elements } = node;
for (const element of elements) {
if (ts.isStringLiteralLike(element)) {
literals.push(element);
}
else if (ts.isObjectLiteralExpression(element)) {
walkObjectLiteral(element);
}
}
}

function walkObjectLiteral(node: ts.ObjectLiteralExpression) {
const { properties } = node;
for (const property of properties) {
if (ts.isPropertyAssignment(property)) {
const { name } = property;
if (ts.isIdentifier(name)) {
walkIdentifier(name);
}
else if (ts.isComputedPropertyName(name)) {
const { expression } = name;
if (ts.isStringLiteralLike(expression)) {
literals.push(expression);
}
}
}
else if (ts.isShorthandPropertyAssignment(property)) {
walkIdentifier(property.name);
}
}
}

function walkIdentifier(node: ts.Identifier) {
ctx.scopedClasses.push({
source: 'template',
className: node.text,
offset: node.end - node.text.length + startOffset
});
}
}
}
}
Expand All @@ -638,3 +699,37 @@ function camelizeComponentName(newName: string) {
function getTagRenameApply(oldName: string) {
return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined;
}

function collectClasses(content: string, startOffset = 0) {
const classes: {
source: string;
className: string;
offset: number;
}[] = [];

let currentClassName = '';
let offset = 0;
for (const char of (content + ' ')) {
if (char.trim() === '') {
if (currentClassName !== '') {
classes.push({
source: 'template',
className: currentClassName,
offset: offset + startOffset
});
offset += currentClassName.length;
currentClassName = '';
}
offset += char.length;
}
else {
currentClassName += char;
}
}
return classes;
}

// isTemplateExpression is missing in tsc
function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression {
return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression;
}
78 changes: 2 additions & 76 deletions packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TemplateCodegenContext, createTemplateCodegenContext } from './context'
import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element';
import { generateObjectProperty } from './objectProperty';
import { generateTemplateChild, getVForNode } from './templateChild';
import { generateStyleScopedClasses } from './styleScopedClasses';

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

yield* generateStyleScopedClasses();
yield* generateStyleScopedClasses(ctx);

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

function* generateStyleScopedClasses(): Generator<Code> {
yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`;
for (const offset of ctx.emptyClassOffsets) {
yield `__VLS_styleScopedClasses['`;
yield [
'',
'template',
offset,
ctx.codeFeatures.additionalCompletion,
];
yield `']${endOfLine}`;
}
for (const { className, offset } of ctx.scopedClasses) {
yield `__VLS_styleScopedClasses[`;
yield [
'',
'template',
offset,
ctx.codeFeatures.navigationWithoutRename,
];
yield `'`;

// fix https://github.com/vuejs/language-tools/issues/4537
yield* escapeString(className, offset, ['\\', '\'']);
yield `'`;
yield [
'',
'template',
offset + className.length,
ctx.codeFeatures.navigationWithoutRename,
];
yield `]${endOfLine}`;
}
yield `}${newLine}`;
}

function* generatePreResolveComponents(): Generator<Code> {
yield `let __VLS_resolvedLocalAndGlobalComponents!: {}`;
if (options.template.ast) {
Expand All @@ -144,45 +109,6 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
}
yield endOfLine;
}

function* escapeString(className: string, offset: number, escapeTargets: string[]): Generator<Code> {
let count = 0;

const currentEscapeTargets = [...escapeTargets];
const firstEscapeTarget = currentEscapeTargets.shift()!;
const splitted = className.split(firstEscapeTarget);

for (let i = 0; i < splitted.length; i++) {
const part = splitted[i];
const partLength = part.length;

if (escapeTargets.length > 0) {
yield* escapeString(part, offset + count, [...currentEscapeTargets]);
} else {
yield [
part,
'template',
offset + count,
ctx.codeFeatures.navigationAndAdditionalCompletion,
];
}

if (i !== splitted.length - 1) {
yield '\\';

yield [
firstEscapeTarget,
'template',
offset + count + partLength,
ctx.codeFeatures.navigationAndAdditionalCompletion,
];

count += partLength + 1;
} else {
count += partLength;
}
}
}
}

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