diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index b8a46710..dcd751be 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -73,9 +73,16 @@ module.exports = { })), registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })), showWarningMessage: jest.fn(), + showErrorMessage: jest.fn(), createStatusBarItem: jest.fn(), showQuickPick: jest.fn(), }, + env: { + clipboard: { + readText: jest.fn(), + writeText: jest.fn() + } + }, workspace: { getConfiguration: jest.fn(() => ({ get: jest.fn(), diff --git a/package.json b/package.json index 5c65fb11..aa739d79 100644 --- a/package.json +++ b/package.json @@ -83,30 +83,81 @@ { "command": "vscode-cmsis-debugger.liveWatch.add", "title": "Add Expression", - "icon": "$(add)" + "icon": "$(add)", + "category": "Live Watch" }, { "command": "vscode-cmsis-debugger.liveWatch.deleteAll", "title": "Delete All Expressions", - "icon": "$(close-all)" + "icon": "$(close-all)", + "category": "Live Watch" }, { "command": "vscode-cmsis-debugger.liveWatch.delete", "title": "Delete Expression", - "icon": "$(close)" + "icon": "$(close)", + "category": "Live Watch" }, { "command": "vscode-cmsis-debugger.liveWatch.refresh", "title": "Refresh", - "icon": "$(refresh)" + "icon": "$(refresh)", + "category": "Live Watch" }, { "command": "vscode-cmsis-debugger.liveWatch.modify", "title": "Modify Expression", - "icon": "$(pencil)" + "icon": "$(pencil)", + "category": "Live Watch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.copy", + "title": "Copy Expression", + "category": "Live Watch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor", + "title": "Add to Live Watch", + "category": "Live Watch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow", + "title": "Add to Live Watch", + "category": "Live Watch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView", + "title": "Add to Live Watch", + "category": "Live Watch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector", + "title": "Show in Memory Inspector", + "category": "Live Watch" } ], "menus": { + "editor/context": [ + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor", + "when": "editorTextFocus && resourceScheme == file", + "group": "z_commands" + } + ], + "debug/variables/context": [ + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView", + "when": "inDebugMode", + "group": "z_commands" + } + ], + "debug/watch/context": [ + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow", + "when": "inDebugMode", + "group": "z_commands" + } + ], "commandPalette": [ { "command": "vscode-cmsis-debugger.showCpuTimeHistory", @@ -115,9 +166,61 @@ { "command": "vscode-cmsis-debugger.resetCpuTimeHistory", "when": "inDebugMode" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.add", + "when": "true" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.deleteAll", + "when": "true" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.refresh", + "when": "inDebugMode" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.modify", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.delete", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.copy", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector", + "when": "false" } ], "view/title": [ + { + "command": "vscode-cmsis-debugger.liveWatch.add", + "when": "view == cmsis-debugger.liveWatch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.deleteAll", + "when": "view == cmsis-debugger.liveWatch" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.refresh", + "when": "view == cmsis-debugger.liveWatch" + }, { "command": "vscode-cmsis-debugger.liveWatch.add", "when": "view == cmsis-debugger.liveWatch", @@ -144,6 +247,41 @@ "command": "vscode-cmsis-debugger.liveWatch.delete", "when": "view == cmsis-debugger.liveWatch && viewItem == expression", "group": "inline@2" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.add", + "when": "view == cmsis-debugger.liveWatch", + "group": "contextMenuG1@1" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.modify", + "when": "view == cmsis-debugger.liveWatch && viewItem == expression", + "group": "contextMenuG1@2" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.copy", + "when": "view == cmsis-debugger.liveWatch && viewItem == expression", + "group": "contextMenuG1@3" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.delete", + "when": "view == cmsis-debugger.liveWatch && viewItem == expression", + "group": "contextMenuG1@4" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.deleteAll", + "when": "view == cmsis-debugger.liveWatch", + "group": "contextMenuG2@1" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.refresh", + "when": "view == cmsis-debugger.liveWatch", + "group": "contextMenuG2@2" + }, + { + "command": "vscode-cmsis-debugger.liveWatch.showInMemoryInspector", + "when": "view == cmsis-debugger.liveWatch && viewItem == expression", + "group": "contextMenuG3@1" } ] }, diff --git a/src/views/live-watch/live-watch.test.ts b/src/views/live-watch/live-watch.test.ts index 4a36a4bd..6244450f 100644 --- a/src/views/live-watch/live-watch.test.ts +++ b/src/views/live-watch/live-watch.test.ts @@ -192,6 +192,34 @@ describe('LiveWatchTreeDataProvider', () => { await (liveWatchTreeDataProvider as any).rename(node, 'node-1-renamed'); expect(node.expression).toBe('node-1-renamed'); }); + + it('copy copies node expression to clipboard', async () => { + const node = makeNode('node-to-copy', { result: '1', variablesReference: 0 }, 1); + (liveWatchTreeDataProvider as any).roots = [node]; + await (liveWatchTreeDataProvider as any).handleCopyCommand(node); + expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith('node-to-copy'); + }); + + it('AddFromSelection adds selected text as new live watch expression to roots', async () => { + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: '5678', variablesReference: 0 }); + // Mock the active text editor with a selection whose active position returns a word range + const fakeRange = { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } }; + const mockEditor: any = { + document: { + getWordRangeAtPosition: jest.fn().mockReturnValue(fakeRange), + getText: jest.fn().mockReturnValue('selected-expression') + }, + selection: { active: { line: 0, character: 5 } } + }; + (vscode.window as any).activeTextEditor = mockEditor; + await (liveWatchTreeDataProvider as any).handleAddFromSelectionCommand(); + const roots = (liveWatchTreeDataProvider as any).roots; + expect(mockEditor.document.getWordRangeAtPosition).toHaveBeenCalledWith(mockEditor.selection.active); + expect(mockEditor.document.getText).toHaveBeenCalledWith(fakeRange); + expect(roots.length).toBe(1); + expect(roots[0].expression).toBe('selected-expression'); + expect(roots[0].value.result).toBe('5678'); + }); }); describe('refresh', () => { @@ -247,7 +275,12 @@ describe('LiveWatchTreeDataProvider', () => { 'vscode-cmsis-debugger.liveWatch.deleteAll', 'vscode-cmsis-debugger.liveWatch.delete', 'vscode-cmsis-debugger.liveWatch.refresh', - 'vscode-cmsis-debugger.liveWatch.modify' + 'vscode-cmsis-debugger.liveWatch.modify', + 'vscode-cmsis-debugger.liveWatch.copy', + 'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor', + 'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow', + 'vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView', + 'vscode-cmsis-debugger.liveWatch.showInMemoryInspector' ])); }); @@ -324,6 +357,79 @@ describe('LiveWatchTreeDataProvider', () => { await handler(); expect(refreshSpy).toHaveBeenCalled(); }); + + it('watch window command adds variable name root', async () => { + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: 'value', variablesReference: 0 }); + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow'); + expect(handler).toBeDefined(); + await handler({ variable: { name: 'myWatchVariable' } }); + const roots = (liveWatchTreeDataProvider as any).roots; + expect(roots.length).toBe(1); + expect(roots[0].expression).toBe('myWatchVariable'); + }); + + it('watch window command does nothing with falsy payload', async () => { + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow'); + await handler(undefined); + expect((liveWatchTreeDataProvider as any).roots.length).toBe(0); + }); + + it('variables view command adds variable name root', async () => { + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: '12345', variablesReference: 0 }); + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView'); + expect(handler).toBeDefined(); + const payload = { container: { name: 'local' }, variable: { name: 'localVariable' } }; + await handler(payload); + const roots = (liveWatchTreeDataProvider as any).roots; + expect(roots.length).toBe(1); + expect(roots[0].expression).toBe('localVariable'); + }); + + it('variables view command does nothing when variable missing', async () => { + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView'); + await handler({ container: { name: 'local' } }); + expect((liveWatchTreeDataProvider as any).roots.length).toBe(0); + }); + + it('showInMemoryInspector command does nothing when node is undefined', async () => { + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector'); + expect(handler).toBeDefined(); + await handler(undefined); + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith('memory-inspector.show-variable', expect.anything()); + }); + + it('showInMemoryInspector shows error if extension is missing', async () => { + (vscode.extensions.getExtension as jest.Mock).mockReturnValue(undefined); + (vscode.window.showErrorMessage as jest.Mock).mockClear(); + liveWatchTreeDataProvider.activate(tracker); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector'); + const node = makeNode('node', { result: '0x1234', variablesReference: 77 }, 1); + await handler(node); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(expect.stringContaining('Memory Inspector extension is not installed')); + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith('memory-inspector.show-variable', expect.anything()); + }); + + it('showInMemoryInspector executes command with proper args when extension is present', async () => { + (vscode.extensions.getExtension as jest.Mock).mockReturnValue({ id: 'eclipse-cdt.memory-inspector' }); + (vscode.commands.executeCommand as jest.Mock).mockResolvedValue('ok'); + liveWatchTreeDataProvider.activate(tracker); + (liveWatchTreeDataProvider as any)._activeSession = { session: { id: 'session-1' } }; + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.showInMemoryInspector'); + const node = makeNode('node', { result: '0x1234', variablesReference: 0 }, 1); + await handler(node); + const lastCall = (vscode.commands.executeCommand as jest.Mock).mock.calls.pop(); + expect(lastCall[0]).toBe('memory-inspector.show-variable'); + const args = lastCall[1]; + expect(args.sessionId).toBe('session-1'); + expect(args.container.name).toBe('node'); + expect(args.variable.name).toBe('node'); + expect(args.variable.memoryReference).toBe('&(node)'); + }); }); describe('evaluate', () => { diff --git a/src/views/live-watch/live-watch.ts b/src/views/live-watch/live-watch.ts index 1bb35b72..7b0f72fe 100644 --- a/src/views/live-watch/live-watch.ts +++ b/src/views/live-watch/live-watch.ts @@ -29,6 +29,7 @@ interface LiveWatchNode { export interface LiveWatchValue { result: string; variablesReference: number; + type?: string; } export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider { @@ -80,7 +81,7 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider await this.registerAddCommand()); - const deleteAllCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.deleteAll', async () => await this.registerDeleteAllCommand()); - const deleteCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.delete', async (node) => await this.registerDeleteCommand(node)); + const addCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.add', async () => await this.handleAddCommand()); + const deleteAllCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.deleteAll', async () => await this.handleDeleteAllCommand()); + const deleteCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.delete', async (node) => await this.handleDeleteCommand(node)); const refreshCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.refresh', async () => await this.refresh()); - const modifyCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.modify', async (node) => await this.registerRenameCommand(node)); - this._context.subscriptions.push(registerLiveWatchView, + const modifyCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.modify', async (node) => await this.handleRenameCommand(node)); + const copyCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.copy', async (node) => await this.handleCopyCommand(node)); + const addToLiveWatchCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromTextEditor', + async () => await this.handleAddFromSelectionCommand()); + /* omarArm: I am using the same callback function for both watch window and variables view, as they have the same payload structure for now. + However, I believe the payload structure will change for the watch window in the future as the developer who created the PR for contributing to watch window context menu + mentioned he used variables' window payload structure for simplicity. + Find the PR here: + https://github.com/microsoft/vscode/pull/237751 + */ + const addToLiveWatchFromWatchWindowCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromWatchWindow', + async (payload: { container: DebugProtocol.Scope; variable: DebugProtocol.Variable; }) => await this.handleAddToLiveWatchFromVariablesView(payload)); + const addToLiveWatchFromVariablesViewCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.addToLiveWatchFromVariablesView', + async (payload: { container: DebugProtocol.Scope; variable: DebugProtocol.Variable; }) => await this.handleAddToLiveWatchFromVariablesView(payload)); + const showInMemoryInspectorCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.showInMemoryInspector', + async (node: LiveWatchNode) => await this.handleShowInMemoryInspector(node)); + this._context.subscriptions.push( + registerLiveWatchView, addCommand, - deleteAllCommand, deleteCommand, refreshCommand, modifyCommand); + deleteAllCommand, + deleteCommand, + refreshCommand, + modifyCommand, + copyCommand, + addToLiveWatchCommand, + addToLiveWatchFromWatchWindowCommand, + addToLiveWatchFromVariablesViewCommand, + showInMemoryInspectorCommand + ); } - private async registerAddCommand() { + private async handleAddCommand() { const expression = await vscode.window.showInputBox({ prompt: 'Expression' }); if (!expression) { return; @@ -151,18 +177,18 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider { const response: LiveWatchValue = { result: '', variablesReference: 0 }; if (!this._activeSession) { @@ -186,6 +267,7 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider