/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. 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 { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; 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 { onUnexpectedError } from 'vs/base/common/errors'; 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'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export class WatchExpressionsView extends ViewletPanel { private onWatchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh: boolean; private tree: WorkbenchAsyncDataTree; constructor( options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService); 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(WorkbenchAsyncDataTree, 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), }); this.tree.setInput(this.debugService).then(undefined, onUnexpectedError); CONTEXT_WATCH_EXPRESSIONS_FOCUSED.bindTo(this.tree.contextKeyService); const addWatchExpressionAction = new AddWatchExpressionAction(AddWatchExpressionAction.ID, AddWatchExpressionAction.LABEL, this.debugService, this.keybindingService); const collapseAction = new CollapseAction(this.tree, true, 'explorer-action collapse-explorer'); 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(we => { if (!this.isBodyVisible()) { this.needsRefresh = true; } else { this.tree.updateChildren(); } })); 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): 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): void { const element = e.element; const anchor = e.anchor; if (!anchor) { return; } const actions: IAction[] = []; if (element instanceof Expression) { const 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', this.debugService)); } 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', this.debugService)); } 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 }); } } class WatchExpressionsDelegate implements IListVirtualDelegate { 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 { hasChildren(element: IExpression | IDebugService): boolean { return isDebugService(element) || element.hasChildren; } getChildren(element: IDebugService | IExpression): Promise> { 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 { getAriaLabel(element: IExpression): string { if (element instanceof Expression) { return nls.localize('watchExpressionAriaLabel', "{0} value {1}, watch, debug", (element).name, (element).value); } // Variable return nls.localize('watchVariableAriaLabel', "{0} value {1}, watch, debug", (element).name, (element).value); } } class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { constructor(private debugService: IDebugService) { } onDragOver(data: IDragAndDropData): boolean | ITreeDragOverReaction { if (!(data instanceof ElementsDragAndDropData)) { return false; } const expressions = (data as ElementsDragAndDropData).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).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); } }