Skip to content

Commit f583b3f

Browse files
Prompting users to reload vscode window for proper manifest updates (#811)
* Added log messages to track treeView registration * Changed to same treeView pattern as live watch * Check if views are correctly loaded before registering ** Check if related commands are there ** Ask user to reload VS Code window if not * Added a pop up window to ask the user to reload when component viewer view is not registered * Add tests, move some local mocks to global vscode mock * Include extension.ts in coverage --------- Co-authored-by: Jens Reinecke <jens.reinecke@arm.com>
1 parent f4711cc commit f583b3f

13 files changed

Lines changed: 347 additions & 107 deletions

File tree

__mocks__/vscode.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2025 Arm Limited
2+
* Copyright 2025-2026 Arm Limited
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -73,9 +73,16 @@ module.exports = {
7373
})),
7474
registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })),
7575
showErrorMessage: jest.fn(),
76-
showInformationMessage: jest.fn(),
76+
showInformationMessage: jest.fn(() => Promise.resolve(undefined)),
7777
showWarningMessage: jest.fn(),
78-
createStatusBarItem: jest.fn(),
78+
createStatusBarItem: jest.fn(() => ({
79+
id: 'mockStatusBarItem',
80+
alignment: StatusBarAlignment.Left,
81+
text: '',
82+
show: jest.fn(),
83+
hide: jest.fn(),
84+
dispose: jest.fn(),
85+
})),
7986
showQuickPick: jest.fn(),
8087
},
8188
env: {
@@ -107,6 +114,13 @@ module.exports = {
107114
},
108115
commands: {
109116
executeCommand: jest.fn(),
117+
// Default to all views in extension having been correctly loaded
118+
getCommands: jest.fn(() => Promise.resolve([
119+
'cmsis-debugger.liveWatch.open',
120+
'cmsis-debugger.liveWatch.focus',
121+
'cmsis-debugger.componentViewer.open',
122+
'cmsis-debugger.componentViewer.focus',
123+
])),
110124
registerCommand: jest.fn(),
111125
},
112126
debug: {

jest.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ const config: Config = {
3232
"src/**/*.{ts,tsx}",
3333
"!**/*.d.ts",
3434
"!**/*.factories.{ts,tsx}",
35-
"!**/__test__/**/*",
36-
"!src/desktop/extension.ts",
35+
"!**/__test__/**/*"
3736
],
3837
coverageDirectory: "./coverage",
3938
coverageReporters: ["lcov", "text"],

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@
5757
"views": {
5858
"cmsis-debugger": [
5959
{
60-
"id": "cmsis-debugger.liveWatch",
61-
"name": "Live Watch",
60+
"id": "cmsis-debugger.componentViewer",
61+
"name": "Component Viewer",
6262
"icon": "media/trace-and-live-light.svg"
6363
},
6464
{
65-
"id": "cmsis-debugger.componentViewer",
66-
"name": "Component Viewer",
65+
"id": "cmsis-debugger.liveWatch",
66+
"name": "Live Watch",
6767
"icon": "media/trace-and-live-light.svg"
6868
}
6969
]

src/desktop/extension.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright 2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as vscode from 'vscode';
18+
import { extensionContextFactory } from '../__test__/vscode.factory';
19+
import { logger } from '../logger';
20+
import { activate, deactivate } from './extension';
21+
import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view';
22+
import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch';
23+
24+
describe('extension', () => {
25+
26+
describe('activate', () => {
27+
28+
it('activates extension without asking to reload', async () => {
29+
const loggerSpy = jest.spyOn(logger, 'debug');
30+
await activate(extensionContextFactory());
31+
expect(loggerSpy).toHaveBeenCalledWith('CMSIS Debugger activated');
32+
expect(vscode.window.showWarningMessage).not.toHaveBeenCalledWith('Cannot activate all Arm CMSIS Debugger views. Please reload the window.', 'Reload Window');
33+
});
34+
35+
it.each([
36+
{ missingView: 'live watch view', availableCommands: [ 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus'] },
37+
{ missingView: 'component viewer', availableCommands: [ 'cmsis-debugger.liveWatch.open', 'cmsis-debugger.liveWatch.focus'] }
38+
])('activates extension and asks to reload because $missingView is not loaded', async ({ availableCommands }) => {
39+
const loggerSpy = jest.spyOn(logger, 'debug');
40+
// Resolve once per each view in extension, do not permanently overload global mock
41+
(vscode.commands.getCommands as jest.Mock)
42+
.mockResolvedValueOnce(availableCommands)
43+
.mockResolvedValueOnce(availableCommands);
44+
await activate(extensionContextFactory());
45+
expect(loggerSpy).toHaveBeenCalledWith('CMSIS Debugger activation incomplete');
46+
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith('Cannot activate all Arm CMSIS Debugger views. Please reload the window.', 'Reload Window');
47+
});
48+
49+
it('reloads window if users clicks \'Reload Window\' button', async () => {
50+
(vscode.commands.getCommands as jest.Mock).mockResolvedValueOnce([]);
51+
(vscode.window.showWarningMessage as jest.Mock).mockResolvedValueOnce('Reload Window');
52+
await activate(extensionContextFactory());
53+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith('workbench.action.reloadWindow');
54+
});
55+
56+
it('does not reload window if users clicks \'x\' button', async () => {
57+
(vscode.commands.getCommands as jest.Mock).mockResolvedValueOnce([]);
58+
(vscode.window.showWarningMessage as jest.Mock).mockResolvedValueOnce(undefined);
59+
await activate(extensionContextFactory());
60+
expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith('workbench.action.reloadWindow');
61+
});
62+
63+
});
64+
65+
describe('deactivate', () => {
66+
const loggerSpy = jest.spyOn(logger, 'debug');
67+
const componentViewerClearSpy = jest.spyOn(ComponentViewerTreeDataProvider.prototype, 'clear');
68+
const liveWatchDeactivateSpy = jest.spyOn(LiveWatchTreeDataProvider.prototype, 'deactivate');
69+
70+
beforeEach(() => {
71+
loggerSpy.mockClear();
72+
componentViewerClearSpy.mockClear();
73+
liveWatchDeactivateSpy.mockClear();
74+
});
75+
76+
it('deactivates extension after activation', async () => {
77+
await activate(extensionContextFactory());
78+
await deactivate();
79+
expect(loggerSpy).toHaveBeenCalledWith('CMSIS Debugger deactivated');
80+
expect(componentViewerClearSpy).toHaveBeenCalled();
81+
expect(liveWatchDeactivateSpy).toHaveBeenCalled();
82+
});
83+
84+
// Cannot test deactivation without activation due to global variables in
85+
// extension.ts that are set in other test cases after loading module.
86+
87+
});
88+
});
89+

src/desktop/extension.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,35 @@ import { CpuStatesCommands } from '../features/cpu-states/cpu-states-commands';
2525
import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch';
2626
import { GenericCommands } from '../features/generic-commands';
2727
import { ComponentViewer } from '../views/component-viewer/component-viewer-main';
28+
import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view';
2829

2930
const BUILTIN_TOOLS_PATHS = [
3031
'tools/pyocd/pyocd',
3132
'tools/gdb/bin/arm-none-eabi-gdb'
3233
];
3334

3435
let liveWatchTreeDataProvider: LiveWatchTreeDataProvider;
36+
let componentViewerTreeDataProvider: ComponentViewerTreeDataProvider;
37+
38+
const askForReload = async (): Promise<void> => {
39+
const result = await vscode.window.showWarningMessage('Cannot activate all Arm CMSIS Debugger views. Please reload the window.', 'Reload Window');
40+
if (result === 'Reload Window') {
41+
await vscode.commands.executeCommand('workbench.action.reloadWindow');
42+
}
43+
};
3544

3645
export const activate = async (context: vscode.ExtensionContext): Promise<void> => {
46+
let canCompleteActivation = true;
3747
const genericCommands = new GenericCommands();
3848
const gdbtargetDebugTracker = new GDBTargetDebugTracker();
3949
const gdbtargetConfigurationProvider = new GDBTargetConfigurationProvider();
4050
const cpuStates = new CpuStates();
4151
const cpuStatesCommands = new CpuStatesCommands();
4252
const cpuStatesStatusBarItem = new CpuStatesStatusBarItem();
43-
const componentViewer = new ComponentViewer(context);
4453
// Register the Tree View under the id from package.json
4554
liveWatchTreeDataProvider = new LiveWatchTreeDataProvider(context);
55+
componentViewerTreeDataProvider = new ComponentViewerTreeDataProvider();
56+
const componentViewer = new ComponentViewer(context, componentViewerTreeDataProvider);
4657

4758
addToolsToPath(context, BUILTIN_TOOLS_PATHS);
4859
// Activate generic commands
@@ -55,17 +66,34 @@ export const activate = async (context: vscode.ExtensionContext): Promise<void>
5566
cpuStatesCommands.activate(context, cpuStates);
5667
cpuStatesStatusBarItem.activate(context, cpuStates);
5768
// Live Watch view
58-
liveWatchTreeDataProvider.activate(gdbtargetDebugTracker);
69+
logger.debug('Activating Live Watch Tree Data Provider');
70+
if (!await liveWatchTreeDataProvider.activate(gdbtargetDebugTracker)) {
71+
canCompleteActivation = false;
72+
}
5973
// Component Viewer
60-
componentViewer.activate(gdbtargetDebugTracker);
74+
logger.debug('Activating Component Viewer');
75+
if (!await componentViewer.activate(gdbtargetDebugTracker)) {
76+
canCompleteActivation = false;
77+
}
78+
79+
if (!canCompleteActivation) {
80+
logger.debug('CMSIS Debugger activation incomplete');
81+
// Let promise float, we reload the window.
82+
askForReload()
83+
.catch(error => logger.error(`Error while asking user to reload window: ${error instanceof Error ? error.message : error}`));
84+
return;
85+
}
6186

62-
logger.debug('Extension Pack activated');
87+
logger.debug('CMSIS Debugger activated');
6388
};
6489

6590
export const deactivate = async (): Promise<void> => {
6691
// Call deactivate of Live Watch to save its state
6792
if (liveWatchTreeDataProvider) {
6893
await liveWatchTreeDataProvider.deactivate();
6994
}
70-
logger.debug('Extension Pack deactivated');
95+
if (componentViewerTreeDataProvider) {
96+
componentViewerTreeDataProvider.clear();
97+
}
98+
logger.debug('CMSIS Debugger deactivated');
7199
};

src/manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export const PUBLISHER_NAME = 'arm';
1818
export const EXTENSION_NAME = 'vscode-cmsis-debugger';
1919
export const EXTENSION_ID = `${PUBLISHER_NAME}.${EXTENSION_NAME}`;
2020
export const DISPLAY_NAME = 'Arm CMSIS Debugger';
21+
export const VIEW_PREFIX = 'cmsis-debugger';
2122

2223
export const COMPONENT_VIEWER_DISPLAY_NAME = 'Arm CMSIS Component Viewer';

src/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2025 Arm Limited
2+
* Copyright 2025-2026 Arm Limited
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,6 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17+
// NOTE: Keep vscode imports out of this file.
18+
// Mocking path in this module's tests causes trouble with path-require in global vscode mock.
19+
// Can be merged once we found a better solution for path usage in global vscode mock.
1720

1821
import * as os from 'os';
1922
import * as path from 'path';

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import { GDBTargetDebugTracker, GDBTargetDebugSession } from '../../debug-sessio
1919
import { ComponentViewerInstance } from './component-viewer-instance';
2020
import { URI } from 'vscode-uri';
2121
import { ComponentViewerTreeDataProvider } from './component-viewer-tree-view';
22-
import { componentViewerLogger } from '../../logger';
22+
import { componentViewerLogger, logger } from '../../logger';
2323
import type { ScvdGuiInterface } from './model/scvd-gui-interface';
2424
import { perf, parsePerf } from './stats-config';
25+
import { vscodeViewExists } from '../../vscode-utils';
2526

2627
export type UpdateReason = 'sessionChanged' | 'refreshTimer' | 'stackTrace' | 'stackItemChanged' | 'unlockingInstance';
2728

@@ -35,7 +36,7 @@ export interface ComponentViewerInstancesWrapper {
3536
export class ComponentViewer {
3637
private _activeSession: GDBTargetDebugSession | undefined;
3738
private _instances: ComponentViewerInstancesWrapper[] = [];
38-
private _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider | undefined;
39+
private _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider;
3940
private _context: vscode.ExtensionContext;
4041
private _instanceUpdateCounter: number = 0;
4142
private _loadingCounter: number = 0;
@@ -45,20 +46,30 @@ export class ComponentViewer {
4546
private _refreshTimerEnabled: boolean = true;
4647
private static readonly pendingUpdateDelayMs = 150;
4748

48-
public constructor(context: vscode.ExtensionContext) {
49+
public constructor(context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider) {
4950
this._context = context;
51+
this._componentViewerTreeDataProvider = componentViewerTreeDataProvider;
5052
}
5153

52-
public activate(tracker: GDBTargetDebugTracker): void {
54+
public async activate(tracker: GDBTargetDebugTracker): Promise<boolean> {
5355
// Register Component Viewer tree view
54-
this.registerTreeView();
56+
logger.debug('Activating Component Viewer Tree View and commands');
57+
if (!await this.registerTreeView()) {
58+
logger.error('Component Viewer: Component Viewer cannot be registered, abort activation');
59+
return false;
60+
}
5561
// Subscribe to debug tracker events to update active session
62+
componentViewerLogger.debug('Subscribing to debug tracker events');
5663
this.subscribetoDebugTrackerEvents(tracker);
64+
return true;
5765
}
5866

59-
protected registerTreeView(): void {
60-
this._componentViewerTreeDataProvider = new ComponentViewerTreeDataProvider();
67+
protected async registerTreeView(): Promise<boolean> {
68+
if (!await vscodeViewExists('componentViewer')) {
69+
return false;
70+
}
6171
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');
6273
const lockInstanceCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.lockComponent', async (node) => {
6374
this.handleLockInstance(node);
6475
});
@@ -80,6 +91,7 @@ export class ComponentViewer {
8091
enablePeriodicUpdateCommandDisposable,
8192
disablePeriodicUpdateCommandDisposable
8293
);
94+
return true;
8395
}
8496

8597
protected handleLockInstance(node: ScvdGuiInterface): void {
@@ -112,7 +124,7 @@ export class ComponentViewer {
112124
this.schedulePendingUpdate('unlockingInstance');
113125
instance.dirtyWhileLocked = false;
114126
}
115-
this._componentViewerTreeDataProvider?.refresh();
127+
this._componentViewerTreeDataProvider.refresh();
116128
}
117129

118130
protected async readScvdFiles(tracker: GDBTargetDebugTracker,session?: GDBTargetDebugSession): Promise<void> {
@@ -305,7 +317,7 @@ export class ComponentViewer {
305317

306318
private async updateInstances(updateReason: UpdateReason): Promise<void> {
307319
if (!this._activeSession) {
308-
this._componentViewerTreeDataProvider?.clear();
320+
this._componentViewerTreeDataProvider.clear();
309321
return;
310322
}
311323
componentViewerLogger.debug(`Component Viewer: Queuing update due to '${updateReason}'`);
@@ -345,6 +357,6 @@ export class ComponentViewer {
345357
}
346358
}
347359
perf?.logSummaries();
348-
this._componentViewerTreeDataProvider?.setRoots(roots);
360+
this._componentViewerTreeDataProvider.setRoots(roots);
349361
}
350362
}

0 commit comments

Comments
 (0)