Skip to content

Commit 6341825

Browse files
authored
Merge branch 'main' into update-node-version
2 parents 22f0053 + d16e705 commit 6341825

11 files changed

Lines changed: 197 additions & 161 deletions

src/desktop/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export const activate = async (context: ExtensionContext): Promise<CsolutionExte
248248
const cmsisCommands = new CmsisCommands(configurationProvider, commandsProvider, solutionManager, debugProvider, serialMonitorExtension);
249249
const buildStopCommand = new BuildStopCommand(commandsProvider, buildTaskProvider);
250250
const configurationWizardView = new ConfWizWebview(context);
251-
const solutionLanguageFeatures = new SolutionLanguageFeaturesProvider();
251+
const solutionLanguageFeatures = new SolutionLanguageFeaturesProvider(solutionManager);
252252
const packInstallCommands = new PackInstallCommands(commandsProvider, cmsisToolboxManager, outputChannelProvider);
253253
const protocolHandler = new ProtocolHandler(cmsisToolboxManager, outputChannelProvider);
254254

src/generic/tree-item-yaml-parser.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ describe('parseYamlToCTreeItem', () => {
5959
expect(yamlString).toEqual('scalar\n');
6060
});
6161

62+
it('stores scalar range as start and value-end offsets', async () => {
63+
const input = 'key: value\n';
64+
const root = parseYamlToCTreeItem(input, './dummyFile');
65+
const valueNode = root?.getChild('key');
66+
67+
expect(valueNode).toBeDefined();
68+
expect(valueNode?.range).toEqual([5, 10]);
69+
});
70+
6271
it('test parseYamlToCTreeItem parsing sequence', async () => {
6372
const prime = dedent`
6473
# document comment

src/generic/tree-item-yaml-parser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ export class GenericTreeItemYamlParser<T extends ITreeItem<T>> extends GenericTr
131131
protected parseScalar(node: YAML.Scalar, treeItem: ITreeItem<T>): void {
132132
treeItem.setProperty('scalarType', node.type);
133133
treeItem.setKind(ETreeItemKind.Scalar);
134+
if (node.range) {
135+
treeItem.range = [node.range[0], node.range[1]];
136+
}
134137
if (node.value !== null && node.value !== undefined) {
135138
const text = node.source ? node.source : node.toString();
136139
treeItem.setText(text);

src/generic/tree-item.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,33 @@ describe('CTreeItem', () => {
126126
expect(data.getKind()).toEqual(ETreeItemKind.Scalar);
127127
});
128128

129+
it('stores and retrieves range via range get/set', () => {
130+
const item = new CTreeItem('root');
131+
const range: [number, number] = [1, 4];
132+
133+
expect(item.range).toBeUndefined();
134+
135+
item.range = range;
136+
expect(item.range).toEqual([1, 4]);
137+
138+
item.range = undefined;
139+
expect(item.range).toBeUndefined();
140+
});
141+
142+
it('filters items across the entire tree using a predicate', () => {
143+
const root = new CTreeItem('root');
144+
root.createChild('group').createChild('file').setText('first.txt');
145+
root.getChild('group')?.createChild('nested')?.createChild('file').setText('second.txt');
146+
root.createChild('file').setText('third.txt');
147+
root.createChild('other').setText('ignore.txt');
148+
149+
const fileTexts = root
150+
.filterItems(item => item.getTag() === 'file')
151+
.map(item => item.getText());
152+
153+
expect(fileTexts).toEqual(['first.txt', 'second.txt', 'third.txt']);
154+
});
155+
129156
it('toObject converts item tree to plain object', async () => {
130157
const myObject = {
131158
'target-types': [{

src/generic/tree-item.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { CAttributedItem, IAttributedItem } from './attributed-item';
1818

19+
export type SourceRange = [number, number];
1920

2021
/** An enum to describe ITreeItem kind */
2122
export enum ETreeItemKind {
@@ -58,6 +59,12 @@ export interface ITreeItem<T extends ITreeItem<T>> extends IAttributedItem {
5859
*/
5960
get isPlain(): boolean;
6061

62+
/** Returns the item's source range if assigned */
63+
get range(): SourceRange | undefined;
64+
65+
/** Sets or clears the item's source range */
66+
set range(range: SourceRange | undefined);
67+
6168
/** Returns item's parent matching tag
6269
* @param tag optional parent's tag
6370
* @returns parent item or undefined
@@ -197,6 +204,13 @@ export interface ITreeItem<T extends ITreeItem<T>> extends IAttributedItem {
197204
*/
198205
childAtIndex(index: number): ITreeItem<T> | undefined;
199206

207+
/**
208+
* Traverses this item and all descendants depth-first and returns items matching the predicate.
209+
* @param predicate match function applied to each visited item, including this item
210+
* @returns array of matching items in depth-first order
211+
*/
212+
filterItems(predicate: (item: ITreeItem<T>) => boolean): ITreeItem<T>[];
213+
200214
/**
201215
* Adds a child and returns the supplied item
202216
* @param c child to add
@@ -398,6 +412,14 @@ export class GenericTreeItem<T extends GenericTreeItem<T>> extends CAttributedIt
398412
return this;
399413
}
400414

415+
get range(): SourceRange | undefined {
416+
return this.getProperty('range') as SourceRange | undefined;
417+
}
418+
419+
set range(range: SourceRange | undefined) {
420+
this.setProperty('range', range);
421+
}
422+
401423
getKind(): ETreeItemKind {
402424
let kind = this.data.kind as ETreeItemKind;
403425
if (kind) {
@@ -576,6 +598,21 @@ export class GenericTreeItem<T extends GenericTreeItem<T>> extends CAttributedIt
576598
return this.getChildren().at(index);
577599
}
578600

601+
filterItems(predicate: (item: ITreeItem<T>) => boolean): ITreeItem<T>[] {
602+
const matches: ITreeItem<T>[] = [];
603+
const visit = (item: ITreeItem<T>) => {
604+
if (predicate(item)) {
605+
matches.push(item);
606+
}
607+
for (const child of item.getChildren()) {
608+
visit(child);
609+
}
610+
};
611+
612+
visit(this);
613+
return matches;
614+
}
615+
579616
findChild(tags: string[]): ITreeItem<T> | undefined {
580617
let c: ITreeItem<T> | undefined = this;
581618
for (const tag of tags) {

src/solutions/language-features/reference-link-provider.test.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
import 'jest';
1818
import { textDocumentFactory } from '../../vscode-api/text-document.factories';
19-
import { createReferenceLinkProvider } from './reference-link-provider';
19+
import { ReferenceLinkProvider } from './reference-link-provider';
2020
import { URI as Uri, Utils as UriUtils } from 'vscode-uri';
2121
import * as path from 'path';
22+
import { solutionManagerFactory } from '../solution-manager.factories';
23+
import { SolutionRpcDataMock } from '../solution-rpc-data.factory';
2224

23-
describe('createReferenceLinkProvider', () => {
25+
describe('ReferenceLinkProvider', () => {
2426
it('returns no links if the document cannot be parsed', () => {
2527
const unparsableDoc = `
2628
notMe:
@@ -33,30 +35,41 @@ describe('createReferenceLinkProvider', () => {
3335
const textDocument = textDocumentFactory();
3436
textDocument.getText.mockReturnValue(unparsableDoc);
3537

36-
const provider = createReferenceLinkProvider({ referenceNode: 'sublayer', parentNode: 'layer', listNode: 'sublayers' });
38+
const provider = new ReferenceLinkProvider(solutionManagerFactory());
3739
const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() });
3840

3941
expect(output).toEqual([]);
4042
});
4143

4244
it('returns links for link nodes that can be parsed', () => {
4345
const parsableDoc = `
44-
layer:
45-
sublayers:
46-
- sublayer: ./file1.sublayer.yml
47-
- sublayer: ./file2.sublayer.yml
46+
project:
47+
files:
48+
- file: ./file1.c
49+
linker:
50+
- script: ./my.sct
51+
layers:
52+
- layer: ./$Board-layer$
4853
`;
4954

50-
const documentUri = Uri.file(path.join(__dirname, 'my.clayer.yml'));
51-
const textDocument = textDocumentFactory({ uri: documentUri });
55+
const documentFileName = path.join(__dirname, 'my.cproject.yml');
56+
const documentUri = Uri.file(documentFileName);
57+
const textDocument = textDocumentFactory({ uri: documentUri, fileName: documentFileName });
5258
textDocument.getText.mockReturnValue(parsableDoc);
5359

54-
const provider = createReferenceLinkProvider({ referenceNode: 'sublayer', parentNode: 'layer', listNode: 'sublayers' });
60+
const solutionManager = solutionManagerFactory();
61+
const csolution = solutionManager.getCsolution();
62+
jest.spyOn(csolution!, 'actionContext', 'get').mockReturnValue('my.Build+Target');
63+
const rpcData = solutionManager.getRpcData() as SolutionRpcDataMock;
64+
rpcData.seedVariables('my.Build+Target', { 'Board-layer': 'mylayer.clayer.yml' });
65+
66+
const provider = new ReferenceLinkProvider(solutionManager);
5567
const output = provider.provideDocumentLinks(textDocument, { isCancellationRequested: false, onCancellationRequested: jest.fn() });
5668

5769
expect(output).toEqual([
58-
expect.objectContaining({ target: UriUtils.joinPath(documentUri, '../file1.sublayer.yml') }),
59-
expect.objectContaining({ target: UriUtils.joinPath(documentUri, '../file2.sublayer.yml') }),
70+
expect.objectContaining({ target: expect.objectContaining({ fsPath: UriUtils.joinPath(documentUri, '../file1.c').fsPath }) }),
71+
expect.objectContaining({ target: expect.objectContaining({ fsPath: UriUtils.joinPath(documentUri, '../my.sct').fsPath }) }),
72+
expect.objectContaining({ target: expect.objectContaining({ fsPath: UriUtils.joinPath(documentUri, '../mylayer.clayer.yml').fsPath }) }),
6073
]);
6174
});
6275
});

src/solutions/language-features/reference-link-provider.ts

Lines changed: 80 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,65 +14,95 @@
1414
* limitations under the License.
1515
*/
1616

17-
import * as YAML from 'yaml';
18-
import { DocumentLink, DocumentLinkProvider, TextDocument, Uri } from 'vscode';
19-
import { readMapFromMap, readSeqFromMap, requireMap } from '../parsing/yaml-file-parsing';
20-
import { rangeFromYamlNode } from './yaml-range';
17+
import { CancellationToken, DocumentLink, DocumentLinkProvider, Range, TextDocument, Uri } from 'vscode';
18+
import { parseYamlToCTreeItem } from '../../generic/tree-item-yaml-parser';
19+
import { CTreeItem, ETreeItemKind, ITreeItem } from '../../generic/tree-item';
20+
import type { SolutionManager } from '../solution-manager';
2121

22-
export type ReferenceLinkProviderOptions = {
23-
/**
24-
* Name of the root node for the YAML file, e.g., solution.
25-
*/
26-
parentNode: string;
22+
/**
23+
* Provide links for file references in solution and project files.
24+
*/
25+
export class ReferenceLinkProvider implements DocumentLinkProvider<DocumentLink> {
26+
constructor(
27+
private readonly solutionManager: SolutionManager,
28+
) {
29+
}
2730

28-
/**
29-
* Name of the node containing the list of linkable references, e.g., projects.
30-
*/
31-
listNode: string;
31+
public provideDocumentLinks(textDocument: TextDocument, _token?: CancellationToken): DocumentLink[] {
32+
try {
33+
const topItem = parseYamlToCTreeItem(textDocument.getText(), textDocument.fileName);
3234

33-
/**
34-
* Name of the node on the linkable reference, e.g., project.
35-
*/
36-
referenceNode: string;
37-
}
35+
return (topItem?.filterItems(item => this.isReferenceFileItem(item)) ?? [])?.flatMap((item): DocumentLink[] => {
36+
const documentLink = this.treeItemToDocumentLink(item, textDocument);
37+
return documentLink ? [documentLink] : [];
38+
}) ?? [];
39+
} catch {
40+
// If we can't parse the document, we can't provide links
41+
return [];
42+
}
43+
}
3844

39-
const projectNodeToDocumentLink = (
40-
textDocument: TextDocument,
41-
options: ReferenceLinkProviderOptions,
42-
projectNode: YAML.YAMLMap,
43-
): DocumentLink | undefined => {
44-
const projectScalar = projectNode.get(options.referenceNode, true);
45+
public resolveDocumentLink(link: DocumentLink): DocumentLink {
46+
return link;
47+
}
4548

46-
if (projectScalar && typeof projectScalar.value === 'string') {
49+
protected isReferenceFileItem(item: ITreeItem<CTreeItem>): item is CTreeItem {
50+
const tag = item.getTag();
51+
return !!tag && this.getReferenceItemTags().includes(tag);
52+
}
53+
54+
protected getReferenceItemTags(): string[] {
55+
return ['file', 'layer', 'project', 'script', 'regions'];
56+
}
57+
58+
private treeItemToDocumentLink(item: ITreeItem<CTreeItem> | undefined, textDocument: TextDocument) : DocumentLink | undefined {
59+
const uri = this.getUriFromItem(item);
60+
if (!uri) {
61+
return undefined;
62+
}
63+
const range = this.rangeFromItem(item, textDocument);
4764
return {
48-
range: rangeFromYamlNode(textDocument, projectScalar),
49-
target: Uri.joinPath(textDocument.uri, '..', projectScalar.value),
65+
range,
66+
target: uri,
5067
};
5168
}
5269

53-
return undefined;
54-
};
5570

56-
/**
57-
* Provide links for file references in solution and project files.
58-
*/
59-
export const createReferenceLinkProvider = (options: ReferenceLinkProviderOptions): DocumentLinkProvider<DocumentLink> => ({
60-
provideDocumentLinks: (textDocument: TextDocument): DocumentLink[] => {
61-
try {
62-
const yamlDocument = YAML.parseDocument(textDocument.getText());
63-
const root = requireMap(yamlDocument.contents);
64-
const parent = readMapFromMap(options.parentNode)(root);
65-
const list = readSeqFromMap(options.listNode)(parent);
66-
const itemMaps = list.items.filter(YAML.isMap);
71+
private getUriFromItem(item?: ITreeItem<CTreeItem>): Uri | undefined {
72+
if (!item || item.getKind() !== ETreeItemKind.Scalar) {
73+
return undefined;
74+
}
75+
let text = item.getText();
76+
if (!text) {
77+
return undefined;
78+
}
79+
const rpcData = this.solutionManager.getRpcData();
80+
const context = this.getItemContext(item);
81+
if (rpcData && context) {
82+
text = rpcData.expandString(text, context);
83+
}
6784

68-
return itemMaps.flatMap((projectNode): DocumentLink[] => {
69-
const documentLink = projectNodeToDocumentLink(textDocument, options, projectNode);
70-
return documentLink ? [documentLink] : [];
71-
});
72-
} catch {
73-
// If we can't parse the document, we can't provide links
74-
return [];
85+
const resolvedPath = item.resolvePath(text);
86+
return resolvedPath ? Uri.file(resolvedPath) : undefined;
87+
}
88+
89+
private rangeFromItem(item: ITreeItem<CTreeItem> | undefined, textDocument: TextDocument): Range {
90+
return new Range(
91+
textDocument.positionAt(item?.range?.[0] ?? 0),
92+
textDocument.positionAt(item?.range?.[1] ?? 0),
93+
);
94+
}
95+
96+
private getItemContext(item: ITreeItem<CTreeItem>): string | undefined {
97+
const csolution = this.solutionManager.getCsolution();
98+
if (!csolution) {
99+
return undefined;
100+
}
101+
const rootFileName = item.rootFileName;
102+
let context = undefined;
103+
if (rootFileName.includes('.cproject.y')) {
104+
context = csolution.getContextDescriptor(rootFileName)?.displayName;
75105
}
76-
},
77-
resolveDocumentLink: (link: DocumentLink): DocumentLink => link,
78-
});
106+
return context ?? csolution.actionContext;
107+
}
108+
}

src/solutions/language-features/solution-language-features-provider.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,19 @@
1515
*/
1616

1717
import 'jest';
18-
import { SolutionLanguageFeaturesProvider, projectSelectors, solutionSelectors } from './solution-language-features-provider';
19-
import type { DocumentLink, DocumentLinkProvider } from 'vscode';
18+
import { SolutionLanguageFeaturesProvider, solutionFilesSelectors } from './solution-language-features-provider';
19+
import { ReferenceLinkProvider } from './reference-link-provider';
20+
import type { SolutionManager } from '../solution-manager';
2021

2122
describe('SolutionLanguageFeaturesProvider', () => {
22-
it('registers document link providers for solution and project files on activation', async () => {
23+
it('registers a document link provider for solution, project, and layer files on activation', async () => {
2324
const registerDocumentLinkProvider = jest.fn();
24-
const provider = new SolutionLanguageFeaturesProvider({ registerDocumentLinkProvider });
25+
const solutionManager = {} as SolutionManager;
26+
const provider = new SolutionLanguageFeaturesProvider(solutionManager, { registerDocumentLinkProvider });
2527

2628
await provider.activate({ subscriptions: [] });
2729

28-
const expectedLinkProvider: DocumentLinkProvider<DocumentLink> = {
29-
resolveDocumentLink: expect.any(Function),
30-
provideDocumentLinks: expect.any(Function),
31-
};
32-
33-
expect(registerDocumentLinkProvider).toHaveBeenCalledWith(solutionSelectors, expectedLinkProvider);
34-
expect(registerDocumentLinkProvider).toHaveBeenCalledWith(projectSelectors, expectedLinkProvider);
30+
expect(registerDocumentLinkProvider).toHaveBeenCalledWith(solutionFilesSelectors, expect.any(ReferenceLinkProvider));
31+
expect(registerDocumentLinkProvider).toHaveBeenCalledTimes(1);
3532
});
3633
});

0 commit comments

Comments
 (0)