Skip to content

Commit 7f359bb

Browse files
authored
debug: allow editing visualizers in debug tree (microsoft#205163)
* debug: allow editing visualizers in debug tree * fix visibility lint
1 parent 702a1ff commit 7f359bb

File tree

9 files changed

+132
-117
lines changed

9 files changed

+132
-117
lines changed

src/vs/workbench/contrib/debug/browser/baseDebugView.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabe
1010
import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
1111
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
1212
import { Codicon } from 'vs/base/common/codicons';
13-
import { ThemeIcon } from 'vs/base/common/themables';
14-
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
13+
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
1514
import { createSingleCallFunction } from 'vs/base/common/functional';
1615
import { KeyCode } from 'vs/base/common/keyCodes';
17-
import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
16+
import { DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
17+
import { ThemeIcon } from 'vs/base/common/themables';
1818
import { localize } from 'vs/nls';
1919
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
2020
import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles';
2121
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
2222
import { IDebugService, IExpression, IExpressionValue } from 'vs/workbench/contrib/debug/common/debug';
2323
import { Expression, ExpressionContainer, Variable } from 'vs/workbench/contrib/debug/common/debugModel';
24+
import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers';
2425
import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel';
2526

2627
const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
@@ -146,24 +147,31 @@ export interface IExpressionTemplateData {
146147
}
147148

148149
export abstract class AbstractExpressionDataSource<Input, Element extends IExpression> implements IAsyncDataSource<Input, Element> {
149-
constructor(@IDebugService protected debugService: IDebugService) { }
150+
constructor(
151+
@IDebugService protected debugService: IDebugService,
152+
@IDebugVisualizerService protected debugVisualizer: IDebugVisualizerService,
153+
) { }
150154

151155
public abstract hasChildren(element: Input | Element): boolean;
152156

153-
public getChildren(element: Input | Element): Promise<Element[]> {
157+
public async getChildren(element: Input | Element): Promise<Element[]> {
154158
const vm = this.debugService.getViewModel();
155-
return this.doGetChildren(element).then(r => {
156-
let dup: Element[] | undefined;
157-
for (let i = 0; i < r.length; i++) {
158-
const visualized = vm.getVisualizedExpression(r[i] as IExpression);
159-
if (visualized) {
160-
dup ||= r.slice();
161-
dup[i] = visualized as Element;
159+
const children = await this.doGetChildren(element);
160+
return Promise.all(children.map(async r => {
161+
const vizOrTree = vm.getVisualizedExpression(r as IExpression);
162+
if (typeof vizOrTree === 'string') {
163+
const viz = await this.debugVisualizer.getVisualizedNodeFor(vizOrTree, r);
164+
if (viz) {
165+
vm.setVisualizedExpression(r, viz);
166+
return viz as IExpression as Element;
162167
}
168+
} else if (vizOrTree) {
169+
return vizOrTree as Element;
163170
}
164171

165-
return dup || r;
166-
});
172+
173+
return r;
174+
}));
167175
}
168176

169177
protected abstract doGetChildren(element: Input | Element): Promise<Element[]>;

src/vs/workbench/contrib/debug/browser/debugHover.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class DebugHoverWidget implements IContentWidget {
127127
this.treeContainer.setAttribute('role', 'tree');
128128
const tip = dom.append(this.complexValueContainer, $('.tip'));
129129
tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt');
130-
const dataSource = new DebugHoverDataSource(this.debugService);
130+
const dataSource = this.instantiationService.createInstance(DebugHoverDataSource);
131131
const linkeDetector = this.instantiationService.createInstance(LinkDetector);
132132
this.tree = <WorkbenchAsyncDataTree<IExpression, IExpression, any>>this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [
133133
this.instantiationService.createInstance(VariablesRenderer, linkeDetector),
@@ -414,7 +414,7 @@ class DebugHoverDataSource extends AbstractExpressionDataSource<IExpression, IEx
414414
return element.hasChildren;
415415
}
416416

417-
public override doGetChildren(element: IExpression): Promise<IExpression[]> {
417+
protected override doGetChildren(element: IExpression): Promise<IExpression[]> {
418418
return element.getChildren();
419419
}
420420
}

src/vs/workbench/contrib/debug/browser/variablesView.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class VariablesView extends ViewPane {
126126
new ScopesRenderer(),
127127
new ScopeErrorRenderer(),
128128
],
129-
new VariablesDataSource(this.debugService), {
129+
this.instantiationService.createInstance(VariablesDataSource), {
130130
accessibilityProvider: new VariablesAccessibilityProvider(),
131131
identityProvider: { getId: (element: IExpression | IScope) => element.getId() },
132132
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e.name },
@@ -171,7 +171,7 @@ export class VariablesView extends ViewPane {
171171
let horizontalScrolling: boolean | undefined;
172172
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
173173
const variable = e?.expression;
174-
if (variable instanceof Variable && !e?.settingWatch) {
174+
if (variable && this.tree.hasNode(variable)) {
175175
horizontalScrolling = this.tree.options.horizontalScrolling;
176176
if (horizontalScrolling) {
177177
this.tree.updateOptions({ horizontalScrolling: false });
@@ -210,12 +210,24 @@ export class VariablesView extends ViewPane {
210210
}
211211

212212
private onMouseDblClick(e: ITreeMouseEvent<IExpression | IScope>): void {
213-
const session = this.debugService.getViewModel().focusedSession;
214-
if (session && e.element instanceof Variable && session.capabilities.supportsSetVariable && !e.element.presentationHint?.attributes?.includes('readOnly') && !e.element.presentationHint?.lazy) {
213+
if (this.canSetExpressionValue(e.element)) {
215214
this.debugService.getViewModel().setSelectedExpression(e.element, false);
216215
}
217216
}
218217

218+
private canSetExpressionValue(e: IExpression | IScope | null): e is IExpression {
219+
const session = this.debugService.getViewModel().focusedSession;
220+
if (!session) {
221+
return false;
222+
}
223+
224+
if (e instanceof VisualizedExpression) {
225+
return !!e.treeItem.canEdit;
226+
}
227+
228+
return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy;
229+
}
230+
219231
private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
220232
const variable = e.element;
221233
if (!(variable instanceof Variable) || !variable.value) {
@@ -415,7 +427,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
415427
*/
416428
public static rendererOnVisualizationRange(model: IViewModel, tree: AsyncDataTree<any, any, any>): IDisposable {
417429
return model.onDidChangeVisualization(({ original }) => {
418-
if (!tree.hasElement(original)) {
430+
if (!tree.hasNode(original)) {
419431
return;
420432
}
421433

@@ -461,24 +473,21 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
461473
}
462474

463475
protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
464-
const variable = <Variable>expression;
476+
const viz = <VisualizedExpression>expression;
465477
return {
466478
initialValue: expression.value,
467479
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
468480
validationOptions: {
469-
validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null
481+
validation: () => viz.errorMessage ? ({ content: viz.errorMessage }) : null
470482
},
471483
onFinish: (value: string, success: boolean) => {
472-
variable.errorMessage = undefined;
473-
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
474-
if (success && variable.value !== value && focusedStackFrame) {
475-
variable.setVariable(value, focusedStackFrame)
476-
// Need to force watch expressions and variables to update since a variable change can have an effect on both
477-
.then(() => {
478-
// Do not refresh scopes due to a node limitation #15520
479-
forgetScopes = false;
480-
this.debugService.getViewModel().updateViews();
481-
});
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+
});
482491
}
483492
}
484493
};
@@ -494,7 +503,10 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
494503
createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline');
495504

496505
if (viz.original) {
497-
primary.push(new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.close), undefined, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined)));
506+
const action = new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.eye), true, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined));
507+
action.checked = true;
508+
primary.push(action);
509+
actionBar.domNode.style.display = 'initial';
498510
}
499511
actionBar.clear();
500512
actionBar.context = context;
@@ -601,7 +613,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer {
601613
if (resolved.type === DebugVisualizationType.Command) {
602614
viz.execute();
603615
} else {
604-
const replacement = await this.visualization.setVisualizedNodeFor(resolved.id, expression);
616+
const replacement = await this.visualization.getVisualizedNodeFor(resolved.id, expression);
605617
if (replacement) {
606618
this.debugService.getViewModel().setVisualizedExpression(expression, replacement);
607619
}

src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class WatchExpressionsView extends ViewPane {
9393
this.instantiationService.createInstance(VariablesRenderer, linkDetector),
9494
this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector),
9595
],
96-
new WatchExpressionsDataSource(this.debugService), {
96+
this.instantiationService.createInstance(WatchExpressionsDataSource), {
9797
accessibilityProvider: new WatchExpressionsAccessibilityProvider(),
9898
identityProvider: { getId: (element: IExpression) => element.getId() },
9999
keyboardNavigationLabelProvider: {
@@ -157,7 +157,7 @@ export class WatchExpressionsView extends ViewPane {
157157
let horizontalScrolling: boolean | undefined;
158158
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
159159
const expression = e?.expression;
160-
if (expression instanceof Expression || (expression instanceof Variable && e?.settingWatch)) {
160+
if (expression && this.tree.hasElement(expression)) {
161161
horizontalScrolling = this.tree.options.horizontalScrolling;
162162
if (horizontalScrolling) {
163163
this.tree.updateOptions({ horizontalScrolling: false });
@@ -204,7 +204,7 @@ export class WatchExpressionsView extends ViewPane {
204204
const element = e.element;
205205
// double click on primitive value: open input box to be able to select and copy value.
206206
const selectedExpression = this.debugService.getViewModel().getSelectedExpression();
207-
if (element instanceof Expression && element !== selectedExpression?.expression) {
207+
if ((element instanceof Expression && element !== selectedExpression?.expression) || (element instanceof VisualizedExpression && element.treeItem.canEdit)) {
208208
this.debugService.getViewModel().setSelectedExpression(element, false);
209209
} else if (!element) {
210210
// Double click in watch panel triggers to add a new watch expression
@@ -259,7 +259,7 @@ class WatchExpressionsDataSource extends AbstractExpressionDataSource<IDebugServ
259259
return isDebugService(element) || element.hasChildren;
260260
}
261261

262-
public override doGetChildren(element: IDebugService | IExpression): Promise<Array<IExpression>> {
262+
protected override doGetChildren(element: IDebugService | IExpression): Promise<Array<IExpression>> {
263263
if (isDebugService(element)) {
264264
const debugService = element as IDebugService;
265265
const watchExpressions = debugService.getModel().getWatchExpressions();

src/vs/workbench/contrib/debug/common/debug.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -634,8 +634,9 @@ export interface IViewModel extends ITreeElement {
634634
*/
635635
readonly focusedStackFrame: IStackFrame | undefined;
636636

637-
setVisualizedExpression(original: IExpression, visualized: IExpression | undefined): void;
638-
getVisualizedExpression(expression: IExpression): IExpression | undefined;
637+
setVisualizedExpression(original: IExpression, visualized: IExpression & { treeId: string } | undefined): void;
638+
/** Returns the visualized expression if loaded, or a tree it should be visualized with, or undefined */
639+
getVisualizedExpression(expression: IExpression): IExpression | string | undefined;
639640
getSelectedExpression(): { expression: IExpression; settingWatch: boolean } | undefined;
640641
setSelectedExpression(expression: IExpression | undefined, settingWatch: boolean): void;
641642
updateViews(): void;
@@ -1265,7 +1266,7 @@ export interface IReplOptions {
12651266

12661267
export interface IDebugVisualizationContext {
12671268
variable: DebugProtocol.Variable;
1268-
containerId?: string;
1269+
containerId?: number;
12691270
frameId?: number;
12701271
threadId: number;
12711272
sessionId: string;

src/vs/workbench/contrib/debug/common/debugModel.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,7 @@ function handleSetResponse(expression: ExpressionContainer, response: DebugProto
246246
}
247247

248248
export class VisualizedExpression implements IExpression {
249-
public readonly name: string;
250-
public readonly hasChildren: boolean;
251-
public readonly value: string;
249+
public errorMessage?: string;
252250
private readonly id = generateUuid();
253251

254252
evaluateLazy(): Promise<void> {
@@ -262,15 +260,34 @@ export class VisualizedExpression implements IExpression {
262260
return this.id;
263261
}
264262

263+
get name() {
264+
return this.treeItem.label;
265+
}
266+
267+
get value() {
268+
return this.treeItem.description || '';
269+
}
270+
271+
get hasChildren() {
272+
return this.treeItem.collapsibleState !== DebugTreeItemCollapsibleState.None;
273+
}
274+
265275
constructor(
266276
private readonly visualizer: IDebugVisualizerService,
267-
private readonly treeId: string,
277+
public readonly treeId: string,
268278
public readonly treeItem: IDebugVisualizationTreeItem,
269279
public readonly original?: Variable,
270-
) {
271-
this.name = treeItem.label;
272-
this.hasChildren = treeItem.collapsibleState !== DebugTreeItemCollapsibleState.None;
273-
this.value = treeItem.description || '';
280+
) { }
281+
282+
/** Edits the value, sets the {@link errorMessage} and returns false if unsuccessful */
283+
public async edit(newValue: string) {
284+
try {
285+
await this.visualizer.editTreeItem(this.treeId, this.treeItem, newValue);
286+
return true;
287+
} catch (e) {
288+
this.errorMessage = e.message;
289+
return false;
290+
}
274291
}
275292
}
276293

src/vs/workbench/contrib/debug/common/debugViewModel.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class ViewModel implements IViewModel {
2424
private readonly _onWillUpdateViews = new Emitter<void>();
2525
private readonly _onDidChangeVisualization = new Emitter<{ original: IExpression; replacement: IExpression }>();
2626
private readonly visualized = new WeakMap<IExpression, IExpression>();
27+
private readonly preferredVisualizers = new Map</** cache key */ string, /* tree ID */ string>();
2728
private expressionSelectedContextKey!: IContextKey<boolean>;
2829
private loadedScriptsSupportedContextKey!: IContextKey<boolean>;
2930
private stepBackSupportedContextKey!: IContextKey<boolean>;
@@ -165,23 +166,33 @@ export class ViewModel implements IViewModel {
165166
this.multiSessionDebug.set(isMultiSessionView);
166167
}
167168

168-
setVisualizedExpression(original: IExpression, visualized: IExpression | undefined): void {
169+
setVisualizedExpression(original: IExpression, visualized: IExpression & { treeId: string } | undefined): void {
169170
const current = this.visualized.get(original) || original;
170-
171+
const key = this.getPreferredVisualizedKey(original);
171172
if (visualized) {
172173
this.visualized.set(original, visualized);
174+
this.preferredVisualizers.set(key, visualized.treeId);
173175
} else {
174176
this.visualized.delete(original);
177+
this.preferredVisualizers.delete(key);
175178
}
176179
this._onDidChangeVisualization.fire({ original: current, replacement: visualized || original });
177180
}
178181

179-
getVisualizedExpression(expression: IExpression): IExpression | undefined {
180-
return this.visualized.get(expression);
182+
getVisualizedExpression(expression: IExpression): IExpression | string | undefined {
183+
return this.visualized.get(expression) || this.preferredVisualizers.get(this.getPreferredVisualizedKey(expression));
181184
}
182185

183186
async evaluateLazyExpression(expression: IExpressionContainer): Promise<void> {
184187
await expression.evaluateLazy();
185188
this._onDidEvaluateLazyExpression.fire(expression);
186189
}
190+
191+
private getPreferredVisualizedKey(expr: IExpression) {
192+
return JSON.stringify([
193+
expr.name,
194+
expr.type,
195+
!!expr.memoryReference,
196+
].join('\0'));
197+
}
187198
}

0 commit comments

Comments
 (0)