Skip to content

Commit b963aac

Browse files
committed
Merge branch 'pre-release-extn' of https://github.com/Open-CMSIS-Pack/vscode-cmsis-debugger into pre-release-extn
2 parents b0023c0 + a9473b3 commit b963aac

7 files changed

Lines changed: 257 additions & 62 deletions

File tree

__mocks__/vscode.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ module.exports = {
7171
warn: jest.fn(),
7272
error: jest.fn(),
7373
})),
74-
registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })),
75-
showErrorMessage: jest.fn(),
76-
showInformationMessage: jest.fn(() => Promise.resolve(undefined)),
77-
showWarningMessage: jest.fn(),
7874
createStatusBarItem: jest.fn(() => ({
7975
id: 'mockStatusBarItem',
8076
alignment: StatusBarAlignment.Left,
@@ -83,6 +79,15 @@ module.exports = {
8379
hide: jest.fn(),
8480
dispose: jest.fn(),
8581
})),
82+
createTreeView: jest.fn(() => ({
83+
dispose: jest.fn(),
84+
onDidExpandElement: jest.fn(),
85+
onDidCollapseElement: jest.fn(),
86+
})),
87+
registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })),
88+
showErrorMessage: jest.fn(),
89+
showInformationMessage: jest.fn(() => Promise.resolve(undefined)),
90+
showWarningMessage: jest.fn(),
8691
showQuickPick: jest.fn(),
8792
},
8893
env: {

src/views/component-viewer/component-viewer-instance.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class ComponentViewerInstance {
5151
private _statementEngine: StatementEngine | undefined;
5252
private _guiTree: ScvdGuiTree | undefined;
5353
private _fileKey: string | undefined;
54+
private _instanceKey: string | undefined;
5455
private _scvdEvalContext: ScvdEvalContext | undefined;
5556

5657
public constructor(
@@ -107,6 +108,7 @@ export class ComponentViewerInstance {
107108
public async readModel(filename: URI, debugSession: GDBTargetDebugSession, debugTracker: GDBTargetDebugTracker): Promise<void> {
108109
const stats: string[] = [];
109110
this._fileKey = ComponentViewerInstance.getFileKey(filename);
111+
this._instanceKey = `${debugSession.session.id}/${this._fileKey}`;
110112

111113
stats.push(this.getStats(` Start reading SCVD file ${filename}`));
112114
const buf = (await this.readFileToBuffer(filename)).toString('utf-8');
@@ -158,7 +160,7 @@ export class ComponentViewerInstance {
158160
this.statementEngine.initialize();
159161
stats.push(this.getStats(' statementEngine.initialize'));
160162
this._guiTree = new ScvdGuiTree(undefined);
161-
this._guiTree.setId(this._fileKey);
163+
this._guiTree.setId(this._instanceKey);
162164
this._guiTree.setGuiName('component-viewer-root');
163165

164166
componentViewerLogger.info(`ComponentViewerInstance readModel stats:\n ${stats.join('\n ')}`);

src/views/component-viewer/component-viewer-main.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,13 @@ export class ComponentViewer {
6868
if (!await vscodeViewExists('componentViewer')) {
6969
return false;
7070
}
71-
const treeProviderDisposable = vscode.window.registerTreeDataProvider('cmsis-debugger.componentViewer', this._componentViewerTreeDataProvider);
72-
componentViewerLogger.debug('Component Viewer: Registered tree data provider for Component Viewer Tree View id: cmsis-debugger.componentViewer');
71+
const treeView = vscode.window.createTreeView('cmsis-debugger.componentViewer', {
72+
treeDataProvider: this._componentViewerTreeDataProvider,
73+
showCollapseAll: true
74+
});
75+
componentViewerLogger.debug('Component Viewer: Created Component Viewer tree view: cmsis-debugger.componentViewer');
76+
const onDidExpandElementDisposable = treeView.onDidExpandElement(event => this.handleOnDidToggleExpand(event, true));
77+
const onDidCollapseElementDisposable = treeView.onDidCollapseElement(event => this.handleOnDidToggleExpand(event, false));
7378
const lockInstanceCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.lockComponent', async (node) => {
7479
this.handleLockInstance(node);
7580
});
@@ -85,7 +90,9 @@ export class ComponentViewer {
8590
componentViewerLogger.info('Component Viewer: Auto refresh disabled');
8691
});
8792
this._context.subscriptions.push(
88-
treeProviderDisposable,
93+
treeView,
94+
onDidExpandElementDisposable,
95+
onDidCollapseElementDisposable,
8996
lockInstanceCommandDisposable,
9097
unlockInstanceCommandDisposable,
9198
enablePeriodicUpdateCommandDisposable,
@@ -94,6 +101,13 @@ export class ComponentViewer {
94101
return true;
95102
}
96103

104+
protected handleOnDidToggleExpand(expansionEvent: vscode.TreeViewExpansionEvent<ScvdGuiInterface>, expand: boolean): void {
105+
const expandStateString = expand ? 'expanded' : 'collapsed';
106+
const elementName = expansionEvent.element.getGuiName() ?? 'unknown';
107+
componentViewerLogger.debug(`Component Viewer: Tree item ${expandStateString} - ${elementName}`);
108+
this._componentViewerTreeDataProvider.setElementExpanded(expansionEvent.element, expand);
109+
}
110+
97111
protected handleLockInstance(node: ScvdGuiInterface): void {
98112
let shouldTriggerUpdate: boolean = false; // Unlocking a node should trigger an update
99113
const instance = this._instances.find((inst) => {
@@ -249,6 +263,7 @@ export class ComponentViewer {
249263
return true;
250264
});
251265
this.schedulePendingUpdate('sessionChanged');
266+
this._componentViewerTreeDataProvider.onWillStopSession(session.session.id);
252267
}
253268

254269
private async handleOnWillStartSession(session: GDBTargetDebugSession): Promise<void> {
@@ -265,7 +280,8 @@ export class ComponentViewer {
265280

266281
private async handleRefreshTimerEvent(session: GDBTargetDebugSession): Promise<void> {
267282
if(this._activeSession?.session.id !== session.session.id) {
268-
throw new Error(`Component Viewer: Received refresh timer event for session ${session.session.id} while active session is ${this._activeSession?.session.id}`);
283+
// Don't throw an error here, just return. Refresh timer events don't know about currently active session.
284+
return;
269285
}
270286
if (this._refreshTimerEnabled) {
271287
// Update component viewer instance(s)

src/views/component-viewer/component-viewer-tree-view.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,46 @@ export class ComponentViewerTreeDataProvider implements vscode.TreeDataProvider<
2424
private readonly _onDidChangeTreeData = new vscode.EventEmitter<ScvdGuiInterface | void>();
2525
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
2626
private _roots: ScvdGuiInterface[] = [];
27+
private _expandedIds: string[] = [];
2728

2829
constructor () {
2930
}
3031

32+
public onWillStopSession(sessionId: string): void {
33+
// Filter expanded elements by session ID encoded into unique GUI ID.
34+
this._expandedIds = this._expandedIds.filter(expandedId => !expandedId.startsWith(sessionId + '/'));
35+
}
36+
37+
public setElementExpanded(element: ScvdGuiInterface, expanded: boolean): void {
38+
const hasChildren = element.hasGuiChildren();
39+
const elementId = element.getGuiId();
40+
if (elementId === undefined) {
41+
return;
42+
}
43+
const wasExpanded = this._expandedIds.find(expandedId => expandedId === elementId);
44+
if (hasChildren && expanded && wasExpanded === undefined) {
45+
this._expandedIds.push(elementId);
46+
return;
47+
} else if (wasExpanded) {
48+
this._expandedIds = this._expandedIds.filter(expandedId => expandedId !== elementId);
49+
}
50+
}
51+
3152
public getTreeItem(element: ScvdGuiInterface): vscode.TreeItem {
3253
const perfStartTime = perf?.startUi() ?? 0;
3354
const treeItemLabel = element.getGuiName() ?? 'UNKNOWN';
55+
const guiId = element.getGuiId();
3456
const treeItem = new vscode.TreeItem(treeItemLabel);
35-
treeItem.collapsibleState = element.hasGuiChildren()
36-
? vscode.TreeItemCollapsibleState.Collapsed
37-
: vscode.TreeItemCollapsibleState.None;
57+
const hasChildren = element.hasGuiChildren();
58+
const wasExpanded = this._expandedIds.find(expandedId => expandedId === guiId);
59+
if (hasChildren) {
60+
treeItem.collapsibleState = wasExpanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed;
61+
} else {
62+
treeItem.collapsibleState = vscode.TreeItemCollapsibleState.None;
63+
if (wasExpanded) {
64+
this._expandedIds = this._expandedIds.filter(expandedId => expandedId !== guiId);
65+
}
66+
}
3867
// Needs fixing, getGuiValue() for ScvdNode returns 0 when undefined
3968
treeItem.description = element.getGuiValue() ?? '';
4069
let intermediateContextValue = '';
@@ -46,7 +75,6 @@ export class ComponentViewerTreeDataProvider implements vscode.TreeDataProvider<
4675
}
4776

4877
treeItem.contextValue = element.isLocked ? `locked.${intermediateContextValue}` : intermediateContextValue;
49-
const guiId = element.getGuiId();
5078
if (guiId !== undefined) {
5179
treeItem.id = guiId;
5280
}

src/views/component-viewer/test/unit/component-viewer-instance.test.ts

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,10 @@ import { Resolver } from '../../resolver';
2929
import { ScvdEvalContext } from '../../scvd-eval-context';
3030
import { StatementEngine } from '../../statement-engine/statement-engine';
3131
import { ScvdGuiTree } from '../../scvd-gui-tree';
32-
import type { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../../../debug-session';
32+
import { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../../../debug-session';
3333
import { componentViewerLogger } from '../../../../logger';
34-
35-
jest.mock('vscode', () => ({
36-
workspace: {
37-
fs: {
38-
readFile: jest.fn(),
39-
},
40-
},
41-
window: {
42-
createOutputChannel: jest.fn(() => ({
43-
appendLine: jest.fn(),
44-
trace: jest.fn(),
45-
debug: jest.fn(),
46-
info: jest.fn(),
47-
warn: jest.fn(),
48-
error: jest.fn(),
49-
})),
50-
},
51-
}));
34+
import { debugSessionFactory } from '../../../../__test__/vscode.factory';
35+
import { gdbTargetConfiguration } from '../../../../debug-configuration/debug-configuration.factory';
5236

5337
jest.mock('xml2js', () => ({
5438
parseStringPromise: jest.fn(),
@@ -75,8 +59,15 @@ jest.mock('../../scvd-gui-tree', () => ({
7559
}));
7660

7761
describe('ComponentViewerInstance', () => {
62+
let debugSession: GDBTargetDebugSession;
63+
let debugTracker: GDBTargetDebugTracker;
64+
let instance: ComponentViewerInstance;
65+
7866
beforeEach(() => {
7967
jest.clearAllMocks();
68+
debugSession = new GDBTargetDebugSession(debugSessionFactory(gdbTargetConfiguration(), 'test-session-id'));
69+
debugTracker = new GDBTargetDebugTracker();
70+
instance = new ComponentViewerInstance();
8071
});
8172

8273
it('reads a model, initializes the engine, and updates', async () => {
@@ -130,9 +121,6 @@ describe('ComponentViewerInstance', () => {
130121
setGuiName,
131122
}));
132123

133-
const instance = new ComponentViewerInstance();
134-
const debugSession = {} as unknown as GDBTargetDebugSession;
135-
const debugTracker = {} as unknown as GDBTargetDebugTracker;
136124
await instance.readModel(URI.file('/tmp/example.scvd'), debugSession, debugTracker);
137125

138126
expect(readXml).toHaveBeenCalled();
@@ -160,7 +148,6 @@ describe('ComponentViewerInstance', () => {
160148

161149
it('skips update and executeStatements when dependencies are missing', async () => {
162150
const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
163-
const instance = new ComponentViewerInstance();
164151

165152
expect(instance.getGuiTree()).toBeUndefined();
166153
await instance.update();
@@ -177,8 +164,7 @@ describe('ComponentViewerInstance', () => {
177164
readFileMock.mockResolvedValue(Buffer.from('<root/>'));
178165
parseStringMock.mockRejectedValue(new Error('parse failed'));
179166

180-
const instance = new ComponentViewerInstance();
181-
await instance.readModel(URI.file('/tmp/invalid.scvd'), {} as unknown as GDBTargetDebugSession, {} as unknown as GDBTargetDebugTracker);
167+
await instance.readModel(URI.file('/tmp/invalid.scvd'), debugSession, debugTracker);
182168

183169
expect(consoleError).toHaveBeenCalled();
184170
consoleError.mockRestore();
@@ -199,12 +185,11 @@ describe('ComponentViewerInstance', () => {
199185
calculateTypedefs: jest.fn(),
200186
}));
201187

202-
const instance = new ComponentViewerInstance();
203188
const modelGetter = jest
204189
.spyOn(instance as unknown as { model: ScvdComponentViewer | undefined }, 'model', 'get')
205190
.mockReturnValue(undefined);
206191

207-
await instance.readModel(URI.file('/tmp/model.scvd'), {} as unknown as GDBTargetDebugSession, {} as unknown as GDBTargetDebugTracker);
192+
await instance.readModel(URI.file('/tmp/model.scvd'), debugSession, debugTracker);
208193

209194
expect(consoleError).toHaveBeenCalledWith('Failed to create SCVD model');
210195

@@ -233,8 +218,7 @@ describe('ComponentViewerInstance', () => {
233218
getExecutionContext: jest.fn().mockReturnValue(undefined),
234219
}));
235220

236-
const instance = new ComponentViewerInstance();
237-
await instance.readModel(URI.file('/tmp/no-exec.scvd'), {} as unknown as GDBTargetDebugSession, {} as unknown as GDBTargetDebugTracker);
221+
await instance.readModel(URI.file('/tmp/no-exec.scvd'), debugSession, debugTracker);
238222

239223
expect(consoleError).toHaveBeenCalledWith('Failed to get execution context from SCVD EvalContext');
240224
consoleError.mockRestore();
@@ -245,7 +229,6 @@ describe('ComponentViewerInstance', () => {
245229
const consoleError = jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
246230

247231
readFileMock.mockRejectedValue(new Error('read failed'));
248-
const instance = new ComponentViewerInstance();
249232
const instanceWithReader = instance as unknown as { readFileToBuffer: (filePath: URI) => Promise<Buffer> };
250233
await expect(instanceWithReader.readFileToBuffer(URI.file('/tmp/missing'))).rejects.toThrow('read failed');
251234

@@ -254,7 +237,6 @@ describe('ComponentViewerInstance', () => {
254237
});
255238

256239
it('injects line numbers and reports stats twice', () => {
257-
const instance = new ComponentViewerInstance();
258240
const injectLineNumbers = (instance as unknown as { injectLineNumbers: (xml: string) => string }).injectLineNumbers;
259241
const tagged = injectLineNumbers('<root>\n<child/>\n</root>');
260242

@@ -306,13 +288,11 @@ describe('ComponentViewerInstance', () => {
306288
});
307289

308290
it('cancelExecution is a no-op when the context is not initialised', () => {
309-
const instance = new ComponentViewerInstance();
310291
expect(() => instance.cancelExecution('test')).not.toThrow();
311292
});
312293

313294
it('cancelExecution delegates to scvdEvalContext cancellation', () => {
314295
const cancelMock = jest.fn();
315-
const instance = new ComponentViewerInstance();
316296
(instance as unknown as { _scvdEvalContext: { cancellation: { cancel: jest.Mock } } | undefined })._scvdEvalContext = {
317297
cancellation: { cancel: cancelMock },
318298
};

0 commit comments

Comments
 (0)