diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index 2b4a26c2..b8a46710 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -71,6 +71,7 @@ module.exports = { warn: jest.fn(), error: jest.fn(), })), + registerTreeDataProvider: jest.fn(() => ({ dispose: jest.fn() })), showWarningMessage: jest.fn(), createStatusBarItem: jest.fn(), showQuickPick: jest.fn(), diff --git a/media/trace-and-live-dark.svg b/media/trace-and-live-dark.svg new file mode 100644 index 00000000..28c6e80a --- /dev/null +++ b/media/trace-and-live-dark.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + diff --git a/media/trace-and-live-light.svg b/media/trace-and-live-light.svg new file mode 100644 index 00000000..61fdca39 --- /dev/null +++ b/media/trace-and-live-light.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/package.json b/package.json index 095e6e4e..5c65fb11 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "activitybar": [ { "id": "cmsis-debugger", - "title": "CMSIS Debug", - "icon": "$(rocket)" + "title": "Trace and Live View", + "icon": "media/trace-and-live-dark.svg" } ] }, @@ -60,7 +60,8 @@ "cmsis-debugger": [ { "id": "cmsis-debugger.liveWatch", - "name": "Live Watch" + "name": "Live Watch", + "icon": "media/trace-and-live-light.svg" } ] }, @@ -80,27 +81,27 @@ "when": "inDebugMode" }, { - "command": "cmsis-debugger.liveWatch.add", + "command": "vscode-cmsis-debugger.liveWatch.add", "title": "Add Expression", "icon": "$(add)" }, { - "command": "cmsis-debugger.liveWatch.deleteAll", + "command": "vscode-cmsis-debugger.liveWatch.deleteAll", "title": "Delete All Expressions", "icon": "$(close-all)" }, { - "command": "cmsis-debugger.liveWatch.delete", + "command": "vscode-cmsis-debugger.liveWatch.delete", "title": "Delete Expression", "icon": "$(close)" }, { - "command": "cmsis-debugger.liveWatch.refresh", + "command": "vscode-cmsis-debugger.liveWatch.refresh", "title": "Refresh", "icon": "$(refresh)" }, { - "command": "cmsis-debugger.liveWatch.modify", + "command": "vscode-cmsis-debugger.liveWatch.modify", "title": "Modify Expression", "icon": "$(pencil)" } @@ -118,29 +119,29 @@ ], "view/title": [ { - "command": "cmsis-debugger.liveWatch.add", + "command": "vscode-cmsis-debugger.liveWatch.add", "when": "view == cmsis-debugger.liveWatch", "group": "navigation@1" }, { - "command": "cmsis-debugger.liveWatch.deleteAll", + "command": "vscode-cmsis-debugger.liveWatch.deleteAll", "when": "view == cmsis-debugger.liveWatch", "group": "navigation@3" }, { - "command": "cmsis-debugger.liveWatch.refresh", + "command": "vscode-cmsis-debugger.liveWatch.refresh", "when": "view == cmsis-debugger.liveWatch", "group": "navigation@2" } ], "view/item/context": [ { - "command": "cmsis-debugger.liveWatch.modify", + "command": "vscode-cmsis-debugger.liveWatch.modify", "when": "view == cmsis-debugger.liveWatch && viewItem == expression", "group": "inline@1" }, { - "command": "cmsis-debugger.liveWatch.delete", + "command": "vscode-cmsis-debugger.liveWatch.delete", "when": "view == cmsis-debugger.liveWatch && viewItem == expression", "group": "inline@2" } diff --git a/src/debug-session/gdbtarget-debug-session.test.ts b/src/debug-session/gdbtarget-debug-session.test.ts index 12176f4b..0da77663 100644 --- a/src/debug-session/gdbtarget-debug-session.test.ts +++ b/src/debug-session/gdbtarget-debug-session.test.ts @@ -59,18 +59,18 @@ describe('GDBTargetDebugSession', () => { it('evaluates a global expression without active stack frame and returns a value', async () => { // Only mock relevant properties, return value is body of EvaluateResponse - (debugSession.customRequest as jest.Mock).mockReturnValueOnce({ result: '1234567' }); + (debugSession.customRequest as jest.Mock).mockReturnValueOnce({ result: '1234567', variableReference: 0 }); const result = await gdbTargetSession.evaluateGlobalExpression('myGlobalVariable'); - expect(result).toEqual('1234567'); + expect(result).toEqual({ result: '1234567', variableReference: 0 }); expect(debugSession.customRequest as jest.Mock).toHaveBeenCalledWith('evaluate', { expression: 'myGlobalVariable', frameId: 0, context: 'hover' }); }); it('evaluates a global expression with active stack frame and returns a value', async () => { // Only mock relevant properties, return value is body of EvaluateResponse - (debugSession.customRequest as jest.Mock).mockReturnValueOnce({ result: '1234567' }); + (debugSession.customRequest as jest.Mock).mockReturnValueOnce({ result: '1234567', variableReference: 0 }); (vscode.debug.activeStackItem as unknown) = { session: debugSession, threadId: 1, frameId: 2 }; const result = await gdbTargetSession.evaluateGlobalExpression('myGlobalVariable'); - expect(result).toEqual('1234567'); + expect(result).toEqual({ result: '1234567', variableReference: 0 }); expect(debugSession.customRequest as jest.Mock).toHaveBeenCalledWith('evaluate', { expression: 'myGlobalVariable', frameId: 2, context: 'hover' }); // restore default (vscode.debug.activeStackItem as unknown) = undefined; diff --git a/src/debug-session/gdbtarget-debug-session.ts b/src/debug-session/gdbtarget-debug-session.ts index 82197fed..77d4a690 100644 --- a/src/debug-session/gdbtarget-debug-session.ts +++ b/src/debug-session/gdbtarget-debug-session.ts @@ -54,7 +54,8 @@ export class GDBTargetDebugSession { this._cbuildRunParsePromise = undefined; } - public async evaluateGlobalExpression(expression: string, context = 'hover'): Promise { + /** Function returns string only in case of failure */ + public async evaluateGlobalExpression(expression: string, context = 'hover'): Promise { try { const frameId = (vscode.debug.activeStackItem as vscode.DebugStackFrame)?.frameId ?? 0; const args: DebugProtocol.EvaluateArguments = { @@ -63,7 +64,7 @@ export class GDBTargetDebugSession { context: context }; const response = await this.session.customRequest('evaluate', args) as DebugProtocol.EvaluateResponse['body']; - return response.result; + return response; } catch (error: unknown) { const errorMessage = (error as Error)?.message; logger.debug(`Session '${this.session.name}': Failed to evaluate global expression '${expression}' - '${errorMessage}'`); diff --git a/src/features/cpu-states/cpu-states.ts b/src/features/cpu-states/cpu-states.ts index 7c1da098..e6cececf 100644 --- a/src/features/cpu-states/cpu-states.ts +++ b/src/features/cpu-states/cpu-states.ts @@ -215,7 +215,10 @@ export class CpuStates { protected async getFrequency(): Promise { const result = await this.activeSession?.evaluateGlobalExpression('SystemCoreClock'); - const frequencyString = result?.match(/\d+/) ? result : undefined; + if (typeof result == 'string') { + return undefined; + } + const frequencyString = result?.result.match(/\d+/) ? result.result : undefined; if (!frequencyString) { return undefined; } diff --git a/src/views/live-watch/live-watch.test.ts b/src/views/live-watch/live-watch.test.ts index 5b700124..4a36a4bd 100644 --- a/src/views/live-watch/live-watch.test.ts +++ b/src/views/live-watch/live-watch.test.ts @@ -17,13 +17,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as vscode from 'vscode'; import { debugSessionFactory, extensionContextFactory } from '../../__test__/vscode.factory'; -import { LiveWatchTreeDataProvider } from './live-watch'; +import { LiveWatchValue, LiveWatchTreeDataProvider } from './live-watch'; import { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../debug-session'; import { gdbTargetConfiguration } from '../../debug-configuration/debug-configuration.factory'; import { GDBTargetConfiguration } from '../../debug-configuration'; -// Inline mock for registerTreeDataProvider specific to these tests -const registerTreeDataProviderMock = jest.fn(() => ({ dispose: jest.fn() })); describe('LiveWatchTreeDataProvider', () => { let liveWatchTreeDataProvider: LiveWatchTreeDataProvider; @@ -33,8 +31,8 @@ describe('LiveWatchTreeDataProvider', () => { let debugConfig: GDBTargetConfiguration; // Helper: create a dummy node - function makeNode(expression = 'x', value = '1', id = 1) { - return { id, expression, value, parent: undefined }; + function makeNode(expression = 'x', value: LiveWatchValue = { result: '1', variablesReference: 0 }, id = 1) { + return { id, expression, value, parent: undefined, children: [] }; } beforeEach(() => { @@ -50,19 +48,15 @@ describe('LiveWatchTreeDataProvider', () => { describe('session management and connection tests', () => { it('should activate the live watch tree data provider', () => { - (vscode.window).registerTreeDataProvider = registerTreeDataProviderMock; liveWatchTreeDataProvider.activate(tracker); }); it('registers the live watch tree data provider', async () => { - (vscode.window).registerTreeDataProvider = registerTreeDataProviderMock; - registerTreeDataProviderMock.mockClear(); liveWatchTreeDataProvider.activate(tracker); - expect(registerTreeDataProviderMock).toHaveBeenCalledWith('cmsis-debugger.liveWatch', liveWatchTreeDataProvider); + expect(vscode.window.registerTreeDataProvider).toHaveBeenCalledWith('cmsis-debugger.liveWatch', liveWatchTreeDataProvider); }); it('manages session lifecycles correctly', async () => { - (vscode.window).registerTreeDataProvider = registerTreeDataProviderMock; liveWatchTreeDataProvider.activate(tracker); // No active session yet expect((liveWatchTreeDataProvider as any).activeSession).toBeUndefined(); @@ -129,7 +123,7 @@ describe('LiveWatchTreeDataProvider', () => { describe('tree data methods', () => { it('getChildren returns roots when no element is passed', async () => { - (liveWatchTreeDataProvider as any).roots = [makeNode('node-1', '1', 1), makeNode('node-2', '2', 2)]; + (liveWatchTreeDataProvider as any).roots = [makeNode('node-1', { result: '1', variablesReference: 0 }, 1), makeNode('node-2', { result: '2', variablesReference: 0 }, 2)]; const children = await liveWatchTreeDataProvider.getChildren(); expect(children.length).toBe(2); expect(children[0].expression).toBe('node-1'); @@ -137,16 +131,31 @@ describe('LiveWatchTreeDataProvider', () => { }); it('getChildren returns children of element', async () => { - const childNode = makeNode('childNode', '2', 2); - const parent = { ...makeNode('parentNode', '1', 1), children: [childNode] }; + const parent = makeNode('parentNode', { result: '1', variablesReference: 123 }, 1); (liveWatchTreeDataProvider as any).roots = [parent]; + // Mock active session with customRequest returning one variable + (liveWatchTreeDataProvider as any)._activeSession = { + session: { + customRequest: jest.fn().mockResolvedValue({ + variables: [ + { name: 'childNode', value: '2', variablesReference: 0 } + ] + }) + }, + evaluateGlobalExpression: jest.fn() + }; const children = await liveWatchTreeDataProvider.getChildren(parent); + expect((liveWatchTreeDataProvider as any)._activeSession.session.customRequest).toHaveBeenCalledWith('variables', { variablesReference: parent.value.variablesReference }); expect(children.length).toBe(1); expect(children[0].expression).toBe('childNode'); + expect(children[0].value.result).toBe('2'); + expect(children[0].value.variablesReference).toBe(0); + // Ensure dynamic children not persisted on parent + expect(parent.children.length).toBe(0); }); it('getTreeItem returns correct TreeItem', () => { - const node = makeNode('expression', 'value', 1); + const node = makeNode('expression', { result: 'value', variablesReference: 1 }, 1); const item = liveWatchTreeDataProvider.getTreeItem(node); expect(item.label).toBe('expression = '); expect(item.description).toBe('value'); @@ -156,28 +165,29 @@ describe('LiveWatchTreeDataProvider', () => { describe('node management', () => { it('add creates a new root node', async () => { - jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('1234'); - await (liveWatchTreeDataProvider as any).add('expression'); + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: '1234', variablesReference: 0 }); + // adapt method name addToRoots (changed implementation) + await (liveWatchTreeDataProvider as any).addToRoots('expression'); expect((liveWatchTreeDataProvider as any).roots.length).toBe(1); expect((liveWatchTreeDataProvider as any).roots[0].expression).toBe('expression'); - expect((liveWatchTreeDataProvider as any).roots[0].value).toBe('1234'); + expect((liveWatchTreeDataProvider as any).roots[0].value.result).toBe('1234'); }); it('clear removes all nodes', async () => { - (liveWatchTreeDataProvider as any).roots = [makeNode('expression', '1', 1)]; + (liveWatchTreeDataProvider as any).roots = [makeNode('expression', { result: '1', variablesReference: 0 })]; await (liveWatchTreeDataProvider as any).clear(); expect((liveWatchTreeDataProvider as any).roots.length).toBe(0); }); it('delete removes a node by id', async () => { - (liveWatchTreeDataProvider as any).roots = [makeNode('node-1', '1', 1), makeNode('node-2', '2', 2)]; + (liveWatchTreeDataProvider as any).roots = [makeNode('node-1', { result: '1', variablesReference: 0 }, 1), makeNode('node-2', { result: '2', variablesReference: 0 }, 2)]; await (liveWatchTreeDataProvider as any).delete({ id: 1 }); expect((liveWatchTreeDataProvider as any).roots.length).toBe(1); expect((liveWatchTreeDataProvider as any).roots[0].id).toBe(2); }); it('rename updates node expression', async () => { - const node = makeNode('node-1', '1', 1); + const node = makeNode('node-1', { result: '1', variablesReference: 0 }, 1); (liveWatchTreeDataProvider as any).roots = [node]; await (liveWatchTreeDataProvider as any).rename(node, 'node-1-renamed'); expect(node.expression).toBe('node-1-renamed'); @@ -186,26 +196,41 @@ describe('LiveWatchTreeDataProvider', () => { describe('refresh', () => { it('refresh updates all root node values', async () => { - const node = makeNode('expression', 'old-value', 1); + const node = makeNode('expression', { result: 'old-value', variablesReference: 1 }, 1); (liveWatchTreeDataProvider as any).roots = [node]; - jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('new-value'); + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: 'new-value', variablesReference: 0 }); await (liveWatchTreeDataProvider as any).refresh(); - expect(node.value).toBe('new-value'); + expect(node.value.result).toBe('new-value'); }); it('refresh(node) updates only that node', async () => { - const node = makeNode('expression', 'old-value', 1); + const node = makeNode('expression', { result: 'old-value', variablesReference: 1 }, 1); (liveWatchTreeDataProvider as any).roots = [node]; - jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('new-value'); + jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue({ result: 'new-value', variablesReference: 0 }); await (liveWatchTreeDataProvider as any).refresh(node); - expect(node.value).toBe('new-value'); + expect(node.value.result).toBe('new-value'); + }); + + it('refresh without argument evaluates each root and fires tree change once', async () => { + const nodeA = makeNode('node-A', { result: 'value-A', variablesReference: 0 }, 1); + const nodeB = makeNode('node-B', { result: 'value-B', variablesReference: 0 }, 2); + (liveWatchTreeDataProvider as any).roots = [nodeA, nodeB]; + const evalMock = jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate') + .mockImplementation(async (expr: unknown) => ({ result: String(expr) + '-updated', variablesReference: 0 })); + const fireSpy = jest.spyOn((liveWatchTreeDataProvider as any)._onDidChangeTreeData, 'fire'); + await (liveWatchTreeDataProvider as any).refresh(); + expect(evalMock).toHaveBeenCalledTimes(2); + expect(nodeA.value.result).toBe('node-A-updated'); + expect(nodeB.value.result).toBe('node-B-updated'); + expect(fireSpy).toHaveBeenCalledTimes(1); + // fire called with undefined (no specific node) per implementation + expect(fireSpy.mock.calls[0][0]).toBeUndefined(); }); }); describe('command registration', () => { beforeEach(() => { (vscode.commands as any).registerCommand?.mockClear?.(); - (vscode.window as any).registerTreeDataProvider = registerTreeDataProviderMock; }); function getRegisteredHandler(commandId: string) { @@ -218,11 +243,11 @@ describe('LiveWatchTreeDataProvider', () => { liveWatchTreeDataProvider.activate(tracker); const calls = (vscode.commands.registerCommand as jest.Mock).mock.calls.map(call => call[0]); expect(calls).toEqual(expect.arrayContaining([ - 'cmsis-debugger.liveWatch.add', - 'cmsis-debugger.liveWatch.deleteAll', - 'cmsis-debugger.liveWatch.delete', - 'cmsis-debugger.liveWatch.refresh', - 'cmsis-debugger.liveWatch.modify' + 'vscode-cmsis-debugger.liveWatch.add', + 'vscode-cmsis-debugger.liveWatch.deleteAll', + 'vscode-cmsis-debugger.liveWatch.delete', + 'vscode-cmsis-debugger.liveWatch.refresh', + 'vscode-cmsis-debugger.liveWatch.modify' ])); }); @@ -230,7 +255,7 @@ describe('LiveWatchTreeDataProvider', () => { (vscode.window as any).showInputBox = jest.fn().mockResolvedValue('expression'); const evaluateSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'evaluate').mockResolvedValue('someValue'); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.add'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.add'); expect(handler).toBeDefined(); await handler(); const roots = (liveWatchTreeDataProvider as any).roots; @@ -242,26 +267,26 @@ describe('LiveWatchTreeDataProvider', () => { it('add command does nothing when expression undefined', async () => { (vscode.window as any).showInputBox = jest.fn().mockResolvedValue(undefined); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.add'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.add'); await handler(); expect((liveWatchTreeDataProvider as any).roots.length).toBe(0); }); it('deleteAll command clears roots', async () => { - (liveWatchTreeDataProvider as any).roots = [makeNode('nodeA','1',1), makeNode('nodeB','2',2)]; + (liveWatchTreeDataProvider as any).roots = [makeNode('nodeA', { result: '1', variablesReference: 0 }, 1), makeNode('nodeB', { result: '2', variablesReference: 0 }, 2)]; const clearSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'clear'); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.deleteAll'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.deleteAll'); await handler(); expect(clearSpy).toHaveBeenCalled(); expect((liveWatchTreeDataProvider as any).roots.length).toBe(0); }); it('delete command removes provided node', async () => { - (liveWatchTreeDataProvider as any).roots = [makeNode('nodeA','1',1), makeNode('nodeB','2',2)]; + (liveWatchTreeDataProvider as any).roots = [makeNode('nodeA', { result: '1', variablesReference: 0 }, 1), makeNode('nodeB', { result: '2', variablesReference: 0 }, 2)]; const deleteSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'delete'); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.delete'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.delete'); const target = (liveWatchTreeDataProvider as any).roots[0]; await handler(target); expect(deleteSpy).toHaveBeenCalledWith(target); @@ -269,24 +294,24 @@ describe('LiveWatchTreeDataProvider', () => { }); it('modify command renames a node when expression provided', async () => { - const node = makeNode('oldExpression','1',1); + const node = makeNode('oldExpression',{ result: '1', variablesReference: 0 },1); (liveWatchTreeDataProvider as any).roots = [node]; (vscode.window as any).showInputBox = jest.fn().mockResolvedValue('newExpression'); const renameSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'rename'); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.modify'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.modify'); await handler(node); expect(renameSpy).toHaveBeenCalledWith(node,'newExpression'); expect(node.expression).toBe('newExpression'); }); it('modify command does nothing when expression undefined', async () => { - const node = makeNode('oldExpression','1',1); + const node = makeNode('oldExpression', { result: '1', variablesReference: 0 }, 1); (liveWatchTreeDataProvider as any).roots = [node]; (vscode.window as any).showInputBox = jest.fn().mockResolvedValue(undefined); const renameSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'rename'); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.modify'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.modify'); await handler(node); expect(renameSpy).not.toHaveBeenCalled(); expect(node.expression).toBe('oldExpression'); @@ -295,10 +320,40 @@ describe('LiveWatchTreeDataProvider', () => { it('refresh command triggers provider.refresh()', async () => { const refreshSpy = jest.spyOn(liveWatchTreeDataProvider as any, 'refresh').mockResolvedValue(undefined); liveWatchTreeDataProvider.activate(tracker); - const handler = getRegisteredHandler('cmsis-debugger.liveWatch.refresh'); + const handler = getRegisteredHandler('vscode-cmsis-debugger.liveWatch.refresh'); await handler(); expect(refreshSpy).toHaveBeenCalled(); }); }); + + describe('evaluate', () => { + it('returns No active session when none set', async () => { + const result = await (liveWatchTreeDataProvider as any).evaluate('myExpression'); + expect(result.result).toBe('No active session'); + expect(result.variablesReference).toBe(0); + }); + + it('maps string result into LiveWatchValue', async () => { + // mock active session with evaluateGlobalExpression returning a string + (liveWatchTreeDataProvider as any)._activeSession = { + evaluateGlobalExpression: jest.fn().mockResolvedValue('string-value'), + session: {} + }; + const evalResult = await (liveWatchTreeDataProvider as any).evaluate('myExpression'); + expect(evalResult.result).toBe('string-value'); + expect(evalResult.variablesReference).toBe(0); + }); + + it('maps object result into LiveWatchValue', async () => { + const responseObj = { result: 'value', variablesReference: 1234 }; + (liveWatchTreeDataProvider as any)._activeSession = { + evaluateGlobalExpression: jest.fn().mockResolvedValue(responseObj), + session: {} + }; + const evalResult = await (liveWatchTreeDataProvider as any).evaluate('myExpression'); + expect(evalResult.result).toBe('value'); + expect(evalResult.variablesReference).toBe(1234); + }); + }); }); /* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/src/views/live-watch/live-watch.ts b/src/views/live-watch/live-watch.ts index beab4e16..1bb35b72 100644 --- a/src/views/live-watch/live-watch.ts +++ b/src/views/live-watch/live-watch.ts @@ -15,14 +15,20 @@ */ import * as vscode from 'vscode'; +import { DebugProtocol } from '@vscode/debugprotocol'; import { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../debug-session'; interface LiveWatchNode { id: number; expression: string; - value: string; parent: LiveWatchNode | undefined; // if undefined, it's a root node - children?: LiveWatchNode[]; // keep for future grouping; flat list for now + children: LiveWatchNode[]; + value: LiveWatchValue +} + +export interface LiveWatchValue { + result: string; + variablesReference: number; } export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider { @@ -45,18 +51,37 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider { + public async getChildren(element?: LiveWatchNode): Promise { if (!element) { return Promise.resolve(this.roots); } - return Promise.resolve(element.children ?? []); + try { + const children = await this._activeSession?.session.customRequest('variables', { variablesReference: element.value.variablesReference }); + const childNodes = children?.variables.map((child: DebugProtocol.Variable) => ({ + id: this.nodeID++, + expression: child.name, + children: [], + parent: element, + value: { + result: child.value, + variablesReference: child.variablesReference + } + })) ?? []; + + // We do not store children of nodes in the tree, as they are dynamic + return childNodes; + } catch (error) { + console.error('Error fetching children:', error); + return []; + } } public getTreeItem(element: LiveWatchNode): vscode.TreeItem { - const item = new vscode.TreeItem(element.expression + ' = ', vscode.TreeItemCollapsibleState.None); - item.description = element.value || ''; + const item = new vscode.TreeItem(element.expression + ' = '); + item.description = element.value.result; item.contextValue = 'expression'; item.tooltip = element.expression; + item.collapsibleState = element.value.variablesReference !== 0 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; return item; } @@ -108,11 +133,11 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider await this.registerAddCommand()); - const deleteAllCommand = vscode.commands.registerCommand('cmsis-debugger.liveWatch.deleteAll', async () => await this.registerDeleteAllCommand()); - const deleteCommand = vscode.commands.registerCommand('cmsis-debugger.liveWatch.delete', async (node) => await this.registerDeleteCommand(node)); - const refreshCommand = vscode.commands.registerCommand('cmsis-debugger.liveWatch.refresh', async () => await this.refresh()); - const modifyCommand = vscode.commands.registerCommand('cmsis-debugger.liveWatch.modify', async (node) => await this.registerRenameCommand(node)); + const addCommand = vscode.commands.registerCommand('vscode-cmsis-debugger.liveWatch.add', async () => 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 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, addCommand, deleteAllCommand, deleteCommand, refreshCommand, modifyCommand); @@ -123,7 +148,7 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider { + private async evaluate(expression: string): Promise { + const response: LiveWatchValue = { result: '', variablesReference: 0 }; if (!this._activeSession) { - return 'No active session'; + response.result = 'No active session'; + return response; } const result = await this._activeSession.evaluateGlobalExpression(expression, 'watch'); - return result; + if (typeof result == 'string') { + response.result = result; + return response; + } + response.result = result.result; + response.variablesReference = result.variablesReference; + return response; } - private async add(expression: string, parent?: LiveWatchNode) { + private async addToRoots(expression: string, parent?: LiveWatchNode) { // Create a new node with a unique ID and evaluate its value const newNode: LiveWatchNode = { - id: ++this.nodeID, + id: this.nodeID++, + children: [], expression, - value : await this.evaluate(expression), - parent: parent ?? undefined + parent: parent ?? undefined, + value: await this.evaluate(expression) }; + if (!parent) { this.roots.push(newNode); } else { @@ -176,6 +211,7 @@ export class LiveWatchTreeDataProvider implements vscode.TreeDataProvider