diff --git a/packages/language-server/index.ts b/packages/language-server/index.ts index 34b40ed8a9..978ae054ae 100644 --- a/packages/language-server/index.ts +++ b/packages/language-server/index.ts @@ -132,6 +132,9 @@ connection.onInitialize(params => { getPropertiesAtLocation(...args) { return sendTsServerRequest('_vue:getPropertiesAtLocation', 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 a95353cb72..1381b60495 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -40,7 +40,6 @@ export function createVueLanguageServicePlugins( }) as NonNullable; return [ - createCssPlugin(), createJsonPlugin(), createPugFormatPlugin(), createVueAutoSpacePlugin(), @@ -64,6 +63,7 @@ export function createVueLanguageServicePlugins( createVueInlayHintsPlugin(ts), // type aware plugins + createCssPlugin(tsPluginClient), createTypescriptSemanticTokensPlugin(tsPluginClient), createVueAutoDotValuePlugin(ts, tsPluginClient), createVueComponentSemanticTokensPlugin(tsPluginClient), diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index fb168961a8..6d7efc82d8 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 { getEmbeddedInfo } from '../utils'; +import { createTsAliasDocumentLinksProviders, getEmbeddedInfo } 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,13 @@ export function create(): LanguageServicePlugin { return cssLs.prepareRename(document, position, stylesheet); }); }, + + ...createTsAliasDocumentLinksProviders( + context, + baseServiceInstance, + id => id.startsWith('style_'), + resolveModuleName, + ), }; function isWithinNavigationVirtualCode( diff --git a/packages/language-service/lib/plugins/vue-template.ts b/packages/language-service/lib/plugins/vue-template.ts index 42a73b4243..f85cbe8d9f 100644 --- a/packages/language-service/lib/plugins/vue-template.ts +++ b/packages/language-service/lib/plugins/vue-template.ts @@ -15,7 +15,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 { getEmbeddedInfo } from '../utils'; +import { createTsAliasDocumentLinksProviders, getEmbeddedInfo } from '../utils'; const specialTags = new Set([ 'slot', @@ -39,11 +39,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[] = []; @@ -93,6 +94,9 @@ export function create( ], }, hoverProvider: true, + documentLinkProvider: { + resolveProvider: true, + }, }, create(context) { const baseServiceInstance = baseService.create(context); @@ -323,6 +327,13 @@ export function create( return baseServiceInstance.provideHover?.(document, position, token); }, + + ...createTsAliasDocumentLinksProviders( + context, + baseServiceInstance, + 'template', + 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 f915843766..cb28ee2440 100644 --- a/packages/language-service/lib/utils.ts +++ b/packages/language-service/lib/utils.ts @@ -1,4 +1,9 @@ -import { type LanguageServiceContext, type SourceScript, type TextDocument } from '@volar/language-service'; +import { + type LanguageServiceContext, + type LanguageServicePluginInstance, + type SourceScript, + type TextDocument, +} from '@volar/language-service'; import { VueVirtualCode } from '@vue/language-core'; import { URI } from 'vscode-uri'; @@ -50,3 +55,56 @@ export function getEmbeddedInfo( root, }; } + +export function createTsAliasDocumentLinksProviders( + context: LanguageServiceContext, + service: LanguageServicePluginInstance, + filter: string | ((id: string) => boolean), + resolveModuleName: import('@vue/typescript-plugin/lib/requests').Requests['resolveModuleName'], +): Pick< + LanguageServicePluginInstance, + 'provideDocumentLinks' | 'resolveDocumentLink' +> { + return { + async provideDocumentLinks(document, token) { + const info = getEmbeddedInfo(context, document, filter); + 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 a098844a1c..5736486111 100644 --- a/packages/typescript-plugin/index.ts +++ b/packages/typescript-plugin/index.ts @@ -12,6 +12,7 @@ import { getElementAttrs } from './lib/requests/getElementAttrs'; import { getElementNames } from './lib/requests/getElementNames'; import { getImportPathForFile } from './lib/requests/getImportPathForFile'; import { getPropertiesAtLocation } from './lib/requests/getPropertiesAtLocation'; +import { resolveModuleName } from './lib/requests/resolveModuleName'; import type { RequestContext } from './lib/requests/types'; const windowsPathReg = /\\/g; @@ -147,6 +148,11 @@ export = createLanguageServicePlugin( response: getElementNames.apply(getRequestContext(args[0]), args), }; }); + session.addProtocolHandler('_vue:resolveModuleName', ({ arguments: args }) => { + return { + response: resolveModuleName.apply(getRequestContext(args[0]), args), + }; + }); 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 5f71d49db8..76efd7a2f2 100644 --- a/packages/typescript-plugin/lib/requests/index.ts +++ b/packages/typescript-plugin/lib/requests/index.ts @@ -16,6 +16,7 @@ export interface Requests { getComponentSlots: Request; getElementAttrs: Request; getElementNames: Request; + resolveModuleName: Request; getDocumentHighlights: Request<(fileName: string, position: number) => ts.DocumentHighlights[]>; getEncodedSemanticClassifications: Request<(fileName: string, span: ts.TextSpan) => ts.Classifications>; getQuickInfoAtPosition: Request<(fileName: string, position: ts.LineAndCharacter) => string>; diff --git a/packages/typescript-plugin/lib/requests/resolveModuleName.ts b/packages/typescript-plugin/lib/requests/resolveModuleName.ts new file mode 100644 index 0000000000..650cfda5cd --- /dev/null +++ b/packages/typescript-plugin/lib/requests/resolveModuleName.ts @@ -0,0 +1,39 @@ +import type * as ts from 'typescript'; +import type { RequestContext } from './types'; + +export function resolveModuleName( + this: RequestContext, + fileName: string, + moduleName: string, +): { name?: string } { + const { typescript: ts, languageServiceHost } = this; + const compilerOptions = languageServiceHost.getCompilationSettings(); + + const ext = moduleName.split('.').pop(); + const result = ts.resolveModuleName( + moduleName, + fileName, + { + ...compilerOptions, + allowArbitraryExtensions: true, + }, + { + fileExists(fileName) { + fileName = transformFileName(fileName, ext); + return languageServiceHost.fileExists(fileName); + }, + } 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; +}