Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/solutions/language-features/reference-link-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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([
Expand All @@ -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();
}
});
});
51 changes: 37 additions & 14 deletions src/solutions/language-features/reference-link-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocumentLink> {
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,
) {
Comment thread
edriouk marked this conversation as resolved.
}

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] : [];
Expand All @@ -47,15 +52,30 @@ export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink>
}

protected isReferenceFileItem(item: ITreeItem<CTreeItem>): 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<CTreeItem> | undefined, textDocument: TextDocument) : DocumentLink | undefined {
private treeItemToDocumentLink(item: ITreeItem<CTreeItem>, textDocument: TextDocument): DocumentLink | undefined {
const uri = this.getUriFromItem(item);
if (!uri) {
return undefined;
Expand All @@ -68,20 +88,23 @@ export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink>
}


private getUriFromItem(item?: ITreeItem<CTreeItem>): Uri | undefined {
if (!item || item.getKind() !== ETreeItemKind.Scalar) {
return undefined;
}
private getUriFromItem(item: ITreeItem<CTreeItem>): 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);
Comment thread
edriouk marked this conversation as resolved.
}
}
const resolvedPath = item.resolvePath(text);
return resolvedPath ? Uri.file(resolvedPath) : undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ export const solutionFilesSelectors: Readonly<DocumentSelector> = [
{ pattern: '**/*.cproject.yaml' },
{ pattern: '**/*.clayer.yml' },
{ pattern: '**/*.clayer.yaml' },
{ pattern: '**/*.cgen.yml' },
{ pattern: '**/*.cgen.yaml' },
];

export const solutionBuildFilesSelectors: Readonly<DocumentSelector> = [
{ pattern: '**/*.cbuild.yml' },
{ pattern: '**/*.cbuild-idx.yml' },
{ pattern: '**/*.cbuild-run.yml' },
Comment thread
edriouk marked this conversation as resolved.
];

export class SolutionLanguageFeaturesProvider {
Expand All @@ -36,7 +44,10 @@ export class SolutionLanguageFeaturesProvider {

public async activate(context: Pick<ExtensionContext, 'subscriptions'>) {
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)),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading