Skip to content

refactor(language-service): reduce boilerplate code for virtual code operation #5556

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

Merged
merged 2 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions packages/language-service/lib/plugins/css.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { LanguageServicePlugin, TextDocument, VirtualCode } from '@volar/language-service';
import { isRenameEnabled, VueVirtualCode } from '@vue/language-core';
import { isRenameEnabled } from '@vue/language-core';
import { create as baseCreate, type Provide } from 'volar-service-css';
import type * as css from 'vscode-css-languageservice';
import { URI } from 'vscode-uri';
import { getEmbeddedInfo } from './utils';

export function create(): LanguageServicePlugin {
const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] });
Expand Down Expand Up @@ -54,20 +54,13 @@ export function create(): LanguageServicePlugin {
document: TextDocument,
position: css.Position,
) {
const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
if (!sourceScript?.generated || !virtualCode?.id.startsWith('style_')) {
const info = getEmbeddedInfo(context, document, id => id.startsWith('style_'));
if (!info) {
return false;
}
const { sourceScript, virtualCode, root } = info;

const root = sourceScript.generated.root;
if (!(root instanceof VueVirtualCode)) {
return false;
}

const block = root.sfc.styles.find(style => style.name === decoded![1]);
const block = root.sfc.styles.find(style => style.name === virtualCode.id);
if (!block) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service';
import { VueVirtualCode } from '@vue/language-core';
import { convertClassificationsToSemanticTokens } from 'volar-service-typescript/lib/semanticFeatures/semanticTokens';
import { URI } from 'vscode-uri';
import { getEmbeddedInfo } from './utils';

export function create(
getTsPluginClient?: (
Expand Down Expand Up @@ -43,18 +42,11 @@ export function create(

return {
async provideDocumentSemanticTokens(document, range, legend) {
const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
if (!sourceScript?.generated || virtualCode?.id !== 'main') {
return;
}

const root = sourceScript.generated.root;
if (!(root instanceof VueVirtualCode)) {
const info = getEmbeddedInfo(context, document, 'main');
if (!info) {
return;
}
const { root } = info;

const start = document.offsetAt(range.start);
const end = document.offsetAt(range.end);
Expand Down
56 changes: 50 additions & 6 deletions packages/language-service/lib/plugins/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,56 @@
import type { TextDocument } from '@volar/language-service';
import { type LanguageServiceContext, type SourceScript, type TextDocument } from '@volar/language-service';
import { VueVirtualCode } from '@vue/language-core';
import { URI } from 'vscode-uri';

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

export function isTsDocument(document: TextDocument) {
return document.languageId === 'javascript'
|| document.languageId === 'typescript'
|| document.languageId === 'javascriptreact'
|| document.languageId === 'typescriptreact';
export function getEmbeddedInfo(
context: LanguageServiceContext,
document: TextDocument,
embeddedCodeId?: string | ((id: string) => boolean),
languageId?: string,
) {
const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
if (!decoded) {
return;
}

if (embeddedCodeId) {
if (typeof embeddedCodeId === 'string') {
if (decoded[1] !== embeddedCodeId) {
return;
}
}
else if (!embeddedCodeId(decoded[1])) {
return;
}
}

if (languageId && document.languageId !== languageId) {
return;
}

const sourceScript = context.language.scripts.get(decoded[0]);
if (!sourceScript?.generated) {
return;
}

const virtualCode = sourceScript.generated.embeddedCodes.get(decoded[1]);
if (!virtualCode) {
return;
}

const root = sourceScript.generated.root;
if (!(root instanceof VueVirtualCode)) {
return;
}

return {
sourceScript: sourceScript as Required<SourceScript<URI>>,
virtualCode,
root,
};
}
25 changes: 7 additions & 18 deletions packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { LanguageServiceContext, LanguageServicePlugin, TextDocument } from '@volar/language-service';
import { hyphenateAttr, VueVirtualCode } from '@vue/language-core';
import { hyphenateAttr } from '@vue/language-core';
import type * as ts from 'typescript';
import { URI } from 'vscode-uri';
import { isTsDocument, sleep } from './utils';
import { getEmbeddedInfo, sleep } from './utils';

export function create(
ts: typeof import('typescript'),
Expand All @@ -24,12 +23,13 @@ export function create(

return {
async provideAutoInsertSnippet(document, selection, change) {
// selection must at end of change
if (document.offsetAt(selection) !== change.rangeOffset + change.text.length) {
const info = getEmbeddedInfo(context, document, id => id.startsWith('script_'));
if (!info) {
return;
}

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

Expand All @@ -49,18 +49,7 @@ export function create(
return;
}

const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
if (!sourceScript?.generated || !virtualCode) {
return;
}

const root = sourceScript.generated.root;
if (!(root instanceof VueVirtualCode)) {
return;
}
const { sourceScript, virtualCode, root } = info;

const { sfc } = root;
const blocks = [sfc.script, sfc.scriptSetup].filter(block => !!block);
Expand Down
60 changes: 20 additions & 40 deletions packages/language-service/lib/plugins/vue-compiler-dom-errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin, TextDocument } from '@volar/language-service';
import { VueVirtualCode } from '@vue/language-core';
import { URI } from 'vscode-uri';
import type { Diagnostic, DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service';
import { getEmbeddedInfo } from './utils';

export function create(): LanguageServicePlugin {
return {
Expand All @@ -14,61 +13,42 @@ export function create(): LanguageServicePlugin {
create(context) {
return {
provideDiagnostics(document) {
if (!isSupportedDocument(document)) {
const info = getEmbeddedInfo(context, document, 'template');
if (!info) {
return;
}
const { root } = info;

const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);

const root = sourceScript?.generated?.root;
if (!(root instanceof VueVirtualCode)) {
const { template } = root.sfc;
if (!template) {
return;
}

const templateErrors: Diagnostic[] = [];
const { template } = root.sfc;

if (template) {
for (const error of template.errors) {
onCompilerError(error, 1 satisfies typeof DiagnosticSeverity.Error);
}
const diagnostics: Diagnostic[] = [];

for (const warning of template.warnings) {
onCompilerError(warning, 2 satisfies typeof DiagnosticSeverity.Warning);
}

function onCompilerError(
error: NonNullable<typeof template>['errors'][number],
severity: DiagnosticSeverity,
) {
const templateHtmlRange = {
start: error.loc?.start.offset ?? 0,
end: error.loc?.end.offset ?? 0,
};
let errorMessage = error.message;

templateErrors.push({
for (
const [errors, severity] of [
[template.errors, 1 satisfies typeof DiagnosticSeverity.Error],
[template.warnings, 2 satisfies typeof DiagnosticSeverity.Warning],
] as const
) {
for (const error of errors) {
diagnostics.push({
range: {
start: document.positionAt(templateHtmlRange.start),
end: document.positionAt(templateHtmlRange.end),
start: document.positionAt(error.loc?.start.offset ?? 0),
end: document.positionAt(error.loc?.end.offset ?? 0),
},
severity,
code: error.code,
source: 'vue',
message: errorMessage,
message: error.message,
});
}
}

return templateErrors;
return diagnostics;
},
};
},
};

function isSupportedDocument(document: TextDocument) {
return document.languageId === 'jade' || document.languageId === 'html';
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from '@volar/language-service';
import { forEachElementNode, hyphenateTag, VueVirtualCode } from '@vue/language-core';
import { forEachElementNode, hyphenateTag } from '@vue/language-core';
import type * as ts from 'typescript';
import { URI } from 'vscode-uri';
import { getEmbeddedInfo } from './utils';

export function create(
getTsPluginClient?: (
Expand All @@ -23,64 +23,32 @@ export function create(

return {
async provideDocumentSemanticTokens(document, range, legend) {
const uri = URI.parse(document.uri);
const decoded = context.decodeEmbeddedDocumentUri(uri);
const sourceScript = decoded && context.language.scripts.get(decoded[0]);
const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]);
if (!sourceScript?.generated || virtualCode?.id !== 'template') {
return;
}

const root = sourceScript.generated.root;
if (!(root instanceof VueVirtualCode)) {
const info = getEmbeddedInfo(context, document, 'template');
if (!info) {
return;
}
const { root } = info;

const { template } = root.sfc;
if (!template) {
if (!template?.ast) {
return;
}

const result: SemanticToken[] = [];

const tokenTypes = legend.tokenTypes.indexOf('component');
const componentSpans = await getComponentSpans(root.fileName, template, {
start: document.offsetAt(range.start),
length: document.offsetAt(range.end) - document.offsetAt(range.start),
});

for (const span of componentSpans) {
const position = document.positionAt(span.start);
result.push([
position.line,
position.character,
span.length,
tokenTypes,
0,
]);
}
return result;
},
};
const componentSpans: ts.TextSpan[] = [];
const start = document.offsetAt(range.start);
const end = document.offsetAt(range.end);

async function getComponentSpans(
fileName: string,
template: NonNullable<VueVirtualCode['_sfc']['template']>,
spanTemplateRange: ts.TextSpan,
) {
const result: ts.TextSpan[] = [];
const validComponentNames = await tsPluginClient?.getComponentNames(fileName) ?? [];
const elements = new Set(await tsPluginClient?.getElementNames(fileName) ?? []);
const components = new Set([
...validComponentNames,
...validComponentNames.map(hyphenateTag),
]);
const validComponentNames = await tsPluginClient?.getComponentNames(root.fileName) ?? [];
const elements = new Set(await tsPluginClient?.getElementNames(root.fileName) ?? []);
const components = new Set([
...validComponentNames,
...validComponentNames.map(hyphenateTag),
]);

if (template.ast) {
for (const node of forEachElementNode(template.ast)) {
if (
node.loc.end.offset <= spanTemplateRange.start
|| node.loc.start.offset >= (spanTemplateRange.start + spanTemplateRange.length)
node.loc.end.offset <= start
|| node.loc.start.offset >= end
) {
continue;
}
Expand All @@ -89,21 +57,36 @@ export function create(
if (template.lang === 'html') {
start += '<'.length;
}
result.push({
componentSpans.push({
start,
length: node.tag.length,
});
if (template.lang === 'html' && !node.isSelfClosing) {
result.push({
componentSpans.push({
start: node.loc.start.offset + node.loc.source.lastIndexOf(node.tag),
length: node.tag.length,
});
}
}
}
}
return result;
}

const result: SemanticToken[] = [];
const tokenType = legend.tokenTypes.indexOf('component');

for (const span of componentSpans) {
const position = document.positionAt(span.start);
result.push([
position.line,
position.character,
span.length,
tokenType,
0,
]);
}

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