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

380 lines
14 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { RunOnceScheduler } from 'vs/base/common/async';
import * as dom from 'vs/base/browser/dom';
import { CollapseAction } from 'vs/workbench/browser/viewlet';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame } from 'vs/workbench/contrib/debug/common/debug';
import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView';
import { IAction, Action, Separator } from 'vs/base/common/actions';
import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter } from 'vs/base/common/event';
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { dispose } from 'vs/base/common/lifecycle';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { withUndefinedAsNull } from 'vs/base/common/types';
const $ = dom.$;
let forgetScopes = true;
export const variableSetEmitter = new Emitter<void>();
export class VariablesView extends ViewPane {
private onFocusStackFrameScheduler: RunOnceScheduler;
private needsRefresh = false;
private tree!: WorkbenchAsyncDataTree<IStackFrame | null, IExpression | IScope, FuzzyScore>;
private savedViewState = new Map<string, IAsyncDataTreeViewState>();
private autoExpandedScopes = new Set<string>();
constructor(
options: IViewletViewOptions,
@IContextMenuService contextMenuService: IContextMenuService,
@IDebugService private readonly debugService: IDebugService,
@IKeybindingService keybindingService: IKeybindingService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IClipboardService private readonly clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
// Use scheduler to prevent unnecessary flashing
this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => {
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
this.needsRefresh = false;
const input = this.tree.getInput();
if (input) {
this.savedViewState.set(input.getId(), this.tree.getViewState());
}
if (!stackFrame) {
await this.tree.setInput(null);
return;
}
const viewState = this.savedViewState.get(stackFrame.getId());
await this.tree.setInput(stackFrame, viewState);
// Automatically expand the first scope if it is not expensive and if all scopes are collapsed
const scopes = await stackFrame.getScopes();
const toExpand = scopes.find(s => !s.expensive);
if (toExpand && (scopes.every(s => this.tree.isCollapsed(s)) || !this.autoExpandedScopes.has(toExpand.getId()))) {
this.autoExpandedScopes.add(toExpand.getId());
await this.tree.expand(toExpand);
}
}, 400);
}
renderBody(container: HTMLElement): void {
super.renderBody(container);
dom.addClass(this.element, 'debug-pane');
dom.addClass(container, 'debug-variables');
const treeContainer = renderViewTree(container);
this.tree = <WorkbenchAsyncDataTree<IStackFrame | null, IExpression | IScope, FuzzyScore>>this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'VariablesView', treeContainer, new VariablesDelegate(),
[this.instantiationService.createInstance(VariablesRenderer), new ScopesRenderer(), new ScopeErrorRenderer()],
new VariablesDataSource(), {
accessibilityProvider: new VariablesAccessibilityProvider(),
identityProvider: { getId: (element: IExpression | IScope) => element.getId() },
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression | IScope) => e },
overrideStyles: {
listBackground: this.getBackgroundColor()
}
});
this.tree.setInput(withUndefinedAsNull(this.debugService.getViewModel().focusedStackFrame));
CONTEXT_VARIABLES_FOCUSED.bindTo(this.tree.contextKeyService);
this._register(this.debugService.getViewModel().onDidFocusStackFrame(sf => {
if (!this.isBodyVisible()) {
this.needsRefresh = true;
return;
}
// Refresh the tree immediately if the user explictly changed stack frames.
// Otherwise postpone the refresh until user stops stepping.
const timeout = sf.explicit ? 0 : undefined;
this.onFocusStackFrameScheduler.schedule(timeout);
}));
this._register(variableSetEmitter.event(() => {
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
if (stackFrame && forgetScopes) {
stackFrame.forgetScopes();
}
forgetScopes = true;
this.tree.updateChildren();
}));
this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
this._register(this.tree.onContextMenu(async e => await this.onContextMenu(e)));
this._register(this.onDidChangeBodyVisibility(visible => {
if (visible && this.needsRefresh) {
this.onFocusStackFrameScheduler.schedule();
}
}));
let horizontalScrolling: boolean | undefined;
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
if (e instanceof Variable) {
horizontalScrolling = this.tree.options.horizontalScrolling;
if (horizontalScrolling) {
this.tree.updateOptions({ horizontalScrolling: false });
}
this.tree.rerender(e);
} else if (!e && horizontalScrolling !== undefined) {
this.tree.updateOptions({ horizontalScrolling: horizontalScrolling });
horizontalScrolling = undefined;
}
}));
this._register(this.debugService.onDidEndSession(() => {
this.savedViewState.clear();
this.autoExpandedScopes.clear();
}));
}
getActions(): IAction[] {
return [new CollapseAction(() => this.tree, true, 'explorer-action codicon-collapse-all')];
}
layoutBody(width: number, height: number): void {
super.layoutBody(height, width);
this.tree.layout(width, height);
}
focus(): void {
this.tree.domFocus();
}
private onMouseDblClick(e: ITreeMouseEvent<IExpression | IScope>): void {
const session = this.debugService.getViewModel().focusedSession;
if (session && e.element instanceof Variable && session.capabilities.supportsSetVariable) {
this.debugService.getViewModel().setSelectedExpression(e.element);
}
}
private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
const variable = e.element;
if (variable instanceof Variable && !!variable.value) {
const actions: IAction[] = [];
const session = this.debugService.getViewModel().focusedSession;
if (session && session.capabilities.supportsSetVariable) {
actions.push(new Action('workbench.setValue', nls.localize('setValue', "Set Value"), undefined, true, () => {
this.debugService.getViewModel().setSelectedExpression(variable);
return Promise.resolve();
}));
}
actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'variables'));
if (variable.evaluateName) {
actions.push(new Action('debug.copyEvaluatePath', nls.localize('copyAsExpression', "Copy as Expression"), undefined, true, () => {
return this.clipboardService.writeText(variable.evaluateName!);
}));
actions.push(new Separator());
actions.push(new Action('debug.addToWatchExpressions', nls.localize('addToWatchExpressions', "Add to Watch"), undefined, true, () => {
this.debugService.addWatchExpression(variable.evaluateName);
return Promise.resolve(undefined);
}));
}
if (session && session.capabilities.supportsDataBreakpoints) {
const response = await session.dataBreakpointInfo(variable.name, variable.parent.reference);
const dataid = response?.dataId;
if (response && dataid) {
actions.push(new Separator());
actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => {
return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes);
}));
}
}
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions,
getActionsContext: () => variable,
onHide: () => dispose(actions)
});
}
}
}
function isStackFrame(obj: any): obj is IStackFrame {
return obj instanceof StackFrame;
}
export class VariablesDataSource implements IAsyncDataSource<IStackFrame | null, IExpression | IScope> {
hasChildren(element: IStackFrame | null | IExpression | IScope): boolean {
if (!element) {
return false;
}
if (isStackFrame(element)) {
return true;
}
return element.hasChildren;
}
getChildren(element: IStackFrame | IExpression | IScope): Promise<(IExpression | IScope)[]> {
if (isStackFrame(element)) {
return element.getScopes();
}
return element.getChildren();
}
}
interface IScopeTemplateData {
name: HTMLElement;
label: HighlightedLabel;
}
class VariablesDelegate implements IListVirtualDelegate<IExpression | IScope> {
getHeight(element: IExpression | IScope): number {
return 22;
}
getTemplateId(element: IExpression | IScope): string {
if (element instanceof ErrorScope) {
return ScopeErrorRenderer.ID;
}
if (element instanceof Scope) {
return ScopesRenderer.ID;
}
return VariablesRenderer.ID;
}
}
class ScopesRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeTemplateData> {
static readonly ID = 'scope';
get templateId(): string {
return ScopesRenderer.ID;
}
renderTemplate(container: HTMLElement): IScopeTemplateData {
const name = dom.append(container, $('.scope'));
const label = new HighlightedLabel(name, false);
return { name, label };
}
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeTemplateData): void {
templateData.label.set(element.element.name, createMatches(element.filterData));
}
disposeTemplate(templateData: IScopeTemplateData): void {
// noop
}
}
interface IScopeErrorTemplateData {
error: HTMLElement;
}
class ScopeErrorRenderer implements ITreeRenderer<IScope, FuzzyScore, IScopeErrorTemplateData> {
static readonly ID = 'scopeError';
get templateId(): string {
return ScopeErrorRenderer.ID;
}
renderTemplate(container: HTMLElement): IScopeErrorTemplateData {
const wrapper = dom.append(container, $('.scope'));
const error = dom.append(wrapper, $('.error'));
return { error };
}
renderElement(element: ITreeNode<IScope, FuzzyScore>, index: number, templateData: IScopeErrorTemplateData): void {
templateData.error.innerText = element.element.name;
}
disposeTemplate(): void {
// noop
}
}
export class VariablesRenderer extends AbstractExpressionsRenderer {
static readonly ID = 'variable';
get templateId(): string {
return VariablesRenderer.ID;
}
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
renderVariable(expression as Variable, data, true, highlights);
}
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions {
const variable = <Variable>expression;
return {
initialValue: expression.value,
ariaLabel: nls.localize('variableValueAriaLabel', "Type new variable value"),
validationOptions: {
validation: () => variable.errorMessage ? ({ content: variable.errorMessage }) : null
},
onFinish: (value: string, success: boolean) => {
variable.errorMessage = undefined;
if (success && variable.value !== value) {
variable.setVariable(value)
// 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;
variableSetEmitter.fire();
});
}
}
};
}
}
class VariablesAccessibilityProvider implements IListAccessibilityProvider<IExpression | IScope> {
getWidgetAriaLabel(): string {
return nls.localize('variablesAriaTreeLabel', "Debug Variables");
}
getAriaLabel(element: IExpression | IScope): string | null {
if (element instanceof Scope) {
return nls.localize('variableScopeAriaLabel', "Scope {0}", element.name);
}
if (element instanceof Variable) {
return nls.localize({ key: 'variableAriaLabel', comment: ['Placeholders are variable name and variable value respectivly. They should not be translated.'] }, "{0}, value {1}", element.name, element.value);
}
return null;
}
}