From a30eef15a94dcbba5ad112227924f51f0f458383 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Tue, 27 May 2025 20:54:17 -0700 Subject: [PATCH 1/3] feat(language-service): document links for template refs --- .../tests/documentLinks.spec.ts | 135 ++++++++++++++++++ .../lib/plugins/vue-document-links.ts | 115 ++++++++++----- 2 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 packages/language-server/tests/documentLinks.spec.ts diff --git a/packages/language-server/tests/documentLinks.spec.ts b/packages/language-server/tests/documentLinks.spec.ts new file mode 100644 index 0000000000..9b5ea91a98 --- /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 = documentLink.target!.replace(testWorkspacePath, "${testWorkspacePath}"); + } + + 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..1bce8e7061 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; } @@ -30,48 +30,95 @@ export function create(): LanguageServicePlugin { const { sfc } = root; const codegen = tsCodegen.get(sfc); - const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; - const styleClasses = new Map(); - const option = root.vueCompilerOptions.experimentalResolveStyleCssClasses; + 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), []); + 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 templateRefs = codegen?.getGeneratedTemplate()?.templateRefs ?? []; + const scriptSetupUseTemplateRefs = codegen?.getScriptSetupRanges()?.useTemplateRef ?? []; + const templateRefOffsetsByName = new Map(); + for (const [name, refs] of templateRefs) { + templateRefOffsetsByName.set(name, refs.map(ref => ref.offset)); + } + + const templateDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'template'); + const templateVirtualCode = sourceScript.generated.embeddedCodes.get('template'); + if (!templateVirtualCode) { + return; + } + const templateDocument = context.documents.get(templateDocumentUri, templateVirtualCode.languageId, templateVirtualCode.snapshot); + + for (const { arg } of scriptSetupUseTemplateRefs) { + if (!arg) { + continue; + } + + const scriptSetupTemplateRefName = sfc.scriptSetup.content.slice(arg.start + 1, arg.end - 1); + + const templateRefOffsets = templateRefOffsetsByName.get(scriptSetupTemplateRefName); + if (!templateRefOffsets) { + continue; + } + + for (const templateRefOffset of templateRefOffsets) { + const targetStart = templateDocument.positionAt(templateRefOffset); + const targetEnd = templateDocument.positionAt(templateRefOffset + scriptSetupTemplateRefName.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${targetStart.line + 1},${targetStart.character + 1}-L${targetEnd.line + 1},${targetEnd.character + 1}`, }); } } From fe8f772aeb5bcec6b0a44c85cc1499c4747b2979 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Wed, 28 May 2025 16:34:18 +0800 Subject: [PATCH 2/3] refactor: simplify --- .../lib/plugins/vue-document-links.ts | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/language-service/lib/plugins/vue-document-links.ts b/packages/language-service/lib/plugins/vue-document-links.ts index 1bce8e7061..aea939a470 100644 --- a/packages/language-service/lib/plugins/vue-document-links.ts +++ b/packages/language-service/lib/plugins/vue-document-links.ts @@ -26,10 +26,10 @@ export function create(): LanguageServicePlugin { return; } - const result: vscode.DocumentLink[] = []; - const { sfc } = root; const codegen = tsCodegen.get(sfc); + const result: vscode.DocumentLink[] = []; + if (virtualCode.id === 'template') { const scopedClasses = codegen?.getGeneratedTemplate()?.scopedClasses ?? []; const styleClasses = new Map(); - for (const [name, refs] of templateRefs) { - templateRefOffsetsByName.set(name, refs.map(ref => ref.offset)); - } - - const templateDocumentUri = context.encodeEmbeddedDocumentUri(decoded![0], 'template'); 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); - for (const { arg } of scriptSetupUseTemplateRefs) { + const templateRefs = codegen?.getGeneratedTemplate()?.templateRefs; + const useTemplateRefs = codegen?.getScriptSetupRanges()?.useTemplateRef ?? []; + + for (const { arg } of useTemplateRefs) { if (!arg) { continue; } - const scriptSetupTemplateRefName = sfc.scriptSetup.content.slice(arg.start + 1, arg.end - 1); - - const templateRefOffsets = templateRefOffsetsByName.get(scriptSetupTemplateRefName); - if (!templateRefOffsets) { - continue; - } + const name = sfc.scriptSetup.content.slice(arg.start + 1, arg.end - 1); - for (const templateRefOffset of templateRefOffsets) { - const targetStart = templateDocument.positionAt(templateRefOffset); - const targetEnd = templateDocument.positionAt(templateRefOffset + scriptSetupTemplateRefName.length); + 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(arg.start + 1), end: document.positionAt(arg.end - 1), }, - target: templateDocumentUri + `#L${targetStart.line + 1},${targetStart.character + 1}-L${targetEnd.line + 1},${targetEnd.character + 1}`, + target: templateDocumentUri + `#L${start.line + 1},${start.character + 1}-L${end.line + 1},${end.character + 1}`, }); } } From fc3976f22ec9448628749620ace1805ae1830cf9 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Wed, 28 May 2025 22:37:15 -0700 Subject: [PATCH 3/3] fix: update test for CI --- packages/language-server/tests/documentLinks.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/tests/documentLinks.spec.ts b/packages/language-server/tests/documentLinks.spec.ts index 9b5ea91a98..12549879d3 100644 --- a/packages/language-server/tests/documentLinks.spec.ts +++ b/packages/language-server/tests/documentLinks.spec.ts @@ -118,7 +118,7 @@ async function requestDocumentLinks(fileName: string, languageId: string, conten expect(documentLinks!.length).greaterThan(0); for (const documentLink of documentLinks!) { - documentLink.target = documentLink.target!.replace(testWorkspacePath, "${testWorkspacePath}"); + documentLink.target = 'file://${testWorkspacePath}' + documentLink.target!.slice(URI.file(testWorkspacePath).toString().length) } return documentLinks!;