Skip to content

Commit 0176ba9

Browse files
authored
Add context menu to live watch and contribute to text editor context menu for adding expressions to Live Watch window (#594)
* Adding a context menu to live watch * Adding copy expression command to context menu * Adding support for expression addition from source file to Live Watch window * Adding type of expression to tooltip of the item * Adding add to live watch command in both variables window and watch window * Adding command Show in Memory Inspector to Live Watch context menu * Adding tests
1 parent bd3fcbd commit 0176ba9

File tree

4 files changed

+350
-17
lines changed

4 files changed

+350
-17
lines changed

__mocks__/vscode.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ module.exports = {
7373
})),
7474
registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })),
7575
showWarningMessage: jest.fn(),
76+
showErrorMessage: jest.fn(),
7677
createStatusBarItem: jest.fn(),
7778
showQuickPick: jest.fn(),
7879
},
80+
env: {
81+
clipboard: {
82+
readText: jest.fn(),
83+
writeText: jest.fn()
84+
}
85+
},
7986
workspace: {
8087
getConfiguration: jest.fn(() => ({
8188
get: jest.fn(),

package.json

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,30 +83,81 @@
8383
{
8484
"command": "vscode-cmsis-debugger.liveWatch.add",
8585
"title": "Add Expression",
86-
"icon": "$(add)"
86+
"icon": "$(add)",
87+
"category": "Live Watch"
8788
},
8889
{
8990
"command": "vscode-cmsis-debugger.liveWatch.deleteAll",
9091
"title": "Delete All Expressions",
91-
"icon": "$(close-all)"
92+
"icon": "$(close-all)",
93+
"category": "Live Watch"
9294
},
9395
{
9496
"command": "vscode-cmsis-debugger.liveWatch.delete",
9597
"title": "Delete Expression",
96-
"icon": "$(close)"
98+
"icon": "$(close)",
99+
"category": "Live Watch"
97100
},
98101
{
99102
"command": "vscode-cmsis-debugger.liveWatch.refresh",
100103
"title": "Refresh",
101-
"icon": "$(refresh)"
104+
"icon": "$(refresh)",
105+
"category": "Live Watch"
102106
},
103107
{
104108
"command": "vscode-cmsis-debugger.liveWatch.modify",
105109
"title": "Modify Expression",
106-
"icon": "$(pencil)"
110+
"icon": "$(pencil)",
111+
"category": "Live Watch"
112+
},
113+
{
114+
"command": "vscode-cmsis-debugger.liveWatch.copy",
115+
"title": "Copy Expression",
116+
"category": "Live Watch"
117+
},
118+
{
119+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor",
120+
"title": "Add to Live Watch",
121+
"category": "Live Watch"
122+
},
123+
{
124+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow",
125+
"title": "Add to Live Watch",
126+
"category": "Live Watch"
127+
},
128+
{
129+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView",
130+
"title": "Add to Live Watch",
131+
"category": "Live Watch"
132+
},
133+
{
134+
"command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector",
135+
"title": "Show in Memory Inspector",
136+
"category": "Live Watch"
107137
}
108138
],
109139
"menus": {
140+
"editor/context": [
141+
{
142+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor",
143+
"when": "editorTextFocus && resourceScheme == file",
144+
"group": "z_commands"
145+
}
146+
],
147+
"debug/variables/context": [
148+
{
149+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView",
150+
"when": "inDebugMode",
151+
"group": "z_commands"
152+
}
153+
],
154+
"debug/watch/context": [
155+
{
156+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow",
157+
"when": "inDebugMode",
158+
"group": "z_commands"
159+
}
160+
],
110161
"commandPalette": [
111162
{
112163
"command": "vscode-cmsis-debugger.showCpuTimeHistory",
@@ -115,9 +166,61 @@
115166
{
116167
"command": "vscode-cmsis-debugger.resetCpuTimeHistory",
117168
"when": "inDebugMode"
169+
},
170+
{
171+
"command": "vscode-cmsis-debugger.liveWatch.add",
172+
"when": "true"
173+
},
174+
{
175+
"command": "vscode-cmsis-debugger.liveWatch.deleteAll",
176+
"when": "true"
177+
},
178+
{
179+
"command": "vscode-cmsis-debugger.liveWatch.refresh",
180+
"when": "inDebugMode"
181+
},
182+
{
183+
"command": "vscode-cmsis-debugger.liveWatch.modify",
184+
"when": "false"
185+
},
186+
{
187+
"command": "vscode-cmsis-debugger.liveWatch.delete",
188+
"when": "false"
189+
},
190+
{
191+
"command": "vscode-cmsis-debugger.liveWatch.copy",
192+
"when": "false"
193+
},
194+
{
195+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor",
196+
"when": "false"
197+
},
198+
{
199+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow",
200+
"when": "false"
201+
},
202+
{
203+
"command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView",
204+
"when": "false"
205+
},
206+
{
207+
"command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector",
208+
"when": "false"
118209
}
119210
],
120211
"view/title": [
212+
{
213+
"command": "vscode-cmsis-debugger.liveWatch.add",
214+
"when": "view == cmsis-debugger.liveWatch"
215+
},
216+
{
217+
"command": "vscode-cmsis-debugger.liveWatch.deleteAll",
218+
"when": "view == cmsis-debugger.liveWatch"
219+
},
220+
{
221+
"command": "vscode-cmsis-debugger.liveWatch.refresh",
222+
"when": "view == cmsis-debugger.liveWatch"
223+
},
121224
{
122225
"command": "vscode-cmsis-debugger.liveWatch.add",
123226
"when": "view == cmsis-debugger.liveWatch",
@@ -144,6 +247,41 @@
144247
"command": "vscode-cmsis-debugger.liveWatch.delete",
145248
"when": "view == cmsis-debugger.liveWatch && viewItem == expression",
146249
"group": "inline@2"
250+
},
251+
{
252+
"command": "vscode-cmsis-debugger.liveWatch.add",
253+
"when": "view == cmsis-debugger.liveWatch",
254+
"group": "contextMenuG1@1"
255+
},
256+
{
257+
"command": "vscode-cmsis-debugger.liveWatch.modify",
258+
"when": "view == cmsis-debugger.liveWatch && viewItem == expression",
259+
"group": "contextMenuG1@2"
260+
},
261+
{
262+
"command": "vscode-cmsis-debugger.liveWatch.copy",
263+
"when": "view == cmsis-debugger.liveWatch && viewItem == expression",
264+
"group": "contextMenuG1@3"
265+
},
266+
{
267+
"command": "vscode-cmsis-debugger.liveWatch.delete",
268+
"when": "view == cmsis-debugger.liveWatch && viewItem == expression",
269+
"group": "contextMenuG1@4"
270+
},
271+
{
272+
"command": "vscode-cmsis-debugger.liveWatch.deleteAll",
273+
"when": "view == cmsis-debugger.liveWatch",
274+
"group": "contextMenuG2@1"
275+
},
276+
{
277+
"command": "vscode-cmsis-debugger.liveWatch.refresh",
278+
"when": "view == cmsis-debugger.liveWatch",
279+
"group": "contextMenuG2@2"
280+
},
281+
{
282+
"command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector",
283+
"when": "view == cmsis-debugger.liveWatch && viewItem == expression",
284+
"group": "contextMenuG3@1"
147285
}
148286
]
149287
},

src/views/live-watch/live-watch.test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,34 @@ describe('LiveWatchTreeDataProvider', () => {
192192
await (liveWatchTreeDataProvider as any).rename(node, 'node-1-renamed');
193193
expect(node.expression).toBe('node-1-renamed');
194194
});
195+
196+
it('copy copies node expression to clipboard', async () => {
197+
const node = makeNode('node-to-copy', { result: '1', variablesReference: 0 }, 1);
198+
(liveWatchTreeDataProvider as any).roots = [node];
199+
await (liveWatchTreeDataProvider as any).handleCopyCommand(node);
200+
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith('node-to-copy');
201+
});
202+
203+
it('AddFromSelection adds selected text as new live watch expression to roots', async () => {
204+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: '5678', variablesReference: 0 });
205+
// Mock the active text editor with a selection whose active position returns a word range
206+
const fakeRange = { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } };
207+
const mockEditor: any = {
208+
document: {
209+
getWordRangeAtPosition: jest.fn().mockReturnValue(fakeRange),
210+
getText: jest.fn().mockReturnValue('selected-expression')
211+
},
212+
selection: { active: { line: 0, character: 5 } }
213+
};
214+
(vscode.window as any).activeTextEditor = mockEditor;
215+
await (liveWatchTreeDataProvider as any).handleAddFromSelectionCommand();
216+
const roots = (liveWatchTreeDataProvider as any).roots;
217+
expect(mockEditor.document.getWordRangeAtPosition).toHaveBeenCalledWith(mockEditor.selection.active);
218+
expect(mockEditor.document.getText).toHaveBeenCalledWith(fakeRange);
219+
expect(roots.length).toBe(1);
220+
expect(roots[0].expression).toBe('selected-expression');
221+
expect(roots[0].value.result).toBe('5678');
222+
});
195223
});
196224

197225
describe('refresh', () => {
@@ -247,7 +275,12 @@ describe('LiveWatchTreeDataProvider', () => {
247275
'vscode-cmsis-debugger.liveWatch.deleteAll',
248276
'vscode-cmsis-debugger.liveWatch.delete',
249277
'vscode-cmsis-debugger.liveWatch.refresh',
250-
'vscode-cmsis-debugger.liveWatch.modify'
278+
'vscode-cmsis-debugger.liveWatch.modify',
279+
'vscode-cmsis-debugger.liveWatch.copy',
280+
'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor',
281+
'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow',
282+
'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView',
283+
'vscode-cmsis-debugger.liveWatch.showInMemoryInspector'
251284
]));
252285
});
253286

@@ -324,6 +357,79 @@ describe('LiveWatchTreeDataProvider', () => {
324357
await handler();
325358
expect(refreshSpy).toHaveBeenCalled();
326359
});
360+
361+
it('watch window command adds variable name root', async () => {
362+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: 'value', variablesReference: 0 });
363+
liveWatchTreeDataProvider.activate(tracker);
364+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow');
365+
expect(handler).toBeDefined();
366+
await handler({ variable: { name: 'myWatchVariable' } });
367+
const roots = (liveWatchTreeDataProvider as any).roots;
368+
expect(roots.length).toBe(1);
369+
expect(roots[0].expression).toBe('myWatchVariable');
370+
});
371+
372+
it('watch window command does nothing with falsy payload', async () => {
373+
liveWatchTreeDataProvider.activate(tracker);
374+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow');
375+
await handler(undefined);
376+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(0);
377+
});
378+
379+
it('variables view command adds variable name root', async () => {
380+
jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: '12345', variablesReference: 0 });
381+
liveWatchTreeDataProvider.activate(tracker);
382+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView');
383+
expect(handler).toBeDefined();
384+
const payload = { container: { name: 'local' }, variable: { name: 'localVariable' } };
385+
await handler(payload);
386+
const roots = (liveWatchTreeDataProvider as any).roots;
387+
expect(roots.length).toBe(1);
388+
expect(roots[0].expression).toBe('localVariable');
389+
});
390+
391+
it('variables view command does nothing when variable missing', async () => {
392+
liveWatchTreeDataProvider.activate(tracker);
393+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView');
394+
await handler({ container: { name: 'local' } });
395+
expect((liveWatchTreeDataProvider as any).roots.length).toBe(0);
396+
});
397+
398+
it('showInMemoryInspector command does nothing when node is undefined', async () => {
399+
liveWatchTreeDataProvider.activate(tracker);
400+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector');
401+
expect(handler).toBeDefined();
402+
await handler(undefined);
403+
expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith('memory-inspector.show-variable', expect.anything());
404+
});
405+
406+
it('showInMemoryInspector shows error if extension is missing', async () => {
407+
(vscode.extensions.getExtension as jest.Mock).mockReturnValue(undefined);
408+
(vscode.window.showErrorMessage as jest.Mock).mockClear();
409+
liveWatchTreeDataProvider.activate(tracker);
410+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector');
411+
const node = makeNode('node', { result: '0x1234', variablesReference: 77 }, 1);
412+
await handler(node);
413+
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(expect.stringContaining('Memory Inspector extension is not installed'));
414+
expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith('memory-inspector.show-variable', expect.anything());
415+
});
416+
417+
it('showInMemoryInspector executes command with proper args when extension is present', async () => {
418+
(vscode.extensions.getExtension as jest.Mock).mockReturnValue({ id: 'eclipse-cdt.memory-inspector' });
419+
(vscode.commands.executeCommand as jest.Mock).mockResolvedValue('ok');
420+
liveWatchTreeDataProvider.activate(tracker);
421+
(liveWatchTreeDataProvider as any)._activeSession = { session: { id: 'session-1' } };
422+
const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector');
423+
const node = makeNode('node', { result: '0x1234', variablesReference: 0 }, 1);
424+
await handler(node);
425+
const lastCall = (vscode.commands.executeCommand as jest.Mock).mock.calls.pop();
426+
expect(lastCall[0]).toBe('memory-inspector.show-variable');
427+
const args = lastCall[1];
428+
expect(args.sessionId).toBe('session-1');
429+
expect(args.container.name).toBe('node');
430+
expect(args.variable.name).toBe('node');
431+
expect(args.variable.memoryReference).toBe('&(node)');
432+
});
327433
});
328434

329435
describe('evaluate', () => {

0 commit comments

Comments
 (0)