Files
azuredatastudio/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts
Anthony Dresser f5ce7fb2a5 Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c (#8525)
* Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c

* remove files we don't want

* fix hygiene

* update distro

* update distro

* fix hygiene

* fix strict nulls

* distro

* distro

* fix tests

* fix tests

* add another edit

* fix viewlet icon

* fix azure dialog

* fix some padding

* fix more padding issues
2019-12-04 19:28:22 -08:00

322 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, CONTEXT_WATCH_EXPRESSIONS_FOCUSED } from 'vs/workbench/contrib/debug/common/debug';
import { Expression, Variable } from 'vs/workbench/contrib/debug/common/debugModel';
import { AddWatchExpressionAction, RemoveAllWatchExpressionsAction, CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IAction, Action } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IViewletPaneOptions, ViewletPane } from 'vs/workbench/browser/parts/views/paneViewlet';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IAsyncDataSource, ITreeMouseEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { FuzzyScore } from 'vs/base/common/filters';
import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { variableSetEmitter, VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { dispose } from 'vs/base/common/lifecycle';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
export class WatchExpressionsView extends ViewletPane {
private onWatchExpressionsUpdatedScheduler: RunOnceScheduler;
private needsRefresh = false;
private tree!: WorkbenchAsyncDataTree<IDebugService | IExpression, IExpression, FuzzyScore>;
constructor(
options: IViewletViewOptions,
@IContextMenuService contextMenuService: IContextMenuService,
@IDebugService private readonly debugService: IDebugService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super({ ...(options as IViewletPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService);
this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => {
this.needsRefresh = false;
this.tree.updateChildren();
}, 50);
}
renderBody(container: HTMLElement): void {
dom.addClass(container, 'debug-watch');
const treeContainer = renderViewTree(container);
const expressionsRenderer = this.instantiationService.createInstance(WatchExpressionsRenderer);
this.tree = this.instantiationService.createInstance<typeof WorkbenchAsyncDataTree, WorkbenchAsyncDataTree<IDebugService | IExpression, IExpression, FuzzyScore>>(WorkbenchAsyncDataTree, 'WatchExpressions', treeContainer, new WatchExpressionsDelegate(), [expressionsRenderer, this.instantiationService.createInstance(VariablesRenderer)],
new WatchExpressionsDataSource(), {
ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"),
accessibilityProvider: new WatchExpressionsAccessibilityProvider(),
identityProvider: { getId: (element: IExpression) => element.getId() },
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e },
dnd: new WatchExpressionsDragAndDrop(this.debugService),
overrideStyles: {
listBackground: SIDE_BAR_BACKGROUND
}
});
this.tree.setInput(this.debugService);
CONTEXT_WATCH_EXPRESSIONS_FOCUSED.bindTo(this.tree.contextKeyService);
if (this.toolbar) {
const addWatchExpressionAction = new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService);
const collapseAction = new CollapseAction(this.tree, true, 'explorer-action codicon-collapse-all');
const removeAllWatchExpressionsAction = new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService);
this.toolbar.setActions([addWatchExpressionAction, collapseAction, removeAllWatchExpressionsAction])();
}
this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e)));
this._register(this.debugService.getModel().onDidChangeWatchExpressions(async we => {
if (!this.isBodyVisible()) {
this.needsRefresh = true;
} else {
await this.tree.updateChildren();
if (we instanceof Expression) {
this.tree.reveal(we);
}
}
}));
this._register(this.debugService.getViewModel().onDidFocusStackFrame(() => {
if (!this.isBodyVisible()) {
this.needsRefresh = true;
return;
}
if (!this.onWatchExpressionsUpdatedScheduler.isScheduled()) {
this.onWatchExpressionsUpdatedScheduler.schedule();
}
}));
this._register(variableSetEmitter.event(() => this.tree.updateChildren()));
this._register(this.onDidChangeBodyVisibility(visible => {
if (visible && this.needsRefresh) {
this.onWatchExpressionsUpdatedScheduler.schedule();
}
}));
this._register(this.debugService.getViewModel().onDidSelectExpression(e => {
if (e instanceof Expression && e.name) {
this.tree.rerender(e);
}
}));
}
layoutBody(height: number, width: number): void {
this.tree.layout(height, width);
}
focus(): void {
this.tree.domFocus();
}
private onMouseDblClick(e: ITreeMouseEvent<IExpression>): void {
if ((e.browserEvent.target as HTMLElement).className.indexOf('twistie') >= 0) {
// Ignore double click events on twistie
return;
}
const element = e.element;
// double click on primitive value: open input box to be able to select and copy value.
if (element instanceof Expression && element !== this.debugService.getViewModel().getSelectedExpression()) {
this.debugService.getViewModel().setSelectedExpression(element);
} else if (!element) {
// Double click in watch panel triggers to add a new watch expression
this.debugService.addWatchExpression();
}
}
private onContextMenu(e: ITreeContextMenuEvent<IExpression>): void {
const element = e.element;
const anchor = e.anchor;
if (!anchor) {
return;
}
const actions: IAction[] = [];
if (element instanceof Expression) {
const expression = <Expression>element;
actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService));
actions.push(new Action('debug.editWatchExpression', nls.localize('editWatchExpression', "Edit Expression"), undefined, true, () => {
this.debugService.getViewModel().setSelectedExpression(expression);
return Promise.resolve();
}));
if (!expression.hasChildren) {
actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, expression.value, 'watch'));
}
actions.push(new Separator());
actions.push(new Action('debug.removeWatchExpression', nls.localize('removeWatchExpression', "Remove Expression"), undefined, true, () => {
this.debugService.removeWatchExpressions(expression.getId());
return Promise.resolve();
}));
actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService));
} else {
actions.push(new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService));
if (element instanceof Variable) {
const variable = element as Variable;
if (!variable.hasChildren) {
actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'watch'));
}
actions.push(new Separator());
}
actions.push(new RemoveAllWatchExpressionsAction(RemoveAllWatchExpressionsAction.ID, RemoveAllWatchExpressionsAction.LABEL, this.debugService, this.keybindingService));
}
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => actions,
getActionsContext: () => element,
onHide: () => dispose(actions)
});
}
}
class WatchExpressionsDelegate implements IListVirtualDelegate<IExpression> {
getHeight(element: IExpression): number {
return 22;
}
getTemplateId(element: IExpression): string {
if (element instanceof Expression) {
return WatchExpressionsRenderer.ID;
}
// Variable
return VariablesRenderer.ID;
}
}
function isDebugService(element: any): element is IDebugService {
return typeof element.getConfigurationManager === 'function';
}
class WatchExpressionsDataSource implements IAsyncDataSource<IDebugService, IExpression> {
hasChildren(element: IExpression | IDebugService): boolean {
return isDebugService(element) || element.hasChildren;
}
getChildren(element: IDebugService | IExpression): Promise<Array<IExpression>> {
if (isDebugService(element)) {
const debugService = element as IDebugService;
const watchExpressions = debugService.getModel().getWatchExpressions();
const viewModel = debugService.getViewModel();
return Promise.all(watchExpressions.map(we => !!we.name
? we.evaluate(viewModel.focusedSession!, viewModel.focusedStackFrame!, 'watch').then(() => we)
: Promise.resolve(we)));
}
return element.getChildren();
}
}
export class WatchExpressionsRenderer extends AbstractExpressionsRenderer {
static readonly ID = 'watchexpression';
get templateId() {
return WatchExpressionsRenderer.ID;
}
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
const text = typeof expression.value === 'string' ? `${expression.name}:` : expression.name;
data.label.set(text, highlights, expression.type ? expression.type : expression.value);
renderExpressionValue(expression, data.value, {
showChanged: true,
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
preserveWhitespace: false,
showHover: true,
colorize: true
});
}
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions {
return {
initialValue: expression.name ? expression.name : '',
ariaLabel: nls.localize('watchExpressionInputAriaLabel', "Type watch expression"),
placeholder: nls.localize('watchExpressionPlaceholder', "Expression to watch"),
onFinish: (value: string, success: boolean) => {
if (success && value) {
this.debugService.renameWatchExpression(expression.getId(), value);
variableSetEmitter.fire();
} else if (!expression.name) {
this.debugService.removeWatchExpressions(expression.getId());
}
}
};
}
}
class WatchExpressionsAccessibilityProvider implements IAccessibilityProvider<IExpression> {
getAriaLabel(element: IExpression): string {
if (element instanceof Expression) {
return nls.localize('watchExpressionAriaLabel', "{0} value {1}, watch, debug", (<Expression>element).name, (<Expression>element).value);
}
// Variable
return nls.localize('watchVariableAriaLabel', "{0} value {1}, watch, debug", (<Variable>element).name, (<Variable>element).value);
}
}
class WatchExpressionsDragAndDrop implements ITreeDragAndDrop<IExpression> {
constructor(private debugService: IDebugService) { }
onDragOver(data: IDragAndDropData): boolean | ITreeDragOverReaction {
if (!(data instanceof ElementsDragAndDropData)) {
return false;
}
const expressions = (data as ElementsDragAndDropData<IExpression>).elements;
return expressions.length > 0 && expressions[0] instanceof Expression;
}
getDragURI(element: IExpression): string | null {
if (!(element instanceof Expression) || element === this.debugService.getViewModel().getSelectedExpression()) {
return null;
}
return element.getId();
}
getDragLabel(elements: IExpression[]): string | undefined {
if (elements.length === 1) {
return elements[0].name;
}
return undefined;
}
drop(data: IDragAndDropData, targetElement: IExpression): void {
if (!(data instanceof ElementsDragAndDropData)) {
return;
}
const draggedElement = (data as ElementsDragAndDropData<IExpression>).elements[0];
const watches = this.debugService.getModel().getWatchExpressions();
const position = targetElement instanceof Expression ? watches.indexOf(targetElement) : watches.length - 1;
this.debugService.moveWatchExpression(draggedElement.getId(), position);
}
}