Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/variablesView.ts
5240 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 * as dom from '../../../../base/browser/dom.js';
7
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
8
import { HighlightedLabel, IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
9
import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
10
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
11
import { AsyncDataTree, IAsyncDataTreeViewState } from '../../../../base/browser/ui/tree/asyncDataTree.js';
12
import { ITreeContextMenuEvent, ITreeMouseEvent, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js';
13
import { IAction, toAction } from '../../../../base/common/actions.js';
14
import { coalesce } from '../../../../base/common/arrays.js';
15
import { RunOnceScheduler } from '../../../../base/common/async.js';
16
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
17
import { Codicon } from '../../../../base/common/codicons.js';
18
import { FuzzyScore, createMatches } from '../../../../base/common/filters.js';
19
import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
20
import { ThemeIcon } from '../../../../base/common/themables.js';
21
import { localize } from '../../../../nls.js';
22
import { getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
23
import { IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
24
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
25
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
26
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
27
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
28
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
29
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
30
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
31
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
32
import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';
33
import { INotificationService } from '../../../../platform/notification/common/notification.js';
34
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
35
import { ProgressLocation } from '../../../../platform/progress/common/progress.js';
36
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
37
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
38
import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js';
39
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
40
import { IViewDescriptorService } from '../../../common/views.js';
41
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
42
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
43
import { IViewsService } from '../../../services/views/common/viewsService.js';
44
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
45
import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DebugVisualizationType, IDebugService, IDebugViewWithVariables, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID, WATCH_VIEW_ID } from '../common/debug.js';
46
import { getContextForVariable } from '../common/debugContext.js';
47
import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from '../common/debugModel.js';
48
import { DebugVisualizer, IDebugVisualizerService } from '../common/debugVisualizers.js';
49
import { AbstractExpressionDataSource, AbstractExpressionsRenderer, expressionAndScopeLabelProvider, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js';
50
import { ADD_TO_WATCH_ID, ADD_TO_WATCH_LABEL, COPY_EVALUATE_PATH_ID, COPY_EVALUATE_PATH_LABEL, COPY_VALUE_ID, COPY_VALUE_LABEL, setDataBreakpointInfoResponse } from './debugCommands.js';
51
import { DebugExpressionRenderer } from './debugExpressionRenderer.js';
52
53
const $ = dom.$;
54
let forgetScopes = true;
55
56
let variableInternalContext: Variable | undefined;
57
58
interface IVariablesContext {
59
sessionId: string | undefined;
60
container: DebugProtocol.Variable | DebugProtocol.Scope | DebugProtocol.EvaluateArguments;
61
variable: DebugProtocol.Variable;
62
}
63
64
export class VariablesView extends ViewPane implements IDebugViewWithVariables {
65
66
private updateTreeScheduler: RunOnceScheduler;
67
private needsRefresh = false;
68
private tree!: WorkbenchAsyncDataTree<IStackFrame | null, IExpression | IScope, FuzzyScore>;
69
private savedViewState = new Map<string, IAsyncDataTreeViewState>();
70
private autoExpandedScopes = new Set<string>();
71
72
public get treeSelection() {
73
return this.tree.getSelection();
74
}
75
76
constructor(
77
options: IViewletViewOptions,
78
@IContextMenuService contextMenuService: IContextMenuService,
79
@IDebugService private readonly debugService: IDebugService,
80
@IKeybindingService keybindingService: IKeybindingService,
81
@IConfigurationService configurationService: IConfigurationService,
82
@IInstantiationService instantiationService: IInstantiationService,
83
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
84
@IContextKeyService contextKeyService: IContextKeyService,
85
@IOpenerService openerService: IOpenerService,
86
@IThemeService themeService: IThemeService,
87
@IHoverService hoverService: IHoverService,
88
@IMenuService private readonly menuService: IMenuService
89
) {
90
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
91
92
// Use scheduler to prevent unnecessary flashing
93
this.updateTreeScheduler = new RunOnceScheduler(async () => {
94
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
95
96
this.needsRefresh = false;
97
const input = this.tree.getInput();
98
if (input) {
99
this.savedViewState.set(input.getId(), this.tree.getViewState());
100
}
101
if (!stackFrame) {
102
await this.tree.setInput(null);
103
return;
104
}
105
106
const viewState = this.savedViewState.get(stackFrame.getId());
107
await this.tree.setInput(stackFrame, viewState);
108
109
// Automatically expand the first non-expensive scope
110
const scopes = await stackFrame.getScopes();
111
const toExpand = scopes.find(s => !s.expensive);
112
113
// A race condition could be present causing the scopes here to be different from the scopes that the tree just retrieved.
114
// If that happened, don't try to reveal anything, it will be straightened out on the next update
115
if (toExpand && this.tree.hasNode(toExpand)) {
116
this.autoExpandedScopes.add(toExpand.getId());
117
await this.tree.expand(toExpand);
118
}
119
}, 400);
120
}
121
122
protected override renderBody(container: HTMLElement): void {
123
super.renderBody(container);
124
125
this.element.classList.add('debug-pane');
126
container.classList.add('debug-variables');
127
const treeContainer = renderViewTree(container);
128
const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);
129
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree<IStackFrame | null, IExpression | IScope, FuzzyScore>, 'VariablesView', treeContainer, new VariablesDelegate(),
130
[
131
this.instantiationService.createInstance(VariablesRenderer, expressionRenderer),
132
this.instantiationService.createInstance(VisualizedVariableRenderer, expressionRenderer),
133
new ScopesRenderer(),
134
new ScopeErrorRenderer(),
135
],
136
this.instantiationService.createInstance(VariablesDataSource), {
137
accessibilityProvider: new VariablesAccessibilityProvider(),
138
identityProvider: { getId: (element: IExpression | IScope) => element.getId() },
139
keyboardNavigationLabelProvider: expressionAndScopeLabelProvider,
140
overrideStyles: this.getLocationBasedColors().listOverrideStyles
141
});
142
143
this._register(VisualizedVariableRenderer.rendererOnVisualizationRange(this.debugService.getViewModel(), this.tree));
144
this.tree.setInput(this.debugService.getViewModel().focusedStackFrame ?? null);
145
146
CONTEXT_VARIABLES_FOCUSED.bindTo(this.tree.contextKeyService);
147
148
this._register(this.debugService.getViewModel().onDidFocusStackFrame(sf => {
149
if (!this.isBodyVisible()) {
150
this.needsRefresh = true;
151
return;
152
}
153
154
// Refresh the tree immediately if the user explictly changed stack frames.
155
// Otherwise postpone the refresh until user stops stepping.
156
const timeout = sf.explicit ? 0 : undefined;
157
this.updateTreeScheduler.schedule(timeout);
158
}));
159
this._register(this.debugService.getViewModel().onWillUpdateViews(() => {
160
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
161
if (stackFrame && forgetScopes) {
162
stackFrame.forgetScopes();
163
}
164
forgetScopes = true;
165
this.tree.updateChildren();
166
}));
167
this._register(this.tree);
168
this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
169
this._register(this.tree.onContextMenu(async e => await this.onContextMenu(e)));
170
171
this._register(this.onDidChangeBodyVisibility(visible => {
172
if (visible && this.needsRefresh) {
173
this.updateTreeScheduler.schedule();
174
}
175
}));
176
let horizontalScrolling: boolean | undefined;
177
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
178
const variable = e?.expression;
179
if (variable && this.tree.hasNode(variable)) {
180
horizontalScrolling = this.tree.options.horizontalScrolling;
181
if (horizontalScrolling) {
182
this.tree.updateOptions({ horizontalScrolling: false });
183
}
184
185
this.tree.rerender(variable);
186
} else if (!e && horizontalScrolling !== undefined) {
187
this.tree.updateOptions({ horizontalScrolling: horizontalScrolling });
188
horizontalScrolling = undefined;
189
}
190
}));
191
this._register(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {
192
if (e instanceof Variable && this.tree.hasNode(e)) {
193
await this.tree.updateChildren(e, false, true);
194
await this.tree.expand(e);
195
}
196
}));
197
this._register(this.debugService.onDidEndSession(() => {
198
this.savedViewState.clear();
199
this.autoExpandedScopes.clear();
200
}));
201
}
202
203
protected override layoutBody(width: number, height: number): void {
204
super.layoutBody(height, width);
205
this.tree.layout(width, height);
206
}
207
208
override focus(): void {
209
super.focus();
210
this.tree.domFocus();
211
}
212
213
collapseAll(): void {
214
this.tree.collapseAll();
215
}
216
217
private onMouseDblClick(e: ITreeMouseEvent<IExpression | IScope>): void {
218
if (this.canSetExpressionValue(e.element)) {
219
this.debugService.getViewModel().setSelectedExpression(e.element, false);
220
}
221
}
222
223
private canSetExpressionValue(e: IExpression | IScope | null): e is IExpression {
224
const session = this.debugService.getViewModel().focusedSession;
225
if (!session) {
226
return false;
227
}
228
229
if (e instanceof VisualizedExpression) {
230
return !!e.treeItem.canEdit;
231
}
232
233
if (!session.capabilities?.supportsSetVariable && !session.capabilities?.supportsSetExpression) {
234
return false;
235
}
236
237
return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy;
238
}
239
240
private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
241
const element = e.element;
242
243
// Handle scope context menu
244
if (element instanceof Scope) {
245
return this.openContextMenuForScope(e, element);
246
}
247
248
// Handle variable context menu
249
if (!(element instanceof Variable) || !element.value) {
250
return;
251
}
252
253
return openContextMenuForVariableTreeElement(this.contextKeyService, this.menuService, this.contextMenuService, MenuId.DebugVariablesContext, e);
254
}
255
256
private openContextMenuForScope(e: ITreeContextMenuEvent<IExpression | IScope>, scope: Scope): void {
257
const context = { scope: { name: scope.name } };
258
const menu = this.menuService.getMenuActions(MenuId.DebugScopesContext, this.contextKeyService, { arg: context, shouldForwardArgs: false });
259
const { secondary } = getContextMenuActions(menu, 'inline');
260
261
this.contextMenuService.showContextMenu({
262
getAnchor: () => e.anchor,
263
getActions: () => secondary
264
});
265
}
266
}
267
268
export async function openContextMenuForVariableTreeElement(parentContextKeyService: IContextKeyService, menuService: IMenuService, contextMenuService: IContextMenuService, menuId: MenuId, e: ITreeContextMenuEvent<IExpression | IScope>) {
269
const variable = e.element;
270
if (!(variable instanceof Variable) || !variable.value) {
271
return;
272
}
273
274
const contextKeyService = await getContextForVariableMenuWithDataAccess(parentContextKeyService, variable);
275
const context: IVariablesContext = getVariablesContext(variable);
276
const menu = menuService.getMenuActions(menuId, contextKeyService, { arg: context, shouldForwardArgs: false });
277
278
const { secondary } = getContextMenuActions(menu, 'inline');
279
contextMenuService.showContextMenu({
280
getAnchor: () => e.anchor,
281
getActions: () => secondary
282
});
283
}
284
285
const getVariablesContext = (variable: Variable): IVariablesContext => ({
286
sessionId: variable.getSession()?.getId(),
287
container: variable.parent instanceof Expression
288
? { expression: variable.parent.name }
289
: (variable.parent as (Variable | Scope)).toDebugProtocolObject(),
290
variable: variable.toDebugProtocolObject()
291
});
292
293
/**
294
* Gets a context key overlay that has context for the given variable, including data access info.
295
*/
296
async function getContextForVariableMenuWithDataAccess(parentContext: IContextKeyService, variable: Variable) {
297
const session = variable.getSession();
298
if (!session || !session.capabilities.supportsDataBreakpoints) {
299
return getContextForVariableMenuBase(parentContext, variable);
300
}
301
302
const contextKeys: [string, unknown][] = [];
303
const dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference);
304
const dataBreakpointId = dataBreakpointInfoResponse?.dataId;
305
const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes;
306
setDataBreakpointInfoResponse(dataBreakpointInfoResponse);
307
308
if (!dataBreakpointAccessTypes) {
309
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]);
310
} else {
311
for (const accessType of dataBreakpointAccessTypes) {
312
switch (accessType) {
313
case 'read':
314
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.key, !!dataBreakpointId]);
315
break;
316
case 'write':
317
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]);
318
break;
319
case 'readWrite':
320
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.key, !!dataBreakpointId]);
321
break;
322
}
323
}
324
}
325
326
return getContextForVariableMenuBase(parentContext, variable, contextKeys);
327
}
328
329
/**
330
* Gets a context key overlay that has context for the given variable.
331
*/
332
function getContextForVariableMenuBase(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) {
333
variableInternalContext = variable;
334
return getContextForVariable(parentContext, variable, additionalContext);
335
}
336
337
function isStackFrame(obj: any): obj is IStackFrame {
338
return obj instanceof StackFrame;
339
}
340
341
class VariablesDataSource extends AbstractExpressionDataSource<IStackFrame | null, IExpression | IScope> {
342
343
public override hasChildren(element: IStackFrame | null | IExpression | IScope): boolean {
344
if (!element) {
345
return false;
346
}
347
if (isStackFrame(element)) {
348
return true;
349
}
350
351
return element.hasChildren;
352
}
353
354
protected override doGetChildren(element: IStackFrame | IExpression | IScope): Promise<(IExpression | IScope)[]> {
355
if (isStackFrame(element)) {
356
return element.getScopes();
357
}
358
359
return element.getChildren();
360
}
361
}
362
363
interface IScopeTemplateData {
364
name: HTMLElement;
365
label: HighlightedLabel;
366
}
367
368
class VariablesDelegate implements IListVirtualDelegate<IExpression | IScope> {
369
370
getHeight(element: IExpression | IScope): number {
371
return 22;
372
}
373
374
getTemplateId(element: IExpression | IScope): string {
375
if (element instanceof ErrorScope) {
376
return ScopeErrorRenderer.ID;
377
}
378
379
if (element instanceof Scope) {
380
return ScopesRenderer.ID;
381
}
382
383
if (element instanceof VisualizedExpression) {
384
return VisualizedVariableRenderer.ID;
385
}
386
387
return VariablesRenderer.ID;
388
}
389
}
390
391
class ScopesRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeTemplateData> {
392
393
static readonly ID = 'scope';
394
395
get templateId(): string {
396
return ScopesRenderer.ID;
397
}
398
399
renderTemplate(container: HTMLElement): IScopeTemplateData {
400
const name = dom.append(container, $('.scope'));
401
const label = new HighlightedLabel(name);
402
403
return { name, label };
404
}
405
406
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeTemplateData): void {
407
templateData.label.set(element.element.name, createMatches(element.filterData));
408
}
409
410
disposeTemplate(templateData: IScopeTemplateData): void {
411
templateData.label.dispose();
412
}
413
}
414
415
interface IScopeErrorTemplateData {
416
error: HTMLElement;
417
}
418
419
class ScopeErrorRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeErrorTemplateData> {
420
421
static readonly ID = 'scopeError';
422
423
get templateId(): string {
424
return ScopeErrorRenderer.ID;
425
}
426
427
renderTemplate(container: HTMLElement): IScopeErrorTemplateData {
428
const wrapper = dom.append(container, $('.scope'));
429
const error = dom.append(wrapper, $('.error'));
430
return { error };
431
}
432
433
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeErrorTemplateData): void {
434
templateData.error.innerText = element.element.name;
435
}
436
437
disposeTemplate(): void {
438
// noop
439
}
440
}
441
442
export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
443
public static readonly ID = 'viz';
444
445
/**
446
* Registers a helper that rerenders the tree when visualization is requested
447
* or cancelled./
448
*/
449
public static rendererOnVisualizationRange(model: IViewModel, tree: AsyncDataTree<any, any, any>): IDisposable {
450
return model.onDidChangeVisualization(({ original }) => {
451
if (!tree.hasNode(original)) {
452
return;
453
}
454
455
const parent: IExpression = tree.getParentElement(original);
456
tree.updateChildren(parent, false, false);
457
});
458
459
}
460
461
constructor(
462
private readonly expressionRenderer: DebugExpressionRenderer,
463
@IDebugService debugService: IDebugService,
464
@IContextViewService contextViewService: IContextViewService,
465
@IHoverService hoverService: IHoverService,
466
@IMenuService private readonly menuService: IMenuService,
467
@IContextKeyService private readonly contextKeyService: IContextKeyService,
468
) {
469
super(debugService, contextViewService, hoverService);
470
}
471
472
public override get templateId(): string {
473
return VisualizedVariableRenderer.ID;
474
}
475
476
public override renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
477
data.elementDisposable.clear();
478
super.renderExpressionElement(node.element, node, data);
479
}
480
481
protected override renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
482
const viz = expression as VisualizedExpression;
483
484
let text = viz.name;
485
if (viz.value && typeof viz.name === 'string') {
486
text += ':';
487
}
488
data.label.set(text, highlights, viz.name);
489
data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, viz, {
490
showChanged: false,
491
maxValueLength: 1024,
492
colorize: true,
493
session: expression.getSession(),
494
}));
495
}
496
497
protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
498
const viz = <VisualizedExpression>expression;
499
return {
500
initialValue: expression.value,
501
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
502
validationOptions: {
503
validation: () => viz.errorMessage ? ({ content: viz.errorMessage }) : null
504
},
505
onFinish: (value: string, success: boolean) => {
506
viz.errorMessage = undefined;
507
if (success) {
508
viz.edit(value).then(() => {
509
// Do not refresh scopes due to a node limitation #15520
510
forgetScopes = false;
511
this.debugService.getViewModel().updateViews();
512
});
513
}
514
}
515
};
516
}
517
518
protected override renderActionBar(actionBar: ActionBar, expression: IExpression, _data: IExpressionTemplateData) {
519
const viz = expression as VisualizedExpression;
520
const contextKeyService = viz.original ? getContextForVariableMenuBase(this.contextKeyService, viz.original) : this.contextKeyService;
521
const context = viz.original ? getVariablesContext(viz.original) : undefined;
522
const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false });
523
524
const { primary } = getContextMenuActions(menu, 'inline');
525
526
if (viz.original) {
527
const action = toAction({
528
id: 'debugViz', label: localize('removeVisualizer', 'Remove Visualizer'), class: ThemeIcon.asClassName(Codicon.eye), run: () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined)
529
});
530
action.checked = true;
531
primary.push(action);
532
actionBar.domNode.style.display = 'initial';
533
}
534
actionBar.clear();
535
actionBar.context = context;
536
actionBar.push(primary, { icon: true, label: false });
537
}
538
}
539
540
export class VariablesRenderer extends AbstractExpressionsRenderer {
541
542
static readonly ID = 'variable';
543
544
constructor(
545
private readonly expressionRenderer: DebugExpressionRenderer,
546
@IMenuService private readonly menuService: IMenuService,
547
@IContextKeyService private readonly contextKeyService: IContextKeyService,
548
@IDebugVisualizerService private readonly visualization: IDebugVisualizerService,
549
@IContextMenuService private readonly contextMenuService: IContextMenuService,
550
@IDebugService debugService: IDebugService,
551
@IContextViewService contextViewService: IContextViewService,
552
@IHoverService hoverService: IHoverService,
553
) {
554
super(debugService, contextViewService, hoverService);
555
}
556
557
get templateId(): string {
558
return VariablesRenderer.ID;
559
}
560
561
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
562
data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, {
563
highlights,
564
showChanged: true,
565
}));
566
}
567
568
public override renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
569
data.elementDisposable.clear();
570
super.renderExpressionElement(node.element, node, data);
571
}
572
573
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions {
574
const variable = <Variable>expression;
575
return {
576
initialValue: expression.value,
577
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
578
validationOptions: {
579
validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null
580
},
581
onFinish: (value: string, success: boolean) => {
582
variable.errorMessage = undefined;
583
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
584
if (success && variable.value !== value && focusedStackFrame) {
585
variable.setVariable(value, focusedStackFrame)
586
// Need to force watch expressions and variables to update since a variable change can have an effect on both
587
.then(() => {
588
// Do not refresh scopes due to a node limitation #15520
589
forgetScopes = false;
590
this.debugService.getViewModel().updateViews();
591
});
592
}
593
}
594
};
595
}
596
597
protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) {
598
const variable = expression as Variable;
599
const contextKeyService = getContextForVariableMenuBase(this.contextKeyService, variable);
600
601
const context = getVariablesContext(variable);
602
const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false });
603
const { primary } = getContextMenuActions(menu, 'inline');
604
605
actionBar.clear();
606
actionBar.context = context;
607
actionBar.push(primary, { icon: true, label: false });
608
609
const cts = new CancellationTokenSource();
610
data.elementDisposable.add(toDisposable(() => cts.dispose(true)));
611
this.visualization.getApplicableFor(expression, cts.token).then(result => {
612
data.elementDisposable.add(result);
613
614
const originalExpression = (expression instanceof VisualizedExpression && expression.original) || expression;
615
const actions = result.object.map(v => toAction({ id: 'debugViz', label: v.name, class: v.iconClass || 'debug-viz-icon', run: this.useVisualizer(v, originalExpression, cts.token) }));
616
if (actions.length === 0) {
617
// no-op
618
} else if (actions.length === 1) {
619
actionBar.push(actions[0], { icon: true, label: false });
620
} else {
621
actionBar.push(toAction({ id: 'debugViz', label: localize('useVisualizer', 'Visualize Variable...'), class: ThemeIcon.asClassName(Codicon.eye), run: () => this.pickVisualizer(actions, originalExpression, data) }), { icon: true, label: false });
622
}
623
});
624
}
625
626
private pickVisualizer(actions: IAction[], expression: IExpression, data: IExpressionTemplateData) {
627
this.contextMenuService.showContextMenu({
628
getAnchor: () => data.actionBar!.getContainer(),
629
getActions: () => actions,
630
});
631
}
632
633
private useVisualizer(viz: DebugVisualizer, expression: IExpression, token: CancellationToken) {
634
return async () => {
635
const resolved = await viz.resolve(token);
636
if (token.isCancellationRequested) {
637
return;
638
}
639
640
if (resolved.type === DebugVisualizationType.Command) {
641
viz.execute();
642
} else {
643
const replacement = await this.visualization.getVisualizedNodeFor(resolved.id, expression);
644
if (replacement) {
645
this.debugService.getViewModel().setVisualizedExpression(expression, replacement);
646
}
647
}
648
};
649
}
650
}
651
652
class VariablesAccessibilityProvider implements IListAccessibilityProvider<IExpression | IScope> {
653
654
getWidgetAriaLabel(): string {
655
return localize('variablesAriaTreeLabel', "Debug Variables");
656
}
657
658
getAriaLabel(element: IExpression | IScope): string | null {
659
if (element instanceof Scope) {
660
return localize('variableScopeAriaLabel', "Scope {0}", element.name);
661
}
662
if (element instanceof Variable) {
663
return localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value);
664
}
665
666
return null;
667
}
668
}
669
670
export const SET_VARIABLE_ID = 'debug.setVariable';
671
CommandsRegistry.registerCommand({
672
id: SET_VARIABLE_ID,
673
handler: (accessor: ServicesAccessor) => {
674
const debugService = accessor.get(IDebugService);
675
debugService.getViewModel().setSelectedExpression(variableInternalContext, false);
676
}
677
});
678
679
CommandsRegistry.registerCommand({
680
metadata: {
681
description: COPY_VALUE_LABEL,
682
},
683
id: COPY_VALUE_ID,
684
handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext | undefined, ctx?: (Variable | Expression)[]) => {
685
const debugService = accessor.get(IDebugService);
686
const clipboardService = accessor.get(IClipboardService);
687
let elementContext = '';
688
let elements: (Variable | Expression)[];
689
if (!arg) {
690
const viewService = accessor.get(IViewsService);
691
const focusedView = viewService.getFocusedView();
692
let view: IDebugViewWithVariables | null | undefined;
693
if (focusedView?.id === WATCH_VIEW_ID) {
694
view = viewService.getActiveViewWithId<IDebugViewWithVariables>(WATCH_VIEW_ID);
695
elementContext = 'watch';
696
} else if (focusedView?.id === VARIABLES_VIEW_ID) {
697
view = viewService.getActiveViewWithId<IDebugViewWithVariables>(VARIABLES_VIEW_ID);
698
elementContext = 'variables';
699
}
700
if (!view) {
701
return;
702
}
703
elements = view.treeSelection.filter(e => e instanceof Expression || e instanceof Variable);
704
} else if (arg instanceof Variable || arg instanceof Expression) {
705
elementContext = 'watch';
706
elements = [arg];
707
} else {
708
elementContext = 'variables';
709
elements = variableInternalContext ? [variableInternalContext] : [];
710
}
711
712
const stackFrame = debugService.getViewModel().focusedStackFrame;
713
const session = debugService.getViewModel().focusedSession;
714
if (!stackFrame || !session || elements.length === 0) {
715
return;
716
}
717
718
const evalContext = session.capabilities.supportsClipboardContext ? 'clipboard' : elementContext;
719
const toEvaluate = elements.map(element => element instanceof Variable ? (element.evaluateName || element.value) : element.name);
720
721
try {
722
const evaluations = await Promise.all(toEvaluate.map(expr => session.evaluate(expr, stackFrame.frameId, evalContext)));
723
const result = coalesce(evaluations).map(evaluation => evaluation.body.result);
724
if (result.length) {
725
clipboardService.writeText(result.join('\n'));
726
}
727
} catch (e) {
728
const result = elements.map(element => element.value);
729
clipboardService.writeText(result.join('\n'));
730
}
731
}
732
});
733
734
export const VIEW_MEMORY_ID = 'workbench.debug.viewlet.action.viewMemory';
735
736
const HEX_EDITOR_EXTENSION_ID = 'ms-vscode.hexeditor';
737
const HEX_EDITOR_EDITOR_ID = 'hexEditor.hexedit';
738
739
CommandsRegistry.registerCommand({
740
id: VIEW_MEMORY_ID,
741
handler: async (accessor: ServicesAccessor, arg: IVariablesContext | IExpression, ctx?: (Variable | Expression)[]) => {
742
const debugService = accessor.get(IDebugService);
743
let sessionId: string;
744
let memoryReference: string;
745
if ('sessionId' in arg) { // IVariablesContext
746
if (!arg.sessionId || !arg.variable.memoryReference) {
747
return;
748
}
749
sessionId = arg.sessionId;
750
memoryReference = arg.variable.memoryReference;
751
} else { // IExpression
752
if (!arg.memoryReference) {
753
return;
754
}
755
const focused = debugService.getViewModel().focusedSession;
756
if (!focused) {
757
return;
758
}
759
760
sessionId = focused.getId();
761
memoryReference = arg.memoryReference;
762
}
763
764
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
765
const editorService = accessor.get(IEditorService);
766
const notificationService = accessor.get(INotificationService);
767
const extensionService = accessor.get(IExtensionService);
768
const telemetryService = accessor.get(ITelemetryService);
769
770
const ext = await extensionService.getExtension(HEX_EDITOR_EXTENSION_ID);
771
if (ext || await tryInstallHexEditor(extensionsWorkbenchService, notificationService)) {
772
/* __GDPR__
773
"debug/didViewMemory" : {
774
"owner": "connor4312",
775
"debugType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
776
}
777
*/
778
telemetryService.publicLog('debug/didViewMemory', {
779
debugType: debugService.getModel().getSession(sessionId)?.configuration.type,
780
});
781
782
await editorService.openEditor({
783
resource: getUriForDebugMemory(sessionId, memoryReference),
784
options: {
785
revealIfOpened: true,
786
override: HEX_EDITOR_EDITOR_ID,
787
},
788
}, SIDE_GROUP);
789
}
790
}
791
});
792
793
async function tryInstallHexEditor(extensionsWorkbenchService: IExtensionsWorkbenchService, notificationService: INotificationService): Promise<boolean> {
794
try {
795
await extensionsWorkbenchService.install(HEX_EDITOR_EXTENSION_ID, {
796
justification: localize("viewMemory.prompt", "Inspecting binary data requires this extension."),
797
enable: true
798
}, ProgressLocation.Notification);
799
return true;
800
} catch (error) {
801
notificationService.error(error);
802
return false;
803
}
804
}
805
806
CommandsRegistry.registerCommand({
807
metadata: {
808
description: COPY_EVALUATE_PATH_LABEL,
809
},
810
id: COPY_EVALUATE_PATH_ID,
811
handler: async (accessor: ServicesAccessor, context: IVariablesContext | Variable) => {
812
const clipboardService = accessor.get(IClipboardService);
813
if (context instanceof Variable) {
814
await clipboardService.writeText(context.evaluateName!);
815
} else {
816
await clipboardService.writeText(context.variable.evaluateName!);
817
}
818
}
819
});
820
821
CommandsRegistry.registerCommand({
822
metadata: {
823
description: ADD_TO_WATCH_LABEL,
824
},
825
id: ADD_TO_WATCH_ID,
826
handler: async (accessor: ServicesAccessor, context: IVariablesContext) => {
827
const debugService = accessor.get(IDebugService);
828
debugService.addWatchExpression(context.variable.evaluateName);
829
}
830
});
831
832
registerAction2(class extends ViewAction<VariablesView> {
833
constructor() {
834
super({
835
id: 'variables.collapse',
836
viewId: VARIABLES_VIEW_ID,
837
title: localize('collapse', "Collapse All"),
838
f1: false,
839
icon: Codicon.collapseAll,
840
menu: {
841
id: MenuId.ViewTitle,
842
group: 'navigation',
843
when: ContextKeyExpr.equals('view', VARIABLES_VIEW_ID)
844
}
845
});
846
}
847
848
runInView(_accessor: ServicesAccessor, view: VariablesView) {
849
view.collapseAll();
850
}
851
});
852
853