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
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 * 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 { Action, IAction } 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
return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy;
234
}
235
236
private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
237
const variable = e.element;
238
if (!(variable instanceof Variable) || !variable.value) {
239
return;
240
}
241
242
return openContextMenuForVariableTreeElement(this.contextKeyService, this.menuService, this.contextMenuService, MenuId.DebugVariablesContext, e);
243
}
244
}
245
246
export async function openContextMenuForVariableTreeElement(parentContextKeyService: IContextKeyService, menuService: IMenuService, contextMenuService: IContextMenuService, menuId: MenuId, e: ITreeContextMenuEvent<IExpression | IScope>) {
247
const variable = e.element;
248
if (!(variable instanceof Variable) || !variable.value) {
249
return;
250
}
251
252
const contextKeyService = await getContextForVariableMenuWithDataAccess(parentContextKeyService, variable);
253
const context: IVariablesContext = getVariablesContext(variable);
254
const menu = menuService.getMenuActions(menuId, contextKeyService, { arg: context, shouldForwardArgs: false });
255
256
const { secondary } = getContextMenuActions(menu, 'inline');
257
contextMenuService.showContextMenu({
258
getAnchor: () => e.anchor,
259
getActions: () => secondary
260
});
261
}
262
263
const getVariablesContext = (variable: Variable): IVariablesContext => ({
264
sessionId: variable.getSession()?.getId(),
265
container: variable.parent instanceof Expression
266
? { expression: variable.parent.name }
267
: (variable.parent as (Variable | Scope)).toDebugProtocolObject(),
268
variable: variable.toDebugProtocolObject()
269
});
270
271
/**
272
* Gets a context key overlay that has context for the given variable, including data access info.
273
*/
274
async function getContextForVariableMenuWithDataAccess(parentContext: IContextKeyService, variable: Variable) {
275
const session = variable.getSession();
276
if (!session || !session.capabilities.supportsDataBreakpoints) {
277
return getContextForVariableMenuBase(parentContext, variable);
278
}
279
280
const contextKeys: [string, unknown][] = [];
281
const dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference);
282
const dataBreakpointId = dataBreakpointInfoResponse?.dataId;
283
const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes;
284
setDataBreakpointInfoResponse(dataBreakpointInfoResponse);
285
286
if (!dataBreakpointAccessTypes) {
287
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]);
288
} else {
289
for (const accessType of dataBreakpointAccessTypes) {
290
switch (accessType) {
291
case 'read':
292
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.key, !!dataBreakpointId]);
293
break;
294
case 'write':
295
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]);
296
break;
297
case 'readWrite':
298
contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.key, !!dataBreakpointId]);
299
break;
300
}
301
}
302
}
303
304
return getContextForVariableMenuBase(parentContext, variable, contextKeys);
305
}
306
307
/**
308
* Gets a context key overlay that has context for the given variable.
309
*/
310
function getContextForVariableMenuBase(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) {
311
variableInternalContext = variable;
312
return getContextForVariable(parentContext, variable, additionalContext);
313
}
314
315
function isStackFrame(obj: any): obj is IStackFrame {
316
return obj instanceof StackFrame;
317
}
318
319
class VariablesDataSource extends AbstractExpressionDataSource<IStackFrame | null, IExpression | IScope> {
320
321
public override hasChildren(element: IStackFrame | null | IExpression | IScope): boolean {
322
if (!element) {
323
return false;
324
}
325
if (isStackFrame(element)) {
326
return true;
327
}
328
329
return element.hasChildren;
330
}
331
332
protected override doGetChildren(element: IStackFrame | IExpression | IScope): Promise<(IExpression | IScope)[]> {
333
if (isStackFrame(element)) {
334
return element.getScopes();
335
}
336
337
return element.getChildren();
338
}
339
}
340
341
interface IScopeTemplateData {
342
name: HTMLElement;
343
label: HighlightedLabel;
344
}
345
346
class VariablesDelegate implements IListVirtualDelegate<IExpression | IScope> {
347
348
getHeight(element: IExpression | IScope): number {
349
return 22;
350
}
351
352
getTemplateId(element: IExpression | IScope): string {
353
if (element instanceof ErrorScope) {
354
return ScopeErrorRenderer.ID;
355
}
356
357
if (element instanceof Scope) {
358
return ScopesRenderer.ID;
359
}
360
361
if (element instanceof VisualizedExpression) {
362
return VisualizedVariableRenderer.ID;
363
}
364
365
return VariablesRenderer.ID;
366
}
367
}
368
369
class ScopesRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeTemplateData> {
370
371
static readonly ID = 'scope';
372
373
get templateId(): string {
374
return ScopesRenderer.ID;
375
}
376
377
renderTemplate(container: HTMLElement): IScopeTemplateData {
378
const name = dom.append(container, $('.scope'));
379
const label = new HighlightedLabel(name);
380
381
return { name, label };
382
}
383
384
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeTemplateData): void {
385
templateData.label.set(element.element.name, createMatches(element.filterData));
386
}
387
388
disposeTemplate(templateData: IScopeTemplateData): void {
389
templateData.label.dispose();
390
}
391
}
392
393
interface IScopeErrorTemplateData {
394
error: HTMLElement;
395
}
396
397
class ScopeErrorRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeErrorTemplateData> {
398
399
static readonly ID = 'scopeError';
400
401
get templateId(): string {
402
return ScopeErrorRenderer.ID;
403
}
404
405
renderTemplate(container: HTMLElement): IScopeErrorTemplateData {
406
const wrapper = dom.append(container, $('.scope'));
407
const error = dom.append(wrapper, $('.error'));
408
return { error };
409
}
410
411
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeErrorTemplateData): void {
412
templateData.error.innerText = element.element.name;
413
}
414
415
disposeTemplate(): void {
416
// noop
417
}
418
}
419
420
export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
421
public static readonly ID = 'viz';
422
423
/**
424
* Registers a helper that rerenders the tree when visualization is requested
425
* or cancelled./
426
*/
427
public static rendererOnVisualizationRange(model: IViewModel, tree: AsyncDataTree<any, any, any>): IDisposable {
428
return model.onDidChangeVisualization(({ original }) => {
429
if (!tree.hasNode(original)) {
430
return;
431
}
432
433
const parent: IExpression = tree.getParentElement(original);
434
tree.updateChildren(parent, false, false);
435
});
436
437
}
438
439
constructor(
440
private readonly expressionRenderer: DebugExpressionRenderer,
441
@IDebugService debugService: IDebugService,
442
@IContextViewService contextViewService: IContextViewService,
443
@IHoverService hoverService: IHoverService,
444
@IMenuService private readonly menuService: IMenuService,
445
@IContextKeyService private readonly contextKeyService: IContextKeyService,
446
) {
447
super(debugService, contextViewService, hoverService);
448
}
449
450
public override get templateId(): string {
451
return VisualizedVariableRenderer.ID;
452
}
453
454
public override renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
455
data.elementDisposable.clear();
456
super.renderExpressionElement(node.element, node, data);
457
}
458
459
protected override renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
460
const viz = expression as VisualizedExpression;
461
462
let text = viz.name;
463
if (viz.value && typeof viz.name === 'string') {
464
text += ':';
465
}
466
data.label.set(text, highlights, viz.name);
467
data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, viz, {
468
showChanged: false,
469
maxValueLength: 1024,
470
colorize: true,
471
session: expression.getSession(),
472
}));
473
}
474
475
protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
476
const viz = <VisualizedExpression>expression;
477
return {
478
initialValue: expression.value,
479
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
480
validationOptions: {
481
validation: () => viz.errorMessage ? ({ content: viz.errorMessage }) : null
482
},
483
onFinish: (value: string, success: boolean) => {
484
viz.errorMessage = undefined;
485
if (success) {
486
viz.edit(value).then(() => {
487
// Do not refresh scopes due to a node limitation #15520
488
forgetScopes = false;
489
this.debugService.getViewModel().updateViews();
490
});
491
}
492
}
493
};
494
}
495
496
protected override renderActionBar(actionBar: ActionBar, expression: IExpression, _data: IExpressionTemplateData) {
497
const viz = expression as VisualizedExpression;
498
const contextKeyService = viz.original ? getContextForVariableMenuBase(this.contextKeyService, viz.original) : this.contextKeyService;
499
const context = viz.original ? getVariablesContext(viz.original) : undefined;
500
const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false });
501
502
const { primary } = getContextMenuActions(menu, 'inline');
503
504
if (viz.original) {
505
const action = new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.eye), true, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined));
506
action.checked = true;
507
primary.push(action);
508
actionBar.domNode.style.display = 'initial';
509
}
510
actionBar.clear();
511
actionBar.context = context;
512
actionBar.push(primary, { icon: true, label: false });
513
}
514
}
515
516
export class VariablesRenderer extends AbstractExpressionsRenderer {
517
518
static readonly ID = 'variable';
519
520
constructor(
521
private readonly expressionRenderer: DebugExpressionRenderer,
522
@IMenuService private readonly menuService: IMenuService,
523
@IContextKeyService private readonly contextKeyService: IContextKeyService,
524
@IDebugVisualizerService private readonly visualization: IDebugVisualizerService,
525
@IContextMenuService private readonly contextMenuService: IContextMenuService,
526
@IDebugService debugService: IDebugService,
527
@IContextViewService contextViewService: IContextViewService,
528
@IHoverService hoverService: IHoverService,
529
) {
530
super(debugService, contextViewService, hoverService);
531
}
532
533
get templateId(): string {
534
return VariablesRenderer.ID;
535
}
536
537
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
538
data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, {
539
highlights,
540
showChanged: true,
541
}));
542
}
543
544
public override renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
545
data.elementDisposable.clear();
546
super.renderExpressionElement(node.element, node, data);
547
}
548
549
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions {
550
const variable = <Variable>expression;
551
return {
552
initialValue: expression.value,
553
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
554
validationOptions: {
555
validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null
556
},
557
onFinish: (value: string, success: boolean) => {
558
variable.errorMessage = undefined;
559
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
560
if (success && variable.value !== value && focusedStackFrame) {
561
variable.setVariable(value, focusedStackFrame)
562
// Need to force watch expressions and variables to update since a variable change can have an effect on both
563
.then(() => {
564
// Do not refresh scopes due to a node limitation #15520
565
forgetScopes = false;
566
this.debugService.getViewModel().updateViews();
567
});
568
}
569
}
570
};
571
}
572
573
protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) {
574
const variable = expression as Variable;
575
const contextKeyService = getContextForVariableMenuBase(this.contextKeyService, variable);
576
577
const context = getVariablesContext(variable);
578
const menu = this.menuService.getMenuActions(MenuId.DebugVariablesContext, contextKeyService, { arg: context, shouldForwardArgs: false });
579
const { primary } = getContextMenuActions(menu, 'inline');
580
581
actionBar.clear();
582
actionBar.context = context;
583
actionBar.push(primary, { icon: true, label: false });
584
585
const cts = new CancellationTokenSource();
586
data.elementDisposable.add(toDisposable(() => cts.dispose(true)));
587
this.visualization.getApplicableFor(expression, cts.token).then(result => {
588
data.elementDisposable.add(result);
589
590
const originalExpression = (expression instanceof VisualizedExpression && expression.original) || expression;
591
const actions = result.object.map(v => new Action('debugViz', v.name, v.iconClass || 'debug-viz-icon', undefined, this.useVisualizer(v, originalExpression, cts.token)));
592
if (actions.length === 0) {
593
// no-op
594
} else if (actions.length === 1) {
595
actionBar.push(actions[0], { icon: true, label: false });
596
} else {
597
actionBar.push(new Action('debugViz', localize('useVisualizer', 'Visualize Variable...'), ThemeIcon.asClassName(Codicon.eye), undefined, () => this.pickVisualizer(actions, originalExpression, data)), { icon: true, label: false });
598
}
599
});
600
}
601
602
private pickVisualizer(actions: IAction[], expression: IExpression, data: IExpressionTemplateData) {
603
this.contextMenuService.showContextMenu({
604
getAnchor: () => data.actionBar!.getContainer(),
605
getActions: () => actions,
606
});
607
}
608
609
private useVisualizer(viz: DebugVisualizer, expression: IExpression, token: CancellationToken) {
610
return async () => {
611
const resolved = await viz.resolve(token);
612
if (token.isCancellationRequested) {
613
return;
614
}
615
616
if (resolved.type === DebugVisualizationType.Command) {
617
viz.execute();
618
} else {
619
const replacement = await this.visualization.getVisualizedNodeFor(resolved.id, expression);
620
if (replacement) {
621
this.debugService.getViewModel().setVisualizedExpression(expression, replacement);
622
}
623
}
624
};
625
}
626
}
627
628
class VariablesAccessibilityProvider implements IListAccessibilityProvider<IExpression | IScope> {
629
630
getWidgetAriaLabel(): string {
631
return localize('variablesAriaTreeLabel', "Debug Variables");
632
}
633
634
getAriaLabel(element: IExpression | IScope): string | null {
635
if (element instanceof Scope) {
636
return localize('variableScopeAriaLabel', "Scope {0}", element.name);
637
}
638
if (element instanceof Variable) {
639
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);
640
}
641
642
return null;
643
}
644
}
645
646
export const SET_VARIABLE_ID = 'debug.setVariable';
647
CommandsRegistry.registerCommand({
648
id: SET_VARIABLE_ID,
649
handler: (accessor: ServicesAccessor) => {
650
const debugService = accessor.get(IDebugService);
651
debugService.getViewModel().setSelectedExpression(variableInternalContext, false);
652
}
653
});
654
655
CommandsRegistry.registerCommand({
656
metadata: {
657
description: COPY_VALUE_LABEL,
658
},
659
id: COPY_VALUE_ID,
660
handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext | undefined, ctx?: (Variable | Expression)[]) => {
661
if (!arg) {
662
const viewService = accessor.get(IViewsService);
663
const view = viewService.getActiveViewWithId(WATCH_VIEW_ID) || viewService.getActiveViewWithId(VARIABLES_VIEW_ID);
664
if (view) {
665
666
}
667
}
668
const debugService = accessor.get(IDebugService);
669
const clipboardService = accessor.get(IClipboardService);
670
let elementContext = '';
671
let elements: (Variable | Expression)[];
672
if (!arg) {
673
const viewService = accessor.get(IViewsService);
674
const focusedView = viewService.getFocusedView();
675
let view: IDebugViewWithVariables | null | undefined;
676
if (focusedView?.id === WATCH_VIEW_ID) {
677
view = viewService.getActiveViewWithId<IDebugViewWithVariables>(WATCH_VIEW_ID);
678
elementContext = 'watch';
679
} else if (focusedView?.id === VARIABLES_VIEW_ID) {
680
view = viewService.getActiveViewWithId<IDebugViewWithVariables>(VARIABLES_VIEW_ID);
681
elementContext = 'variables';
682
}
683
if (!view) {
684
return;
685
}
686
elements = view.treeSelection.filter(e => e instanceof Expression || e instanceof Variable);
687
} else if (arg instanceof Variable || arg instanceof Expression) {
688
elementContext = 'watch';
689
elements = ctx ? ctx : [];
690
} else {
691
elementContext = 'variables';
692
elements = variableInternalContext ? [variableInternalContext] : [];
693
}
694
695
const stackFrame = debugService.getViewModel().focusedStackFrame;
696
const session = debugService.getViewModel().focusedSession;
697
if (!stackFrame || !session || elements.length === 0) {
698
return;
699
}
700
701
const evalContext = session.capabilities.supportsClipboardContext ? 'clipboard' : elementContext;
702
const toEvaluate = elements.map(element => element instanceof Variable ? (element.evaluateName || element.value) : element.name);
703
704
try {
705
const evaluations = await Promise.all(toEvaluate.map(expr => session.evaluate(expr, stackFrame.frameId, evalContext)));
706
const result = coalesce(evaluations).map(evaluation => evaluation.body.result);
707
if (result.length) {
708
clipboardService.writeText(result.join('\n'));
709
}
710
} catch (e) {
711
const result = elements.map(element => element.value);
712
clipboardService.writeText(result.join('\n'));
713
}
714
}
715
});
716
717
export const VIEW_MEMORY_ID = 'workbench.debug.viewlet.action.viewMemory';
718
719
const HEX_EDITOR_EXTENSION_ID = 'ms-vscode.hexeditor';
720
const HEX_EDITOR_EDITOR_ID = 'hexEditor.hexedit';
721
722
CommandsRegistry.registerCommand({
723
id: VIEW_MEMORY_ID,
724
handler: async (accessor: ServicesAccessor, arg: IVariablesContext | IExpression, ctx?: (Variable | Expression)[]) => {
725
const debugService = accessor.get(IDebugService);
726
let sessionId: string;
727
let memoryReference: string;
728
if ('sessionId' in arg) { // IVariablesContext
729
if (!arg.sessionId || !arg.variable.memoryReference) {
730
return;
731
}
732
sessionId = arg.sessionId;
733
memoryReference = arg.variable.memoryReference;
734
} else { // IExpression
735
if (!arg.memoryReference) {
736
return;
737
}
738
const focused = debugService.getViewModel().focusedSession;
739
if (!focused) {
740
return;
741
}
742
743
sessionId = focused.getId();
744
memoryReference = arg.memoryReference;
745
}
746
747
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
748
const editorService = accessor.get(IEditorService);
749
const notificationService = accessor.get(INotificationService);
750
const extensionService = accessor.get(IExtensionService);
751
const telemetryService = accessor.get(ITelemetryService);
752
753
const ext = await extensionService.getExtension(HEX_EDITOR_EXTENSION_ID);
754
if (ext || await tryInstallHexEditor(extensionsWorkbenchService, notificationService)) {
755
/* __GDPR__
756
"debug/didViewMemory" : {
757
"owner": "connor4312",
758
"debugType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
759
}
760
*/
761
telemetryService.publicLog('debug/didViewMemory', {
762
debugType: debugService.getModel().getSession(sessionId)?.configuration.type,
763
});
764
765
await editorService.openEditor({
766
resource: getUriForDebugMemory(sessionId, memoryReference),
767
options: {
768
revealIfOpened: true,
769
override: HEX_EDITOR_EDITOR_ID,
770
},
771
}, SIDE_GROUP);
772
}
773
}
774
});
775
776
async function tryInstallHexEditor(extensionsWorkbenchService: IExtensionsWorkbenchService, notificationService: INotificationService): Promise<boolean> {
777
try {
778
await extensionsWorkbenchService.install(HEX_EDITOR_EXTENSION_ID, {
779
justification: localize("viewMemory.prompt", "Inspecting binary data requires this extension."),
780
enable: true
781
}, ProgressLocation.Notification);
782
return true;
783
} catch (error) {
784
notificationService.error(error);
785
return false;
786
}
787
}
788
789
CommandsRegistry.registerCommand({
790
metadata: {
791
description: COPY_EVALUATE_PATH_LABEL,
792
},
793
id: COPY_EVALUATE_PATH_ID,
794
handler: async (accessor: ServicesAccessor, context: IVariablesContext) => {
795
const clipboardService = accessor.get(IClipboardService);
796
await clipboardService.writeText(context.variable.evaluateName!);
797
}
798
});
799
800
CommandsRegistry.registerCommand({
801
metadata: {
802
description: ADD_TO_WATCH_LABEL,
803
},
804
id: ADD_TO_WATCH_ID,
805
handler: async (accessor: ServicesAccessor, context: IVariablesContext) => {
806
const debugService = accessor.get(IDebugService);
807
debugService.addWatchExpression(context.variable.evaluateName);
808
}
809
});
810
811
registerAction2(class extends ViewAction<VariablesView> {
812
constructor() {
813
super({
814
id: 'variables.collapse',
815
viewId: VARIABLES_VIEW_ID,
816
title: localize('collapse', "Collapse All"),
817
f1: false,
818
icon: Codicon.collapseAll,
819
menu: {
820
id: MenuId.ViewTitle,
821
group: 'navigation',
822
when: ContextKeyExpr.equals('view', VARIABLES_VIEW_ID)
823
}
824
});
825
}
826
827
runInView(_accessor: ServicesAccessor, view: VariablesView) {
828
view.collapseAll();
829
}
830
});
831
832