Skip to content

Commit

Permalink
feat(language-service): re-support scoped class links in template (#4357
Browse files Browse the repository at this point in the history
)
  • Loading branch information
johnsoncodehk committed May 9, 2024
1 parent e50c882 commit b23e5ba
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 64 deletions.
6 changes: 1 addition & 5 deletions packages/language-core/lib/codegen/script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@ export interface ScriptCodegenOptions {
lang: string;
scriptRanges: ScriptRanges | undefined;
scriptSetupRanges: ScriptSetupRanges | undefined;
templateCodegen: {
tsCodes: Code[];
ctx: TemplateCodegenContext;
hasSlot: boolean;
} | undefined;
templateCodegen: TemplateCodegenContext & { codes: Code[]; } | undefined;
globalTypes: boolean;
getGeneratedLength: () => number;
linkedCodeMappings: Mapping[];
Expand Down
4 changes: 2 additions & 2 deletions packages/language-core/lib/codegen/script/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function* generateTemplateContext(
yield* generateCssVars(options, templateCodegenCtx);

if (options.templateCodegen) {
for (const code of options.templateCodegen.tsCodes) {
for (const code of options.templateCodegen.codes) {
yield code;
}
}
Expand Down Expand Up @@ -249,7 +249,7 @@ export function getTemplateUsageVars(options: ScriptCodegenOptions, ctx: ScriptC
usageVars.add(component.split('.')[0]);
}
}
for (const [varName] of options.templateCodegen.ctx.accessExternalVariables) {
for (const [varName] of options.templateCodegen.accessExternalVariables) {
usageVars.add(varName);
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const _codeFeatures = {
navigation: {
navigation: true,
} as VueCodeInformation,
navigationWithoutRename: {
navigation: {
shouldRename() {
return false;
},
},
} as VueCodeInformation,
navigationAndCompletion: {
navigation: true,
} as VueCodeInformation,
Expand Down Expand Up @@ -107,6 +114,7 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo
blockConditions,
usedComponentCtxVars,
scopedClasses,
hasSlot: false,
accessExternalVariable(name: string, offset?: number) {
let arr = accessExternalVariables.get(name);
if (!arr) {
Expand Down
4 changes: 1 addition & 3 deletions packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,7 @@ function* generateVScope(

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

if (inScope) {
yield `}${newLine}`;
Expand Down
42 changes: 23 additions & 19 deletions packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@ import * as CompilerDOM from '@vue/compiler-dom';
import type * as ts from 'typescript';
import type { Code, Sfc, VueCompilerOptions } from '../../types';
import { endOfLine, newLine, wrapWith } from '../common';
import { createTemplateCodegenContext } from './context';
import { TemplateCodegenContext, createTemplateCodegenContext } from './context';
import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element';
import { generateObjectProperty } from './objectProperty';
import { generateStringLiteralKey } from './stringLiteralKey';
import { generateTemplateChild, getVForNode } from './templateChild';

export interface TemplateCodegenOptions {
ts: typeof ts;
compilerOptions: ts.CompilerOptions;
vueCompilerOptions: VueCompilerOptions;
template: NonNullable<Sfc['template']>;
shouldGenerateScopedClasses?: boolean;
stylesScopedClasses: Set<string>;
scriptSetupBindingNames: Set<string>;
scriptSetupImportComponentNames: Set<string>;
hasDefineSlots?: boolean;
slotsAssignName?: string;
propsAssignName?: string;
}

export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code> {
export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code, TemplateCodegenContext> {
const ctx = createTemplateCodegenContext(options.scriptSetupBindingNames);

let hasSlot = false;

if (options.slotsAssignName) {
ctx.addLocalVariable(options.slotsAssignName);
}
Expand All @@ -50,19 +45,16 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co

yield* ctx.generateAutoImportCompletion();

return {
ctx,
hasSlot,
};
return ctx;

function* generateSlotsType(): Generator<Code> {
for (const { expVar, varName } of ctx.dynamicSlots) {
hasSlot = true;
ctx.hasSlot = true;
yield `Partial<Record<NonNullable<typeof ${expVar}>, (_: typeof ${varName}) => any>> &${newLine}`;
}
yield `{${newLine}`;
for (const slot of ctx.slots) {
hasSlot = true;
ctx.hasSlot = true;
if (slot.name && slot.loc !== undefined) {
yield* generateObjectProperty(
options,
Expand Down Expand Up @@ -96,14 +88,26 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`;
for (const { className, offset } of ctx.scopedClasses) {
yield `__VLS_styleScopedClasses[`;
yield* generateStringLiteralKey(
yield [
'',
'template',
offset,
ctx.codeFeatures.navigationWithoutRename,
];
yield `'`;
yield [
className,
'template',
offset,
{
...ctx.codeFeatures.navigationAndCompletion,
__displayWithLink: options.stylesScopedClasses.has(className),
},
);
ctx.codeFeatures.navigationAndCompletion,
];
yield `'`;
yield [
'',
'template',
offset + className.length,
ctx.codeFeatures.navigationWithoutRename,
];
yield `]${endOfLine}`;
}
yield `}${newLine}`;
Expand Down
35 changes: 2 additions & 33 deletions packages/language-core/lib/plugins/vue-tsx.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Mapping } from '@volar/language-core';
import { computed, computedSet } from 'computeds';
import { computed } from 'computeds';
import * as path from 'path-browserify';
import { generateScript } from '../codegen/script';
import { generateTemplate } from '../codegen/template';
Expand Down Expand Up @@ -80,31 +80,6 @@ function createTsx(
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, ctx.vueCompilerOptions)
: undefined
);
const shouldGenerateScopedClasses = computed(() => {
const option = ctx.vueCompilerOptions.experimentalResolveStyleCssClasses;
return _sfc.styles.some(s => {
return option === 'always' || (option === 'scoped' && s.scoped);
});
});
const stylesScopedClasses = computedSet(() => {

const classes = new Set<string>();

if (!shouldGenerateScopedClasses()) {
return classes;
}

for (const style of _sfc.styles) {
const option = ctx.vueCompilerOptions.experimentalResolveStyleCssClasses;
if (option === 'always' || (option === 'scoped' && style.scoped)) {
for (const className of style.classNames) {
classes.add(className.text.substring(1));
}
}
}

return classes;
});
const generatedTemplate = computed(() => {

if (!_sfc.template) {
Expand All @@ -117,8 +92,6 @@ function createTsx(
compilerOptions: ctx.compilerOptions,
vueCompilerOptions: ctx.vueCompilerOptions,
template: _sfc.template,
shouldGenerateScopedClasses: shouldGenerateScopedClasses(),
stylesScopedClasses: stylesScopedClasses(),
scriptSetupBindingNames: scriptSetupBindingNames(),
scriptSetupImportComponentNames: scriptSetupImportComponentNames(),
hasDefineSlots: hasDefineSlots(),
Expand Down Expand Up @@ -175,11 +148,7 @@ function createTsx(
lang: lang(),
scriptRanges: scriptRanges(),
scriptSetupRanges: scriptSetupRanges(),
templateCodegen: _template ? {
tsCodes: _template.codes,
ctx: _template.ctx,
hasSlot: _template.hasSlot,
} : undefined,
templateCodegen: _template,
compilerOptions: ctx.compilerOptions,
vueCompilerOptions: ctx.vueCompilerOptions,
getGeneratedLength: () => generatedLength,
Expand Down
1 change: 0 additions & 1 deletion packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export type RawVueCompilerOptions = Partial<Omit<VueCompilerOptions, 'target' |

export interface VueCodeInformation extends CodeInformation {
__referencesCodeLens?: boolean;
__displayWithLink?: boolean;
__hint?: {
setting: string;
label: string;
Expand Down
4 changes: 3 additions & 1 deletion packages/language-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from '@vue/language-core';
export * from './lib/ideFeatures/nameCasing';
export * from './lib/types';

import type { ServiceContext, ServiceEnvironment, LanguageServicePlugin } from '@volar/language-service';
import type { LanguageServicePlugin, ServiceContext, ServiceEnvironment } from '@volar/language-service';
import type { VueCompilerOptions } from './lib/types';

import { create as createEmmetPlugin } from 'volar-service-emmet';
Expand All @@ -20,6 +20,7 @@ import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoins
import { create as createVueReferencesCodeLensPlugin } from './lib/plugins/vue-codelens-references';
import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments';
import { create as createVueDocumentDropPlugin } from './lib/plugins/vue-document-drop';
import { create as createVueDocumentLinksPlugin } from './lib/plugins/vue-document-links';
import { create as createVueExtractFilePlugin } from './lib/plugins/vue-extract-file';
import { create as createVueSfcPlugin } from './lib/plugins/vue-sfc';
import { create as createVueTemplatePlugin } from './lib/plugins/vue-template';
Expand Down Expand Up @@ -79,6 +80,7 @@ export function getVueLanguageServicePlugins(
createVueSfcPlugin(),
createVueTwoslashQueriesPlugin(ts, getTsPluginClient),
createVueReferencesCodeLensPlugin(),
createVueDocumentLinksPlugin(),
createVueDocumentDropPlugin(ts, getTsPluginClient),
createVueAutoDotValuePlugin(ts, getTsPluginClient),
createVueAutoWrapParenthesesPlugin(ts),
Expand Down
74 changes: 74 additions & 0 deletions packages/language-service/lib/plugins/vue-document-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service';
import { Sfc, VueVirtualCode } from '@vue/language-core';
import { tsCodegen } from '@vue/language-core/lib/plugins/vue-tsx';
import type * as vscode from 'vscode-languageserver-protocol';

export function create(): LanguageServicePlugin {
return {
name: 'vue-document-links',
create(context): LanguageServicePluginInstance {
return {
provideDocumentLinks(document) {

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 (sourceScript?.generated?.root instanceof VueVirtualCode && virtualCode?.id === 'template') {

const result: vscode.DocumentLink[] = [];
const codegen = tsCodegen.get(sourceScript.generated.root.sfc);
const scopedClasses = codegen?.generatedTemplate()?.scopedClasses ?? [];
const styleClasses = new Map<string, {
index: number;
style: Sfc['styles'][number];
classOffset: number;
}[]>();
const option = sourceScript.generated.root.vueCompilerOptions.experimentalResolveStyleCssClasses;

for (let i = 0; i < sourceScript.generated.root.sfc.styles.length; i++) {
const style = sourceScript.generated.root.sfc.styles[i];
if (option === 'always' || (option === 'scoped' && style.scoped)) {
for (const className of style.classNames) {
if (!styleClasses.has(className.text.substring(1))) {
styleClasses.set(className.text.substring(1), []);
}
styleClasses.get(className.text.substring(1))!.push({
index: i,
style,
classOffset: className.offset,
});
}
}
}

for (const { className, offset } of scopedClasses) {
const styles = styleClasses.get(className);
if (styles) {
for (const style of styles) {
const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index);
const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + style.index);
if (!styleVirtualCode) {
continue;
}
const styleDocument = context.documents.get(styleDocumentUri, styleVirtualCode.languageId, styleVirtualCode.snapshot);
const start = styleDocument.positionAt(style.classOffset);
const end = styleDocument.positionAt(style.classOffset + className.length + 1);
result.push({
range: {
start: document.positionAt(offset),
end: document.positionAt(offset + className.length),
},
target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`,
});
}
}
}

return result;
}
},
};
},
};
}

0 comments on commit b23e5ba

Please sign in to comment.