Skip to content

Commit b23e5ba

Browse files
feat(language-service): re-support scoped class links in template (#4357)
1 parent e50c882 commit b23e5ba

File tree

9 files changed

+114
-64
lines changed

9 files changed

+114
-64
lines changed

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ export interface ScriptCodegenOptions {
4343
lang: string;
4444
scriptRanges: ScriptRanges | undefined;
4545
scriptSetupRanges: ScriptSetupRanges | undefined;
46-
templateCodegen: {
47-
tsCodes: Code[];
48-
ctx: TemplateCodegenContext;
49-
hasSlot: boolean;
50-
} | undefined;
46+
templateCodegen: TemplateCodegenContext & { codes: Code[]; } | undefined;
5147
globalTypes: boolean;
5248
getGeneratedLength: () => number;
5349
linkedCodeMappings: Mapping[];

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ function* generateTemplateContext(
148148
yield* generateCssVars(options, templateCodegenCtx);
149149

150150
if (options.templateCodegen) {
151-
for (const code of options.templateCodegen.tsCodes) {
151+
for (const code of options.templateCodegen.codes) {
152152
yield code;
153153
}
154154
}
@@ -249,7 +249,7 @@ export function getTemplateUsageVars(options: ScriptCodegenOptions, ctx: ScriptC
249249
usageVars.add(component.split('.')[0]);
250250
}
251251
}
252-
for (const [varName] of options.templateCodegen.ctx.accessExternalVariables) {
252+
for (const [varName] of options.templateCodegen.accessExternalVariables) {
253253
usageVars.add(varName);
254254
}
255255
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ const _codeFeatures = {
2222
navigation: {
2323
navigation: true,
2424
} as VueCodeInformation,
25+
navigationWithoutRename: {
26+
navigation: {
27+
shouldRename() {
28+
return false;
29+
},
30+
},
31+
} as VueCodeInformation,
2532
navigationAndCompletion: {
2633
navigation: true,
2734
} as VueCodeInformation,
@@ -107,6 +114,7 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo
107114
blockConditions,
108115
usedComponentCtxVars,
109116
scopedClasses,
117+
hasSlot: false,
110118
accessExternalVariable(name: string, offset?: number) {
111119
let arr = accessExternalVariables.get(name);
112120
if (!arr) {

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,7 @@ function* generateVScope(
353353

354354
yield* generateElementDirectives(options, ctx, node);
355355
yield* generateReferencesForElements(options, ctx, node); // <el ref="foo" />
356-
if (options.shouldGenerateScopedClasses) {
357-
yield* generateReferencesForScopedCssClasses(ctx, node);
358-
}
356+
yield* generateReferencesForScopedCssClasses(ctx, node);
359357

360358
if (inScope) {
361359
yield `}${newLine}`;

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,26 @@ import * as CompilerDOM from '@vue/compiler-dom';
22
import type * as ts from 'typescript';
33
import type { Code, Sfc, VueCompilerOptions } from '../../types';
44
import { endOfLine, newLine, wrapWith } from '../common';
5-
import { createTemplateCodegenContext } from './context';
5+
import { TemplateCodegenContext, createTemplateCodegenContext } from './context';
66
import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element';
77
import { generateObjectProperty } from './objectProperty';
8-
import { generateStringLiteralKey } from './stringLiteralKey';
98
import { generateTemplateChild, getVForNode } from './templateChild';
109

1110
export interface TemplateCodegenOptions {
1211
ts: typeof ts;
1312
compilerOptions: ts.CompilerOptions;
1413
vueCompilerOptions: VueCompilerOptions;
1514
template: NonNullable<Sfc['template']>;
16-
shouldGenerateScopedClasses?: boolean;
17-
stylesScopedClasses: Set<string>;
1815
scriptSetupBindingNames: Set<string>;
1916
scriptSetupImportComponentNames: Set<string>;
2017
hasDefineSlots?: boolean;
2118
slotsAssignName?: string;
2219
propsAssignName?: string;
2320
}
2421

25-
export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code> {
22+
export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code, TemplateCodegenContext> {
2623
const ctx = createTemplateCodegenContext(options.scriptSetupBindingNames);
2724

28-
let hasSlot = false;
29-
3025
if (options.slotsAssignName) {
3126
ctx.addLocalVariable(options.slotsAssignName);
3227
}
@@ -50,19 +45,16 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
5045

5146
yield* ctx.generateAutoImportCompletion();
5247

53-
return {
54-
ctx,
55-
hasSlot,
56-
};
48+
return ctx;
5749

5850
function* generateSlotsType(): Generator<Code> {
5951
for (const { expVar, varName } of ctx.dynamicSlots) {
60-
hasSlot = true;
52+
ctx.hasSlot = true;
6153
yield `Partial<Record<NonNullable<typeof ${expVar}>, (_: typeof ${varName}) => any>> &${newLine}`;
6254
}
6355
yield `{${newLine}`;
6456
for (const slot of ctx.slots) {
65-
hasSlot = true;
57+
ctx.hasSlot = true;
6658
if (slot.name && slot.loc !== undefined) {
6759
yield* generateObjectProperty(
6860
options,
@@ -96,14 +88,26 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
9688
yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`;
9789
for (const { className, offset } of ctx.scopedClasses) {
9890
yield `__VLS_styleScopedClasses[`;
99-
yield* generateStringLiteralKey(
91+
yield [
92+
'',
93+
'template',
94+
offset,
95+
ctx.codeFeatures.navigationWithoutRename,
96+
];
97+
yield `'`;
98+
yield [
10099
className,
100+
'template',
101101
offset,
102-
{
103-
...ctx.codeFeatures.navigationAndCompletion,
104-
__displayWithLink: options.stylesScopedClasses.has(className),
105-
},
106-
);
102+
ctx.codeFeatures.navigationAndCompletion,
103+
];
104+
yield `'`;
105+
yield [
106+
'',
107+
'template',
108+
offset + className.length,
109+
ctx.codeFeatures.navigationWithoutRename,
110+
];
107111
yield `]${endOfLine}`;
108112
}
109113
yield `}${newLine}`;

packages/language-core/lib/plugins/vue-tsx.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Mapping } from '@volar/language-core';
2-
import { computed, computedSet } from 'computeds';
2+
import { computed } from 'computeds';
33
import * as path from 'path-browserify';
44
import { generateScript } from '../codegen/script';
55
import { generateTemplate } from '../codegen/template';
@@ -80,31 +80,6 @@ function createTsx(
8080
? parseScriptSetupRanges(ts, _sfc.scriptSetup.ast, ctx.vueCompilerOptions)
8181
: undefined
8282
);
83-
const shouldGenerateScopedClasses = computed(() => {
84-
const option = ctx.vueCompilerOptions.experimentalResolveStyleCssClasses;
85-
return _sfc.styles.some(s => {
86-
return option === 'always' || (option === 'scoped' && s.scoped);
87-
});
88-
});
89-
const stylesScopedClasses = computedSet(() => {
90-
91-
const classes = new Set<string>();
92-
93-
if (!shouldGenerateScopedClasses()) {
94-
return classes;
95-
}
96-
97-
for (const style of _sfc.styles) {
98-
const option = ctx.vueCompilerOptions.experimentalResolveStyleCssClasses;
99-
if (option === 'always' || (option === 'scoped' && style.scoped)) {
100-
for (const className of style.classNames) {
101-
classes.add(className.text.substring(1));
102-
}
103-
}
104-
}
105-
106-
return classes;
107-
});
10883
const generatedTemplate = computed(() => {
10984

11085
if (!_sfc.template) {
@@ -117,8 +92,6 @@ function createTsx(
11792
compilerOptions: ctx.compilerOptions,
11893
vueCompilerOptions: ctx.vueCompilerOptions,
11994
template: _sfc.template,
120-
shouldGenerateScopedClasses: shouldGenerateScopedClasses(),
121-
stylesScopedClasses: stylesScopedClasses(),
12295
scriptSetupBindingNames: scriptSetupBindingNames(),
12396
scriptSetupImportComponentNames: scriptSetupImportComponentNames(),
12497
hasDefineSlots: hasDefineSlots(),
@@ -175,11 +148,7 @@ function createTsx(
175148
lang: lang(),
176149
scriptRanges: scriptRanges(),
177150
scriptSetupRanges: scriptSetupRanges(),
178-
templateCodegen: _template ? {
179-
tsCodes: _template.codes,
180-
ctx: _template.ctx,
181-
hasSlot: _template.hasSlot,
182-
} : undefined,
151+
templateCodegen: _template,
183152
compilerOptions: ctx.compilerOptions,
184153
vueCompilerOptions: ctx.vueCompilerOptions,
185154
getGeneratedLength: () => generatedLength,

packages/language-core/lib/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export type RawVueCompilerOptions = Partial<Omit<VueCompilerOptions, 'target' |
1515

1616
export interface VueCodeInformation extends CodeInformation {
1717
__referencesCodeLens?: boolean;
18-
__displayWithLink?: boolean;
1918
__hint?: {
2019
setting: string;
2120
label: string;

packages/language-service/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export * from '@vue/language-core';
33
export * from './lib/ideFeatures/nameCasing';
44
export * from './lib/types';
55

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

99
import { create as createEmmetPlugin } from 'volar-service-emmet';
@@ -20,6 +20,7 @@ import { create as createVueAutoAddSpacePlugin } from './lib/plugins/vue-autoins
2020
import { create as createVueReferencesCodeLensPlugin } from './lib/plugins/vue-codelens-references';
2121
import { create as createVueDirectiveCommentsPlugin } from './lib/plugins/vue-directive-comments';
2222
import { create as createVueDocumentDropPlugin } from './lib/plugins/vue-document-drop';
23+
import { create as createVueDocumentLinksPlugin } from './lib/plugins/vue-document-links';
2324
import { create as createVueExtractFilePlugin } from './lib/plugins/vue-extract-file';
2425
import { create as createVueSfcPlugin } from './lib/plugins/vue-sfc';
2526
import { create as createVueTemplatePlugin } from './lib/plugins/vue-template';
@@ -79,6 +80,7 @@ export function getVueLanguageServicePlugins(
7980
createVueSfcPlugin(),
8081
createVueTwoslashQueriesPlugin(ts, getTsPluginClient),
8182
createVueReferencesCodeLensPlugin(),
83+
createVueDocumentLinksPlugin(),
8284
createVueDocumentDropPlugin(ts, getTsPluginClient),
8385
createVueAutoDotValuePlugin(ts, getTsPluginClient),
8486
createVueAutoWrapParenthesesPlugin(ts),
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service';
2+
import { Sfc, VueVirtualCode } from '@vue/language-core';
3+
import { tsCodegen } from '@vue/language-core/lib/plugins/vue-tsx';
4+
import type * as vscode from 'vscode-languageserver-protocol';
5+
6+
export function create(): LanguageServicePlugin {
7+
return {
8+
name: 'vue-document-links',
9+
create(context): LanguageServicePluginInstance {
10+
return {
11+
provideDocumentLinks(document) {
12+
13+
const decoded = context.decodeEmbeddedDocumentUri(document.uri);
14+
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
15+
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
16+
17+
if (sourceScript?.generated?.root instanceof VueVirtualCode && virtualCode?.id === 'template') {
18+
19+
const result: vscode.DocumentLink[] = [];
20+
const codegen = tsCodegen.get(sourceScript.generated.root.sfc);
21+
const scopedClasses = codegen?.generatedTemplate()?.scopedClasses ?? [];
22+
const styleClasses = new Map<string, {
23+
index: number;
24+
style: Sfc['styles'][number];
25+
classOffset: number;
26+
}[]>();
27+
const option = sourceScript.generated.root.vueCompilerOptions.experimentalResolveStyleCssClasses;
28+
29+
for (let i = 0; i < sourceScript.generated.root.sfc.styles.length; i++) {
30+
const style = sourceScript.generated.root.sfc.styles[i];
31+
if (option === 'always' || (option === 'scoped' && style.scoped)) {
32+
for (const className of style.classNames) {
33+
if (!styleClasses.has(className.text.substring(1))) {
34+
styleClasses.set(className.text.substring(1), []);
35+
}
36+
styleClasses.get(className.text.substring(1))!.push({
37+
index: i,
38+
style,
39+
classOffset: className.offset,
40+
});
41+
}
42+
}
43+
}
44+
45+
for (const { className, offset } of scopedClasses) {
46+
const styles = styleClasses.get(className);
47+
if (styles) {
48+
for (const style of styles) {
49+
const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index);
50+
const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + style.index);
51+
if (!styleVirtualCode) {
52+
continue;
53+
}
54+
const styleDocument = context.documents.get(styleDocumentUri, styleVirtualCode.languageId, styleVirtualCode.snapshot);
55+
const start = styleDocument.positionAt(style.classOffset);
56+
const end = styleDocument.positionAt(style.classOffset + className.length + 1);
57+
result.push({
58+
range: {
59+
start: document.positionAt(offset),
60+
end: document.positionAt(offset + className.length),
61+
},
62+
target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`,
63+
});
64+
}
65+
}
66+
}
67+
68+
return result;
69+
}
70+
},
71+
};
72+
},
73+
};
74+
}

0 commit comments

Comments
 (0)