Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/common/debugVisualizers.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { isDefined } from '../../../../base/common/types.js';
9
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
10
import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
11
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
12
import { ILogService } from '../../../../platform/log/common/log.js';
13
import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem, IDebugSession } from './debug.js';
14
import { getContextForVariable } from './debugContext.js';
15
import { Scope, Variable, VisualizedExpression } from './debugModel.js';
16
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
17
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
18
19
export const IDebugVisualizerService = createDecorator<IDebugVisualizerService>('debugVisualizerService');
20
21
interface VisualizerHandle {
22
id: string;
23
extensionId: ExtensionIdentifier;
24
provideDebugVisualizers(context: IDebugVisualizationContext, token: CancellationToken): Promise<IDebugVisualization[]>;
25
resolveDebugVisualizer(viz: IDebugVisualization, token: CancellationToken): Promise<MainThreadDebugVisualization>;
26
executeDebugVisualizerCommand(id: number): Promise<void>;
27
disposeDebugVisualizers(ids: number[]): void;
28
}
29
30
interface VisualizerTreeHandle {
31
getTreeItem(element: IDebugVisualizationContext): Promise<IDebugVisualizationTreeItem | undefined>;
32
getChildren(element: number): Promise<IDebugVisualizationTreeItem[]>;
33
disposeItem(element: number): void;
34
editItem?(item: number, value: string): Promise<IDebugVisualizationTreeItem | undefined>;
35
}
36
37
export class DebugVisualizer {
38
public get name() {
39
return this.viz.name;
40
}
41
42
public get iconPath() {
43
return this.viz.iconPath;
44
}
45
46
public get iconClass() {
47
return this.viz.iconClass;
48
}
49
50
constructor(private readonly handle: VisualizerHandle, private readonly viz: IDebugVisualization) { }
51
52
public async resolve(token: CancellationToken) {
53
return this.viz.visualization ??= await this.handle.resolveDebugVisualizer(this.viz, token);
54
}
55
56
public async execute() {
57
await this.handle.executeDebugVisualizerCommand(this.viz.id);
58
}
59
}
60
61
export interface IDebugVisualizerService {
62
_serviceBrand: undefined;
63
64
/**
65
* Gets visualizers applicable for the given Expression.
66
*/
67
getApplicableFor(expression: IExpression, token: CancellationToken): Promise<IReference<DebugVisualizer[]>>;
68
69
/**
70
* Registers a new visualizer (called from the main thread debug service)
71
*/
72
register(handle: VisualizerHandle): IDisposable;
73
74
/**
75
* Registers a new visualizer tree.
76
*/
77
registerTree(treeId: string, handle: VisualizerTreeHandle): IDisposable;
78
79
/**
80
* Sets that a certa tree should be used for the visualized node
81
*/
82
getVisualizedNodeFor(treeId: string, expr: IExpression): Promise<VisualizedExpression | undefined>;
83
84
/**
85
* Gets children for a visualized tree node.
86
*/
87
getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise<IExpression[]>;
88
89
/**
90
* Gets children for a visualized tree node.
91
*/
92
editTreeItem(treeId: string, item: IDebugVisualizationTreeItem, newValue: string): Promise<void>;
93
}
94
95
const emptyRef: IReference<DebugVisualizer[]> = { object: [], dispose: () => { } };
96
97
export class DebugVisualizerService implements IDebugVisualizerService {
98
declare public readonly _serviceBrand: undefined;
99
100
private readonly handles = new Map</* extId + \0 + vizId */ string, VisualizerHandle>();
101
private readonly trees = new Map</* extId + \0 + treeId */ string, VisualizerTreeHandle>();
102
private readonly didActivate = new Map<string, Promise<void>>();
103
private registrations: { expr: ContextKeyExpression; id: string; extensionId: ExtensionIdentifier }[] = [];
104
105
constructor(
106
@IContextKeyService private readonly contextKeyService: IContextKeyService,
107
@IExtensionService private readonly extensionService: IExtensionService,
108
@ILogService private readonly logService: ILogService,
109
) {
110
visualizersExtensionPoint.setHandler((_, { added, removed }) => {
111
this.registrations = this.registrations.filter(r =>
112
!removed.some(e => ExtensionIdentifier.equals(e.description.identifier, r.extensionId)));
113
added.forEach(e => this.processExtensionRegistration(e.description));
114
});
115
}
116
117
/** @inheritdoc */
118
public async getApplicableFor(variable: IExpression, token: CancellationToken): Promise<IReference<DebugVisualizer[]>> {
119
if (!(variable instanceof Variable)) {
120
return emptyRef;
121
}
122
const threadId = variable.getThreadId();
123
if (threadId === undefined) { // an expression, not a variable
124
return emptyRef;
125
}
126
127
const context = this.getVariableContext(threadId, variable);
128
const overlay = getContextForVariable(this.contextKeyService, variable, [
129
[CONTEXT_VARIABLE_NAME.key, variable.name],
130
[CONTEXT_VARIABLE_VALUE.key, variable.value],
131
[CONTEXT_VARIABLE_TYPE.key, variable.type],
132
]);
133
134
const maybeVisualizers = await Promise.all(this.registrations.map(async registration => {
135
if (!overlay.contextMatchesRules(registration.expr)) {
136
return;
137
}
138
139
let prom = this.didActivate.get(registration.id);
140
if (!prom) {
141
prom = this.extensionService.activateByEvent(`onDebugVisualizer:${registration.id}`);
142
this.didActivate.set(registration.id, prom);
143
}
144
145
await prom;
146
if (token.isCancellationRequested) {
147
return;
148
}
149
150
const handle = this.handles.get(toKey(registration.extensionId, registration.id));
151
return handle && { handle, result: await handle.provideDebugVisualizers(context, token) };
152
}));
153
154
const ref = {
155
object: maybeVisualizers.filter(isDefined).flatMap(v => v.result.map(r => new DebugVisualizer(v.handle, r))),
156
dispose: () => {
157
for (const viz of maybeVisualizers) {
158
viz?.handle.disposeDebugVisualizers(viz.result.map(r => r.id));
159
}
160
},
161
};
162
163
if (token.isCancellationRequested) {
164
ref.dispose();
165
}
166
167
return ref;
168
}
169
170
/** @inheritdoc */
171
public register(handle: VisualizerHandle): IDisposable {
172
const key = toKey(handle.extensionId, handle.id);
173
this.handles.set(key, handle);
174
return toDisposable(() => this.handles.delete(key));
175
}
176
177
/** @inheritdoc */
178
public registerTree(treeId: string, handle: VisualizerTreeHandle): IDisposable {
179
this.trees.set(treeId, handle);
180
return toDisposable(() => this.trees.delete(treeId));
181
}
182
183
/** @inheritdoc */
184
public async getVisualizedNodeFor(treeId: string, expr: IExpression): Promise<VisualizedExpression | undefined> {
185
if (!(expr instanceof Variable)) {
186
return;
187
}
188
189
const threadId = expr.getThreadId();
190
if (threadId === undefined) {
191
return;
192
}
193
194
const tree = this.trees.get(treeId);
195
if (!tree) {
196
return;
197
}
198
199
try {
200
const treeItem = await tree.getTreeItem(this.getVariableContext(threadId, expr));
201
if (!treeItem) {
202
return;
203
}
204
205
return new VisualizedExpression(expr.getSession(), this, treeId, treeItem, expr);
206
} catch (e) {
207
this.logService.warn('Failed to get visualized node', e);
208
return;
209
}
210
}
211
212
/** @inheritdoc */
213
public async getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise<IExpression[]> {
214
const node = this.trees.get(treeId);
215
const children = await node?.getChildren(treeElementId) || [];
216
return children.map(c => new VisualizedExpression(session, this, treeId, c, undefined));
217
}
218
219
/** @inheritdoc */
220
public async editTreeItem(treeId: string, treeItem: IDebugVisualizationTreeItem, newValue: string): Promise<void> {
221
const newItem = await this.trees.get(treeId)?.editItem?.(treeItem.id, newValue);
222
if (newItem) {
223
Object.assign(treeItem, newItem); // replace in-place so rerenders work
224
}
225
}
226
227
private getVariableContext(threadId: number, variable: Variable) {
228
const context: IDebugVisualizationContext = {
229
sessionId: variable.getSession()?.getId() || '',
230
containerId: (variable.parent instanceof Variable ? variable.reference : undefined),
231
threadId,
232
variable: {
233
name: variable.name,
234
value: variable.value,
235
type: variable.type,
236
evaluateName: variable.evaluateName,
237
variablesReference: variable.reference || 0,
238
indexedVariables: variable.indexedVariables,
239
memoryReference: variable.memoryReference,
240
namedVariables: variable.namedVariables,
241
presentationHint: variable.presentationHint,
242
}
243
};
244
245
for (let p: IExpressionContainer = variable; p instanceof Variable; p = p.parent) {
246
if (p.parent instanceof Scope) {
247
context.frameId = p.parent.stackFrame.frameId;
248
}
249
}
250
251
return context;
252
}
253
254
private processExtensionRegistration(ext: IExtensionDescription) {
255
const viz = ext.contributes?.debugVisualizers;
256
if (!(viz instanceof Array)) {
257
return;
258
}
259
260
for (const { when, id } of viz) {
261
try {
262
const expr = ContextKeyExpr.deserialize(when);
263
if (expr) {
264
this.registrations.push({ expr, id, extensionId: ext.identifier });
265
}
266
} catch (e) {
267
this.logService.error(`Error processing debug visualizer registration from extension '${ext.identifier.value}'`, e);
268
}
269
}
270
}
271
}
272
273
const toKey = (extensionId: ExtensionIdentifier, id: string) => `${ExtensionIdentifier.toKey(extensionId)}\0${id}`;
274
275
const visualizersExtensionPoint = ExtensionsRegistry.registerExtensionPoint<{ id: string; when: string }[]>({
276
extensionPoint: 'debugVisualizers',
277
jsonSchema: {
278
type: 'array',
279
items: {
280
type: 'object',
281
properties: {
282
id: {
283
type: 'string',
284
description: 'Name of the debug visualizer'
285
},
286
when: {
287
type: 'string',
288
description: 'Condition when the debug visualizer is applicable'
289
}
290
},
291
required: ['id', 'when']
292
}
293
},
294
activationEventsGenerator: (contribs, result: { push(item: string): void }) => {
295
for (const contrib of contribs) {
296
if (contrib.id) {
297
result.push(`onDebugVisualizer:${contrib.id}`);
298
}
299
}
300
}
301
});
302
303