From e6bfcaf7bb62c3b744bf9596eab1732341a4dd6c Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Fri, 13 Mar 2026 16:32:56 +0100 Subject: [PATCH 1/3] Solution outline: use RPC data --- src/solutions/solution-manager.factories.ts | 6 +-- src/solutions/solution-manager.ts | 7 ++++ src/solutions/solution-rpc-data.factory.ts | 28 ++++++++++++++ .../solution-outline/solution-outline.ts | 9 ++--- .../solution-outline-file-item.test.ts | 8 ++-- .../solution-outline-file-item.ts | 7 ++-- .../solution-outline-hardware-item.test.ts | 12 +++--- .../solution-outline-hardware-item.ts | 8 ++-- .../solution-outline-item-builder.ts | 33 ++++++++++++++++ .../solution-outline-project-items.ts | 18 ++++----- .../solution-outline-tree.test.ts | 38 +++++++++---------- .../tree-structure/solution-outline-tree.ts | 23 +++++------ 12 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 src/solutions/solution-rpc-data.factory.ts create mode 100644 src/views/solution-outline/tree-structure/solution-outline-item-builder.ts diff --git a/src/solutions/solution-manager.factories.ts b/src/solutions/solution-manager.factories.ts index c8512d471..61572e237 100644 --- a/src/solutions/solution-manager.factories.ts +++ b/src/solutions/solution-manager.factories.ts @@ -21,15 +21,15 @@ import { faker } from '@faker-js/faker'; import path from 'path'; import { csolutionFactory } from './csolution.factory'; import { Severity } from './constants'; +import { solutionRpcDataFactory } from './solution-rpc-data.factory'; export type MockSolutionManager = jest.Mocked> & { fireOnDidChangeLoadState: ReturnType }; export const idleSolutionLoadStateFactory = makeFactory({ solutionPath: () => undefined, - activated: () => false, + activated: () => undefined, loaded: () => undefined, converted: () => undefined, - activated: () => undefined, }); export const activeSolutionLoadStateFactory = makeFactory({ @@ -37,7 +37,6 @@ export const activeSolutionLoadStateFactory = makeFactory({ activated: () => true, loaded: () => undefined, converted: () => undefined, - activated: () => true, }); const fireOnDidChangeLoadState = (emitter: vscode.EventEmitter) => { @@ -52,6 +51,7 @@ const fireOnDidChangeLoadState = (emitter: vscode.EventEmitter({ loadState: () => idleSolutionLoadStateFactory(), getCsolution: () => jest.fn().mockReturnValue(csolutionFactory()), + getRpcData: () => jest.fn().mockReturnValue(solutionRpcDataFactory()), onDidChangeLoadStateEmitter: () => new vscode.EventEmitter(), onDidChangeLoadState: (r) => jest.fn(r.onDidChangeLoadStateEmitter!.event), onLoadedBuildFilesEmitter: () => new vscode.EventEmitter<[Severity, boolean]>(), diff --git a/src/solutions/solution-manager.ts b/src/solutions/solution-manager.ts index dfe148d6b..218a4c6b0 100644 --- a/src/solutions/solution-manager.ts +++ b/src/solutions/solution-manager.ts @@ -57,6 +57,8 @@ export interface SolutionManager { readonly getCsolution: () => CSolution | undefined; + readonly getRpcData: () => SolutionRpcData | undefined; + readonly onDidChangeLoadState: vscode.Event; readonly onLoadedBuildFiles: vscode.Event<[Severity, boolean]>; @@ -119,6 +121,11 @@ export class SolutionManagerImpl implements SolutionManager { return this.csolution; } + public getRpcData(): SolutionRpcData | undefined { + return this.rpcData; + } + + public get loadState(): SolutionLoadState { return this._loadState; } diff --git a/src/solutions/solution-rpc-data.factory.ts b/src/solutions/solution-rpc-data.factory.ts new file mode 100644 index 000000000..3f38a7392 --- /dev/null +++ b/src/solutions/solution-rpc-data.factory.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { csolutionServiceFactory } from '../json-rpc/csolution-rpc-client.factory'; +import { SolutionRpcData } from './solution-rpc-data'; + +export type SolutionRpcDataMock = jest.MockedObject; + +export function solutionRpcDataFactory(options: Partial = {}): SolutionRpcDataMock { + const solutionRpcDataMock = jest.mocked(new SolutionRpcData(csolutionServiceFactory()), { shallow: true }); + + Object.assign(solutionRpcDataMock, options); + + return solutionRpcDataMock; +} diff --git a/src/views/solution-outline/solution-outline.ts b/src/views/solution-outline/solution-outline.ts index aa489c3e2..759a26017 100644 --- a/src/views/solution-outline/solution-outline.ts +++ b/src/views/solution-outline/solution-outline.ts @@ -21,7 +21,6 @@ import { TreeViewProvider } from './treeview-provider'; import { CsolutionGlobalState, GlobalState } from '../../vscode-api/global-state'; import { SolutionOutlineTree } from './tree-structure/solution-outline-tree'; import { COutlineItem } from './tree-structure/solution-outline-item'; -import { CSolution } from '../../solutions/csolution'; import { TreeViewFileDecorationProvider } from './treeview-decoration-provider'; export class SolutionOutlineView { @@ -34,7 +33,6 @@ export class SolutionOutlineView { private readonly treeViewProvider: TreeViewProvider, private readonly globalStateProvider: GlobalState, private readonly treeViewFileDecorationProvider: TreeViewFileDecorationProvider, - private readonly solutionOutlineTree = new SolutionOutlineTree() ) { } public async activate(context: Pick): Promise { @@ -75,15 +73,16 @@ export class SolutionOutlineView { this.treeViewProvider.setDescription(''); this.treeViewProvider.setTitle(''); } - this.createTree(loadState, csolution, thisTreeUpdateNumber); + this.createTree(loadState, thisTreeUpdateNumber); } - private createTree(loadState: SolutionLoadState, csolution: CSolution | undefined, thisTreeUpdateNumber: number) { + private createTree(loadState: SolutionLoadState, thisTreeUpdateNumber: number) { if (loadState.solutionPath) { if (this.treeUpdateCount !== thisTreeUpdateNumber) { return; } - const tree = this.solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(this.solutionManager.getCsolution(), this.solutionManager.getRpcData()); + const tree = solutionOutlineTree.createTree(); this.treeViewProvider.updateTree(tree); this.treeViewFileDecorationProvider.setTreeRoot(tree); } else if (!loadState.solutionPath) { diff --git a/src/views/solution-outline/tree-structure/solution-outline-file-item.test.ts b/src/views/solution-outline/tree-structure/solution-outline-file-item.test.ts index 1fc4fd79e..355c06157 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-file-item.test.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-file-item.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FileItem } from './solution-outline-file-item'; +import { FileItemBuilder } from './solution-outline-file-item'; import { parseYamlToCTreeItem } from '../../../generic/tree-item-yaml-parser'; import fs from 'fs'; import os from 'os'; @@ -23,19 +23,19 @@ import { COutlineItem } from './solution-outline-item'; describe('FileItem', () => { - let fileItem: FileItem; + let fileItem: FileItemBuilder; let projectDir: string; let cSolFile: string; let componentNode: COutlineItem; beforeEach(async () => { - fileItem = new FileItem(); + fileItem = new FileItemBuilder(); const tmpDir = os.tmpdir(); projectDir = fs.mkdtempSync(path.join(tmpDir, 'myProject')); cSolFile = `${projectDir}/Blinky.csolution.yml`; - fileItem = new FileItem(); + fileItem = new FileItemBuilder(); componentNode = new COutlineItem('component'); componentNode.setTag('component'); 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 cdc24ea2c..1ee7761ea 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 @@ -21,10 +21,9 @@ import { COutlineItem } from './solution-outline-item'; import { getStatusTooltip, setContextMenuAttributes, setHeaderContext, setMergeDescription, setMergeFileContext } from './solution-outline-utils'; import { getCmsisPackRoot } from '../../../utils/path-utils'; import { matchesContext } from '../../../utils/context-utils'; +import { SolutionOutlineItemBuilder } from './solution-outline-item-builder'; -export class FileItem { - constructor(private readonly activeContext?: string) { } - +export class FileItemBuilder extends SolutionOutlineItemBuilder { public createFileNodes(cgroupItem: COutlineItem, files: ITreeItem[], docs?: ITreeItem[], isApi?: boolean, addContextMenu?: boolean) { for (const f of files) { const category = f.getValue('category'); @@ -53,7 +52,7 @@ export class FileItem { const cfileItem = this.createFileItem(cgroupItem, fileBaseName, resourcePath, description); // Check if file is excluded based on context restrictions - if (this.activeContext && !matchesContext(f, this.activeContext)) { + if (this.context && !matchesContext(f, this.context)) { cfileItem.setAttribute('excluded', '1'); } diff --git a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.test.ts b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.test.ts index 015202653..e16cdd04f 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.test.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.test.ts @@ -18,19 +18,17 @@ import { CTreeItem } from '../../../generic/tree-item'; import { ETextFileResult } from '../../../generic/text-file'; import { parseYamlToCTreeItem } from '../../../generic/tree-item-yaml-parser'; import { CSolution } from '../../../solutions/csolution'; -import { HardwareItem } from './solution-outline-hardware-item'; +import { HardwareItemBuilder } from './solution-outline-hardware-item'; import { TestDataHandler } from '../../../__test__/test-data'; import path from 'node:path'; -describe('HardwareItem', () => { - let hwItem: HardwareItem; +describe('HardwareItemBuilder', () => { let cSolFile: string; let tmpSolutionDir: string; const testDataHandler = new TestDataHandler(); beforeAll(async () => { tmpSolutionDir = testDataHandler.copyTestDataToTmp('solutions'); - hwItem = new HardwareItem(); cSolFile = path.join(tmpSolutionDir, 'USBD', 'USB_Device.csolution.yml'); }); @@ -54,7 +52,8 @@ describe('HardwareItem', () => { expect(loadResult).toEqual(ETextFileResult.Success); const want = 'Hello+CS300.dbgconf'; - const hwNodes = hwItem.createHardwareNodes(csolution, topChild as CTreeItem); + const hwItemBuilder = new HardwareItemBuilder(csolution); + const hwNodes = hwItemBuilder.createHardwareNodes(csolution, topChild as CTreeItem); const node = hwNodes.get('STM32U585AIIx'); let got: string = ''; @@ -84,7 +83,8 @@ describe('HardwareItem', () => { const loadResult = await csolution.load(cSolFile); expect(loadResult).toEqual(ETextFileResult.Success); - const hwNodes = hwItem.createHardwareNodes(csolution, topChild as CTreeItem); + const hwItemBuilder = new HardwareItemBuilder(csolution); + const hwNodes = hwItemBuilder.createHardwareNodes(csolution, topChild as CTreeItem); const node = hwNodes.get('STM32U585AIIx'); // find dbgConfFile child node diff --git a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts index c6b41ffae..ef3949835 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts @@ -18,11 +18,11 @@ import { COutlineItem } from './solution-outline-item'; import { buildDocFilePath, isWebAddress } from '../../../util'; import { CTreeItem, ITreeItem } from '../../../generic/tree-item'; import path from 'path'; -import { FileItem } from './solution-outline-file-item'; +import { FileItemBuilder } from './solution-outline-file-item'; import { CSolution } from '../../../solutions/csolution'; +import { SolutionOutlineItemBuilder } from './solution-outline-item-builder'; -export class HardwareItem { - constructor() { } +export class HardwareItemBuilder extends SolutionOutlineItemBuilder { public createHardwareNodes(csolution: CSolution, cbuild?: CTreeItem): Map { const hardwareTreeNodes = new Map(); @@ -173,7 +173,7 @@ export class HardwareItem { // overwrite tootltip dbgconfFileItem.setAttribute('tooltip', ''); - const fileItem = new FileItem(); + const fileItem = new FileItemBuilder(); fileItem.addMergeFeature(file, dbgconfFileItem, { skipValidation: true, localPathOverride: filePath }); } } diff --git a/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts new file mode 100644 index 000000000..d6ab0d7e5 --- /dev/null +++ b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CSolution } from '../../../solutions/csolution'; +import { SolutionRpcData } from '../../../solutions/solution-rpc-data'; + +export class SolutionOutlineItemBuilder { + constructor( + protected csolution?: CSolution, + protected rpcData?: SolutionRpcData, + protected context?: string, + ) { } + + public expandAccessSequences(str: string | undefined) { + if (!this.rpcData || !this.context || !str) { + return str; + } + return this.rpcData.expandString(str, this.context); + } +} \ No newline at end of file diff --git a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts index abc2646f8..8fcf42675 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts @@ -17,29 +17,25 @@ import path from 'path'; import { CTreeItem, ITreeItem } from '../../../generic/tree-item'; import { buildDocFilePath } from '../../../util'; -import { FileItem } from './solution-outline-file-item'; +import { FileItemBuilder } from './solution-outline-file-item'; import { COutlineItem } from './solution-outline-item'; import * as manifest from '../../../manifest'; import { CSolution } from '../../../solutions/csolution'; import { getMapFilePath, getStatusTooltip, setDocContext, setHeaderContext, setLinkerContext, setMergeDescription, setMergeUpdate } from './solution-outline-utils'; import { CProjectYamlFile } from '../../../solutions/files/cproject-yaml-file'; +import { SolutionOutlineItemBuilder } from './solution-outline-item-builder'; -export class ProjectItemsBuilder { +export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { private _lastPrioritizedComponentList: COutlineItem[] = []; public get lastPrioritizedComponentList(): COutlineItem[] { return this._lastPrioritizedComponentList; } - - csolution?: CSolution; - private activeContext?: string; - constructor() { } - public addProjectChildren(csolution: CSolution | undefined, cprojectItem: COutlineItem, cprojectFile: CProjectYamlFile, cbuild?: CTreeItem): void { this.csolution = csolution; // Get context from cbuild (contains full context: PROJECT.BUILD-TYPE+TARGET-TYPE) // Falls back to actionContext if cbuild is not available - this.activeContext = cbuild?.getValueAsString('context') ?? csolution?.actionContext; + this.context = cbuild?.getValueAsString('context') ?? csolution?.actionContext; const cproject = cprojectFile.topItem; if (!cproject) { return; @@ -136,7 +132,7 @@ export class ProjectItemsBuilder { private createGroupChildren(cgroupItem: COutlineItem, group: CTreeItem): void { const isRegularGroup = !group.getTag() || group.getTag() === '-'; - const fileTreeItem = new FileItem(this.activeContext); + const fileTreeItem = new FileItemBuilder(this.csolution, this.rpcData, this.context); if (isRegularGroup) { this.createGroupTree(cgroupItem, group, cgroupItem.getAttribute('groupPath') ?? ''); @@ -360,7 +356,7 @@ export class ProjectItemsBuilder { const docs: ITreeItem[] = []; const apiFiles = apiChild.getGrandChildren('files'); - const fileTreeItem = new FileItem(this.activeContext); + const fileTreeItem = new FileItemBuilder(this.csolution, this.rpcData, this.context); fileTreeItem.createFileNodes(node, apiFiles, docs, true); const fileNodes = node.getChildren(); @@ -372,7 +368,7 @@ export class ProjectItemsBuilder { private addComponentData(node: COutlineItem, component: ITreeItem, cbuild: CTreeItem) { // add files const docs: ITreeItem[] = []; - const fileTreeItem = new FileItem(this.activeContext); + const fileTreeItem = new FileItemBuilder(this.csolution, this.rpcData, this.context); const componentFiles = component.getGrandChildren('files'); fileTreeItem.createFileNodes(node, componentFiles, docs); diff --git a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts index d9eb4d61b..9d046414a 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts @@ -93,8 +93,8 @@ describe('CSolution', () => { } // get results from tree - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); const got = new Map(); let count = 1; @@ -108,7 +108,7 @@ describe('CSolution', () => { it('creates error node when csolution is undefined', () => { const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(undefined); + const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; expect(children).toHaveLength(1); @@ -124,8 +124,8 @@ describe('CSolution', () => { ['dummy.cbuild.yml', new CTreeItem('cbuild')], ]); - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; expect(children).toHaveLength(1); @@ -138,8 +138,8 @@ describe('CSolution', () => { it('creates error node when no projects can be loaded', () => { const csolution = new CSolution(); - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; expect(children).toHaveLength(1); @@ -161,8 +161,8 @@ describe('CSolution', () => { let loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(); - let tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + let tree = solutionOutlineTree.createTree(); let topItems = tree.getChildren(); expect(topItems.length).toBe(4); // device, board and two projects @@ -201,14 +201,14 @@ describe('CSolution', () => { let res = await dumpOutline(tree, 'USBD', 'CmsisViewTreeDmp.txt','CmsisViewTreeRef.txt'); expect(res.dump).toEqual(res.ref); - // emulate removing the project from target set b modifying cbuild-idx.yml + // emulate removing the project from target set by modifying cbuild-idx.yml const cbuilds = csolution.cbuildIdxFile.topItem?.getChild('cbuilds'); const cbuildToRemove = cbuilds?.getChildByValue('project','HID'); cbuilds?.removeChild(cbuildToRemove); csolution.cbuildIdxFile.save(); loadResult = await csolution.loadBuildFiles(); - tree = solutionOutlineTree.createTree(csolution); + tree = solutionOutlineTree.createTree(); topItems = tree.getChildren(); expect(topItems.length).toBe(3); // device, board, one project res = await dumpOutline(tree, 'USBD', 'CmsisViewTreeOneProjDmp.txt','CmsisViewTreeOneProjRef.txt'); @@ -223,8 +223,8 @@ describe('CSolution', () => { expect(loadResult).toEqual(ETextFileResult.Success); // get results from tree - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); const res = await dumpOutline(tree, 'WestSupport', 'CmsisViewTreeDmp.txt','CmsisViewTreeRef.txt'); expect(res.dump).toEqual(res.ref); @@ -237,8 +237,8 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); // Find West project nodes const projectNodes = (tree.getChildren() as COutlineItem[]).filter( @@ -261,8 +261,8 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); // Find West project nodes const westProjects = (tree.getChildren() as COutlineItem[]).filter( @@ -289,8 +289,8 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(); - const tree = solutionOutlineTree.createTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution); + const tree = solutionOutlineTree.createTree(); // Find all project nodes const projectNodes = (tree.getChildren() as COutlineItem[]).filter( diff --git a/src/views/solution-outline/tree-structure/solution-outline-tree.ts b/src/views/solution-outline/tree-structure/solution-outline-tree.ts index 06c772259..970b5cfd3 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-tree.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-tree.ts @@ -20,29 +20,26 @@ import path from 'path'; import * as fsUtils from '../../../utils/fs-utils'; import { CTreeItem } from '../../../generic/tree-item'; import * as manifest from '../../../manifest'; -import { HardwareItem } from './solution-outline-hardware-item'; +import { HardwareItemBuilder } from './solution-outline-hardware-item'; import { ProjectItemsBuilder } from './solution-outline-project-items'; import { getFileNameNoExt } from '../../../utils/path-utils'; import { setMergeDescription } from './solution-outline-utils'; import { CProjectYamlFile } from '../../../solutions/files/cproject-yaml-file'; +import { SolutionOutlineItemBuilder } from './solution-outline-item-builder'; +export class SolutionOutlineTree extends SolutionOutlineItemBuilder { -export class SolutionOutlineTree { - csolution?: CSolution; - constructor() { } - - public createTree(csolution: CSolution | undefined): COutlineItem { - this.csolution = csolution; + public createTree(): COutlineItem { const rootItem = new COutlineItem('solution'); - if (!csolution) { + if (!this.csolution) { this.addLoadError(rootItem); return rootItem; } - const treeNodeItems = csolution.cbuildYmlRoot.size > 0 - ? this.processBuildFiles(rootItem, csolution) - : this.createProjectsFromCsolution(rootItem, csolution); + const treeNodeItems = this.csolution.cbuildYmlRoot.size > 0 + ? this.processBuildFiles(rootItem, this.csolution) + : this.createProjectsFromCsolution(rootItem, this.csolution); if (treeNodeItems.length === 0) { this.addLoadError(rootItem); @@ -96,7 +93,7 @@ export class SolutionOutlineTree { projectsTreeNode.push(projectTreeNode); // add hardware nodes - const hardwareTreeItem = new HardwareItem(); + const hardwareTreeItem = new HardwareItemBuilder(csolution, this.rpcData); const hardware = hardwareTreeItem.createHardwareNodes(csolution, cbuild); hardware.forEach((value, key) => { @@ -124,7 +121,7 @@ export class SolutionOutlineTree { // add children if (cproject) { - const projectItems = new ProjectItemsBuilder(); + const projectItems = new ProjectItemsBuilder(this.csolution); projectItems.addProjectChildren(this.csolution, cprojectItem, cprojectFile, cbuild); // get prioritized component list and set merge description if available From 63debb000d87aadab73776dfe96a12beea58b3d0 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Fri, 13 Mar 2026 16:37:52 +0100 Subject: [PATCH 2/3] Lint fix --- .../tree-structure/solution-outline-item-builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts index d6ab0d7e5..dd2272a99 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts @@ -30,4 +30,4 @@ export class SolutionOutlineItemBuilder { } return this.rpcData.expandString(str, this.context); } -} \ No newline at end of file +} From 9939bc1c5200589f39a76adb9359c1fc05dba094 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Mon, 16 Mar 2026 18:23:14 +0100 Subject: [PATCH 3/3] Add access sequences support to outline tree --- src/solutions/solution-rpc-data.factory.ts | 33 ++++++++++++++++--- src/solutions/solution-rpc-data.ts | 33 ++++++++++++------- .../commands/delete-command.test.ts | 4 +-- .../solution-outline-file-item.ts | 7 ++-- .../solution-outline-hardware-item.ts | 6 ++-- .../solution-outline-item-builder.ts | 2 +- .../solution-outline-project-items.ts | 7 +--- .../solution-outline-tree.test.ts | 27 ++++++++++----- .../tree-structure/solution-outline-tree.ts | 2 +- test-data/solutions/USBD/CmsisViewTreeRef.txt | 30 +++++++++++++++++ test-data/solutions/USBD/HID/HID.cproject.yml | 7 ++++ 11 files changed, 116 insertions(+), 42 deletions(-) diff --git a/src/solutions/solution-rpc-data.factory.ts b/src/solutions/solution-rpc-data.factory.ts index 3f38a7392..4f45c22b2 100644 --- a/src/solutions/solution-rpc-data.factory.ts +++ b/src/solutions/solution-rpc-data.factory.ts @@ -15,14 +15,39 @@ */ import { csolutionServiceFactory } from '../json-rpc/csolution-rpc-client.factory'; -import { SolutionRpcData } from './solution-rpc-data'; +import { Board, Device, Variables } from '../json-rpc/csolution-rpc-client'; +import { SolutionRpcData, SolutionRpcDataImpl } from './solution-rpc-data'; -export type SolutionRpcDataMock = jest.MockedObject; -export function solutionRpcDataFactory(options: Partial = {}): SolutionRpcDataMock { - const solutionRpcDataMock = jest.mocked(new SolutionRpcData(csolutionServiceFactory()), { shallow: true }); +export class SolutionRpcDataMock extends SolutionRpcDataImpl { + constructor() { + super(csolutionServiceFactory()); + } + + public seedBoard(board?: Board): SolutionRpcDataMock { + this._board = board; + return this; + } + + public seedDevice(device?: Device): SolutionRpcDataMock { + this._device = device; + return this; + } + + public seedVariables(context: string, variables: Variables): SolutionRpcDataMock { + this.contextVariables.set(context, this.variablesFromRpcData(variables)); + return this; + } +} + +export function solutionRpcDataFactory( + options: Partial = {} +): SolutionRpcDataMock { + const solutionRpcDataMock = new SolutionRpcDataMock(); Object.assign(solutionRpcDataMock, options); return solutionRpcDataMock; } + +export type SolutionRpcDataLike = SolutionRpcData | SolutionRpcDataMock; diff --git a/src/solutions/solution-rpc-data.ts b/src/solutions/solution-rpc-data.ts index 041a53d13..cd64f4c9f 100644 --- a/src/solutions/solution-rpc-data.ts +++ b/src/solutions/solution-rpc-data.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { constructor } from '../generic/constructor'; -import { Board, CsolutionService, Device, VariablesResult } from '../json-rpc/csolution-rpc-client'; +import { Board, CsolutionService, Device, Variables } from '../json-rpc/csolution-rpc-client'; import { CSolution } from './csolution'; @@ -34,9 +34,16 @@ export interface SolutionRpcData { */ get device(): Device | undefined; + /** + * Returns variables for given context + * @param context resolving context + * @return ke-value map of variables, key is surrounded with '$'chars + */ + getVariables(context: string): Map | undefined; + /** Resolves a single variable for a context * @param context resolving context - * @param variable name without surrounding '$' chars + * @param variable name with surrounding '$' chars * @return variable value if resolved, undefined otherwise */ resolveVariable(context: string, variable?: string): string | undefined; @@ -52,13 +59,13 @@ export interface SolutionRpcData { expandString(str: string, context: string): string } -class SolutionRpcDataImpl implements SolutionRpcData { - private readonly contextVariables = new Map>(); - private _board?: Board = undefined; - private _device?: Device = undefined; +export class SolutionRpcDataImpl implements SolutionRpcData { + protected readonly contextVariables = new Map>(); + protected _board?: Board = undefined; + protected _device?: Device = undefined; constructor( - private readonly csolutionService: CsolutionService, + protected readonly csolutionService: CsolutionService, ) { } clear() { @@ -107,14 +114,18 @@ class SolutionRpcDataImpl implements SolutionRpcData { for (const context of contexts) { const data = await this.csolutionService.getVariables({ context: context }); if (data.success) { - this.contextVariables.set(context, this.variablesFromRpcData(data)); + this.contextVariables.set(context, this.variablesFromRpcData(data.variables)); } } } - private variablesFromRpcData(data: VariablesResult) { + public getVariables(context: string): Map | undefined { + return this.contextVariables.get(context); + } + + protected variablesFromRpcData(variables: Variables) { const vars = new Map(); - for (const [key, value] of Object.entries(data.variables)) { + for (const [key, value] of Object.entries(variables)) { vars.set('$' + key + '$', value); } return vars; @@ -122,7 +133,7 @@ class SolutionRpcDataImpl implements SolutionRpcData { public resolveVariable(context: string, variable?: string): string | undefined { if (variable) { - const variables = this.contextVariables.get(context); + const variables = this.getVariables(context); if (variables) { return variables.get(variable); } diff --git a/src/views/solution-outline/commands/delete-command.test.ts b/src/views/solution-outline/commands/delete-command.test.ts index ad5d240d0..4c4670e7d 100644 --- a/src/views/solution-outline/commands/delete-command.test.ts +++ b/src/views/solution-outline/commands/delete-command.test.ts @@ -88,7 +88,7 @@ describe('DeleteCommand', () => { const groups = children?.[2]; const groupItems = groups?.getChildren(); - const want: string[] = ['README.md', 'HID.c']; + const want: string[] = ['README.md', 'HID.c', '$OutDir()$/testOutput.test']; const got: string[] = []; if (groupItems) { for (const gi of groupItems) { @@ -194,7 +194,7 @@ describe('DeleteCommand', () => { const groups = children?.[2]; const groupItems = groups?.getChildren(); - const want: string[] = ['Documentation']; + const want: string[] = ['Documentation', 'AccessSequencesTest']; const got: string[] = []; if (groupItems) { for (const gi of groupItems) { 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 1ee7761ea..008feeef4 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 @@ -44,7 +44,7 @@ export class FileItemBuilder extends SolutionOutlineItemBuilder { const hasCmsisPackRoot = fileValue.indexOf('${CMSIS_PACK_ROOT}') !== -1; const filePath = this.resolveFilePath(hasCmsisPackRoot, fileValue); - const fileBaseName = path.basename(filePath); + const fileBaseName = path.basename(fileValue); const resourcePath = hasCmsisPackRoot ? filePath : f.resolvePath(filePath); const description = isApi ? ' (API)' : undefined; const rootFileName = f.rootFileName; @@ -73,12 +73,11 @@ export class FileItemBuilder extends SolutionOutlineItemBuilder { if (hasCmsisPackRoot) { return fileValue.replace('${CMSIS_PACK_ROOT}', getCmsisPackRoot()); } - return fileValue; + return this.expandAccessSequences(fileValue); } private createFileItem(cgroupItem: COutlineItem, fileBaseName: string, resourcePath: string, description?: string): COutlineItem { - const item = cgroupItem.createChild(fileBaseName); - item.setTag('file'); + const item = cgroupItem.createChild('file'); item.setAttribute('label', fileBaseName); item.setAttribute('expandable', '0'); item.setAttribute('resourcePath', resourcePath); diff --git a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts index ef3949835..49dfd65fc 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-hardware-item.ts @@ -98,8 +98,7 @@ export class HardwareItemBuilder extends SolutionOutlineItemBuilder { return; } - const cbookItem = chardwareItem.createChild(title); - cbookItem.setTag('book'); + const cbookItem = chardwareItem.createChild('book'); cbookItem.setAttribute('label', title); cbookItem.setAttribute('expandable', '0'); cbookItem.setAttribute('resourcePath', filePath); @@ -137,8 +136,7 @@ export class HardwareItemBuilder extends SolutionOutlineItemBuilder { const filePath = file.resolvePath(fileName); const dbgConfFile = path.basename(fileName); - const dbgconfFileItem = chardwareItem.createChild('dbgConfFile'); - dbgconfFileItem.setTag('file'); + const dbgconfFileItem = chardwareItem.createChild('file'); dbgconfFileItem.setAttribute('label', dbgConfFile); dbgconfFileItem.setAttribute('expandable', '0'); dbgconfFileItem.setAttribute('resourcePath', filePath); diff --git a/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts index dd2272a99..dfe4d6c75 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-item-builder.ts @@ -24,7 +24,7 @@ export class SolutionOutlineItemBuilder { protected context?: string, ) { } - public expandAccessSequences(str: string | undefined) { + public expandAccessSequences(str: string) { if (!this.rpcData || !this.context || !str) { return str; } diff --git a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts index 8fcf42675..75932877f 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-project-items.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-project-items.ts @@ -108,9 +108,7 @@ export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { } private createGroupItem(cprojectItem: COutlineItem, name: string, mutable: boolean): COutlineItem { - const cgroupItem = cprojectItem.createChild(name) as COutlineItem; - - cgroupItem.setTag('group'); + const cgroupItem = cprojectItem.createChild('group'); cgroupItem.setAttribute('label', name); cgroupItem.setAttribute('iconPath', 'csolution-files'); if (mutable) { @@ -181,7 +179,6 @@ export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { // create layer const clayerItem = node.createChild('layer'); - clayerItem.setTag('layer'); clayerItem.setAttribute('label', label); clayerItem.setAttribute('expandable', '1'); clayerItem.addFeature(`${manifest.LAYER_CONTEXT}`); @@ -208,7 +205,6 @@ export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { const size = children.length; const ccomponentsItem = cprojectItem.createChild('components'); - ccomponentsItem.setTag('components'); ccomponentsItem.setAttribute('label', 'Components' + ` (${size})`); ccomponentsItem.setAttribute('expandable', size > 0 ? '1' : '0'); @@ -316,7 +312,6 @@ export class ProjectItemsBuilder extends SolutionOutlineItemBuilder { // create child const ccomponentItem = ccomponentsItem.createChild('component'); - ccomponentItem.setTag('component'); ccomponentItem.setAttribute('label', refId); ccomponentItem.setAttribute('expandable', '0'); ccomponentItem.setAttribute('iconPath', 'csolution-software-component'); diff --git a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts index 9d046414a..df022cbe4 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-tree.test.ts @@ -24,11 +24,13 @@ import { getCmsisPackRoot } from '../../../utils/path-utils'; import { toGenericString } from '../../../generic/tree-item-parser'; import * as fsUtils from '../../../utils/fs-utils'; import { CTreeItem } from '../../../generic/tree-item'; +import { solutionRpcDataFactory, SolutionRpcDataMock } from '../../../solutions/solution-rpc-data.factory'; describe('CSolution', () => { const testDataHandler = new TestDataHandler(); let tmpSolutionDir: string; let cmsisPackRoot: string; + let rpcData: SolutionRpcDataMock; beforeAll(async () => { @@ -40,6 +42,11 @@ describe('CSolution', () => { afterAll(async () => { testDataHandler.dispose(); }); + + beforeEach(() => { + rpcData = solutionRpcDataFactory(); + }); + const normalize = (s: string) => s.replace(/\r\n/g, '\n') // unify CRLF -> LF .replace(/\r/g, '\n') // stray CR -> LF @@ -93,7 +100,7 @@ describe('CSolution', () => { } // get results from tree - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); const got = new Map(); @@ -107,7 +114,7 @@ describe('CSolution', () => { }); it('creates error node when csolution is undefined', () => { - const solutionOutlineTree = new SolutionOutlineTree(); + const solutionOutlineTree = new SolutionOutlineTree(undefined, rpcData); const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; @@ -124,7 +131,7 @@ describe('CSolution', () => { ['dummy.cbuild.yml', new CTreeItem('cbuild')], ]); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; @@ -138,7 +145,7 @@ describe('CSolution', () => { it('creates error node when no projects can be loaded', () => { const csolution = new CSolution(); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); const children = tree.getChildren() as COutlineItem[]; @@ -157,11 +164,13 @@ describe('CSolution', () => { // get results from tree const fileName = path.join(tmpSolutionDir, 'USBD', 'USB_Device.csolution.yml'); const csolution = new CSolution(); + const outDirHID = path.join(tmpSolutionDir, 'USBD', 'out', 'HID', 'B-U585I-IOT02A', 'Release'); let loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); + rpcData.seedVariables('HID.Release+B-U585I-IOT02A', { 'OutDir()': outDirHID, 'Dname': 'STM32U585AIIx' }); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); let tree = solutionOutlineTree.createTree(); let topItems = tree.getChildren(); @@ -223,7 +232,7 @@ describe('CSolution', () => { expect(loadResult).toEqual(ETextFileResult.Success); // get results from tree - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); const res = await dumpOutline(tree, 'WestSupport', 'CmsisViewTreeDmp.txt','CmsisViewTreeRef.txt'); @@ -237,7 +246,7 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); // Find West project nodes @@ -261,7 +270,7 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); // Find West project nodes @@ -289,7 +298,7 @@ describe('CSolution', () => { const loadResult = await csolution.load(fileName); expect(loadResult).toEqual(ETextFileResult.Success); - const solutionOutlineTree = new SolutionOutlineTree(csolution); + const solutionOutlineTree = new SolutionOutlineTree(csolution, rpcData); const tree = solutionOutlineTree.createTree(); // Find all project nodes diff --git a/src/views/solution-outline/tree-structure/solution-outline-tree.ts b/src/views/solution-outline/tree-structure/solution-outline-tree.ts index 970b5cfd3..9d311f5cc 100644 --- a/src/views/solution-outline/tree-structure/solution-outline-tree.ts +++ b/src/views/solution-outline/tree-structure/solution-outline-tree.ts @@ -121,7 +121,7 @@ export class SolutionOutlineTree extends SolutionOutlineItemBuilder { // add children if (cproject) { - const projectItems = new ProjectItemsBuilder(this.csolution); + const projectItems = new ProjectItemsBuilder(this.csolution, this.rpcData, context); projectItems.addProjectChildren(this.csolution, cprojectItem, cprojectFile, cbuild); // get prioritized component list and set merge description if available diff --git a/test-data/solutions/USBD/CmsisViewTreeRef.txt b/test-data/solutions/USBD/CmsisViewTreeRef.txt index f54edc55d..c504e0dfe 100644 --- a/test-data/solutions/USBD/CmsisViewTreeRef.txt +++ b/test-data/solutions/USBD/CmsisViewTreeRef.txt @@ -22,6 +22,36 @@ solution resourcePath=TEST_DIR/solutions/USBD/HID/HID.cproject.yml projectUri=TEST_DIR/solutions/USBD/HID/HID.cproject.yml tooltip=- File: ` TEST_DIR/solutions/USBD/HID/HID.cproject.yml ` + group + label=AccessSequencesTest + iconPath=csolution-files + features=group + mutable=1 + type=group + groupPath=AccessSequencesTest + projectUri=TEST_DIR/solutions/USBD/HID/HID.cproject.yml + expandable=1 + file + label=$Dname$.test + expandable=0 + resourcePath=TEST_DIR/solutions/USBD/HID/STM32U585AIIx.test + features=file + fileUri=STM32U585AIIx.test + projectUri=TEST_DIR/solutions/USBD/HID/HID.cproject.yml + file + label=testOutput.test + expandable=0 + resourcePath=TEST_DIR/solutions/USBD/out/HID/B-U585I-IOT02A/Release/testOutput.test + features=file + fileUri=TEST_DIR/solutions/USBD/out/HID/B-U585I-IOT02A/Release/testOutput.test + projectUri=TEST_DIR/solutions/USBD/HID/HID.cproject.yml + file + label=unknown.test + expandable=0 + resourcePath=TEST_DIR/solutions/USBD/HID/$UnknownVar$/unknown.test + features=file + fileUri=$UnknownVar$/unknown.test + projectUri=TEST_DIR/solutions/USBD/HID/HID.cproject.yml group label=Documentation iconPath=csolution-files diff --git a/test-data/solutions/USBD/HID/HID.cproject.yml b/test-data/solutions/USBD/HID/HID.cproject.yml index 0e4d96ca2..217eeba76 100644 --- a/test-data/solutions/USBD/HID/HID.cproject.yml +++ b/test-data/solutions/USBD/HID/HID.cproject.yml @@ -26,6 +26,13 @@ project: - file: HID.c - file: USBD_User_HID_0.c + - group: AccessSequencesTest + files: + - file: $OutDir()$/testOutput.test + - file: $UnknownVar$/unknown.test + - file: $Dname$.test + + components: - component: ARM::CMSIS:OS Tick:SysTick - component: ARM::CMSIS:RTOS2:Keil RTX5&Source