Skip to content

Commit 29201f3

Browse files
committed
Add Show in Memory Inspector entry to Watch view context menu
VS Code 1.103 introduced the debug/watch/context menu extension point (microsoft/vscode#237751), which lets extensions contribute actions to the Watch view context menu. The Watch view passes the IExpression instance directly as the command argument instead of the wrapped {sessionId, container, variable} shape used by the Variables view, so a separate type guard is required. - Add WatchView.IWatchItemContext type and isWatchItemContext guard in src/common/external-views.ts. - Teach the show-variable and go-to-value command handlers in memory-webview-main.ts to accept the Watch view argument shape, resolving the memory reference from memoryReference (always populated when canViewMemory is true) with a fallback to getAddressOfVariable(evaluateName ?? name). - Mirror the debug/variables/context menu entries under debug/watch/context in package.json so show-variable, go-to-value and store-file are offered on watch items when the debug adapter supports reading memory. - Wire up mocha + ts-node + chai test infrastructure and add unit tests for isVariablesContext and isWatchItemContext covering wrapper contexts, root/child watch expressions, primitives and discrimination between the two shapes. Fixes: #165 Signed-off-by: Morten Engelhardt Olsen <MortenEngelhardt.Olsen@microchip.com>
1 parent cbfcb9e commit 29201f3

8 files changed

Lines changed: 3268 additions & 1592 deletions

File tree

.mocharc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"require": [
3+
"ts-node/register/transpile-only"
4+
],
5+
"extensions": [
6+
"ts"
7+
],
8+
"spec": [
9+
"src/**/*.spec.ts"
10+
]
11+
}

package.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"watch": "webpack -w --mode development ",
2929
"lint": "eslint . --ext .ts,.tsx",
3030
"package": "vsce package --yarn",
31-
"serve": "serve --cors -p 3333"
31+
"serve": "serve --cors -p 3333",
32+
"test": "mocha"
3233
},
3334
"dependencies": {
3435
"@vscode/codicons": "^0.0.32",
@@ -48,7 +49,9 @@
4849
"vscode-uri": "^3.0.8"
4950
},
5051
"devDependencies": {
52+
"@types/chai": "^5.2.3",
5153
"@types/lodash": "^4.14.202",
54+
"@types/mocha": "^10.0.10",
5255
"@types/node": "^20.17.22",
5356
"@types/react": "^18.0.26",
5457
"@types/react-dom": "^18.0.9",
@@ -59,16 +62,19 @@
5962
"@typescript-eslint/parser": "^5.45.0",
6063
"@vscode/debugprotocol": "^1.55.0",
6164
"@vscode/vsce": "^3.2.2",
65+
"chai": "^5.2.3",
6266
"css-loader": "^6.9.0",
6367
"eslint": "^8.29.0",
6468
"eslint-plugin-deprecation": "^1.3.3",
6569
"eslint-plugin-import": "^2.26.0",
6670
"eslint-plugin-no-null": "^1.0.2",
6771
"eslint-plugin-no-unsanitized": "^4.0.2",
6872
"eslint-plugin-react": "^7.31.11",
73+
"mocha": "^11.7.5",
6974
"serve": "^14.1.2",
7075
"style-loader": "^3.3.4",
7176
"ts-loader": "^9.4.2",
77+
"ts-node": "^10.9.2",
7278
"tslint": "^6.1.3",
7379
"typescript": "^4.9.3",
7480
"webpack": "^5.75.0",
@@ -224,6 +230,20 @@
224230
"when": "canViewMemory && memory-inspector.canRead"
225231
}
226232
],
233+
"debug/watch/context": [
234+
{
235+
"command": "memory-inspector.show-variable",
236+
"when": "canViewMemory && memory-inspector.canRead"
237+
},
238+
{
239+
"command": "memory-inspector.go-to-value",
240+
"when": "canViewMemory && memory-inspector.canRead && memory-inspector.variable.isPointer"
241+
},
242+
{
243+
"command": "memory-inspector.store-file",
244+
"when": "canViewMemory && memory-inspector.canRead"
245+
}
246+
],
227247
"view/item/context": [
228248
{
229249
"command": "memory-inspector.show-variable",
@@ -531,5 +551,6 @@
531551
"extensionKind": [
532552
"workspace",
533553
"ui"
534-
]
554+
],
555+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
535556
}

src/common/debug-requests.ts

Lines changed: 97 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,70 +19,126 @@ import type { DebugProtocol } from '@vscode/debugprotocol';
1919
import type { DebugSession } from 'vscode';
2020

2121
export interface DebugRequestTypes {
22-
'evaluate': [DebugProtocol.EvaluateArguments, DebugProtocol.EvaluateResponse['body']]
23-
'initialize': [DebugProtocol.InitializeRequestArguments, DebugProtocol.InitializeResponse['body']]
24-
'readMemory': [DebugProtocol.ReadMemoryArguments, DebugProtocol.ReadMemoryResponse['body']]
25-
'scopes': [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse['body']]
26-
'variables': [DebugProtocol.VariablesArguments, DebugProtocol.VariablesResponse['body']]
27-
'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse['body']]
28-
'dataBreakpointInfo': [DebugProtocol.DataBreakpointInfoArguments, DebugProtocol.DataBreakpointInfoResponse['body']]
29-
'setDataBreakpoints': [DebugProtocol.SetDataBreakpointsArguments, DebugProtocol.SetDataBreakpointsResponse['body']]
22+
evaluate: [
23+
DebugProtocol.EvaluateArguments,
24+
DebugProtocol.EvaluateResponse['body'],
25+
];
26+
initialize: [
27+
DebugProtocol.InitializeRequestArguments,
28+
DebugProtocol.InitializeResponse['body'],
29+
];
30+
readMemory: [
31+
DebugProtocol.ReadMemoryArguments,
32+
DebugProtocol.ReadMemoryResponse['body'],
33+
];
34+
scopes: [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse['body']];
35+
variables: [
36+
DebugProtocol.VariablesArguments,
37+
DebugProtocol.VariablesResponse['body'],
38+
];
39+
writeMemory: [
40+
DebugProtocol.WriteMemoryArguments,
41+
DebugProtocol.WriteMemoryResponse['body'],
42+
];
43+
dataBreakpointInfo: [
44+
DebugProtocol.DataBreakpointInfoArguments,
45+
DebugProtocol.DataBreakpointInfoResponse['body'],
46+
];
47+
setDataBreakpoints: [
48+
DebugProtocol.SetDataBreakpointsArguments,
49+
DebugProtocol.SetDataBreakpointsResponse['body'],
50+
];
3051
}
3152

3253
export interface DebugEvents {
33-
'memory': DebugProtocol.MemoryEvent,
34-
'continued': DebugProtocol.ContinuedEvent,
35-
'stopped': DebugProtocol.StoppedEvent,
36-
'output': DebugProtocol.OutputEvent
54+
memory: DebugProtocol.MemoryEvent;
55+
continued: DebugProtocol.ContinuedEvent;
56+
stopped: DebugProtocol.StoppedEvent;
57+
output: DebugProtocol.OutputEvent;
3758
}
3859

39-
export type DebugRequest<C, A> = Omit<DebugProtocol.Request, 'command' | 'arguments'> & { command: C, arguments: A };
40-
export type DebugResponse<C, B> = Omit<DebugProtocol.Response, 'command' | 'body'> & { command: C, body: B };
60+
export type DebugRequest<C, A> = Omit<
61+
DebugProtocol.Request,
62+
'command' | 'arguments'
63+
> & { command: C; arguments: A };
64+
export type DebugResponse<C, B> = Omit<
65+
DebugProtocol.Response,
66+
'command' | 'body'
67+
> & { command: C; body: B };
4168
export type DebugEvent<T> = DebugProtocol.Event & { body: T };
4269

43-
export async function sendRequest<K extends keyof DebugRequestTypes>(session: DebugSession,
44-
command: K, args: DebugRequestTypes[K][0]): Promise<DebugRequestTypes[K][1]> {
45-
return session.customRequest(command, args);
70+
export async function sendRequest<K extends keyof DebugRequestTypes>(
71+
session: DebugSession,
72+
command: K,
73+
args: DebugRequestTypes[K][0],
74+
): Promise<DebugRequestTypes[K][1]> {
75+
return session.customRequest(command, args);
4676
}
4777

48-
export function isDebugVariable(variable: DebugProtocol.Variable | unknown): variable is DebugProtocol.Variable {
49-
const assumed = variable ? variable as DebugProtocol.Variable : undefined;
50-
return typeof assumed?.name === 'string' && typeof assumed?.value === 'string';
78+
export function isDebugVariable(
79+
variable: DebugProtocol.Variable | unknown,
80+
): variable is DebugProtocol.Variable {
81+
const assumed = variable ? (variable as DebugProtocol.Variable) : undefined;
82+
return (
83+
typeof assumed?.name === 'string' && typeof assumed?.value === 'string'
84+
);
5185
}
5286

53-
export function isDebugScope(scope: DebugProtocol.Scope | unknown): scope is DebugProtocol.Scope {
54-
const assumed = scope ? scope as DebugProtocol.Scope : undefined;
55-
return typeof assumed?.name === 'string' && typeof assumed?.variablesReference === 'number';
87+
export function isDebugScope(
88+
scope: DebugProtocol.Scope | unknown,
89+
): scope is DebugProtocol.Scope {
90+
const assumed = scope ? (scope as DebugProtocol.Scope) : undefined;
91+
return (
92+
typeof assumed?.name === 'string' &&
93+
typeof assumed?.variablesReference === 'number'
94+
);
5695
}
5796

58-
export function isDebugEvaluateArguments(args: DebugProtocol.EvaluateArguments | unknown): args is DebugProtocol.EvaluateArguments {
59-
const assumed = args ? args as DebugProtocol.EvaluateArguments : undefined;
60-
return typeof assumed?.expression === 'string';
97+
export function isDebugEvaluateArguments(
98+
args: DebugProtocol.EvaluateArguments | unknown,
99+
): args is DebugProtocol.EvaluateArguments {
100+
const assumed = args ? (args as DebugProtocol.EvaluateArguments) : undefined;
101+
return typeof assumed?.expression === 'string';
61102
}
62103

63-
export function isDebugRequest<K extends keyof DebugRequestTypes>(command: K, message: unknown): message is DebugRequest<K, DebugRequestTypes[K][0]> {
64-
return isDebugRequestType(message) && message.command === command;
104+
export function isDebugRequest<K extends keyof DebugRequestTypes>(
105+
command: K,
106+
message: unknown,
107+
): message is DebugRequest<K, DebugRequestTypes[K][0]> {
108+
return isDebugRequestType(message) && message.command === command;
65109
}
66110

67-
export function isDebugResponse<K extends keyof DebugRequestTypes>(command: K, message: unknown): message is DebugResponse<K, DebugRequestTypes[K][1]> {
68-
return isDebugResponseType(message) && message.command === command;
111+
export function isDebugResponse<K extends keyof DebugRequestTypes>(
112+
command: K,
113+
message: unknown,
114+
): message is DebugResponse<K, DebugRequestTypes[K][1]> {
115+
return isDebugResponseType(message) && message.command === command;
69116
}
70117

71-
export function isDebugEvent<K extends keyof DebugEvents>(event: K, message: unknown): message is DebugEvents[K] {
72-
return isDebugEventType(message) && message.event === event;
118+
export function isDebugEvent<K extends keyof DebugEvents>(
119+
event: K,
120+
message: unknown,
121+
): message is DebugEvents[K] {
122+
return isDebugEventType(message) && message.event === event;
73123
}
74124

75-
export function isDebugRequestType(message: unknown): message is DebugProtocol.Request {
76-
const assumed = message ? message as DebugProtocol.Request : undefined;
77-
return !!assumed && assumed.type === 'request';
125+
export function isDebugRequestType(
126+
message: unknown,
127+
): message is DebugProtocol.Request {
128+
const assumed = message ? (message as DebugProtocol.Request) : undefined;
129+
return !!assumed && assumed.type === 'request';
78130
}
79131

80-
export function isDebugResponseType(message: unknown): message is DebugProtocol.Response {
81-
const assumed = message ? message as DebugProtocol.Response : undefined;
82-
return !!assumed && assumed.type === 'response';
132+
export function isDebugResponseType(
133+
message: unknown,
134+
): message is DebugProtocol.Response {
135+
const assumed = message ? (message as DebugProtocol.Response) : undefined;
136+
return !!assumed && assumed.type === 'response';
83137
}
84138

85-
export function isDebugEventType(message: unknown): message is DebugProtocol.Event {
86-
const assumed = message ? message as DebugProtocol.Event : undefined;
87-
return !!assumed && assumed.type === 'event';
139+
export function isDebugEventType(
140+
message: unknown,
141+
): message is DebugProtocol.Event {
142+
const assumed = message ? (message as DebugProtocol.Event) : undefined;
143+
return !!assumed && assumed.type === 'event';
88144
}

src/common/external-views.spec.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/********************************************************************************
2+
* Copyright (C) 2026 Microchip Technology Inc. and its subsidiaries and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { expect } from 'chai';
18+
import { isVariablesContext, isWatchItemContext, VariablesView, WatchView } from './external-views';
19+
20+
describe('external-views', () => {
21+
describe('isVariablesContext', () => {
22+
it('accepts a fully-formed Variables view context with a Scope container', () => {
23+
const ctx: VariablesView.IVariablesContext = {
24+
sessionId: 'session-1',
25+
container: { name: 'Locals', variablesReference: 1000, expensive: false },
26+
variable: { name: 'foo', value: '42', variablesReference: 0 }
27+
};
28+
expect(isVariablesContext(ctx)).to.equal(true);
29+
});
30+
31+
it('accepts a Variables view context with a Variable container (nested variable)', () => {
32+
const ctx: VariablesView.IVariablesContext = {
33+
sessionId: 'session-1',
34+
container: { name: 'parent', value: '{...}', variablesReference: 2000 },
35+
variable: { name: 'child', value: '7', variablesReference: 0 }
36+
};
37+
expect(isVariablesContext(ctx)).to.equal(true);
38+
});
39+
40+
it('accepts a Variables view context with an EvaluateArguments container', () => {
41+
const ctx: VariablesView.IVariablesContext = {
42+
sessionId: 'session-1',
43+
container: { expression: 'someExpr' },
44+
variable: { name: 'child', value: '7', variablesReference: 0 }
45+
};
46+
expect(isVariablesContext(ctx)).to.equal(true);
47+
});
48+
49+
it('rejects undefined / null / primitive values', () => {
50+
expect(isVariablesContext(undefined)).to.equal(false);
51+
// eslint-disable-next-line no-null/no-null
52+
expect(isVariablesContext(null as unknown)).to.equal(false);
53+
expect(isVariablesContext(42)).to.equal(false);
54+
expect(isVariablesContext('string')).to.equal(false);
55+
});
56+
57+
it('rejects an object missing the variable field', () => {
58+
expect(isVariablesContext({ sessionId: 's', container: { expression: 'e' } })).to.equal(false);
59+
});
60+
61+
it('rejects an object with an unexpected container shape', () => {
62+
expect(isVariablesContext({
63+
sessionId: 's',
64+
container: { unexpected: true },
65+
variable: { name: 'x', value: '1' }
66+
})).to.equal(false);
67+
});
68+
69+
it('rejects a watch expression shape that lacks container/variable', () => {
70+
const watchLike = { name: 'g_counter', value: '42', memoryReference: '0xdeadbeef' };
71+
expect(isVariablesContext(watchLike)).to.equal(false);
72+
});
73+
});
74+
75+
describe('isWatchItemContext', () => {
76+
it('accepts a root watch expression with memoryReference set', () => {
77+
const watch: WatchView.IWatchItemContext = {
78+
name: 'g_counter',
79+
value: '42',
80+
type: 'int',
81+
memoryReference: '0xdeadbeef',
82+
evaluateName: 'g_counter'
83+
};
84+
expect(isWatchItemContext(watch)).to.equal(true);
85+
});
86+
87+
it('accepts a child watch variable with only name/value', () => {
88+
const child = { name: 'field', value: '0' };
89+
expect(isWatchItemContext(child)).to.equal(true);
90+
});
91+
92+
it('accepts a minimal watch item with just a name', () => {
93+
expect(isWatchItemContext({ name: 'expr' })).to.equal(true);
94+
});
95+
96+
it('rejects a Variables view wrapper context (handled by isVariablesContext instead)', () => {
97+
const wrapped: VariablesView.IVariablesContext = {
98+
sessionId: 'session-1',
99+
container: { name: 'Locals', variablesReference: 1000, expensive: false },
100+
variable: { name: 'foo', value: '42', variablesReference: 0 }
101+
};
102+
expect(isWatchItemContext(wrapped)).to.equal(false);
103+
});
104+
105+
it('rejects undefined / null / primitive values', () => {
106+
expect(isWatchItemContext(undefined)).to.equal(false);
107+
// eslint-disable-next-line no-null/no-null
108+
expect(isWatchItemContext(null as unknown)).to.equal(false);
109+
expect(isWatchItemContext(42)).to.equal(false);
110+
expect(isWatchItemContext('string')).to.equal(false);
111+
expect(isWatchItemContext(true)).to.equal(false);
112+
});
113+
114+
it('rejects an empty object', () => {
115+
expect(isWatchItemContext({})).to.equal(false);
116+
});
117+
118+
it('rejects an object whose name is not a string', () => {
119+
expect(isWatchItemContext({ name: 42 })).to.equal(false);
120+
expect(isWatchItemContext({ name: undefined })).to.equal(false);
121+
});
122+
123+
it('discriminates: variables wrappers match isVariablesContext only, watch items match isWatchItemContext only', () => {
124+
const wrapped: VariablesView.IVariablesContext = {
125+
sessionId: 'session-1',
126+
container: { expression: 'x' },
127+
variable: { name: 'foo', value: '42', variablesReference: 0 }
128+
};
129+
const watchItem: WatchView.IWatchItemContext = { name: 'expr', memoryReference: '0x1' };
130+
131+
expect(isVariablesContext(wrapped)).to.equal(true);
132+
expect(isWatchItemContext(wrapped)).to.equal(false);
133+
134+
expect(isWatchItemContext(watchItem)).to.equal(true);
135+
expect(isVariablesContext(watchItem)).to.equal(false);
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)