From d6faebbde0cb61b5626b4347faaa7a0fd2d5b318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Sat, 21 Dec 2024 03:42:16 +0800 Subject: [PATCH] refactor(language-service): consistent style of source and virtual code operation (#5053) --- .../lib/ideFeatures/nameCasing.ts | 28 +-- .../lib/plugins/vue-autoinsert-dotvalue.ts | 3 +- .../plugins/vue-complete-define-assignment.ts | 9 +- .../lib/plugins/vue-document-drop.ts | 22 +- .../lib/plugins/vue-document-links.ts | 94 ++++---- .../lib/plugins/vue-extract-file.ts | 31 ++- .../lib/plugins/vue-inlayhints.ts | 127 +++++----- .../language-service/lib/plugins/vue-sfc.ts | 56 ++--- .../lib/plugins/vue-template.ts | 224 ++++++++++-------- .../lib/plugins/vue-twoslash-queries.ts | 14 +- packages/typescript-plugin/lib/common.ts | 9 +- .../lib/requests/collectExtractProps.ts | 15 +- 12 files changed, 347 insertions(+), 285 deletions(-) diff --git a/packages/language-service/lib/ideFeatures/nameCasing.ts b/packages/language-service/lib/ideFeatures/nameCasing.ts index 859301b100..c2040ba1f2 100644 --- a/packages/language-service/lib/ideFeatures/nameCasing.ts +++ b/packages/language-service/lib/ideFeatures/nameCasing.ts @@ -19,21 +19,20 @@ export async function convertTagName( return; } - const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueVirtualCode)) { + const root = sourceFile?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } - const desc = rootCode._sfc; - if (!desc.template) { + const { template } = root._sfc; + if (!template) { return; } - const template = desc.template; const document = context.documents.get(sourceFile.id, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await tsPluginClient?.getComponentNames(rootCode.fileName) ?? []; - const tags = getTemplateTagsAndAttrs(rootCode); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const tags = getTemplateTagsAndAttrs(root); for (const [tagName, { offsets }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); @@ -67,26 +66,25 @@ export async function convertAttrName( return; } - const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof VueVirtualCode)) { + const root = sourceFile?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } - const desc = rootCode._sfc; - if (!desc.template) { + const { template } = root._sfc; + if (!template) { return; } - const template = desc.template; const document = context.documents.get(uri, sourceFile.languageId, sourceFile.snapshot); const edits: vscode.TextEdit[] = []; - const components = await tsPluginClient?.getComponentNames(rootCode.fileName) ?? []; - const tags = getTemplateTagsAndAttrs(rootCode); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const tags = getTemplateTagsAndAttrs(root); for (const [tagName, { attrs }] of tags) { const componentName = components.find(component => component === tagName || hyphenateTag(component) === tagName); if (componentName) { - const props = (await tsPluginClient?.getComponentProps(rootCode.fileName, componentName) ?? []).map(prop => prop.name); + const props = (await tsPluginClient?.getComponentProps(root.fileName, componentName) ?? []).map(prop => prop.name); for (const [attrName, { offsets }] of attrs) { const propName = props.find(prop => prop === attrName || hyphenateAttr(prop) === attrName); if (propName) { diff --git a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts index 68143c99f9..6d0265545c 100644 --- a/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts +++ b/packages/language-service/lib/plugins/vue-autoinsert-dotvalue.ts @@ -57,7 +57,8 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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) { diff --git a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts index 139abab53a..8e2e98129d 100644 --- a/packages/language-service/lib/plugins/vue-complete-define-assignment.ts +++ b/packages/language-service/lib/plugins/vue-complete-define-assignment.ts @@ -23,15 +23,15 @@ export function create(): LanguageServicePlugin { return; } - const result: vscode.CompletionItem[] = []; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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 || !virtualCode) { + if (!sourceScript?.generated || !virtualCode) { return; } - const root = sourceScript?.generated?.root; + const root = sourceScript.generated.root; if (!(root instanceof VueVirtualCode)) { return; } @@ -43,6 +43,7 @@ export function create(): LanguageServicePlugin { return; } + const result: vscode.CompletionItem[] = []; const mappings = [...context.language.maps.forEach(virtualCode)]; addDefineCompletionItem( diff --git a/packages/language-service/lib/plugins/vue-document-drop.ts b/packages/language-service/lib/plugins/vue-document-drop.ts index 44ad8b6cee..79fbb2d67b 100644 --- a/packages/language-service/lib/plugins/vue-document-drop.ts +++ b/packages/language-service/lib/plugins/vue-document-drop.ts @@ -33,11 +33,15 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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]); - const vueVirtualCode = sourceScript?.generated?.root; - if (!sourceScript || !virtualCode || !(vueVirtualCode instanceof VueVirtualCode)) { + if (!sourceScript?.generated) { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -55,7 +59,7 @@ export function create( baseName = baseName.slice(0, baseName.lastIndexOf('.')); const newName = capitalize(camelize(baseName)); - const { _sfc: sfc } = vueVirtualCode; + const sfc = root._sfc; const script = sfc.scriptSetup ?? sfc.script; if (!script) { @@ -63,26 +67,26 @@ export function create( } const additionalEdit: vscode.WorkspaceEdit = {}; - const code = [...forEachEmbeddedCode(vueVirtualCode)].find(code => code.id === (sfc.scriptSetup ? 'scriptsetup_raw' : 'script_raw'))!; + const code = [...forEachEmbeddedCode(root)].find(code => code.id === (sfc.scriptSetup ? 'scriptsetup_raw' : 'script_raw'))!; const lastImportNode = getLastImportNode(ts, script.ast); const incomingFileName = context.project.typescript?.uriConverter.asFileName(URI.parse(importUri)) ?? URI.parse(importUri).fsPath.replace(/\\/g, '/'); let importPath: string | undefined; - const serviceScript = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(vueVirtualCode); + 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(tsDocumentUri, serviceScript.code.languageId, serviceScript.code.snapshot); const preferences = await getUserPreferences(context, tsDocument); - const importPathRequest = await tsPluginClient.getImportPathForFile(vueVirtualCode.fileName, incomingFileName, preferences); + const importPathRequest = await tsPluginClient.getImportPathForFile(root.fileName, incomingFileName, preferences); if (importPathRequest) { importPath = importPathRequest; } } if (!importPath) { - importPath = path.relative(path.dirname(vueVirtualCode.fileName), incomingFileName) + importPath = path.relative(path.dirname(root.fileName), incomingFileName) || importUri.slice(importUri.lastIndexOf('/') + 1); if (!importPath.startsWith('./') && !importPath.startsWith('../')) { diff --git a/packages/language-service/lib/plugins/vue-document-links.ts b/packages/language-service/lib/plugins/vue-document-links.ts index dbb423e111..aae8265e30 100644 --- a/packages/language-service/lib/plugins/vue-document-links.ts +++ b/packages/language-service/lib/plugins/vue-document-links.ts @@ -13,63 +13,69 @@ export function create(): LanguageServicePlugin { return { provideDocumentLinks(document) { - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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; + } - if (sourceScript?.generated?.root instanceof VueVirtualCode && virtualCode?.id === 'template') { + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } - const result: vscode.DocumentLink[] = []; - const codegen = tsCodegen.get(sourceScript.generated.root._sfc); - const scopedClasses = codegen?.generatedTemplate.get()?.scopedClasses ?? []; - const styleClasses = new Map(); - const option = sourceScript.generated.root.vueCompilerOptions.experimentalResolveStyleCssClasses; + const result: vscode.DocumentLink[] = []; + const codegen = tsCodegen.get(root._sfc); + const scopedClasses = codegen?.generatedTemplate.get()?.scopedClasses ?? []; + const styleClasses = new Map(); + const option = 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.slice(1))) { - styleClasses.set(className.text.slice(1), []); - } - styleClasses.get(className.text.slice(1))!.push({ - index: i, - style, - classOffset: className.offset, - }); + for (let i = 0; i < root._sfc.styles.length; i++) { + const style = root._sfc.styles[i]; + if (option === 'always' || (option === 'scoped' && style.scoped)) { + for (const className of style.classNames) { + if (!styleClasses.has(className.text.slice(1))) { + styleClasses.set(className.text.slice(1), []); } + styleClasses.get(className.text.slice(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}`, - }); + 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; } + + return result; }, }; }, diff --git a/packages/language-service/lib/plugins/vue-extract-file.ts b/packages/language-service/lib/plugins/vue-extract-file.ts index de17245580..c580096e58 100644 --- a/packages/language-service/lib/plugins/vue-extract-file.ts +++ b/packages/language-service/lib/plugins/vue-extract-file.ts @@ -35,16 +35,21 @@ export function create( return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { return; } - const { _sfc: sfc } = sourceScript.generated.root; - const script = sfc.scriptSetup ?? sfc.script; + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + const sfc = root._sfc; + const script = sfc.scriptSetup ?? sfc.script; if (!sfc.template || !script) { return; } @@ -71,19 +76,22 @@ 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?.root instanceof VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { return codeAction; } - const document = context.documents.get(parsedUri, virtualCode.languageId, virtualCode.snapshot); - const sfcDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot); - const { _sfc: sfc } = sourceScript.generated.root; - const script = sfc.scriptSetup ?? sfc.script; + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return codeAction; + } + const sfc = root._sfc; + const script = sfc.scriptSetup ?? sfc.script; if (!sfc.template || !script) { return codeAction; } @@ -93,13 +101,16 @@ export function create( return codeAction; } - const toExtract = await tsPluginClient?.collectExtractProps(sourceScript.generated.root.fileName, templateCodeRange) ?? []; + const toExtract = await tsPluginClient?.collectExtractProps(root.fileName, templateCodeRange) ?? []; if (!toExtract) { return codeAction; } const templateInitialIndent = await context.env.getConfiguration!('vue.format.template.initialIndent') ?? true; const scriptInitialIndent = await context.env.getConfiguration!('vue.format.script.initialIndent') ?? false; + + const document = context.documents.get(parsedUri, 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-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index 0282eed999..0891adcb43 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -15,80 +15,89 @@ export function create(ts: typeof import('typescript')): LanguageServicePlugin { return { async provideInlayHints(document, range) { - const settings: Record = {}; - const result: vscode.InlayHint[] = []; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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 (!(virtualCode instanceof VueVirtualCode)) { + return; + } - if (virtualCode instanceof VueVirtualCode) { + const settings: Record = {}; + async function getSettingEnabled(key: string) { + return settings[key] ??= await context.env.getConfiguration?.(key) ?? false; + } - const codegen = tsCodegen.get(virtualCode._sfc); - const inlayHints = [ - ...codegen?.generatedTemplate.get()?.inlayHints ?? [], - ...codegen?.generatedScript.get()?.inlayHints ?? [], - ]; - const scriptSetupRanges = codegen?.scriptSetupRanges.get(); + const result: vscode.InlayHint[] = []; + + const codegen = tsCodegen.get(virtualCode._sfc); + const inlayHints = [ + ...codegen?.generatedTemplate.get()?.inlayHints ?? [], + ...codegen?.generatedScript.get()?.inlayHints ?? [], + ]; + const scriptSetupRanges = codegen?.scriptSetupRanges.get(); - if (scriptSetupRanges?.defineProps?.destructured && virtualCode._sfc.scriptSetup?.ast) { - const setting = 'vue.inlayHints.destructuredProps'; - settings[setting] ??= await context.env.getConfiguration?.(setting) ?? false; + if (scriptSetupRanges?.defineProps?.destructured && virtualCode._sfc.scriptSetup?.ast) { + const setting = 'vue.inlayHints.destructuredProps'; + const enabled = await getSettingEnabled(setting); - if (settings[setting]) { - for (const [prop, isShorthand] of findDestructuredProps( + if (enabled) { + for (const [prop, isShorthand] of findDestructuredProps( ts, virtualCode._sfc.scriptSetup.ast, scriptSetupRanges.defineProps.destructured )) { - const name = prop.text; - const end = prop.getEnd(); - const pos = isShorthand ? end : end - name.length; - const label = isShorthand ? `: props.${name}` : 'props.'; - inlayHints.push({ - blockName: 'scriptSetup', - offset: pos, - setting, - label, - }); - } + const name = prop.text; + const end = prop.getEnd(); + const pos = isShorthand ? end : end - name.length; + const label = isShorthand ? `: props.${name}` : 'props.'; + inlayHints.push({ + blockName: 'scriptSetup', + offset: pos, + setting, + label, + }); } } + } - const blocks = [ - virtualCode._sfc.template, - virtualCode._sfc.script, - virtualCode._sfc.scriptSetup, - ]; - const start = document.offsetAt(range.start); - const end = document.offsetAt(range.end); - - for (const hint of inlayHints) { - - const block = blocks.find(block => block?.name === hint.blockName); - const hintOffset = (block?.startTagEnd ?? 0) + hint.offset; - - if (hintOffset >= start && hintOffset <= end) { - - settings[hint.setting] ??= await context.env.getConfiguration?.(hint.setting) ?? false; - - if (!settings[hint.setting]) { - continue; - } - - result.push({ - label: hint.label, - paddingRight: hint.paddingRight, - paddingLeft: hint.paddingLeft, - position: document.positionAt(hintOffset), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - tooltip: hint.tooltip ? { - kind: 'markdown', - value: hint.tooltip, - } : undefined, - }); - } + const blocks = [ + virtualCode._sfc.template, + virtualCode._sfc.script, + virtualCode._sfc.scriptSetup, + ]; + const start = document.offsetAt(range.start); + const end = document.offsetAt(range.end); + + for (const hint of inlayHints) { + const block = blocks.find(block => block?.name === hint.blockName); + if (!block) { + continue; + } + + const hintOffset = block.startTagEnd + hint.offset; + if (hintOffset < start || hintOffset >= end) { + continue; } + + const enabled = await getSettingEnabled(hint.setting); + if (!enabled) { + continue; + } + + result.push({ + label: hint.label, + paddingRight: hint.paddingRight, + paddingLeft: hint.paddingLeft, + position: document.positionAt(hintOffset), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + tooltip: hint.tooltip ? { + kind: 'markdown', + value: hint.tooltip, + } : undefined, + }); } + return result; }, }; diff --git a/packages/language-service/lib/plugins/vue-sfc.ts b/packages/language-service/lib/plugins/vue-sfc.ts index 849f59bf55..a94ded1900 100644 --- a/packages/language-service/lib/plugins/vue-sfc.ts +++ b/packages/language-service/lib/plugins/vue-sfc.ts @@ -1,5 +1,5 @@ import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service'; -import * as vue from '@vue/language-core'; +import { VueVirtualCode } from '@vue/language-core'; import { create as createHtmlService } from 'volar-service-html'; import * as html from 'vscode-html-languageservice'; import type * as vscode from 'vscode-languageserver-protocol'; @@ -18,12 +18,12 @@ export function create(): LanguageServicePlugin { return [sfcDataProvider]; }, async getFormattingOptions(document, options, context) { - return await worker(document, context, async vueCode => { + return await worker(document, context, async root => { const formatSettings = await context.env.getConfiguration?.('html.format') ?? {}; const blockTypes = ['template', 'script', 'style']; - for (const customBlock of vueCode._sfc.customBlocks) { + for (const customBlock of root._sfc.customBlocks) { blockTypes.push(customBlock.type); } @@ -53,7 +53,7 @@ export function create(): LanguageServicePlugin { provideDocumentLinks: undefined, async resolveEmbeddedCodeFormattingOptions(sourceScript, virtualCode, options) { - if (sourceScript.generated?.root instanceof vue.VueVirtualCode) { + if (sourceScript.generated?.root instanceof VueVirtualCode) { if (virtualCode.id === 'script_raw' || virtualCode.id === 'scriptsetup_raw') { if (await context.env.getConfiguration?.('vue.format.script.initialIndent') ?? false) { options.initialIndentLevel++; @@ -74,54 +74,54 @@ export function create(): LanguageServicePlugin { }, provideDocumentSymbols(document) { - return worker(document, context, vueSourceFile => { + return worker(document, context, root => { const result: vscode.DocumentSymbol[] = []; - const descriptor = vueSourceFile._sfc; + const sfc = root._sfc; - if (descriptor.template) { + if (sfc.template) { result.push({ name: 'template', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.template.start), - end: document.positionAt(descriptor.template.end), + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.end), }, selectionRange: { - start: document.positionAt(descriptor.template.start), - end: document.positionAt(descriptor.template.startTagEnd), + start: document.positionAt(sfc.template.start), + end: document.positionAt(sfc.template.startTagEnd), }, }); } - if (descriptor.script) { + if (sfc.script) { result.push({ name: 'script', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.script.start), - end: document.positionAt(descriptor.script.end), + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.end), }, selectionRange: { - start: document.positionAt(descriptor.script.start), - end: document.positionAt(descriptor.script.startTagEnd), + start: document.positionAt(sfc.script.start), + end: document.positionAt(sfc.script.startTagEnd), }, }); } - if (descriptor.scriptSetup) { + if (sfc.scriptSetup) { result.push({ name: 'script setup', kind: 2 satisfies typeof vscode.SymbolKind.Module, range: { - start: document.positionAt(descriptor.scriptSetup.start), - end: document.positionAt(descriptor.scriptSetup.end), + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.end), }, selectionRange: { - start: document.positionAt(descriptor.scriptSetup.start), - end: document.positionAt(descriptor.scriptSetup.startTagEnd), + start: document.positionAt(sfc.scriptSetup.start), + end: document.positionAt(sfc.scriptSetup.startTagEnd), }, }); } - for (const style of descriptor.styles) { + for (const style of sfc.styles) { let name = 'style'; if (style.scoped) { name += ' scoped'; @@ -142,7 +142,7 @@ export function create(): LanguageServicePlugin { }, }); } - for (const customBlock of descriptor.customBlocks) { + for (const customBlock of sfc.customBlocks) { result.push({ name: `${customBlock.type}`, kind: 2 satisfies typeof vscode.SymbolKind.Module, @@ -237,14 +237,16 @@ export function create(): LanguageServicePlugin { }, }; - function worker(document: TextDocument, context: LanguageServiceContext, callback: (vueSourceFile: vue.VueVirtualCode) => T) { + function worker(document: TextDocument, context: LanguageServiceContext, callback: (root: VueVirtualCode) => T) { if (document.languageId !== 'vue-root-tags') { return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (sourceScript?.generated?.root instanceof vue.VueVirtualCode) { - return callback(sourceScript.generated.root); + const root = sourceScript?.generated?.root; + if (root instanceof VueVirtualCode) { + return callback(root); } } } diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index d76c55d2ef..e2c8320dc3 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -162,14 +162,17 @@ export function create( let sync: (() => Promise) | undefined; let currentVersion: number | undefined; - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if (sourceScript?.generated?.root instanceof VueVirtualCode) { + const root = sourceScript?.generated?.root; + + if (root instanceof VueVirtualCode) { // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver baseServiceInstance.provideCompletionItems?.(document, position, completionContext, token); - sync = (await provideHtmlData(vueCompilerOptions, sourceScript.id, sourceScript.generated.root)).sync; + sync = (await provideHtmlData(vueCompilerOptions, sourceScript!.id, root)).sync; currentVersion = await sync(); } @@ -211,7 +214,6 @@ export function create( return; } - const result: vscode.InlayHint[] = []; const uri = URI.parse(document.uri); const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); @@ -220,101 +222,108 @@ export function create( return; } - const code = context.language.scripts.get(decoded[0])?.generated?.root; + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { + return; + } + const scanner = getScanner(baseServiceInstance, document); + if (!scanner) { + return; + } - if (code instanceof VueVirtualCode && scanner) { - - // visualize missing required props - const casing = await getNameCasing(context, decoded[0]); - const components = await tsPluginClient?.getComponentNames(code.fileName) ?? []; - const componentProps: Record = {}; - let token: html.TokenType; - let current: { - unburnedRequiredProps: string[]; - labelOffset: number; - insertOffset: number; - } | undefined; - while ((token = scanner.scan()) !== html.TokenType.EOS) { - if (token === html.TokenType.StartTag) { - const tagName = scanner.getTokenText(); - const checkTag = tagName.includes('.') - ? tagName - : components.find(component => component === tagName || hyphenateTag(component) === tagName); - if (checkTag) { - componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(code.fileName, checkTag) ?? []) - .filter(prop => prop.required) - .map(prop => prop.name); - current = { - unburnedRequiredProps: [...componentProps[checkTag]], - labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), - insertOffset: scanner.getTokenOffset() + scanner.getTokenLength(), - }; - } + const result: vscode.InlayHint[] = []; + + // visualize missing required props + const casing = await getNameCasing(context, decoded[0]); + const components = await tsPluginClient?.getComponentNames(root.fileName) ?? []; + const componentProps: Record = {}; + let token: html.TokenType; + let current: { + unburnedRequiredProps: string[]; + labelOffset: number; + insertOffset: number; + } | undefined; + + while ((token = scanner.scan()) !== html.TokenType.EOS) { + if (token === html.TokenType.StartTag) { + const tagName = scanner.getTokenText(); + const checkTag = tagName.includes('.') + ? tagName + : components.find(component => component === tagName || hyphenateTag(component) === tagName); + if (checkTag) { + componentProps[checkTag] ??= (await tsPluginClient?.getComponentProps(root.fileName, checkTag) ?? []) + .filter(prop => prop.required) + .map(prop => prop.name); + current = { + unburnedRequiredProps: [...componentProps[checkTag]], + labelOffset: scanner.getTokenOffset() + scanner.getTokenLength(), + insertOffset: scanner.getTokenOffset() + scanner.getTokenLength(), + }; } - else if (token === html.TokenType.AttributeName) { - if (current) { - let attrText = scanner.getTokenText(); + } + else if (token === html.TokenType.AttributeName) { + if (current) { + let attrText = scanner.getTokenText(); - if (attrText === 'v-bind') { - current.unburnedRequiredProps = []; + if (attrText === 'v-bind') { + current.unburnedRequiredProps = []; + } + else { + // remove modifiers + if (attrText.includes('.')) { + attrText = attrText.split('.')[0]; } - else { - // remove modifiers - if (attrText.includes('.')) { - attrText = attrText.split('.')[0]; - } - // normalize - if (attrText.startsWith('v-bind:')) { - attrText = attrText.slice('v-bind:'.length); - } - else if (attrText.startsWith(':')) { - attrText = attrText.slice(':'.length); - } - else if (attrText.startsWith('v-model:')) { - attrText = attrText.slice('v-model:'.length); - } - else if (attrText === 'v-model') { - attrText = vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? - } - else if (attrText.startsWith('v-on:')) { - attrText = 'on-' + hyphenateAttr(attrText.slice('v-on:'.length)); - } - else if (attrText.startsWith('@')) { - attrText = 'on-' + hyphenateAttr(attrText.slice('@'.length)); - } - - current.unburnedRequiredProps = current.unburnedRequiredProps.filter(propName => { - return attrText !== propName - && attrText !== hyphenateAttr(propName); - }); + // normalize + if (attrText.startsWith('v-bind:')) { + attrText = attrText.slice('v-bind:'.length); } - } - } - else if (token === html.TokenType.StartTagSelfClose || token === html.TokenType.StartTagClose) { - if (current) { - for (const requiredProp of current.unburnedRequiredProps) { - result.push({ - label: `${requiredProp}!`, - paddingLeft: true, - position: document.positionAt(current.labelOffset), - kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, - textEdits: [{ - range: { - start: document.positionAt(current.insertOffset), - end: document.positionAt(current.insertOffset), - }, - newText: ` :${casing.attr === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp}=`, - }], - }); + else if (attrText.startsWith(':')) { + attrText = attrText.slice(':'.length); + } + else if (attrText.startsWith('v-model:')) { + attrText = attrText.slice('v-model:'.length); } - current = undefined; + else if (attrText === 'v-model') { + attrText = vueCompilerOptions.target >= 3 ? 'modelValue' : 'value'; // TODO: support for experimentalModelPropName? + } + else if (attrText.startsWith('v-on:')) { + attrText = 'on-' + hyphenateAttr(attrText.slice('v-on:'.length)); + } + else if (attrText.startsWith('@')) { + attrText = 'on-' + hyphenateAttr(attrText.slice('@'.length)); + } + + current.unburnedRequiredProps = current.unburnedRequiredProps.filter(propName => { + return attrText !== propName + && attrText !== hyphenateAttr(propName); + }); } } - if (token === html.TokenType.AttributeName || token === html.TokenType.AttributeValue) { - if (current) { - current.insertOffset = scanner.getTokenOffset() + scanner.getTokenLength(); + } + else if (token === html.TokenType.StartTagSelfClose || token === html.TokenType.StartTagClose) { + if (current) { + for (const requiredProp of current.unburnedRequiredProps) { + result.push({ + label: `${requiredProp}!`, + paddingLeft: true, + position: document.positionAt(current.labelOffset), + kind: 2 satisfies typeof vscode.InlayHintKind.Parameter, + textEdits: [{ + range: { + start: document.positionAt(current.insertOffset), + end: document.positionAt(current.insertOffset), + }, + newText: ` :${casing.attr === AttrNameCasing.Kebab ? hyphenateAttr(requiredProp) : requiredProp}=`, + }], + }); } + current = undefined; + } + } + if (token === html.TokenType.AttributeName || token === html.TokenType.AttributeValue) { + if (current) { + current.insertOffset = scanner.getTokenOffset() + scanner.getTokenLength(); } } } @@ -341,7 +350,6 @@ export function create( return; } - const originalResult = await baseServiceInstance.provideDiagnostics?.(document, token); const uri = URI.parse(document.uri); const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); @@ -350,13 +358,14 @@ export function create( return; } - const code = context.language.scripts.get(decoded[0])?.generated?.root; - if (!(code instanceof VueVirtualCode)) { + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { return; } + const originalResult = await baseServiceInstance.provideDiagnostics?.(document, token); const templateErrors: vscode.Diagnostic[] = []; - const { template } = code._sfc; + const { template } = root._sfc; if (template) { @@ -396,27 +405,34 @@ export function create( }, provideDocumentSemanticTokens(document, range, legend) { + if (!isSupportedDocument(document)) { return; } + if (!context.project.vue) { return; } const vueCompilerOptions = context.project.vue.compilerOptions; + const languageService = context.inject<(import('volar-service-typescript').Provide), 'typescript/languageService'>('typescript/languageService'); if (!languageService) { return; } - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); const sourceScript = decoded && context.language.scripts.get(decoded[0]); - if ( - !sourceScript - || !(sourceScript.generated?.root instanceof VueVirtualCode) - || !sourceScript.generated.root._sfc.template - ) { - return []; + const root = sourceScript?.generated?.root; + if (!(root instanceof VueVirtualCode)) { + return; } - const { template } = sourceScript.generated.root._sfc; + + const { template } = root._sfc; + if (!template) { + return; + } + const spans = getComponentSpans.call( { files: context.language.scripts, @@ -424,13 +440,15 @@ export function create( typescript: ts, vueOptions: vueCompilerOptions, }, - sourceScript.generated.root, + root, template, { start: document.offsetAt(range.start), length: document.offsetAt(range.end) - document.offsetAt(range.start), - }); + } + ); const classTokenIndex = legend.tokenTypes.indexOf('class'); + return spans.map(span => { const start = document.positionAt(span.start); return [ diff --git a/packages/language-service/lib/plugins/vue-twoslash-queries.ts b/packages/language-service/lib/plugins/vue-twoslash-queries.ts index e35b89c243..c011f3cbfb 100644 --- a/packages/language-service/lib/plugins/vue-twoslash-queries.ts +++ b/packages/language-service/lib/plugins/vue-twoslash-queries.ts @@ -1,5 +1,5 @@ import type { LanguageServiceContext, LanguageServicePlugin } from '@volar/language-service'; -import * as vue from '@vue/language-core'; +import { VueVirtualCode } from '@vue/language-core'; import type * as vscode from 'vscode-languageserver-protocol'; import { URI } from 'vscode-uri'; @@ -18,10 +18,16 @@ export function create( return { async provideInlayHints(document, range) { - const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + 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?.root instanceof vue.VueVirtualCode) || virtualCode?.id !== 'template') { + if (!sourceScript?.generated || virtualCode?.id !== 'template') { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -40,7 +46,7 @@ export function create( for (const [pointerPosition, hoverOffset] of hoverOffsets) { const map = context.language.maps.get(virtualCode, sourceScript); for (const [sourceOffset] of map.toSourceLocation(hoverOffset)) { - const quickInfo = await tsPluginClient?.getQuickInfoAtPosition(sourceScript.generated.root.fileName, sourceOffset); + const quickInfo = await tsPluginClient?.getQuickInfoAtPosition(root.fileName, sourceOffset); if (quickInfo) { inlayHints.push({ position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index c83453c349..152bf8fa00 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -194,13 +194,14 @@ function getEncodedSemanticClassifications( return (filePath, span, format) => { const fileName = filePath.replace(windowsPathReg, '/'); const result = getEncodedSemanticClassifications(fileName, span, format); - const file = language.scripts.get(asScriptId(fileName)); - if (file?.generated?.root instanceof VueVirtualCode) { - const { template } = file.generated.root._sfc; + const sourceScript = language.scripts.get(asScriptId(fileName)); + const root = sourceScript?.generated?.root; + if (root instanceof VueVirtualCode) { + const { template } = root._sfc; if (template) { for (const componentSpan of getComponentSpans.call( { typescript: ts, languageService }, - file.generated.root, + root, template, { start: span.start - template.startTagEnd, diff --git a/packages/typescript-plugin/lib/requests/collectExtractProps.ts b/packages/typescript-plugin/lib/requests/collectExtractProps.ts index a3e203d3e9..0c4ee82265 100644 --- a/packages/typescript-plugin/lib/requests/collectExtractProps.ts +++ b/packages/typescript-plugin/lib/requests/collectExtractProps.ts @@ -8,8 +8,13 @@ export function collectExtractProps( ) { const { typescript: ts, languageService, language, isTsPlugin, getFileId } = this; - const volarFile = language.scripts.get(getFileId(fileName)); - if (!(volarFile?.generated?.root instanceof VueVirtualCode)) { + const sourceScript = language.scripts.get(getFileId(fileName)); + if (!sourceScript?.generated) { + return; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { return; } @@ -21,9 +26,9 @@ export function collectExtractProps( const program = languageService.getProgram()!; const sourceFile = program.getSourceFile(fileName)!; const checker = program.getTypeChecker(); - const script = volarFile.generated?.languagePlugin.typescript?.getServiceScript(volarFile.generated.root); + const script = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root); const maps = script ? [...language.maps.forEach(script.code)].map(([_sourceScript, map]) => map) : []; - const sfc = volarFile.generated.root._sfc; + const sfc = root._sfc; sourceFile.forEachChild(function visit(node) { if ( @@ -35,7 +40,7 @@ export function collectExtractProps( const { name } = node; for (const map of maps) { let mapped = false; - for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? volarFile.snapshot.getLength() : 0))) { + for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? sourceScript.snapshot.getLength() : 0))) { if ( source[0] >= sfc.template!.startTagEnd + templateCodeRange[0] && source[0] <= sfc.template!.startTagEnd + templateCodeRange[1]