diff --git a/src/solutions/language-features/reference-link-provider.test.ts b/src/solutions/language-features/reference-link-provider.test.ts index 025697ff..c64b39bb 100644 --- a/src/solutions/language-features/reference-link-provider.test.ts +++ b/src/solutions/language-features/reference-link-provider.test.ts @@ -21,6 +21,7 @@ import { URI as Uri, Utils as UriUtils } from 'vscode-uri'; import * as path from 'path'; import { solutionManagerFactory } from '../solution-manager.factories'; import { SolutionRpcDataMock } from '../solution-rpc-data.factory'; +import * as pathUtils from '../../utils/path-utils'; describe('ReferenceLinkProvider', () => { it('returns no links if the document cannot be parsed', () => { @@ -63,7 +64,7 @@ describe('ReferenceLinkProvider', () => { const rpcData = solutionManager.getRpcData() as SolutionRpcDataMock; rpcData.seedVariables('my.Build+Target', { 'Board-layer': 'mylayer.clayer.yml' }); - const provider = new ReferenceLinkProvider(solutionManager); + const provider = new ReferenceLinkProvider(solutionManager, false); const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() }); expect(output).toEqual([ @@ -72,4 +73,51 @@ describe('ReferenceLinkProvider', () => { expect.objectContaining({ target: expect.objectContaining({ fsPath: UriUtils.joinPath(documentUri, '../mylayer.clayer.yml').fsPath }) }), ]); }); + + it('returns links for cbuild files without expanding RPC variables', () => { + const getCmsisPackRootSpy = jest.spyOn(pathUtils, 'getCmsisPackRoot').mockReturnValue('TEST_CMSIS_PACK_ROOT'); + + try { + const cbuildDoc = ` + build-idx: + csolution: ./my.csolution.yml + cbuilds: + - cbuild: ./my/debug/my.cbuild.yml + clayers: + - clayer: my.clayer.yml + cprojects: + - cproject: my/my.cproject.yml + clayers: + - clayer: my/$Board-layer$ + files: + - file: \${CMSIS_PACK_ROOT}/myPack/0.0.1/myfile.yml + `; + + const documentFileName = path.join(__dirname, 'my.cbuild-idx.yml'); + const documentUri = Uri.file(documentFileName); + const textDocument = textDocumentFactory({ uri: documentUri, fileName: documentFileName }); + textDocument.getText.mockReturnValue(cbuildDoc); + + const solutionManager = solutionManagerFactory(); + const provider = new ReferenceLinkProvider(solutionManager, true); + const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() }); + const outputPaths = output + .map(link => link.target?.fsPath) + .filter((target): target is string => !!target); + + const expectedPaths = [ + UriUtils.joinPath(documentUri, '../my.csolution.yml').fsPath, + UriUtils.joinPath(documentUri, '../my/debug/my.cbuild.yml').fsPath, + UriUtils.joinPath(documentUri, '../my.clayer.yml').fsPath, + UriUtils.joinPath(documentUri, '../my/my.cproject.yml').fsPath, + path.join(path.sep, 'TEST_CMSIS_PACK_ROOT', 'myPack', '0.0.1', 'myfile.yml'), + ]; + + expectedPaths.forEach(expectedPath => { + expect(outputPaths.some(outputPath => pathUtils.pathsEqual(outputPath, expectedPath))).toBe(true); + }); + } finally { + getCmsisPackRootSpy.mockRestore(); + } + }); }); diff --git a/src/solutions/language-features/reference-link-provider.ts b/src/solutions/language-features/reference-link-provider.ts index 42bb8af9..b3d0bc23 100644 --- a/src/solutions/language-features/reference-link-provider.ts +++ b/src/solutions/language-features/reference-link-provider.ts @@ -18,20 +18,25 @@ import { CancellationToken, DocumentLink, DocumentLinkProvider, Range, TextDocum import { parseYamlToCTreeItem } from '../../generic/tree-item-yaml-parser'; import { CTreeItem, ETreeItemKind, ITreeItem } from '../../generic/tree-item'; import type { SolutionManager } from '../solution-manager'; +import { getCmsisPackRoot } from '../../utils/path-utils'; /** - * Provide links for file references in solution and project files. + * Provide links for file references in solution, project, and layer files as well as for *.cbuild*.yml files. + * */ export class ReferenceLinkProvider implements DocumentLinkProvider { + private static readonly REFERENCE_ITEM_TAGS = ['file', 'layer', 'project', 'script', 'regions', + 'solution', 'csolution', 'cbuild', 'clayer', 'cproject', 'cbuild-run', 'cdefault']; + constructor( private readonly solutionManager: SolutionManager, + private readonly cbuildFile?: boolean, ) { } public provideDocumentLinks(textDocument: TextDocument, _token?: CancellationToken): DocumentLink[] { try { const topItem = parseYamlToCTreeItem(textDocument.getText(), textDocument.fileName); - return (topItem?.filterItems(item => this.isReferenceFileItem(item)) ?? [])?.flatMap((item): DocumentLink[] => { const documentLink = this.treeItemToDocumentLink(item, textDocument); return documentLink ? [documentLink] : []; @@ -47,15 +52,30 @@ export class ReferenceLinkProvider implements DocumentLinkProvider } protected isReferenceFileItem(item: ITreeItem): item is CTreeItem { + if (!item || item.getKind() !== ETreeItemKind.Scalar || !item.getText()) + return false; const tag = item.getTag(); - return !!tag && this.getReferenceItemTags().includes(tag); + if (!tag || !this.getReferenceItemTags().includes(tag)) { + return false; + } + if (this.cbuildFile) { + // in case of cbuild-idx.yml we can only expand links under 'cbuilds' section + if (tag === 'clayer' && !item.getParent('cbuilds')) { + return false; + } + // in case of cbuild-idx.yml 'project' under 'cbuilds' is just a name, not path + if (tag === 'project' && !!item.getParent('cbuilds')) { + return false; + } + } + return true; } protected getReferenceItemTags(): string[] { - return ['file', 'layer', 'project', 'script', 'regions']; + return ReferenceLinkProvider.REFERENCE_ITEM_TAGS; } - private treeItemToDocumentLink(item: ITreeItem | undefined, textDocument: TextDocument) : DocumentLink | undefined { + private treeItemToDocumentLink(item: ITreeItem, textDocument: TextDocument): DocumentLink | undefined { const uri = this.getUriFromItem(item); if (!uri) { return undefined; @@ -68,20 +88,23 @@ export class ReferenceLinkProvider implements DocumentLinkProvider } - private getUriFromItem(item?: ITreeItem): Uri | undefined { - if (!item || item.getKind() !== ETreeItemKind.Scalar) { - return undefined; - } + private getUriFromItem(item: ITreeItem): Uri | undefined { let text = item.getText(); if (!text) { return undefined; } - const rpcData = this.solutionManager.getRpcData(); - const context = this.getItemContext(item); - if (rpcData && context) { - text = rpcData.expandString(text, context); - } + // generated files can contain references to files in packs directory + if (text.startsWith('${CMSIS_PACK_ROOT}')) { + return Uri.file(text.replace('${CMSIS_PACK_ROOT}', getCmsisPackRoot())); + } + if (!this.cbuildFile) { // generated files have all sequences expanded + const rpcData = this.solutionManager.getRpcData(); + const context = this.getItemContext(item); + if (rpcData && context) { + text = rpcData.expandString(text, context); + } + } const resolvedPath = item.resolvePath(text); return resolvedPath ? Uri.file(resolvedPath) : undefined; } diff --git a/src/solutions/language-features/solution-language-features-provider.test.ts b/src/solutions/language-features/solution-language-features-provider.test.ts index 79bba181..64305ed8 100644 --- a/src/solutions/language-features/solution-language-features-provider.test.ts +++ b/src/solutions/language-features/solution-language-features-provider.test.ts @@ -15,19 +15,26 @@ */ import 'jest'; -import { SolutionLanguageFeaturesProvider, solutionFilesSelectors } from './solution-language-features-provider'; +import { SolutionLanguageFeaturesProvider, solutionBuildFilesSelectors, solutionFilesSelectors } from './solution-language-features-provider'; import { ReferenceLinkProvider } from './reference-link-provider'; import type { SolutionManager } from '../solution-manager'; describe('SolutionLanguageFeaturesProvider', () => { - it('registers a document link provider for solution, project, and layer files on activation', async () => { + it('registers document link providers for solution and build files on activation', async () => { const registerDocumentLinkProvider = jest.fn(); const solutionManager = {} as SolutionManager; const provider = new SolutionLanguageFeaturesProvider(solutionManager, { registerDocumentLinkProvider }); await provider.activate({ subscriptions: [] }); - expect(registerDocumentLinkProvider).toHaveBeenCalledWith(solutionFilesSelectors, expect.any(ReferenceLinkProvider)); - expect(registerDocumentLinkProvider).toHaveBeenCalledTimes(1); + expect(registerDocumentLinkProvider).toHaveBeenNthCalledWith(1, + solutionFilesSelectors, + expect.any(ReferenceLinkProvider), + ); + expect(registerDocumentLinkProvider).toHaveBeenNthCalledWith(2, + solutionBuildFilesSelectors, + expect.any(ReferenceLinkProvider), + ); + expect(registerDocumentLinkProvider).toHaveBeenCalledTimes(2); }); }); diff --git a/src/solutions/language-features/solution-language-features-provider.ts b/src/solutions/language-features/solution-language-features-provider.ts index c878e6fc..53be0998 100644 --- a/src/solutions/language-features/solution-language-features-provider.ts +++ b/src/solutions/language-features/solution-language-features-provider.ts @@ -26,6 +26,14 @@ export const solutionFilesSelectors: Readonly = [ { pattern: '**/*.cproject.yaml' }, { pattern: '**/*.clayer.yml' }, { pattern: '**/*.clayer.yaml' }, + { pattern: '**/*.cgen.yml' }, + { pattern: '**/*.cgen.yaml' }, +]; + +export const solutionBuildFilesSelectors: Readonly = [ + { pattern: '**/*.cbuild.yml' }, + { pattern: '**/*.cbuild-idx.yml' }, + { pattern: '**/*.cbuild-run.yml' }, ]; export class SolutionLanguageFeaturesProvider { @@ -36,7 +44,10 @@ export class SolutionLanguageFeaturesProvider { public async activate(context: Pick) { context.subscriptions.push( - this.languages.registerDocumentLinkProvider(solutionFilesSelectors, new ReferenceLinkProvider(this.solutionManager)), + this.languages.registerDocumentLinkProvider(solutionFilesSelectors, + new ReferenceLinkProvider(this.solutionManager)), + this.languages.registerDocumentLinkProvider(solutionBuildFilesSelectors, + new ReferenceLinkProvider(this.solutionManager, true)), ); } } diff --git a/src/views/solution-outline/tree-structure/solution-outline-file-item.ts b/src/views/solution-outline/tree-structure/solution-outline-file-item.ts index 4c95c12f..ca8a12dd 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-file-item.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-file-item.ts @@ -42,7 +42,7 @@ export class FileItemBuilder extends SolutionOutlineItemBuilder { return; } - const hasCmsisPackRoot = fileValue.indexOf('${CMSIS_PACK_ROOT}') !== -1; + const hasCmsisPackRoot = fileValue.startsWith('${CMSIS_PACK_ROOT}'); const resolvedFilePath = this.resolveFilePath(hasCmsisPackRoot, fileValue); const fileBaseName = path.basename(resolvedFilePath); const resourcePath = hasCmsisPackRoot ? resolvedFilePath : f.resolvePath(resolvedFilePath);