diff --git a/packages/language-server/tests/documentLinks.spec.ts b/packages/language-server/tests/documentLinks.spec.ts new file mode 100644 index 0000000000..12549879d3 --- /dev/null +++ b/packages/language-server/tests/documentLinks.spec.ts @@ -0,0 +1,135 @@ +import type { TextDocument } from '@volar/language-server'; +import { afterEach, expect, test } from 'vitest'; +import { URI } from 'vscode-uri'; +import { getLanguageServer, testWorkspacePath } from './server.js'; + +test('Document links', async () => { + expect( + await requestDocumentLinks('fixture.vue', 'vue', ` + + + + + + `) + ).toMatchInlineSnapshot(` + [ + { + "range": { + "end": { + "character": 23, + "line": 10, + }, + "start": { + "character": 16, + "line": 10, + }, + }, + "target": "file://\${testWorkspacePath}/fixture.vue#L19%2C4-L19%2C12", + }, + { + "range": { + "end": { + "character": 42, + "line": 3, + }, + "start": { + "character": 32, + "line": 3, + }, + }, + "target": "file://\${testWorkspacePath}/fixture.vue#L12%2C15-L12%2C25", + }, + { + "range": { + "end": { + "character": 41, + "line": 4, + }, + "start": { + "character": 32, + "line": 4, + }, + }, + "target": "file://\${testWorkspacePath}/fixture.vue#L13%2C15-L13%2C24", + }, + { + "range": { + "end": { + "character": 41, + "line": 4, + }, + "start": { + "character": 32, + "line": 4, + }, + }, + "target": "file://\${testWorkspacePath}/fixture.vue#L14%2C16-L14%2C25", + }, + { + "range": { + "end": { + "character": 39, + "line": 5, + }, + "start": { + "character": 32, + "line": 5, + }, + }, + "target": "file://\${testWorkspacePath}/fixture.vue#L15%2C38-L15%2C45", + }, + ] + `); +}); + +const openedDocuments: TextDocument[] = []; + +afterEach(async () => { + const server = await getLanguageServer(); + for (const document of openedDocuments) { + await server.close(document.uri); + } + openedDocuments.length = 0; +}); + +async function requestDocumentLinks(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + let document = await prepareDocument(fileName, languageId, content); + + const documentLinks = await server.vueserver.sendDocumentLinkRequest(document.uri); + expect(documentLinks).toBeDefined(); + expect(documentLinks!.length).greaterThan(0); + + for (const documentLink of documentLinks!) { + documentLink.target = 'file://${testWorkspacePath}' + documentLink.target!.slice(URI.file(testWorkspacePath).toString().length) + } + + return documentLinks!; +} + +async function prepareDocument(fileName: string, languageId: string, content: string) { + const server = await getLanguageServer(); + const uri = URI.file(`${testWorkspacePath}/${fileName}`); + const document = await server.open(uri.toString(), languageId, content); + if (openedDocuments.every(d => d.uri !== document.uri)) { + openedDocuments.push(document); + } + return document; +} diff --git a/packages/language-service/lib/plugins/vue-document-links.ts b/packages/language-service/lib/plugins/vue-document-links.ts index 0db43b4304..aea939a470 100644 --- a/packages/language-service/lib/plugins/vue-document-links.ts +++ b/packages/language-service/lib/plugins/vue-document-links.ts @@ -17,7 +17,7 @@ export function create(): LanguageServicePlugin { 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') { + if (!sourceScript?.generated || (virtualCode?.id !== 'template' && virtualCode?.id !== "scriptsetup_raw")) { return; } @@ -26,52 +26,90 @@ export function create(): LanguageServicePlugin { return; } - const result: vscode.DocumentLink[] = []; - const { sfc } = root; const codegen = tsCodegen.get(sfc); - const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; - const styleClasses = new Map(); - const option = root.vueCompilerOptions.experimentalResolveStyleCssClasses; + const result: vscode.DocumentLink[] = []; - for (let i = 0; i < sfc.styles.length; i++) { - const style = 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), []); + if (virtualCode.id === 'template') { + const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; + const styleClasses = new Map(); + const option = root.vueCompilerOptions.experimentalResolveStyleCssClasses; + + for (let i = 0; i < sfc.styles.length; i++) { + const style = 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, + }); } - 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; + 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}`, + }); } - 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); + } + } + } + else if (virtualCode.id === 'scriptsetup_raw') { + if (!sfc.scriptSetup) { + return; + } + + const templateVirtualCode = sourceScript.generated.embeddedCodes.get('template'); + if (!templateVirtualCode) { + return; + } + const templateDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'template'); + const templateDocument = context.documents.get(templateDocumentUri, templateVirtualCode.languageId, templateVirtualCode.snapshot); + + const templateRefs = codegen?.getGeneratedTemplate()?.templateRefs; + const useTemplateRefs = codegen?.getScriptSetupRanges()?.useTemplateRef ?? []; + + for (const { arg } of useTemplateRefs) { + if (!arg) { + continue; + } + + const name = sfc.scriptSetup.content.slice(arg.start + 1, arg.end - 1); + + for (const { offset } of templateRefs?.get(name) ?? []) { + const start = templateDocument.positionAt(offset); + const end = templateDocument.positionAt(offset + name.length); + result.push({ range: { - start: document.positionAt(offset), - end: document.positionAt(offset + className.length), + start: document.positionAt(arg.start + 1), + end: document.positionAt(arg.end - 1), }, - target: context.encodeEmbeddedDocumentUri(decoded![0], 'style_' + style.index) + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, + target: templateDocumentUri + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, }); } }