Skip to content

Commit 8c7b41f

Browse files
authored
refactor(language-service): reduce boilerplate code for virtual code operation (#5556)
1 parent f24ca40 commit 8c7b41f

19 files changed

+347
-475
lines changed

packages/language-service/lib/plugins/css.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { LanguageServicePlugin, TextDocument, VirtualCode } from '@volar/language-service';
2-
import { isRenameEnabled, VueVirtualCode } from '@vue/language-core';
2+
import { isRenameEnabled } from '@vue/language-core';
33
import { create as baseCreate, type Provide } from 'volar-service-css';
44
import type * as css from 'vscode-css-languageservice';
5-
import { URI } from 'vscode-uri';
5+
import { getEmbeddedInfo } from './utils';
66

77
export function create(): LanguageServicePlugin {
88
const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] });
@@ -54,20 +54,13 @@ export function create(): LanguageServicePlugin {
5454
document: TextDocument,
5555
position: css.Position,
5656
) {
57-
const uri = URI.parse(document.uri);
58-
const decoded = context.decodeEmbeddedDocumentUri(uri);
59-
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
60-
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
61-
if (!sourceScript?.generated || !virtualCode?.id.startsWith('style_')) {
57+
const info = getEmbeddedInfo(context, document, id => id.startsWith('style_'));
58+
if (!info) {
6259
return false;
6360
}
61+
const { sourceScript, virtualCode, root } = info;
6462

65-
const root = sourceScript.generated.root;
66-
if (!(root instanceof VueVirtualCode)) {
67-
return false;
68-
}
69-
70-
const block = root.sfc.styles.find(style => style.name === decoded![1]);
63+
const block = root.sfc.styles.find(style => style.name === virtualCode.id);
7164
if (!block) {
7265
return false;
7366
}

packages/language-service/lib/plugins/typescript-semantic-tokens.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
2-
import { VueVirtualCode } from '@vue/language-core';
32
import { convertClassificationsToSemanticTokens } from 'volar-service-typescript/lib/semanticFeatures/semanticTokens';
4-
import { URI } from 'vscode-uri';
3+
import { getEmbeddedInfo } from './utils';
54

65
export function create(
76
getTsPluginClient?: (
@@ -43,18 +42,11 @@ export function create(
4342

4443
return {
4544
async provideDocumentSemanticTokens(document, range, legend) {
46-
const uri = URI.parse(document.uri);
47-
const decoded = context.decodeEmbeddedDocumentUri(uri);
48-
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
49-
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
50-
if (!sourceScript?.generated || virtualCode?.id !== 'main') {
51-
return;
52-
}
53-
54-
const root = sourceScript.generated.root;
55-
if (!(root instanceof VueVirtualCode)) {
45+
const info = getEmbeddedInfo(context, document, 'main');
46+
if (!info) {
5647
return;
5748
}
49+
const { root } = info;
5850

5951
const start = document.offsetAt(range.start);
6052
const end = document.offsetAt(range.end);
Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,56 @@
1-
import type { TextDocument } from '@volar/language-service';
1+
import { type LanguageServiceContext, type SourceScript, type TextDocument } from '@volar/language-service';
2+
import { VueVirtualCode } from '@vue/language-core';
3+
import { URI } from 'vscode-uri';
24

35
export function sleep(ms: number) {
46
return new Promise(resolve => setTimeout(resolve, ms));
57
}
68

7-
export function isTsDocument(document: TextDocument) {
8-
return document.languageId === 'javascript'
9-
|| document.languageId === 'typescript'
10-
|| document.languageId === 'javascriptreact'
11-
|| document.languageId === 'typescriptreact';
9+
export function getEmbeddedInfo(
10+
context: LanguageServiceContext,
11+
document: TextDocument,
12+
embeddedCodeId?: string | ((id: string) => boolean),
13+
languageId?: string,
14+
) {
15+
const uri = URI.parse(document.uri);
16+
const decoded = context.decodeEmbeddedDocumentUri(uri);
17+
if (!decoded) {
18+
return;
19+
}
20+
21+
if (embeddedCodeId) {
22+
if (typeof embeddedCodeId === 'string') {
23+
if (decoded[1] !== embeddedCodeId) {
24+
return;
25+
}
26+
}
27+
else if (!embeddedCodeId(decoded[1])) {
28+
return;
29+
}
30+
}
31+
32+
if (languageId && document.languageId !== languageId) {
33+
return;
34+
}
35+
36+
const sourceScript = context.language.scripts.get(decoded[0]);
37+
if (!sourceScript?.generated) {
38+
return;
39+
}
40+
41+
const virtualCode = sourceScript.generated.embeddedCodes.get(decoded[1]);
42+
if (!virtualCode) {
43+
return;
44+
}
45+
46+
const root = sourceScript.generated.root;
47+
if (!(root instanceof VueVirtualCode)) {
48+
return;
49+
}
50+
51+
return {
52+
sourceScript: sourceScript as Required<SourceScript<URI>>,
53+
virtualCode,
54+
root,
55+
};
1256
}

packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { LanguageServiceContext, LanguageServicePlugin, TextDocument } from '@volar/language-service';
2-
import { hyphenateAttr, VueVirtualCode } from '@vue/language-core';
2+
import { hyphenateAttr } from '@vue/language-core';
33
import type * as ts from 'typescript';
4-
import { URI } from 'vscode-uri';
5-
import { isTsDocument, sleep } from './utils';
4+
import { getEmbeddedInfo, sleep } from './utils';
65

76
export function create(
87
ts: typeof import('typescript'),
@@ -24,12 +23,13 @@ export function create(
2423

2524
return {
2625
async provideAutoInsertSnippet(document, selection, change) {
27-
// selection must at end of change
28-
if (document.offsetAt(selection) !== change.rangeOffset + change.text.length) {
26+
const info = getEmbeddedInfo(context, document, id => id.startsWith('script_'));
27+
if (!info) {
2928
return;
3029
}
3130

32-
if (!isTsDocument(document)) {
31+
// selection must at end of change
32+
if (document.offsetAt(selection) !== change.rangeOffset + change.text.length) {
3333
return;
3434
}
3535

@@ -49,18 +49,7 @@ export function create(
4949
return;
5050
}
5151

52-
const uri = URI.parse(document.uri);
53-
const decoded = context.decodeEmbeddedDocumentUri(uri);
54-
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
55-
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
56-
if (!sourceScript?.generated || !virtualCode) {
57-
return;
58-
}
59-
60-
const root = sourceScript.generated.root;
61-
if (!(root instanceof VueVirtualCode)) {
62-
return;
63-
}
52+
const { sourceScript, virtualCode, root } = info;
6453

6554
const { sfc } = root;
6655
const blocks = [sfc.script, sfc.scriptSetup].filter(block => !!block);
Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin, TextDocument } from '@volar/language-service';
2-
import { VueVirtualCode } from '@vue/language-core';
3-
import { URI } from 'vscode-uri';
1+
import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service';
2+
import { getEmbeddedInfo } from './utils';
43

54
export function create(): LanguageServicePlugin {
65
return {
@@ -14,61 +13,42 @@ export function create(): LanguageServicePlugin {
1413
create(context) {
1514
return {
1615
provideDiagnostics(document) {
17-
if (!isSupportedDocument(document)) {
16+
const info = getEmbeddedInfo(context, document, 'template');
17+
if (!info) {
1818
return;
1919
}
20+
const { root } = info;
2021

21-
const uri = URI.parse(document.uri);
22-
const decoded = context.decodeEmbeddedDocumentUri(uri);
23-
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
24-
25-
const root = sourceScript?.generated?.root;
26-
if (!(root instanceof VueVirtualCode)) {
22+
const { template } = root.sfc;
23+
if (!template) {
2724
return;
2825
}
2926

30-
const templateErrors: Diagnostic[] = [];
31-
const { template } = root.sfc;
32-
33-
if (template) {
34-
for (const error of template.errors) {
35-
onCompilerError(error, 1 satisfies typeof DiagnosticSeverity.Error);
36-
}
27+
const diagnostics: Diagnostic[] = [];
3728

38-
for (const warning of template.warnings) {
39-
onCompilerError(warning, 2 satisfies typeof DiagnosticSeverity.Warning);
40-
}
41-
42-
function onCompilerError(
43-
error: NonNullable<typeof template>['errors'][number],
44-
severity: DiagnosticSeverity,
45-
) {
46-
const templateHtmlRange = {
47-
start: error.loc?.start.offset ?? 0,
48-
end: error.loc?.end.offset ?? 0,
49-
};
50-
let errorMessage = error.message;
51-
52-
templateErrors.push({
29+
for (
30+
const [errors, severity] of [
31+
[template.errors, 1 satisfies typeof DiagnosticSeverity.Error],
32+
[template.warnings, 2 satisfies typeof DiagnosticSeverity.Warning],
33+
] as const
34+
) {
35+
for (const error of errors) {
36+
diagnostics.push({
5337
range: {
54-
start: document.positionAt(templateHtmlRange.start),
55-
end: document.positionAt(templateHtmlRange.end),
38+
start: document.positionAt(error.loc?.start.offset ?? 0),
39+
end: document.positionAt(error.loc?.end.offset ?? 0),
5640
},
5741
severity,
5842
code: error.code,
5943
source: 'vue',
60-
message: errorMessage,
44+
message: error.message,
6145
});
6246
}
6347
}
6448

65-
return templateErrors;
49+
return diagnostics;
6650
},
6751
};
6852
},
6953
};
70-
71-
function isSupportedDocument(document: TextDocument) {
72-
return document.languageId === 'jade' || document.languageId === 'html';
73-
}
7454
}
Lines changed: 37 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from '@volar/language-service';
2-
import { forEachElementNode, hyphenateTag, VueVirtualCode } from '@vue/language-core';
2+
import { forEachElementNode, hyphenateTag } from '@vue/language-core';
33
import type * as ts from 'typescript';
4-
import { URI } from 'vscode-uri';
4+
import { getEmbeddedInfo } from './utils';
55

66
export function create(
77
getTsPluginClient?: (
@@ -23,64 +23,32 @@ export function create(
2323

2424
return {
2525
async provideDocumentSemanticTokens(document, range, legend) {
26-
const uri = URI.parse(document.uri);
27-
const decoded = context.decodeEmbeddedDocumentUri(uri);
28-
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
29-
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
30-
if (!sourceScript?.generated || virtualCode?.id !== 'template') {
31-
return;
32-
}
33-
34-
const root = sourceScript.generated.root;
35-
if (!(root instanceof VueVirtualCode)) {
26+
const info = getEmbeddedInfo(context, document, 'template');
27+
if (!info) {
3628
return;
3729
}
30+
const { root } = info;
3831

3932
const { template } = root.sfc;
40-
if (!template) {
33+
if (!template?.ast) {
4134
return;
4235
}
4336

44-
const result: SemanticToken[] = [];
45-
46-
const tokenTypes = legend.tokenTypes.indexOf('component');
47-
const componentSpans = await getComponentSpans(root.fileName, template, {
48-
start: document.offsetAt(range.start),
49-
length: document.offsetAt(range.end) - document.offsetAt(range.start),
50-
});
51-
52-
for (const span of componentSpans) {
53-
const position = document.positionAt(span.start);
54-
result.push([
55-
position.line,
56-
position.character,
57-
span.length,
58-
tokenTypes,
59-
0,
60-
]);
61-
}
62-
return result;
63-
},
64-
};
37+
const componentSpans: ts.TextSpan[] = [];
38+
const start = document.offsetAt(range.start);
39+
const end = document.offsetAt(range.end);
6540

66-
async function getComponentSpans(
67-
fileName: string,
68-
template: NonNullable<VueVirtualCode['_sfc']['template']>,
69-
spanTemplateRange: ts.TextSpan,
70-
) {
71-
const result: ts.TextSpan[] = [];
72-
const validComponentNames = await tsPluginClient?.getComponentNames(fileName) ?? [];
73-
const elements = new Set(await tsPluginClient?.getElementNames(fileName) ?? []);
74-
const components = new Set([
75-
...validComponentNames,
76-
...validComponentNames.map(hyphenateTag),
77-
]);
41+
const validComponentNames = await tsPluginClient?.getComponentNames(root.fileName) ?? [];
42+
const elements = new Set(await tsPluginClient?.getElementNames(root.fileName) ?? []);
43+
const components = new Set([
44+
...validComponentNames,
45+
...validComponentNames.map(hyphenateTag),
46+
]);
7847

79-
if (template.ast) {
8048
for (const node of forEachElementNode(template.ast)) {
8149
if (
82-
node.loc.end.offset <= spanTemplateRange.start
83-
|| node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length)
50+
node.loc.end.offset <= start
51+
|| node.loc.start.offset >= end
8452
) {
8553
continue;
8654
}
@@ -89,21 +57,36 @@ export function create(
8957
if (template.lang === 'html') {
9058
start += '<'.length;
9159
}
92-
result.push({
60+
componentSpans.push({
9361
start,
9462
length: node.tag.length,
9563
});
9664
if (template.lang === 'html' && !node.isSelfClosing) {
97-
result.push({
65+
componentSpans.push({
9866
start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag),
9967
length: node.tag.length,
10068
});
10169
}
10270
}
10371
}
104-
}
105-
return result;
106-
}
72+
73+
const result: SemanticToken[] = [];
74+
const tokenType = legend.tokenTypes.indexOf('component');
75+
76+
for (const span of componentSpans) {
77+
const position = document.positionAt(span.start);
78+
result.push([
79+
position.line,
80+
position.character,
81+
span.length,
82+
tokenType,
83+
0,
84+
]);
85+
}
86+
87+
return result;
88+
},
89+
};
10790
},
10891
};
10992
}

0 commit comments

Comments
 (0)