diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 4ce1c98840..6c7925f83b 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -83,6 +83,7 @@ export function getFullLanguageServicePlugins( languageService.getCompletionsAtPosition = proxy.getCompletionsAtPosition; languageService.getCompletionEntryDetails = proxy.getCompletionEntryDetails; languageService.getCodeFixesAtPosition = proxy.getCodeFixesAtPosition; + languageService.getDefinitionAndBoundSpan = proxy.getDefinitionAndBoundSpan; languageService.getQuickInfoAtPosition = proxy.getQuickInfoAtPosition; } return created; diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 6f74181a44..3395b7c500 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -19,6 +19,7 @@ export function proxyLanguageServiceForVue( case 'getCompletionsAtPosition': return getCompletionsAtPosition(vueOptions, target[p]); case 'getCompletionEntryDetails': return getCompletionEntryDetails(language, asScriptId, target[p]); case 'getCodeFixesAtPosition': return getCodeFixesAtPosition(target[p]); + case 'getDefinitionAndBoundSpan': return getDefinitionAndBoundSpan(ts, language, languageService, vueOptions, asScriptId, target[p]); case 'getQuickInfoAtPosition': return getQuickInfoAtPosition(ts, target, target[p]); // TS plugin only case 'getEncodedSemanticClassifications': return getEncodedSemanticClassifications(ts, language, target, asScriptId, target[p]); @@ -91,7 +92,11 @@ function getCompletionsAtPosition(vueOptions: VueCompilerOptions, getCompletions }; } -function getCompletionEntryDetails(language: Language, asScriptId: (fileName: string) => T, getCompletionEntryDetails: ts.LanguageService['getCompletionEntryDetails']): ts.LanguageService['getCompletionEntryDetails'] { +function getCompletionEntryDetails( + language: Language, + asScriptId: (fileName: string) => T, + getCompletionEntryDetails: ts.LanguageService['getCompletionEntryDetails'] +): ts.LanguageService['getCompletionEntryDetails'] { return (...args) => { const details = getCompletionEntryDetails(...args); // modify import statement @@ -132,7 +137,9 @@ function getCompletionEntryDetails(language: Language, asScriptId: (fileNa }; } -function getCodeFixesAtPosition(getCodeFixesAtPosition: ts.LanguageService['getCodeFixesAtPosition']): ts.LanguageService['getCodeFixesAtPosition'] { +function getCodeFixesAtPosition( + getCodeFixesAtPosition: ts.LanguageService['getCodeFixesAtPosition'] +): ts.LanguageService['getCodeFixesAtPosition'] { return (...args) => { let result = getCodeFixesAtPosition(...args); // filter __VLS_ @@ -141,7 +148,115 @@ function getCodeFixesAtPosition(getCodeFixesAtPosition: ts.LanguageService['getC }; } -function getQuickInfoAtPosition(ts: typeof import('typescript'), languageService: ts.LanguageService, getQuickInfoAtPosition: ts.LanguageService['getQuickInfoAtPosition']): ts.LanguageService['getQuickInfoAtPosition'] { +function getDefinitionAndBoundSpan( + ts: typeof import('typescript'), + language: Language, + languageService: ts.LanguageService, + vueOptions: VueCompilerOptions, + asScriptId: (fileName: string) => T, + getDefinitionAndBoundSpan: ts.LanguageService['getDefinitionAndBoundSpan'] +): ts.LanguageService['getDefinitionAndBoundSpan'] { + return (fileName, position) => { + const result = getDefinitionAndBoundSpan(fileName, position); + if (!result?.definitions?.length) { + return result; + } + + const program = languageService.getProgram()!; + const sourceScript = language.scripts.get(asScriptId(fileName)); + if (!sourceScript?.generated) { + return result; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + return result; + } + + if ( + !root.sfc.template + || position < root.sfc.template.startTagEnd + || position > root.sfc.template.endTagStart + ) { + return result; + } + + const definitions = new Set(result.definitions); + const skippedDefinitions: ts.DefinitionInfo[] = []; + + for (const definition of result.definitions) { + if (vueOptions.extensions.some(ext => definition.fileName.endsWith(ext))) { + continue; + } + + const sourceFile = program.getSourceFile(definition.fileName); + if (!sourceFile) { + continue; + } + + visit(sourceFile, definition, sourceFile); + } + + for (const definition of skippedDefinitions) { + definitions.delete(definition); + } + + return { + definitions: [...definitions], + textSpan: result.textSpan, + }; + + function visit( + node: ts.Node, + definition: ts.DefinitionInfo, + sourceFile: ts.SourceFile + ) { + if (ts.isPropertySignature(node) && node.type) { + proxy(node.name, node.type, definition, sourceFile); + } + else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.type && !node.initializer) { + proxy(node.name, node.type, definition, sourceFile); + } + else { + ts.forEachChild(node, child => visit(child, definition, sourceFile)); + } + } + + function proxy( + name: ts.PropertyName, + type: ts.TypeNode, + definition: ts.DefinitionInfo, + sourceFile: ts.SourceFile + ) { + const { textSpan, fileName } = definition; + const start = name.getStart(sourceFile); + const end = name.getEnd(); + + if (start !== textSpan.start || end - start !== textSpan.length) { + return; + } + + if (!ts.isIndexedAccessTypeNode(type)) { + return; + } + + const pos = type.indexType.getStart(sourceFile); + const res = getDefinitionAndBoundSpan(fileName, pos); + if (res?.definitions?.length) { + for (const definition of res.definitions) { + definitions.add(definition); + } + skippedDefinitions.push(definition); + } + } + } +} + +function getQuickInfoAtPosition( + ts: typeof import('typescript'), + languageService: ts.LanguageService, + getQuickInfoAtPosition: ts.LanguageService['getQuickInfoAtPosition'] +): ts.LanguageService['getQuickInfoAtPosition'] { return (...args) => { const result = getQuickInfoAtPosition(...args); if (result && result.documentation?.length === 1 && result.documentation[0].text.startsWith('__VLS_emit,')) {