Skip to content

Commit

Permalink
feat(language-core): document links for classname within :class (#4642
Browse files Browse the repository at this point in the history
)
  • Loading branch information
KazariEX authored Aug 25, 2024
1 parent 9da831f commit bee8ef5
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 107 deletions.
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

0 comments on commit bee8ef5

Please sign in to comment.