diff --git a/packages/language-server/lib/server.ts b/packages/language-server/lib/server.ts index 7edff3994a..dd1787b4a9 100644 --- a/packages/language-server/lib/server.ts +++ b/packages/language-server/lib/server.ts @@ -126,6 +126,9 @@ export function startServer(ts: typeof import('typescript')) { isRefAtPosition(...args) { return sendTsServerRequest('_vue:isRefAtPosition', args); }, + resolveModuleName(...args) { + return sendTsServerRequest('_vue:resolveModuleName', args); + }, getDocumentHighlights(fileName, position) { return sendTsServerRequest( '_vue:documentHighlights-full', diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 6e98951931..6e75d1c858 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -41,7 +41,6 @@ export function createVueLanguageServicePlugins( }), ) { return [ - createCssPlugin(), createJsonPlugin(), createPugFormatPlugin(), createVueAutoSpacePlugin(), @@ -65,6 +64,7 @@ export function createVueLanguageServicePlugins( createVueInlayHintsPlugin(ts), // type aware plugins + createCssPlugin(client), createTypescriptSemanticTokensPlugin(client), createVueAutoDotValuePlugin(ts, client), createVueComponentSemanticTokensPlugin(client), diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index 7ffda3f2af..f56385c7af 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -2,23 +2,31 @@ import type { LanguageServicePlugin, TextDocument, VirtualCode } from '@volar/la 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 { resolveEmbeddedCode } from '../utils'; +import { createTsAliasDocumentLinksProviders, resolveEmbeddedCode } from '../utils'; -export function create(): LanguageServicePlugin { - const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); +export function create( + { resolveModuleName }: import('@vue/typescript-plugin/lib/requests').Requests, +): LanguageServicePlugin { + const baseService = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); return { - ...base, + ...baseService, + capabilities: { + ...baseService.capabilities, + documentLinkProvider: { + resolveProvider: true, + }, + }, create(context) { - const baseInstance = base.create(context); + const baseServiceInstance = baseService.create(context); const { 'css/languageService': getCssLs, 'css/stylesheet': getStylesheet, - } = baseInstance.provide as Provide; + } = baseServiceInstance.provide as Provide; return { - ...baseInstance, + ...baseServiceInstance, async provideDiagnostics(document, token) { - let diagnostics = await baseInstance.provideDiagnostics?.(document, token) ?? []; + let diagnostics = await baseServiceInstance.provideDiagnostics?.(document, token) ?? []; if (document.languageId === 'postcss') { diagnostics = diagnostics.filter(diag => diag.code !== 'css-semicolonexpected' @@ -48,6 +56,11 @@ export function create(): LanguageServicePlugin { return cssLs.prepareRename(document, position, stylesheet); }); }, + ...createTsAliasDocumentLinksProviders( + context, + baseServiceInstance, + resolveModuleName, + ), }; function isWithinNavigationVirtualCode( diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index d07f047b0a..e26c0e4cd3 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -21,7 +21,7 @@ import * as html from 'vscode-html-languageservice'; import { URI, Utils } from 'vscode-uri'; import { loadModelModifiersData, loadTemplateData } from '../data'; import { AttrNameCasing, checkCasing, TagNameCasing } from '../nameCasing'; -import { resolveEmbeddedCode } from '../utils'; +import { createTsAliasDocumentLinksProviders, resolveEmbeddedCode } from '../utils'; const specialTags = new Set([ 'slot', @@ -45,11 +45,12 @@ export function create( languageId: 'html' | 'jade', { getComponentNames, - getElementAttrs, getComponentProps, getComponentEvents, getComponentDirectives, getComponentSlots, + getElementAttrs, + resolveModuleName, }: import('@vue/typescript-plugin/lib/requests').Requests, ): LanguageServicePlugin { let customData: html.IHTMLDataProvider[] = []; @@ -99,6 +100,9 @@ export function create( ], }, hoverProvider: true, + documentLinkProvider: { + resolveProvider: true, + }, }, create(context) { const baseServiceInstance = baseService.create(context); @@ -362,6 +366,12 @@ export function create( return baseServiceInstance.provideHover?.(document, position, token); }, + + ...createTsAliasDocumentLinksProviders( + context, + baseServiceInstance, + resolveModuleName, + ), }; async function runWithVueData(sourceDocumentUri: URI, root: VueVirtualCode, fn: () => T) { diff --git a/packages/language-service/lib/utils.ts b/packages/language-service/lib/utils.ts index 50aa552e5f..eb4b872a61 100644 --- a/packages/language-service/lib/utils.ts +++ b/packages/language-service/lib/utils.ts @@ -1,4 +1,4 @@ -import type { LanguageServiceContext, SourceScript } from '@volar/language-service'; +import type { LanguageServiceContext, LanguageServicePluginInstance, SourceScript } from '@volar/language-service'; import type { VueVirtualCode } from '@vue/language-core'; import { URI } from 'vscode-uri'; @@ -19,3 +19,60 @@ export function resolveEmbeddedCode( root: sourceScript.generated!.root as VueVirtualCode, }; } + +export function createTsAliasDocumentLinksProviders( + context: LanguageServiceContext, + service: LanguageServicePluginInstance, + resolveModuleName: import('@vue/typescript-plugin/lib/requests').Requests['resolveModuleName'], +): Pick< + LanguageServicePluginInstance, + 'provideDocumentLinks' | 'resolveDocumentLink' +> { + return { + async provideDocumentLinks(document, token) { + const info = resolveEmbeddedCode(context, document.uri); + if (!info) { + return; + } + + const { root } = info; + + const documentLinks = await service.provideDocumentLinks?.(document, token); + + for (const link of documentLinks ?? []) { + if (!link.target) { + continue; + } + + let text = document.getText(link.range); + + if (text.startsWith('./') || text.startsWith('../')) { + continue; + } + + if (text.startsWith(`'`) || text.startsWith(`"`)) { + text = text.slice(1, -1); + } + + link.data = { + fileName: root.fileName, + text: text, + originalTarget: link.target, + }; + + delete link.target; + } + return documentLinks; + }, + + async resolveDocumentLink(link) { + const { fileName, text, originalTarget } = link.data; + const { name } = await resolveModuleName(fileName, text) || {}; + + return { + ...link, + target: name ?? originalTarget, + }; + }, + }; +} diff --git a/packages/typescript-plugin/index.ts b/packages/typescript-plugin/index.ts index 41ef3e3967..6ec227b9ba 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -13,6 +13,7 @@ import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; import { isRefAtPosition } from './lib/requests/isRefAtPosition'; +import { resolveModuleName } from './lib/requests/resolveModuleName'; export = createLanguageServicePlugin( (ts, info) => { @@ -170,6 +171,10 @@ export = createLanguageServicePlugin( const { project } = getProject(fileName); return createResponse(getElementNames(ts, project.getLanguageService().getProgram()!)); }); + session.addProtocolHandler('_vue:resolveModuleName', request => { + const [fileName, moduleName]: Parameters = request.arguments; + return createResponse(resolveModuleName(ts, info.languageServiceHost, fileName, moduleName)); + }); projectService.logger.info('Vue specific commands are successfully added.'); diff --git a/packages/typescript-plugin/lib/requests/index.ts b/packages/typescript-plugin/lib/requests/index.ts index 4cf06bf368..9c27fa06d0 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -40,6 +40,10 @@ export interface Requests { getElementNames( fileName: string, ): Response>; + resolveModuleName( + fileName: string, + moduleName: string, + ): Response>; getDocumentHighlights( fileName: string, position: number, diff --git a/packages/typescript-plugin/lib/requests/resolveModuleName.ts b/packages/typescript-plugin/lib/requests/resolveModuleName.ts new file mode 100644 index 0000000000..407be9bcc9 --- /dev/null +++ b/packages/typescript-plugin/lib/requests/resolveModuleName.ts @@ -0,0 +1,38 @@ +import type * as ts from 'typescript'; + +export function resolveModuleName( + ts: typeof import('typescript'), + languageServiceHost: ts.LanguageServiceHost, + fileName: string, + moduleName: string, +): { name?: string } { + const compilerOptions = languageServiceHost.getCompilationSettings(); + + const ext = moduleName.split('.').pop(); + + const result = ts.resolveModuleName( + moduleName, + fileName, + { + ...compilerOptions, + allowArbitraryExtensions: true, + }, + { + fileExists(fileName) { + fileName = transformFileName(fileName, ext); + }, + } as ts.ModuleResolutionHost, + ); + + const resolveFileName = result.resolvedModule?.resolvedFileName; + return { + name: resolveFileName ? transformFileName(resolveFileName, ext) : undefined, + }; +} + +function transformFileName(fileName: string, ext: string | undefined) { + if (ext && fileName.endsWith(`.d.${ext}.ts`)) { + return fileName.slice(0, -`.d.${ext}.ts`.length) + `.${ext}`; + } + return fileName; +}