Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

debug: allow editing visualizers in debug tree #205163

Merged
merged 2 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 22 additions & 14 deletions src/vs/workbench/contrib/debug/browser/baseDebugView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabe
import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { createSingleCallFunction } from 'vs/base/common/functional';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { localize } from 'vs/nls';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { IDebugService, IExpression, IExpressionValue } from 'vs/workbench/contrib/debug/common/debug';
import { Expression, ExpressionContainer, Variable } from 'vs/workbench/contrib/debug/common/debugModel';
import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers';
import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel';

const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
Expand Down Expand Up @@ -146,24 +147,31 @@ export interface IExpressionTemplateData {
}

export abstract class AbstractExpressionDataSource<Input, Element extends IExpression> implements IAsyncDataSource<Input, Element> {
constructor(@IDebugService protected debugService: IDebugService) { }
constructor(
@IDebugService protected debugService: IDebugService,
@IDebugVisualizerService protected debugVisualizer: IDebugVisualizerService,
) { }

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

public getChildren(element: Input | Element): Promise<Element[]> {
public async getChildren(element: Input | Element): Promise<Element[]> {
const vm = this.debugService.getViewModel();
return this.doGetChildren(element).then(r => {
let dup: Element[] | undefined;
for (let i = 0; i < r.length; i++) {
const visualized = vm.getVisualizedExpression(r[i] as IExpression);
if (visualized) {
dup ||= r.slice();
dup[i] = visualized as Element;
const children = await this.doGetChildren(element);
return Promise.all(children.map(async r => {
const vizOrTree = vm.getVisualizedExpression(r as IExpression);
if (typeof vizOrTree === 'string') {
const viz = await this.debugVisualizer.getVisualizedNodeFor(vizOrTree, r);
if (viz) {
vm.setVisualizedExpression(r, viz);
return viz as IExpression as Element;
}
} else if (vizOrTree) {
return vizOrTree as Element;
}

return dup || r;
});

return r;
}));
}

protected abstract doGetChildren(element: Input | Element): Promise<Element[]>;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/debug/browser/debugHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class DebugHoverWidget implements IContentWidget {
this.treeContainer.setAttribute('role', 'tree');
const tip = dom.append(this.complexValueContainer, $('.tip'));
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');
const dataSource = new DebugHoverDataSource(this.debugService);
const dataSource = this.instantiationService.createInstance(DebugHoverDataSource);
const linkeDetector = this.instantiationService.createInstance(LinkDetector);
this.tree = <WorkbenchAsyncDataTree<IExpression, IExpression, any>>this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [
this.instantiationService.createInstance(VariablesRenderer, linkeDetector),
Expand Down Expand Up @@ -414,7 +414,7 @@ class DebugHoverDataSource extends AbstractExpressionDataSource<IExpression, IEx
return element.hasChildren;
}

public override doGetChildren(element: IExpression): Promise<IExpression[]> {
protected override doGetChildren(element: IExpression): Promise<IExpression[]> {
return element.getChildren();
}
}
Expand Down
50 changes: 31 additions & 19 deletions src/vs/workbench/contrib/debug/browser/variablesView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class VariablesView extends ViewPane {
new ScopesRenderer(),
new ScopeErrorRenderer(),
],
new VariablesDataSource(this.debugService), {
this.instantiationService.createInstance(VariablesDataSource), {
accessibilityProvider: new VariablesAccessibilityProvider(),
identityProvider: { getId: (element: IExpression | IScope) => element.getId() },
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e.name },
Expand Down Expand Up @@ -171,7 +171,7 @@ export class VariablesView extends ViewPane {
let horizontalScrolling: boolean | undefined;
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
const variable = e?.expression;
if (variable instanceof Variable && !e?.settingWatch) {
if (variable && this.tree.hasNode(variable)) {
horizontalScrolling = this.tree.options.horizontalScrolling;
if (horizontalScrolling) {
this.tree.updateOptions({ horizontalScrolling: false });
Expand Down Expand Up @@ -210,12 +210,24 @@ export class VariablesView extends ViewPane {
}

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

private canSetExpressionValue(e: IExpression | IScope | null): e is IExpression {
const session = this.debugService.getViewModel().focusedSession;
if (!session) {
return false;
}

if (e instanceof VisualizedExpression) {
return !!e.treeItem.canEdit;
}

return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy;
}

private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
const variable = e.element;
if (!(variable instanceof Variable) || !variable.value) {
Expand Down Expand Up @@ -415,7 +427,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
*/
public static rendererOnVisualizationRange(model: IViewModel, tree: AsyncDataTree<any, any, any>): IDisposable {
return model.onDidChangeVisualization(({ original }) => {
if (!tree.hasElement(original)) {
if (!tree.hasNode(original)) {
return;
}

Expand Down Expand Up @@ -461,24 +473,21 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
}

protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
const variable = <Variable>expression;
const viz = <VisualizedExpression>expression;
return {
initialValue: expression.value,
ariaLabel: localize('variableValueAriaLabel', "Type new variable value"),
validationOptions: {
validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null
validation: () => viz.errorMessage ? ({ content: viz.errorMessage }) : null
},
onFinish: (value: string, success: boolean) => {
variable.errorMessage = undefined;
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
if (success && variable.value !== value && focusedStackFrame) {
variable.setVariable(value, focusedStackFrame)
// Need to force watch expressions and variables to update since a variable change can have an effect on both
.then(() => {
// Do not refresh scopes due to a node limitation #15520
forgetScopes = false;
this.debugService.getViewModel().updateViews();
});
viz.errorMessage = undefined;
if (success) {
viz.edit(value).then(() => {
// Do not refresh scopes due to a node limitation #15520
forgetScopes = false;
this.debugService.getViewModel().updateViews();
});
}
}
};
Expand All @@ -494,7 +503,10 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer {
createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline');

if (viz.original) {
primary.push(new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.close), undefined, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined)));
const action = new Action('debugViz', localize('removeVisualizer', 'Remove Visualizer'), ThemeIcon.asClassName(Codicon.eye), true, () => this.debugService.getViewModel().setVisualizedExpression(viz.original!, undefined));
action.checked = true;
primary.push(action);
actionBar.domNode.style.display = 'initial';
}
actionBar.clear();
actionBar.context = context;
Expand Down Expand Up @@ -601,7 +613,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer {
if (resolved.type === DebugVisualizationType.Command) {
viz.execute();
} else {
const replacement = await this.visualization.setVisualizedNodeFor(resolved.id, expression);
const replacement = await this.visualization.getVisualizedNodeFor(resolved.id, expression);
if (replacement) {
this.debugService.getViewModel().setVisualizedExpression(expression, replacement);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class WatchExpressionsView extends ViewPane {
this.instantiationService.createInstance(VariablesRenderer, linkDetector),
this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector),
],
new WatchExpressionsDataSource(this.debugService), {
this.instantiationService.createInstance(WatchExpressionsDataSource), {
accessibilityProvider: new WatchExpressionsAccessibilityProvider(),
identityProvider: { getId: (element: IExpression) => element.getId() },
keyboardNavigationLabelProvider: {
Expand Down Expand Up @@ -157,7 +157,7 @@ export class WatchExpressionsView extends ViewPane {
let horizontalScrolling: boolean | undefined;
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
const expression = e?.expression;
if (expression instanceof Expression || (expression instanceof Variable && e?.settingWatch)) {
if (expression && this.tree.hasElement(expression)) {
horizontalScrolling = this.tree.options.horizontalScrolling;
if (horizontalScrolling) {
this.tree.updateOptions({ horizontalScrolling: false });
Expand Down Expand Up @@ -204,7 +204,7 @@ export class WatchExpressionsView extends ViewPane {
const element = e.element;
// double click on primitive value: open input box to be able to select and copy value.
const selectedExpression = this.debugService.getViewModel().getSelectedExpression();
if (element instanceof Expression && element !== selectedExpression?.expression) {
if ((element instanceof Expression && element !== selectedExpression?.expression) || (element instanceof VisualizedExpression && element.treeItem.canEdit)) {
this.debugService.getViewModel().setSelectedExpression(element, false);
} else if (!element) {
// Double click in watch panel triggers to add a new watch expression
Expand Down Expand Up @@ -259,7 +259,7 @@ class WatchExpressionsDataSource extends AbstractExpressionDataSource<IDebugServ
return isDebugService(element) || element.hasChildren;
}

public override doGetChildren(element: IDebugService | IExpression): Promise<Array<IExpression>> {
protected override doGetChildren(element: IDebugService | IExpression): Promise<Array<IExpression>> {
if (isDebugService(element)) {
const debugService = element as IDebugService;
const watchExpressions = debugService.getModel().getWatchExpressions();
Expand Down
7 changes: 4 additions & 3 deletions src/vs/workbench/contrib/debug/common/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,9 @@ export interface IViewModel extends ITreeElement {
*/
readonly focusedStackFrame: IStackFrame | undefined;

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

export interface IDebugVisualizationContext {
variable: DebugProtocol.Variable;
containerId?: string;
containerId?: number;
frameId?: number;
threadId: number;
sessionId: string;
Expand Down
33 changes: 25 additions & 8 deletions src/vs/workbench/contrib/debug/common/debugModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,7 @@ function handleSetResponse(expression: ExpressionContainer, response: DebugProto
}

export class VisualizedExpression implements IExpression {
public readonly name: string;
public readonly hasChildren: boolean;
public readonly value: string;
public errorMessage?: string;
private readonly id = generateUuid();

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

get name() {
return this.treeItem.label;
}

get value() {
return this.treeItem.description || '';
}

get hasChildren() {
return this.treeItem.collapsibleState !== DebugTreeItemCollapsibleState.None;
}

constructor(
private readonly visualizer: IDebugVisualizerService,
private readonly treeId: string,
public readonly treeId: string,
public readonly treeItem: IDebugVisualizationTreeItem,
public readonly original?: Variable,
) {
this.name = treeItem.label;
this.hasChildren = treeItem.collapsibleState !== DebugTreeItemCollapsibleState.None;
this.value = treeItem.description || '';
) { }

/** Edits the value, sets the {@link errorMessage} and returns false if unsuccessful */
public async edit(newValue: string) {
try {
await this.visualizer.editTreeItem(this.treeId, this.treeItem, newValue);
return true;
} catch (e) {
this.errorMessage = e.message;
return false;
}
}
}

Expand Down
19 changes: 15 additions & 4 deletions src/vs/workbench/contrib/debug/common/debugViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class ViewModel implements IViewModel {
private readonly _onWillUpdateViews = new Emitter<void>();
private readonly _onDidChangeVisualization = new Emitter<{ original: IExpression; replacement: IExpression }>();
private readonly visualized = new WeakMap<IExpression, IExpression>();
private readonly preferredVisualizers = new Map</** cache key */ string, /* tree ID */ string>();
private expressionSelectedContextKey!: IContextKey<boolean>;
private loadedScriptsSupportedContextKey!: IContextKey<boolean>;
private stepBackSupportedContextKey!: IContextKey<boolean>;
Expand Down Expand Up @@ -165,23 +166,33 @@ export class ViewModel implements IViewModel {
this.multiSessionDebug.set(isMultiSessionView);
}

setVisualizedExpression(original: IExpression, visualized: IExpression | undefined): void {
setVisualizedExpression(original: IExpression, visualized: IExpression & { treeId: string } | undefined): void {
const current = this.visualized.get(original) || original;

const key = this.getPreferredVisualizedKey(original);
if (visualized) {
this.visualized.set(original, visualized);
this.preferredVisualizers.set(key, visualized.treeId);
} else {
this.visualized.delete(original);
this.preferredVisualizers.delete(key);
}
this._onDidChangeVisualization.fire({ original: current, replacement: visualized || original });
}

getVisualizedExpression(expression: IExpression): IExpression | undefined {
return this.visualized.get(expression);
getVisualizedExpression(expression: IExpression): IExpression | string | undefined {
return this.visualized.get(expression) || this.preferredVisualizers.get(this.getPreferredVisualizedKey(expression));
}

async evaluateLazyExpression(expression: IExpressionContainer): Promise<void> {
await expression.evaluateLazy();
this._onDidEvaluateLazyExpression.fire(expression);
}

private getPreferredVisualizedKey(expr: IExpression) {
return JSON.stringify([
expr.name,
expr.type,
!!expr.memoryReference,
].join('\0'));
}
}