diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index 0df678bd71..68b2102959 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -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'] }); @@ -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; } diff --git a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts index 298a3c67e7..11600eb026 100644 --- a/packages/language-service/lib/plugins/typescript-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/typescript-semantic-tokens.ts @@ -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?: ( @@ -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); diff --git a/packages/language-service/lib/plugins/utils.ts b/packages/language-service/lib/plugins/utils.ts index 0abc86981f..60ddd36d42 100644 --- a/packages/language-service/lib/plugins/utils.ts +++ b/packages/language-service/lib/plugins/utils.ts @@ -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>, + virtualCode, + root, + }; } diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index da79c6c3cd..44484695bd 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -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'), @@ -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; } @@ -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); diff --git a/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts b/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts index c62d2f783c..4721d69d32 100644 --- a/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts +++ b/packages/language-service/lib/plugins/vue-compiler-dom-errors.ts @@ -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 { @@ -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['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'; - } } diff --git a/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts b/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts index d4110da8d2..b62c927110 100644 --- a/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts +++ b/packages/language-service/lib/plugins/vue-component-semantic-tokens.ts @@ -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?: ( @@ -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, - 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; } @@ -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; + }, + }; }, }; } diff --git a/packages/language-service/lib/plugins/vue-document-drop.ts b/packages/language-service/lib/plugins/vue-document-drop.ts index caf2c3abfe..f80be41aa3 100644 --- a/packages/language-service/lib/plugins/vue-document-drop.ts +++ b/packages/language-service/lib/plugins/vue-document-drop.ts @@ -4,13 +4,14 @@ import type { LanguageServicePlugin, WorkspaceEdit, } from '@volar/language-service'; -import { forEachEmbeddedCode, VueVirtualCode } from '@vue/language-core'; +import { forEachEmbeddedCode } from '@vue/language-core'; import { camelize, capitalize, hyphenate } from '@vue/shared'; import { posix as path } from 'path-browserify'; import { getUserPreferences } from 'volar-service-typescript/lib/configs/getUserPreferences'; import { URI } from 'vscode-uri'; import { checkCasing, TagNameCasing } from '../nameCasing'; import { createAddComponentToOptionEdit, getLastImportNode } from '../plugins/vue-extract-file'; +import { getEmbeddedInfo } from './utils'; export function create( ts: typeof import('typescript'), @@ -28,21 +29,11 @@ export function create( return { async provideDocumentDropEdits(document, _position, dataTransfer) { - if (document.languageId !== 'html') { - return; - } - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'template', 'html'); + if (!info) { return; } + const { sourceScript, root } = info; let importUri: string | undefined; for (const [mimeType, item] of dataTransfer) { @@ -60,7 +51,7 @@ export function create( return; } - const casing = await checkCasing(context, decoded![0]); + const casing = await checkCasing(context, sourceScript.id); const baseName = path.basename(importUri); const newName = capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.')))); @@ -73,7 +64,7 @@ export function create( let importPath: string | undefined; - const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root); + const serviceScript = sourceScript.generated.languagePlugin.typescript?.getServiceScript(root); if (tsPluginClient && serviceScript) { const tsDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, serviceScript.code.id); const tsDocument = context.documents.get( diff --git a/packages/language-service/lib/plugins/vue-document-highlights.ts b/packages/language-service/lib/plugins/vue-document-highlights.ts index cae6000347..972519c4bd 100644 --- a/packages/language-service/lib/plugins/vue-document-highlights.ts +++ b/packages/language-service/lib/plugins/vue-document-highlights.ts @@ -1,7 +1,6 @@ import type { DocumentHighlightKind, LanguageServicePlugin } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create( getDocumentHighlights: (fileName: string, position: number) => Promise, @@ -14,18 +13,11 @@ export function create( create(context) { return { async provideDocumentHighlights(document, 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 !== 'main') { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'main'); + if (!info) { return; } + const { root } = info; const result = await getDocumentHighlights(root.fileName, document.offsetAt(position)); diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index fcac7a576c..229281d074 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -6,9 +6,10 @@ import type { TextEdit, } from '@volar/language-service'; import type { ExpressionNode, TemplateChildNode } from '@vue/compiler-dom'; -import { type Sfc, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { type Sfc, tsCodegen } from '@vue/language-core'; import type * as ts from 'typescript'; import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; interface ActionData { uri: string; @@ -46,18 +47,11 @@ 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?.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 { sfc } = root; const script = sfc.scriptSetup ?? sfc.script; @@ -87,18 +81,11 @@ export function create( const { uri, range, newName } = codeAction.data as ActionData; const [startOffset, endOffset]: [number, number] = range; - const parsedUri = URI.parse(uri); - const decoded = context.decodeEmbeddedDocumentUri(parsedUri); - 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 codeAction; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, { uri } as any, 'template'); + if (!info) { return codeAction; } + const { sourceScript, virtualCode, root } = info; const { sfc } = root; const script = sfc.scriptSetup ?? sfc.script; @@ -121,7 +108,7 @@ export function create( const scriptInitialIndent = await context.env.getConfiguration!('vue.format.script.initialIndent') ?? false; - const document = context.documents.get(parsedUri, virtualCode.languageId, virtualCode.snapshot); + const document = context.documents.get(URI.parse(uri), virtualCode.languageId, virtualCode.snapshot); const sfcDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); const newUri = sfcDocument.uri.slice(0, sfcDocument.uri.lastIndexOf('/') + 1) + `${newName}.vue`; const lastImportNode = getLastImportNode(ts, script.ast); diff --git a/packages/language-service/lib/plugins/vue-global-types-error.ts b/packages/language-service/lib/plugins/vue-global-types-error.ts index 9248fb1d9d..7bf78db634 100644 --- a/packages/language-service/lib/plugins/vue-global-types-error.ts +++ b/packages/language-service/lib/plugins/vue-global-types-error.ts @@ -1,6 +1,5 @@ import type { DiagnosticSeverity, LanguageServicePlugin } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -14,21 +13,11 @@ export function create(): LanguageServicePlugin { create(context) { return { provideDiagnostics(document) { - if (document.languageId !== 'vue-root-tags') { - return; - } - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { return; } + const { root } = info; const { vueCompilerOptions } = root; const globalTypesPath = vueCompilerOptions.globalTypesPath(root.fileName); diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 24ce557efd..02f5f4bf73 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -1,7 +1,7 @@ import type { InlayHint, InlayHintKind, LanguageServicePlugin } from '@volar/language-service'; -import { collectBindingIdentifiers, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { collectBindingIdentifiers, tsCodegen } from '@vue/language-core'; import type * as ts from 'typescript'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; export function create(ts: typeof import('typescript')): LanguageServicePlugin { return { @@ -12,18 +12,11 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { create(context) { return { async provideInlayHints(document, range) { - 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 settings: Record = {}; async function getSettingEnabled(key: string) { diff --git a/packages/language-service/lib/plugins/vue-missing-props-hints.ts b/packages/language-service/lib/plugins/vue-missing-props-hints.ts index 39f10f3ebb..78bff9182a 100644 --- a/packages/language-service/lib/plugins/vue-missing-props-hints.ts +++ b/packages/language-service/lib/plugins/vue-missing-props-hints.ts @@ -5,10 +5,10 @@ import type { LanguageServicePlugin, TextDocument, } from '@volar/language-service'; -import { hyphenateAttr, hyphenateTag, VueVirtualCode } from '@vue/language-core'; +import { hyphenateAttr, hyphenateTag } from '@vue/language-core'; import * as html from 'vscode-html-languageservice'; -import { URI } from 'vscode-uri'; import { AttrNameCasing, checkCasing } from '../nameCasing'; +import { getEmbeddedInfo } from './utils'; export function create( getTsPluginClient?: ( @@ -26,34 +26,24 @@ export function create( return { async provideInlayHints(document, range, cancellationToken) { - if (!isSupportedDocument(document)) { + const info = getEmbeddedInfo(context, document, 'template'); + if (!info) { return; } + const { sourceScript, root } = info; const enabled = await context.env.getConfiguration?.('vue.inlayHints.missingProps') ?? false; if (!enabled) { return; } - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { - return; - } - const scanner = getScanner(context, document); if (!scanner) { return; } const result: InlayHint[] = []; - const casing = await checkCasing(context, decoded![0]); + const casing = await checkCasing(context, sourceScript.id); const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; const componentProps: Record = {}; @@ -179,8 +169,4 @@ export function create( } } } - - function isSupportedDocument(document: TextDocument) { - return document.languageId === 'jade' || document.languageId === 'html'; - } } diff --git a/packages/language-service/lib/plugins/vue-scoped-class-links.ts b/packages/language-service/lib/plugins/vue-scoped-class-links.ts index 669d8c531a..d02d655df1 100644 --- a/packages/language-service/lib/plugins/vue-scoped-class-links.ts +++ b/packages/language-service/lib/plugins/vue-scoped-class-links.ts @@ -1,6 +1,6 @@ import type { LanguageServicePlugin } from '@volar/language-service'; -import { tsCodegen, VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import { tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -11,18 +11,11 @@ export function create(): LanguageServicePlugin { create(context) { return { provideDocumentLinks(document) { - 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 { sourceScript, root } = info; const { sfc } = root; const codegen = tsCodegen.get(sfc); @@ -37,7 +30,7 @@ export function create(): LanguageServicePlugin { continue; } - const styleDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + i); + const styleDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, 'style_' + i); const styleVirtualCode = sourceScript.generated.embeddedCodes.get('style_' + i); if (!styleVirtualCode) { continue; diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index c902ce89b8..11584a39af 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -4,16 +4,14 @@ import type { Diagnostic, DiagnosticSeverity, DocumentSymbol, - LanguageServiceContext, LanguageServicePlugin, SymbolKind, - TextDocument, } from '@volar/language-service'; import { VueVirtualCode } from '@vue/language-core'; import { create as createHtmlService } from 'volar-service-html'; import * as html from 'vscode-html-languageservice'; -import { URI } from 'vscode-uri'; import { loadLanguageBlocks } from './data'; +import { getEmbeddedInfo } from './utils'; let sfcDataProvider: html.IHTMLDataProvider | undefined; @@ -26,27 +24,31 @@ export function create(): LanguageServicePlugin { return [sfcDataProvider]; }, async getFormattingOptions(document, options, context) { - return await worker(document, context, async root => { - const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; - const blockTypes = ['template', 'script', 'style']; + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return {}; + } + const { root } = info; + + const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; + const blockTypes = ['template', 'script', 'style']; - for (const customBlock of root.sfc.customBlocks) { - blockTypes.push(customBlock.type); - } + for (const customBlock of root.sfc.customBlocks) { + blockTypes.push(customBlock.type); + } - return { - ...options, - ...formatSettings, - wrapAttributes: await context.env.getConfiguration?.('vue.format.wrapAttributes') ?? 'auto', - unformatted: '', - contentUnformatted: blockTypes.join(','), - endWithNewline: options.insertFinalNewline - ? true - : options.trimFinalNewlines - ? false - : document.getText().endsWith('\n'), - }; - }) ?? {}; + return { + ...options, + ...formatSettings, + wrapAttributes: await context.env.getConfiguration?.('vue.format.wrapAttributes') ?? 'auto', + unformatted: '', + contentUnformatted: blockTypes.join(','), + endWithNewline: options.insertFinalNewline + ? true + : options.trimFinalNewlines + ? false + : document.getText().endsWith('\n'), + }; }, }); return { @@ -88,133 +90,141 @@ export function create(): LanguageServicePlugin { return options; }, - provideDiagnostics(document, token) { - return worker(document, context, async root => { - const { vueSfc, sfc } = root; - if (!vueSfc) { - return; - } + async provideDiagnostics(document, token) { + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return []; + } + const { root } = info; + + const { vueSfc, sfc } = root; + if (!vueSfc) { + return; + } - const originalResult = await htmlServiceInstance.provideDiagnostics?.(document, token); - const sfcErrors: Diagnostic[] = []; - const { template } = sfc; + const originalResult = await htmlServiceInstance.provideDiagnostics?.(document, token); + const sfcErrors: Diagnostic[] = []; + const { template } = sfc; - const { - startTagEnd = Infinity, - endTagStart = -Infinity, - } = template ?? {}; + const { + startTagEnd = Infinity, + endTagStart = -Infinity, + } = template ?? {}; - for (const error of vueSfc.errors) { - if ('code' in error) { - const start = error.loc?.start.offset ?? 0; - const end = error.loc?.end.offset ?? 0; - if (end < startTagEnd || start >= endTagStart) { - sfcErrors.push({ - range: { - start: document.positionAt(start), - end: document.positionAt(end), - }, - severity: 1 satisfies typeof DiagnosticSeverity.Error, - code: error.code, - source: 'vue', - message: error.message, - }); - } + for (const error of vueSfc.errors) { + if ('code' in error) { + const start = error.loc?.start.offset ?? 0; + const end = error.loc?.end.offset ?? 0; + if (end < startTagEnd || start >= endTagStart) { + sfcErrors.push({ + range: { + start: document.positionAt(start), + end: document.positionAt(end), + }, + severity: 1 satisfies typeof DiagnosticSeverity.Error, + code: error.code, + source: 'vue', + message: error.message, + }); } } + } - return [ - ...originalResult ?? [], - ...sfcErrors, - ]; - }); + return [ + ...originalResult ?? [], + ...sfcErrors, + ]; }, provideDocumentSymbols(document) { - return worker(document, context, root => { - const result: DocumentSymbol[] = []; - const { sfc } = root; + const info = getEmbeddedInfo(context, document, 'root_tags'); + if (!info) { + return; + } + const { root } = info; - if (sfc.template) { - result.push({ - name: 'template', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.template.start), - end: document.positionAt(sfc.template.end), - }, - selectionRange: { - start: document.positionAt(sfc.template.start), - end: document.positionAt(sfc.template.startTagEnd), - }, - }); - } - if (sfc.script) { - result.push({ - name: 'script', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.script.start), - end: document.positionAt(sfc.script.end), - }, - selectionRange: { - start: document.positionAt(sfc.script.start), - end: document.positionAt(sfc.script.startTagEnd), - }, - }); - } - if (sfc.scriptSetup) { - result.push({ - name: 'script setup', - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(sfc.scriptSetup.start), - end: document.positionAt(sfc.scriptSetup.end), - }, - selectionRange: { - start: document.positionAt(sfc.scriptSetup.start), - end: document.positionAt(sfc.scriptSetup.startTagEnd), - }, - }); - } - for (const style of sfc.styles) { - let name = 'style'; - if (style.scoped) { - name += ' scoped'; - } - if (style.module) { - name += ' module'; - } - result.push({ - name, - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(style.start), - end: document.positionAt(style.end), - }, - selectionRange: { - start: document.positionAt(style.start), - end: document.positionAt(style.startTagEnd), - }, - }); + const result: DocumentSymbol[] = []; + const { sfc } = root; + + if (sfc.template) { + result.push({ + name: 'template', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.end), + }, + selectionRange: { + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.startTagEnd), + }, + }); + } + if (sfc.script) { + result.push({ + name: 'script', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.end), + }, + selectionRange: { + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.startTagEnd), + }, + }); + } + if (sfc.scriptSetup) { + result.push({ + name: 'script setup', + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.end), + }, + selectionRange: { + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.startTagEnd), + }, + }); + } + for (const style of sfc.styles) { + let name = 'style'; + if (style.scoped) { + name += ' scoped'; } - for (const customBlock of sfc.customBlocks) { - result.push({ - name: `${customBlock.type}`, - kind: 2 satisfies typeof SymbolKind.Module, - range: { - start: document.positionAt(customBlock.start), - end: document.positionAt(customBlock.end), - }, - selectionRange: { - start: document.positionAt(customBlock.start), - end: document.positionAt(customBlock.startTagEnd), - }, - }); + if (style.module) { + name += ' module'; } + result.push({ + name, + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(style.start), + end: document.positionAt(style.end), + }, + selectionRange: { + start: document.positionAt(style.start), + end: document.positionAt(style.startTagEnd), + }, + }); + } + for (const customBlock of sfc.customBlocks) { + result.push({ + name: `${customBlock.type}`, + kind: 2 satisfies typeof SymbolKind.Module, + range: { + start: document.positionAt(customBlock.start), + end: document.positionAt(customBlock.end), + }, + selectionRange: { + start: document.positionAt(customBlock.start), + end: document.positionAt(customBlock.startTagEnd), + }, + }); + } - return result; - }); + return result; }, async provideCompletionItems(document, position, context, token) { @@ -300,19 +310,6 @@ export function create(): LanguageServicePlugin { }; }, }; - - function worker(document: TextDocument, context: LanguageServiceContext, callback: (root: VueVirtualCode) => T) { - if (document.languageId !== 'vue-root-tags') { - return; - } - 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) { - return callback(root); - } - } } function getStyleCompletionItem( diff --git a/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts b/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts index e3871ce2f7..e79b554bd5 100644 --- a/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts +++ b/packages/language-service/lib/plugins/vue-suggest-define-assignment.ts @@ -1,7 +1,6 @@ import type { CompletionItem, CompletionItemKind, LanguageServicePlugin } from '@volar/language-service'; -import { type TextRange, tsCodegen, VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; -import { isTsDocument } from './utils'; +import { type TextRange, tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -13,28 +12,17 @@ export function create(): LanguageServicePlugin { return { isAdditionalCompletion: true, async provideCompletionItems(document) { - if (!isTsDocument(document)) { + const info = getEmbeddedInfo(context, document, id => id.startsWith('script_')); + if (!info) { return; } + const { virtualCode, root } = info; const enabled = await context.env.getConfiguration?.('vue.suggest.defineAssignment') ?? true; if (!enabled) { 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 { sfc } = root; const codegen = tsCodegen.get(sfc); const scriptSetup = sfc.scriptSetup; diff --git a/packages/language-service/lib/plugins/vue-template-ref-links.ts b/packages/language-service/lib/plugins/vue-template-ref-links.ts index f2354ea43d..cd526386c3 100644 --- a/packages/language-service/lib/plugins/vue-template-ref-links.ts +++ b/packages/language-service/lib/plugins/vue-template-ref-links.ts @@ -1,6 +1,6 @@ import type { LanguageServicePlugin } from '@volar/language-service'; -import { tsCodegen, VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import { tsCodegen } from '@vue/language-core'; +import { getEmbeddedInfo } from './utils'; export function create(): LanguageServicePlugin { return { @@ -11,18 +11,11 @@ export function create(): LanguageServicePlugin { create(context) { return { provideDocumentLinks(document) { - 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 !== 'scriptsetup_raw') { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'scriptsetup_raw'); + if (!info) { return; } + const { sourceScript, root } = info; const { sfc } = root; const codegen = tsCodegen.get(sfc); @@ -35,7 +28,7 @@ export function create(): LanguageServicePlugin { if (!templateVirtualCode) { return; } - const templateDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'template'); + const templateDocumentUri = context.encodeEmbeddedDocumentUri(sourceScript.id, 'template'); const templateDocument = context.documents.get( templateDocumentUri, templateVirtualCode.languageId, diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index ee8350847d..36059f14fd 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -7,7 +7,7 @@ import type { LanguageServicePlugin, TextDocument, } from '@volar/language-service'; -import { hyphenateAttr, hyphenateTag, tsCodegen, VueVirtualCode } from '@vue/language-core'; +import { hyphenateAttr, hyphenateTag, tsCodegen, type VueVirtualCode } from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; import type { ComponentPropInfo } from '@vue/typescript-plugin/lib/requests/getComponentProps'; import { create as createHtmlService } from 'volar-service-html'; @@ -16,6 +16,7 @@ import * as html from 'vscode-html-languageservice'; import { URI, Utils } from 'vscode-uri'; import { AttrNameCasing, checkCasing, TagNameCasing } from '../nameCasing'; import { loadModelModifiersData, loadTemplateData } from './data'; +import { getEmbeddedInfo } from './utils'; const specialTags = new Set([ 'slot', @@ -75,6 +76,8 @@ export function create( }, onDidChangeCustomData, }); + const htmlDataProvider = html.getDefaultHTMLDataProvider(); + const languageId = mode === 'pug' ? 'jade' : 'html'; return { name: `vue-template (${mode})`, @@ -153,21 +156,11 @@ export function create( }, async provideCompletionItems(document, position, completionContext, token) { - if (!isSupportedDocument(document)) { - return; - } - - const uri = URI.parse(document.uri); - const decoded = context.decodeEmbeddedDocumentUri(uri); - const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (!sourceScript?.generated) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo(context, document, 'template', languageId); + if (!info) { return; } + const { sourceScript, root } = info; const { result: completionList, @@ -315,12 +308,15 @@ export function create( }, provideHover(document, position, token) { - if (!isSupportedDocument(document)) { + const info = getEmbeddedInfo(context, document, 'template', languageId); + if (!info) { return; } if (context.decodeEmbeddedDocumentUri(URI.parse(document.uri))) { - updateExtraCustomData([]); + updateExtraCustomData([ + htmlDataProvider, + ]); } return baseServiceInstance.provideHover?.(document, position, token); @@ -375,7 +371,6 @@ export function create( isGlobal?: boolean; info?: ComponentPropInfo; }>(); - const htmlDataProvider = html.getDefaultHTMLDataProvider(); updateExtraCustomData([ { @@ -722,15 +717,6 @@ export function create( extraCustomData = extraData; onDidChangeCustomDataListeners.forEach(l => l()); } - - function isSupportedDocument(document: TextDocument) { - if (mode === 'pug') { - return document.languageId === 'jade'; - } - else { - return document.languageId === 'html'; - } - } } function getReplacement(list: html.CompletionList, doc: TextDocument) { diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index 16e7de4983..7303174602 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,6 +1,5 @@ import type { InlayHint, LanguageServiceContext, LanguageServicePlugin, Position } from '@volar/language-service'; -import { VueVirtualCode } from '@vue/language-core'; -import { URI } from 'vscode-uri'; +import { getEmbeddedInfo } from './utils'; const twoslashTemplateReg = //g; const twoslashScriptReg = /(?<=^|\n)\s*\/\/\s*\^\?/g; @@ -19,21 +18,15 @@ export function create( const tsPluginClient = getTsPluginClient?.(context); return { async provideInlayHints(document, range) { - 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' && !virtualCode?.id.startsWith('script_')) - ) { - return; - } - - const root = sourceScript.generated.root; - if (!(root instanceof VueVirtualCode)) { + const info = getEmbeddedInfo( + context, + document, + id => id === 'template' || id.startsWith('script_'), + ); + if (!info) { return; } + const { sourceScript, virtualCode, root } = info; const hoverOffsets: [Position, number][] = []; const inlayHints: InlayHint[] = []; @@ -51,7 +44,7 @@ export function create( ]); } - const sourceDocument = context.documents.get(decoded![0], sourceScript.languageId, sourceScript.snapshot); + const sourceDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); for (const [pointerPosition, hoverOffset] of hoverOffsets) { const map = context.language.maps.get(virtualCode, sourceScript); for (const [sourceOffset] of map.toSourceLocation(hoverOffset)) { diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 7ddf482836..a098844a1c 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -15,7 +15,10 @@ import { getPropertiesAtLocation } from './lib/requests/getPropertiesAtLocation' import type { RequestContext } from './lib/requests/types'; const windowsPathReg = /\\/g; -const project2Service = new WeakMap(); +const project2Service = new WeakMap< + ts.server.Project, + [vue.Language, ts.LanguageServiceHost, ts.LanguageService] +>(); export = createLanguageServicePlugin( (ts, info) => {