diff --git a/vue-language-tools/vue-language-service/src/languageService.ts b/vue-language-tools/vue-language-service/src/languageService.ts index 0800568ac..54ce33718 100644 --- a/vue-language-tools/vue-language-service/src/languageService.ts +++ b/vue-language-tools/vue-language-service/src/languageService.ts @@ -17,6 +17,7 @@ import useReferencesCodeLensPlugin from './plugins/vue-codelens-references'; import useHtmlPugConversionsPlugin from './plugins/vue-convert-htmlpug'; import useRefSugarConversionsPlugin from './plugins/vue-convert-refsugar'; import useScriptSetupConversionsPlugin from './plugins/vue-convert-scriptsetup'; +import useTwoslashQueries from './plugins/vue-twoslash-queries'; import useVueTemplateLanguagePlugin, { semanticTokenTypes as vueTemplateSemanticTokenTypes } from './plugins/vue-template'; export function getSemanticTokenLegend() { @@ -96,6 +97,9 @@ export function getLanguageServicePlugins( vueLsHost: host, context: apis.context, }); + const twoslashQueriesPlugin = useTwoslashQueries({ + getVueDocument: (document) => apis.context.documents.get(document.uri), + }); return [ vuePlugin, @@ -111,6 +115,7 @@ export function getLanguageServicePlugins( autoDotValuePlugin, // put emmet plugin at last to fix https://github.com/johnsoncodehk/volar/issues/1088 emmetPlugin, + twoslashQueriesPlugin, ]; } diff --git a/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts b/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts new file mode 100644 index 000000000..78d927224 --- /dev/null +++ b/vue-language-tools/vue-language-service/src/plugins/vue-twoslash-queries.ts @@ -0,0 +1,75 @@ +import { EmbeddedFileKind, forEachEmbeddeds, LanguageServicePlugin, LanguageServicePluginContext, SourceFileDocument } from '@volar/language-service'; +import * as vue from '@volar/vue-language-core'; +import * as vscode from 'vscode-languageserver-protocol'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +export default function (options: { + getVueDocument(document: TextDocument): SourceFileDocument | undefined, +}): LanguageServicePlugin { + + let context: LanguageServicePluginContext; + + return { + + setup(_context) { + context = _context; + }, + + inlayHints: { + + on(document, range) { + return worker(document, (vueDocument, vueFile) => { + + const ts = context.typescript.module; + const hoverOffsets: [vscode.Position, number][] = []; + const inlayHints: vscode.InlayHint[] = []; + + for (const pointer of document.getText(range).matchAll(/\^\?/g)) { + const offset = pointer.index! + document.offsetAt(range.start); + const position = document.positionAt(offset); + hoverOffsets.push([position, document.offsetAt({ + line: position.line - 1, + character: position.character, + })]); + } + + forEachEmbeddeds(vueFile.embeddeds, (embedded) => { + if (embedded.kind === EmbeddedFileKind.TypeScriptHostFile) { + const sourceMap = vueDocument.getSourceMap(embedded); + for (const [pointerPosition, hoverOffset] of hoverOffsets) { + for (const [tsOffset, mapping] of sourceMap.toGeneratedOffsets(hoverOffset)) { + if (mapping.data.hover) { + const quickInfo = context.typescript.languageService.getQuickInfoAtPosition(embedded.fileName, tsOffset); + if (quickInfo) { + inlayHints.push({ + position: { line: pointerPosition.line, character: pointerPosition.character + 2 }, + label: ts.displayPartsToString(quickInfo.displayParts), + paddingLeft: true, + paddingRight: false, + }); + } + break; + } + } + } + } + }); + + return inlayHints; + }); + }, + }, + }; + + function worker(document: TextDocument, callback: (vueDocument: SourceFileDocument, vueSourceFile: vue.VueSourceFile) => T) { + + const vueDocument = options.getVueDocument(document); + if (!vueDocument) + return; + + if (!(vueDocument.file instanceof vue.VueSourceFile)) + return; + + return callback(vueDocument, vueDocument.file); + } +}