From b421edde9cebed63fca17f5d9336f538bbec1bf3 Mon Sep 17 00:00:00 2001 From: isidor Date: Tue, 5 Apr 2016 16:20:41 +0200 Subject: [PATCH] debug: mechanism to register split views --- .../parts/debug/browser/debugViewlet.ts | 438 +---------------- .../parts/debug/browser/debugViews.ts | 443 ++++++++++++++++++ src/vs/workbench/parts/debug/common/debug.ts | 37 ++ .../electron-browser/debug.contribution.ts | 7 + .../debug/test/common/mockDebugService.ts | 2 +- 5 files changed, 493 insertions(+), 434 deletions(-) create mode 100644 src/vs/workbench/parts/debug/browser/debugViews.ts diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index 9560c4591da..369597ac20e 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -5,451 +5,26 @@ import 'vs/css!./media/debugViewlet'; import nls = require('vs/nls'); -import dom = require('vs/base/browser/dom'); import builder = require('vs/base/browser/builder'); import { TPromise } from 'vs/base/common/winjs.base'; -import errors = require('vs/base/common/errors'); import lifecycle = require('vs/base/common/lifecycle'); -import events = require('vs/base/common/events'); import actions = require('vs/base/common/actions'); import actionbar = require('vs/base/browser/ui/actionbar/actionbar'); -import actionbarregistry = require('vs/workbench/browser/actionBarRegistry'); -import tree = require('vs/base/parts/tree/browser/tree'); -import treeimpl = require('vs/base/parts/tree/browser/treeImpl'); import splitview = require('vs/base/browser/ui/splitview/splitview'); import memento = require('vs/workbench/common/memento'); import viewlet = require('vs/workbench/browser/viewlet'); import debug = require('vs/workbench/parts/debug/common/debug'); -import model = require('vs/workbench/parts/debug/common/debugModel'); -import viewer = require('vs/workbench/parts/debug/browser/debugViewer'); import debugactions = require('vs/workbench/parts/debug/electron-browser/debugActions'); import dbgactionitems = require('vs/workbench/parts/debug/browser/debugActionItems'); -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IMessageService } from 'vs/platform/message/common/message'; import { IStorageService } from 'vs/platform/storage/common/storage'; import IDebugService = debug.IDebugService; - -function renderViewTree(container: HTMLElement): HTMLElement { - const treeContainer = document.createElement('div'); - dom.addClass(treeContainer, 'debug-view-content'); - container.appendChild(treeContainer); - return treeContainer; -} - -const debugTreeOptions = (ariaLabel: string) => { - return { - indentPixels: 8, - twistiePixels: 20, - ariaLabel - }; -}; - const $ = builder.$; -class VariablesView extends viewlet.CollapsibleViewletView { - - private static MEMENTO = 'variablesview.memento'; - - constructor(actionRunner: actions.IActionRunner, private settings: any, - @IMessageService messageService: IMessageService, - @IContextMenuService contextMenuService: IContextMenuService, - @ITelemetryService private telemetryService: ITelemetryService, - @IDebugService private debugService: IDebugService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super(actionRunner, !!settings[VariablesView.MEMENTO], nls.localize('variablesSection', "Variables Section"), messageService, contextMenuService); - } - - public renderHeader(container: HTMLElement): void { - const titleDiv = $('div.title').appendTo(container); - $('span').text(nls.localize('variables', "Variables")).appendTo(titleDiv); - - super.renderHeader(container); - } - - public renderBody(container: HTMLElement): void { - dom.addClass(container, 'debug-variables'); - this.treeContainer = renderViewTree(container); - - this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: new viewer.VariablesDataSource(this.debugService), - renderer: this.instantiationService.createInstance(viewer.VariablesRenderer), - accessibilityProvider: new viewer.VariablesAccessibilityProvider(), - controller: new viewer.BaseDebugController(this.debugService, this.contextMenuService, new viewer.VariablesActionProvider(this.instantiationService)) - }, debugTreeOptions(nls.localize('variablesAriaTreeLabel', "Debug Variables"))); - - const viewModel = this.debugService.getViewModel(); - - this.tree.setInput(viewModel); - - const collapseAction = this.instantiationService.createInstance(viewlet.CollapseAction, this.tree, false, 'explorer-action collapse-explorer'); - this.toolBar.setActions(actionbarregistry.prepareActions([collapseAction]))(); - - this.toDispose.push(viewModel.addListener2(debug.ViewModelEvents.FOCUSED_STACK_FRAME_UPDATED, () => this.onFocusedStackFrameUpdated())); - this.toDispose.push(this.debugService.addListener2(debug.ServiceEvents.STATE_CHANGED, () => { - collapseAction.enabled = this.debugService.getState() === debug.State.Running || this.debugService.getState() === debug.State.Stopped; - })); - - this.toDispose.push(this.tree.addListener2(events.EventType.FOCUS, (e: tree.IFocusEvent) => { - const isMouseClick = (e.payload && e.payload.origin === 'mouse'); - const isVariableType = (e.focus instanceof model.Variable); - - if(isMouseClick && isVariableType) { - this.telemetryService.publicLog('debug/variables/selected'); - } - })); - } - - private onFocusedStackFrameUpdated(): void { - this.tree.refresh().then(() => { - const stackFrame = this.debugService.getViewModel().getFocusedStackFrame(); - if (stackFrame) { - return stackFrame.getScopes(this.debugService).then(scopes => { - if (scopes.length > 0) { - return this.tree.expand(scopes[0]); - } - }); - } - }).done(null, errors.onUnexpectedError); - } - - public shutdown(): void { - this.settings[VariablesView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); - super.shutdown(); - } -} - -class WatchExpressionsView extends viewlet.CollapsibleViewletView { - - private static MEMENTO = 'watchexpressionsview.memento'; - - constructor(actionRunner: actions.IActionRunner, private settings: any, - @IMessageService messageService: IMessageService, - @IContextMenuService contextMenuService: IContextMenuService, - @IDebugService private debugService: IDebugService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super(actionRunner, !!settings[WatchExpressionsView.MEMENTO], nls.localize('expressionsSection', "Expressions Section"), messageService, contextMenuService); - this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.WATCH_EXPRESSIONS_UPDATED, (we) => { - // only expand when a new watch expression is added. - if (we instanceof model.Expression) { - this.expand(); - } - })); - } - - public renderHeader(container: HTMLElement): void { - const titleDiv = $('div.title').appendTo(container); - $('span').text(nls.localize('watch', "Watch")).appendTo(titleDiv); - - super.renderHeader(container); - } - - public renderBody(container: HTMLElement): void { - dom.addClass(container, 'debug-watch'); - this.treeContainer = renderViewTree(container); - - const actionProvider = new viewer.WatchExpressionsActionProvider(this.instantiationService); - this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: new viewer.WatchExpressionsDataSource(this.debugService), - renderer: this.instantiationService.createInstance(viewer.WatchExpressionsRenderer, actionProvider, this.actionRunner), - accessibilityProvider: new viewer.WatchExpressionsAccessibilityProvider(), - controller: new viewer.WatchExpressionsController(this.debugService, this.contextMenuService, actionProvider) - }, debugTreeOptions(nls.localize('watchAriaTreeLabel', "Debug Watch Expressions"))); - - this.tree.setInput(this.debugService.getModel()); - - const addWatchExpressionAction = this.instantiationService.createInstance(debugactions.AddWatchExpressionAction, debugactions.AddWatchExpressionAction.ID, debugactions.AddWatchExpressionAction.LABEL); - const collapseAction = this.instantiationService.createInstance(viewlet.CollapseAction, this.tree, false, 'explorer-action collapse-explorer'); - const removeAllWatchExpressionsAction = this.instantiationService.createInstance(debugactions.RemoveAllWatchExpressionsAction, debugactions.RemoveAllWatchExpressionsAction.ID, debugactions.RemoveAllWatchExpressionsAction.LABEL); - this.toolBar.setActions(actionbarregistry.prepareActions([addWatchExpressionAction, collapseAction, removeAllWatchExpressionsAction]))(); - - this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.WATCH_EXPRESSIONS_UPDATED, (we: model.Expression) => this.onWatchExpressionsUpdated(we))); - this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.SELECTED_EXPRESSION_UPDATED, (expression: debug.IExpression) => { - if (!expression || !(expression instanceof model.Expression)) { - return; - } - - this.tree.refresh(expression, false).then(() => { - this.tree.setHighlight(expression); - this.tree.addOneTimeListener(events.EventType.HIGHLIGHT, (e: tree.IHighlightEvent) => { - if (!e.highlight) { - this.debugService.getViewModel().setSelectedExpression(null); - } - }); - }).done(null, errors.onUnexpectedError); - })); - } - - private onWatchExpressionsUpdated(we: model.Expression): void { - this.tree.refresh().done(() => { - return we instanceof model.Expression ? this.tree.reveal(we): TPromise.as(true); - }, errors.onUnexpectedError); - } - - public shutdown(): void { - this.settings[WatchExpressionsView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); - super.shutdown(); - } -} - -class CallStackView extends viewlet.CollapsibleViewletView { - - private static MEMENTO = 'callstackview.memento'; - private pauseMessage: builder.Builder; - private pauseMessageLabel: builder.Builder; - - constructor(actionRunner: actions.IActionRunner, private settings: any, - @IMessageService messageService: IMessageService, - @IContextMenuService contextMenuService: IContextMenuService, - @ITelemetryService private telemetryService: ITelemetryService, - @IDebugService private debugService: IDebugService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super(actionRunner, !!settings[CallStackView.MEMENTO], nls.localize('callstackSection', "Call Stack Section"), messageService, contextMenuService); - } - - public renderHeader(container: HTMLElement): void { - const title = $('div.debug-call-stack-title').appendTo(container); - $('span.title').text(nls.localize('callStack', "Call Stack")).appendTo(title); - this.pauseMessage = $('span.pause-message').appendTo(title); - this.pauseMessage.hide(); - this.pauseMessageLabel = $('span.label').appendTo(this.pauseMessage); - - super.renderHeader(container); - } - - public renderBody(container: HTMLElement): void { - dom.addClass(container, 'debug-call-stack'); - this.treeContainer = renderViewTree(container); - - this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: this.instantiationService.createInstance(viewer.CallStackDataSource), - renderer: this.instantiationService.createInstance(viewer.CallStackRenderer), - accessibilityProvider: this.instantiationService.createInstance(viewer.CallstackAccessibilityProvider) - }, debugTreeOptions(nls.localize('callStackAriaLabel', "Debug Call Stack"))); - - const debugModel = this.debugService.getModel(); - - this.tree.setInput(debugModel); - - this.toDispose.push(this.tree.addListener2('selection', (e: tree.ISelectionEvent) => { - if (!e.selection.length) { - return; - } - const element = e.selection[0]; - - if (element instanceof model.StackFrame) { - const stackFrame = element; - this.debugService.setFocusedStackFrameAndEvaluate(stackFrame); - - const isMouse = (e.payload.origin === 'mouse'); - let preserveFocus = isMouse; - - const originalEvent:KeyboardEvent|MouseEvent = e && e.payload && e.payload.originalEvent; - if (originalEvent && isMouse && originalEvent.detail === 2) { - preserveFocus = false; - originalEvent.preventDefault(); // focus moves to editor, we need to prevent default - } - - const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)); - this.debugService.openOrRevealEditor(stackFrame.source, stackFrame.lineNumber, preserveFocus, sideBySide).done(null, errors.onUnexpectedError); - } - - // user clicked on 'Load More Stack Frames', get those stack frames and refresh the tree. - if (typeof element === 'number') { - const thread = this.debugService.getModel().getThreads()[element]; - if (thread) { - thread.getCallStack(this.debugService, true) - .then(() => this.tree.refresh()) - .then(() => { - this.tree.clearFocus(); - this.tree.clearSelection(); - }).done(null, errors.onUnexpectedError); - } - } - })); - - this.toDispose.push(this.tree.addListener2(events.EventType.FOCUS, (e: tree.IFocusEvent) => { - const isMouseClick = (e.payload && e.payload.origin === 'mouse'); - const isStackFrameType = (e.focus instanceof model.StackFrame); - - if (isMouseClick && isStackFrameType) { - this.telemetryService.publicLog('debug/callStack/selected'); - } - })); - - this.toDispose.push(debugModel.addListener2(debug.ModelEvents.CALLSTACK_UPDATED, () => { - this.tree.refresh().done(null, errors.onUnexpectedError); - })); - - this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.FOCUSED_STACK_FRAME_UPDATED, () => { - const focussedThread = this.debugService.getModel().getThreads()[this.debugService.getViewModel().getFocusedThreadId()]; - if (!focussedThread) { - this.pauseMessage.hide(); - return; - } - - this.tree.expand(focussedThread); - this.tree.setFocus(this.debugService.getViewModel().getFocusedStackFrame()); - if (focussedThread.stoppedDetails && focussedThread.stoppedDetails.reason) { - this.pauseMessageLabel.text(nls.localize('debugStopped', "Paused on {0}", focussedThread.stoppedDetails.reason)); - if (focussedThread.stoppedDetails.text) { - this.pauseMessageLabel.title(focussedThread.stoppedDetails.text); - } - focussedThread.stoppedDetails.reason === 'exception' ? this.pauseMessageLabel.addClass('exception') : this.pauseMessageLabel.removeClass('exception'); - this.pauseMessage.show(); - } else { - this.pauseMessage.hide(); - } - })); - } - - public shutdown(): void { - this.settings[CallStackView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); - super.shutdown(); - } -} - -class BreakpointsView extends viewlet.AdaptiveCollapsibleViewletView { - - private static MAX_VISIBLE_FILES = 9; - private static MEMENTO = 'breakopintsview.memento'; - - constructor(actionRunner: actions.IActionRunner, private settings: any, - @IMessageService messageService: IMessageService, - @IContextMenuService contextMenuService: IContextMenuService, - @IDebugService private debugService: IDebugService, - @IInstantiationService private instantiationService: IInstantiationService - ) { - super(actionRunner, BreakpointsView.getExpandedBodySize( - debugService.getModel().getBreakpoints().length + debugService.getModel().getFunctionBreakpoints().length + debugService.getModel().getExceptionBreakpoints().length), - !!settings[BreakpointsView.MEMENTO], nls.localize('breakpointsSection', "Breakpoints Section"), messageService, contextMenuService); - - this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.BREAKPOINTS_UPDATED,() => this.onBreakpointsChange())); - } - - public renderHeader(container: HTMLElement): void { - const titleDiv = $('div.title').appendTo(container); - $('span').text(nls.localize('breakpoints', "Breakpoints")).appendTo(titleDiv); - - super.renderHeader(container); - } - - public renderBody(container: HTMLElement): void { - dom.addClass(container, 'debug-breakpoints'); - this.treeContainer = renderViewTree(container); - const actionProvider = new viewer.BreakpointsActionProvider(this.instantiationService); - - this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: new viewer.BreakpointsDataSource(), - renderer: this.instantiationService.createInstance(viewer.BreakpointsRenderer, actionProvider, this.actionRunner), - accessibilityProvider: this.instantiationService.createInstance(viewer.BreakpointsAccessibilityProvider), - controller: new viewer.BreakpointsController(this.debugService, this.contextMenuService, actionProvider), - sorter: { - compare(tree: tree.ITree, element: any, otherElement: any): number { - const first = element; - const second = otherElement; - if (first instanceof model.ExceptionBreakpoint) { - return -1; - } - if (second instanceof model.ExceptionBreakpoint) { - return 1; - } - if (first instanceof model.FunctionBreakpoint) { - return -1; - } - if(second instanceof model.FunctionBreakpoint) { - return 1; - } - - if (first.source.uri.toString() !== second.source.uri.toString()) { - return first.source.uri.toString().localeCompare(second.source.uri.toString()); - } - - return first.desiredLineNumber - second.desiredLineNumber; - } - } - }, debugTreeOptions(nls.localize('breakpointsAriaTreeLabel', "Debug Breakpoints"))); - - const debugModel = this.debugService.getModel(); - - this.tree.setInput(debugModel); - - this.toDispose.push(this.tree.addListener2('selection', (e: tree.ISelectionEvent) => { - if (!e.selection.length) { - return; - } - const element = e.selection[0]; - if (!(element instanceof model.Breakpoint)) { - return; - } - - const breakpoint = element; - if (!breakpoint.source.inMemory) { - const isMouse = (e.payload.origin === 'mouse'); - let preserveFocus = isMouse; - - const originalEvent:KeyboardEvent|MouseEvent = e && e.payload && e.payload.originalEvent; - if (originalEvent && isMouse && originalEvent.detail === 2) { - preserveFocus = false; - originalEvent.preventDefault(); // focus moves to editor, we need to prevent default - } - - const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)); - this.debugService.openOrRevealEditor(breakpoint.source, breakpoint.lineNumber, preserveFocus, sideBySide).done(null, errors.onUnexpectedError); - } - })); - - this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.SELECTED_FUNCTION_BREAKPOINT_UPDATED, (fbp: debug.IFunctionBreakpoint) => { - if (!fbp || !(fbp instanceof model.FunctionBreakpoint)) { - return; - } - - this.tree.refresh(fbp, false).then(() => { - this.tree.setHighlight(fbp); - this.tree.addOneTimeListener(events.EventType.HIGHLIGHT, (e: tree.IHighlightEvent) => { - if (!e.highlight) { - this.debugService.getViewModel().setSelectedFunctionBreakpoint(null); - } - }); - }).done(null, errors.onUnexpectedError); - })); - } - - public getActions(): actions.IAction[] { - return [ - this.instantiationService.createInstance(debugactions.AddFunctionBreakpointAction, debugactions.AddFunctionBreakpointAction.ID, debugactions.AddFunctionBreakpointAction.LABEL), - this.instantiationService.createInstance(debugactions.ToggleBreakpointsActivatedAction, debugactions.ToggleBreakpointsActivatedAction.ID, debugactions.ToggleBreakpointsActivatedAction.LABEL), - this.instantiationService.createInstance(debugactions.RemoveAllBreakpointsAction, debugactions.RemoveAllBreakpointsAction.ID, debugactions.RemoveAllBreakpointsAction.LABEL) - ]; - } - - private onBreakpointsChange(): void { - const model = this.debugService.getModel(); - this.expandedBodySize = BreakpointsView.getExpandedBodySize( - model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length); - - if (this.tree) { - this.tree.refresh(); - } - } - - private static getExpandedBodySize(length: number): number { - return Math.min(BreakpointsView.MAX_VISIBLE_FILES, length) * 22; - } - - public shutdown(): void { - this.settings[BreakpointsView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); - super.shutdown(); - } -} - export class DebugViewlet extends viewlet.Viewlet { private toDispose: lifecycle.IDisposable[]; @@ -459,7 +34,7 @@ export class DebugViewlet extends viewlet.Viewlet { private $el: builder.Builder; private splitView: splitview.SplitView; - private views: viewlet.IViewletView[]; + private views: (viewlet.CollapsibleViewletView | viewlet.AdaptiveCollapsibleViewletView)[]; private lastFocusedView: viewlet.CollapsibleViewletView | viewlet.AdaptiveCollapsibleViewletView; @@ -475,7 +50,6 @@ export class DebugViewlet extends viewlet.Viewlet { this.progressRunner = null; this.viewletSettings = this.getMemento(storageService, memento.Scope.WORKSPACE); - this.views = []; this.toDispose = []; this.toDispose.push(this.debugService.addListener2(debug.ServiceEvents.STATE_CHANGED, () => { this.onDebugServiceStateChange(); @@ -490,14 +64,12 @@ export class DebugViewlet extends viewlet.Viewlet { if (this.contextService.getWorkspace()) { const actionRunner = this.getActionRunner(); - this.views.push(this.instantiationService.createInstance(VariablesView, actionRunner, this.viewletSettings)); - this.views.push(this.instantiationService.createInstance(WatchExpressionsView, actionRunner, this.viewletSettings)); - this.views.push(this.instantiationService.createInstance(CallStackView, actionRunner, this.viewletSettings)); - this.views.push(this.instantiationService.createInstance(BreakpointsView, actionRunner, this.viewletSettings)); + const viewDescriptors = debug.DebugViewRegistry.getDebugViews(); + this.views = viewDescriptors.map(dsc => this.instantiationService.createInstance(dsc, actionRunner, this.viewletSettings)); this.splitView = new splitview.SplitView(this.$el.getHTMLElement()); this.toDispose.push(this.splitView); - this.views.forEach(v => this.splitView.addView( v)); + this.views.forEach(v => this.splitView.addView(v)); // Track focus this.toDispose.push(this.splitView.onFocus((view: viewlet.CollapsibleViewletView | viewlet.AdaptiveCollapsibleViewletView) => { @@ -536,7 +108,7 @@ export class DebugViewlet extends viewlet.Viewlet { } if (this.views.length > 0) { - (this.views[0]).focusBody(); + this.views[0].focusBody(); } } diff --git a/src/vs/workbench/parts/debug/browser/debugViews.ts b/src/vs/workbench/parts/debug/browser/debugViews.ts new file mode 100644 index 00000000000..5378794a35e --- /dev/null +++ b/src/vs/workbench/parts/debug/browser/debugViews.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import nls = require('vs/nls'); +import dom = require('vs/base/browser/dom'); +import builder = require('vs/base/browser/builder'); +import { TPromise } from 'vs/base/common/winjs.base'; +import errors = require('vs/base/common/errors'); +import events = require('vs/base/common/events'); +import actions = require('vs/base/common/actions'); +import actionbarregistry = require('vs/workbench/browser/actionBarRegistry'); +import tree = require('vs/base/parts/tree/browser/tree'); +import treeimpl = require('vs/base/parts/tree/browser/treeImpl'); +import splitview = require('vs/base/browser/ui/splitview/splitview'); +import viewlet = require('vs/workbench/browser/viewlet'); +import debug = require('vs/workbench/parts/debug/common/debug'); +import model = require('vs/workbench/parts/debug/common/debugModel'); +import viewer = require('vs/workbench/parts/debug/browser/debugViewer'); +import debugactions = require('vs/workbench/parts/debug/electron-browser/debugActions'); +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IMessageService } from 'vs/platform/message/common/message'; + +import IDebugService = debug.IDebugService; + +const debugTreeOptions = (ariaLabel: string) => { + return { + indentPixels: 8, + twistiePixels: 20, + ariaLabel + }; +}; + +function renderViewTree(container: HTMLElement): HTMLElement { + const treeContainer = document.createElement('div'); + dom.addClass(treeContainer, 'debug-view-content'); + container.appendChild(treeContainer); + return treeContainer; +} + +const $ = builder.$; + +export class VariablesView extends viewlet.CollapsibleViewletView { + + private static MEMENTO = 'variablesview.memento'; + + constructor(actionRunner: actions.IActionRunner, private settings: any, + @IMessageService messageService: IMessageService, + @IContextMenuService contextMenuService: IContextMenuService, + @ITelemetryService private telemetryService: ITelemetryService, + @IDebugService private debugService: IDebugService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(actionRunner, !!settings[VariablesView.MEMENTO], nls.localize('variablesSection', "Variables Section"), messageService, contextMenuService); + } + + public renderHeader(container: HTMLElement): void { + const titleDiv = $('div.title').appendTo(container); + $('span').text(nls.localize('variables', "Variables")).appendTo(titleDiv); + + super.renderHeader(container); + } + + public renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-variables'); + this.treeContainer = renderViewTree(container); + + this.tree = new treeimpl.Tree(this.treeContainer, { + dataSource: new viewer.VariablesDataSource(this.debugService), + renderer: this.instantiationService.createInstance(viewer.VariablesRenderer), + accessibilityProvider: new viewer.VariablesAccessibilityProvider(), + controller: new viewer.BaseDebugController(this.debugService, this.contextMenuService, new viewer.VariablesActionProvider(this.instantiationService)) + }, debugTreeOptions(nls.localize('variablesAriaTreeLabel', "Debug Variables"))); + + const viewModel = this.debugService.getViewModel(); + + this.tree.setInput(viewModel); + + const collapseAction = this.instantiationService.createInstance(viewlet.CollapseAction, this.tree, false, 'explorer-action collapse-explorer'); + this.toolBar.setActions(actionbarregistry.prepareActions([collapseAction]))(); + + this.toDispose.push(viewModel.addListener2(debug.ViewModelEvents.FOCUSED_STACK_FRAME_UPDATED, () => this.onFocusedStackFrameUpdated())); + this.toDispose.push(this.debugService.addListener2(debug.ServiceEvents.STATE_CHANGED, () => { + collapseAction.enabled = this.debugService.getState() === debug.State.Running || this.debugService.getState() === debug.State.Stopped; + })); + + this.toDispose.push(this.tree.addListener2(events.EventType.FOCUS, (e: tree.IFocusEvent) => { + const isMouseClick = (e.payload && e.payload.origin === 'mouse'); + const isVariableType = (e.focus instanceof model.Variable); + + if(isMouseClick && isVariableType) { + this.telemetryService.publicLog('debug/variables/selected'); + } + })); + } + + private onFocusedStackFrameUpdated(): void { + this.tree.refresh().then(() => { + const stackFrame = this.debugService.getViewModel().getFocusedStackFrame(); + if (stackFrame) { + return stackFrame.getScopes(this.debugService).then(scopes => { + if (scopes.length > 0) { + return this.tree.expand(scopes[0]); + } + }); + } + }).done(null, errors.onUnexpectedError); + } + + public shutdown(): void { + this.settings[VariablesView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); + super.shutdown(); + } +} + +export class WatchExpressionsView extends viewlet.CollapsibleViewletView { + + private static MEMENTO = 'watchexpressionsview.memento'; + + constructor(actionRunner: actions.IActionRunner, private settings: any, + @IMessageService messageService: IMessageService, + @IContextMenuService contextMenuService: IContextMenuService, + @IDebugService private debugService: IDebugService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(actionRunner, !!settings[WatchExpressionsView.MEMENTO], nls.localize('expressionsSection', "Expressions Section"), messageService, contextMenuService); + this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.WATCH_EXPRESSIONS_UPDATED, (we) => { + // only expand when a new watch expression is added. + if (we instanceof model.Expression) { + this.expand(); + } + })); + } + + public renderHeader(container: HTMLElement): void { + const titleDiv = $('div.title').appendTo(container); + $('span').text(nls.localize('watch', "Watch")).appendTo(titleDiv); + + super.renderHeader(container); + } + + public renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-watch'); + this.treeContainer = renderViewTree(container); + + const actionProvider = new viewer.WatchExpressionsActionProvider(this.instantiationService); + this.tree = new treeimpl.Tree(this.treeContainer, { + dataSource: new viewer.WatchExpressionsDataSource(this.debugService), + renderer: this.instantiationService.createInstance(viewer.WatchExpressionsRenderer, actionProvider, this.actionRunner), + accessibilityProvider: new viewer.WatchExpressionsAccessibilityProvider(), + controller: new viewer.WatchExpressionsController(this.debugService, this.contextMenuService, actionProvider) + }, debugTreeOptions(nls.localize('watchAriaTreeLabel', "Debug Watch Expressions"))); + + this.tree.setInput(this.debugService.getModel()); + + const addWatchExpressionAction = this.instantiationService.createInstance(debugactions.AddWatchExpressionAction, debugactions.AddWatchExpressionAction.ID, debugactions.AddWatchExpressionAction.LABEL); + const collapseAction = this.instantiationService.createInstance(viewlet.CollapseAction, this.tree, false, 'explorer-action collapse-explorer'); + const removeAllWatchExpressionsAction = this.instantiationService.createInstance(debugactions.RemoveAllWatchExpressionsAction, debugactions.RemoveAllWatchExpressionsAction.ID, debugactions.RemoveAllWatchExpressionsAction.LABEL); + this.toolBar.setActions(actionbarregistry.prepareActions([addWatchExpressionAction, collapseAction, removeAllWatchExpressionsAction]))(); + + this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.WATCH_EXPRESSIONS_UPDATED, (we: model.Expression) => this.onWatchExpressionsUpdated(we))); + this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.SELECTED_EXPRESSION_UPDATED, (expression: debug.IExpression) => { + if (!expression || !(expression instanceof model.Expression)) { + return; + } + + this.tree.refresh(expression, false).then(() => { + this.tree.setHighlight(expression); + this.tree.addOneTimeListener(events.EventType.HIGHLIGHT, (e: tree.IHighlightEvent) => { + if (!e.highlight) { + this.debugService.getViewModel().setSelectedExpression(null); + } + }); + }).done(null, errors.onUnexpectedError); + })); + } + + private onWatchExpressionsUpdated(we: model.Expression): void { + this.tree.refresh().done(() => { + return we instanceof model.Expression ? this.tree.reveal(we): TPromise.as(true); + }, errors.onUnexpectedError); + } + + public shutdown(): void { + this.settings[WatchExpressionsView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); + super.shutdown(); + } +} + +export class CallStackView extends viewlet.CollapsibleViewletView { + + private static MEMENTO = 'callstackview.memento'; + private pauseMessage: builder.Builder; + private pauseMessageLabel: builder.Builder; + + constructor(actionRunner: actions.IActionRunner, private settings: any, + @IMessageService messageService: IMessageService, + @IContextMenuService contextMenuService: IContextMenuService, + @ITelemetryService private telemetryService: ITelemetryService, + @IDebugService private debugService: IDebugService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(actionRunner, !!settings[CallStackView.MEMENTO], nls.localize('callstackSection', "Call Stack Section"), messageService, contextMenuService); + } + + public renderHeader(container: HTMLElement): void { + const title = $('div.debug-call-stack-title').appendTo(container); + $('span.title').text(nls.localize('callStack', "Call Stack")).appendTo(title); + this.pauseMessage = $('span.pause-message').appendTo(title); + this.pauseMessage.hide(); + this.pauseMessageLabel = $('span.label').appendTo(this.pauseMessage); + + super.renderHeader(container); + } + + public renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-call-stack'); + this.treeContainer = renderViewTree(container); + + this.tree = new treeimpl.Tree(this.treeContainer, { + dataSource: this.instantiationService.createInstance(viewer.CallStackDataSource), + renderer: this.instantiationService.createInstance(viewer.CallStackRenderer), + accessibilityProvider: this.instantiationService.createInstance(viewer.CallstackAccessibilityProvider) + }, debugTreeOptions(nls.localize('callStackAriaLabel', "Debug Call Stack"))); + + const debugModel = this.debugService.getModel(); + + this.tree.setInput(debugModel); + + this.toDispose.push(this.tree.addListener2('selection', (e: tree.ISelectionEvent) => { + if (!e.selection.length) { + return; + } + const element = e.selection[0]; + + if (element instanceof model.StackFrame) { + const stackFrame = element; + this.debugService.setFocusedStackFrameAndEvaluate(stackFrame); + + const isMouse = (e.payload.origin === 'mouse'); + let preserveFocus = isMouse; + + const originalEvent:KeyboardEvent|MouseEvent = e && e.payload && e.payload.originalEvent; + if (originalEvent && isMouse && originalEvent.detail === 2) { + preserveFocus = false; + originalEvent.preventDefault(); // focus moves to editor, we need to prevent default + } + + const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)); + this.debugService.openOrRevealEditor(stackFrame.source, stackFrame.lineNumber, preserveFocus, sideBySide).done(null, errors.onUnexpectedError); + } + + // user clicked on 'Load More Stack Frames', get those stack frames and refresh the tree. + if (typeof element === 'number') { + const thread = this.debugService.getModel().getThreads()[element]; + if (thread) { + thread.getCallStack(this.debugService, true) + .then(() => this.tree.refresh()) + .then(() => { + this.tree.clearFocus(); + this.tree.clearSelection(); + }).done(null, errors.onUnexpectedError); + } + } + })); + + this.toDispose.push(this.tree.addListener2(events.EventType.FOCUS, (e: tree.IFocusEvent) => { + const isMouseClick = (e.payload && e.payload.origin === 'mouse'); + const isStackFrameType = (e.focus instanceof model.StackFrame); + + if (isMouseClick && isStackFrameType) { + this.telemetryService.publicLog('debug/callStack/selected'); + } + })); + + this.toDispose.push(debugModel.addListener2(debug.ModelEvents.CALLSTACK_UPDATED, () => { + this.tree.refresh().done(null, errors.onUnexpectedError); + })); + + this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.FOCUSED_STACK_FRAME_UPDATED, () => { + const focussedThread = this.debugService.getModel().getThreads()[this.debugService.getViewModel().getFocusedThreadId()]; + if (!focussedThread) { + this.pauseMessage.hide(); + return; + } + + this.tree.expand(focussedThread); + this.tree.setFocus(this.debugService.getViewModel().getFocusedStackFrame()); + if (focussedThread.stoppedDetails && focussedThread.stoppedDetails.reason) { + this.pauseMessageLabel.text(nls.localize('debugStopped', "Paused on {0}", focussedThread.stoppedDetails.reason)); + if (focussedThread.stoppedDetails.text) { + this.pauseMessageLabel.title(focussedThread.stoppedDetails.text); + } + focussedThread.stoppedDetails.reason === 'exception' ? this.pauseMessageLabel.addClass('exception') : this.pauseMessageLabel.removeClass('exception'); + this.pauseMessage.show(); + } else { + this.pauseMessage.hide(); + } + })); + } + + public shutdown(): void { + this.settings[CallStackView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); + super.shutdown(); + } +} + +export class BreakpointsView extends viewlet.AdaptiveCollapsibleViewletView { + + private static MAX_VISIBLE_FILES = 9; + private static MEMENTO = 'breakopintsview.memento'; + + constructor(actionRunner: actions.IActionRunner, private settings: any, + @IMessageService messageService: IMessageService, + @IContextMenuService contextMenuService: IContextMenuService, + @IDebugService private debugService: IDebugService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(actionRunner, BreakpointsView.getExpandedBodySize( + debugService.getModel().getBreakpoints().length + debugService.getModel().getFunctionBreakpoints().length + debugService.getModel().getExceptionBreakpoints().length), + !!settings[BreakpointsView.MEMENTO], nls.localize('breakpointsSection', "Breakpoints Section"), messageService, contextMenuService); + + this.toDispose.push(this.debugService.getModel().addListener2(debug.ModelEvents.BREAKPOINTS_UPDATED,() => this.onBreakpointsChange())); + } + + public renderHeader(container: HTMLElement): void { + const titleDiv = $('div.title').appendTo(container); + $('span').text(nls.localize('breakpoints', "Breakpoints")).appendTo(titleDiv); + + super.renderHeader(container); + } + + public renderBody(container: HTMLElement): void { + dom.addClass(container, 'debug-breakpoints'); + this.treeContainer = renderViewTree(container); + const actionProvider = new viewer.BreakpointsActionProvider(this.instantiationService); + + this.tree = new treeimpl.Tree(this.treeContainer, { + dataSource: new viewer.BreakpointsDataSource(), + renderer: this.instantiationService.createInstance(viewer.BreakpointsRenderer, actionProvider, this.actionRunner), + accessibilityProvider: this.instantiationService.createInstance(viewer.BreakpointsAccessibilityProvider), + controller: new viewer.BreakpointsController(this.debugService, this.contextMenuService, actionProvider), + sorter: { + compare(tree: tree.ITree, element: any, otherElement: any): number { + const first = element; + const second = otherElement; + if (first instanceof model.ExceptionBreakpoint) { + return -1; + } + if (second instanceof model.ExceptionBreakpoint) { + return 1; + } + if (first instanceof model.FunctionBreakpoint) { + return -1; + } + if(second instanceof model.FunctionBreakpoint) { + return 1; + } + + if (first.source.uri.toString() !== second.source.uri.toString()) { + return first.source.uri.toString().localeCompare(second.source.uri.toString()); + } + + return first.desiredLineNumber - second.desiredLineNumber; + } + } + }, debugTreeOptions(nls.localize('breakpointsAriaTreeLabel', "Debug Breakpoints"))); + + const debugModel = this.debugService.getModel(); + + this.tree.setInput(debugModel); + + this.toDispose.push(this.tree.addListener2('selection', (e: tree.ISelectionEvent) => { + if (!e.selection.length) { + return; + } + const element = e.selection[0]; + if (!(element instanceof model.Breakpoint)) { + return; + } + + const breakpoint = element; + if (!breakpoint.source.inMemory) { + const isMouse = (e.payload.origin === 'mouse'); + let preserveFocus = isMouse; + + const originalEvent:KeyboardEvent|MouseEvent = e && e.payload && e.payload.originalEvent; + if (originalEvent && isMouse && originalEvent.detail === 2) { + preserveFocus = false; + originalEvent.preventDefault(); // focus moves to editor, we need to prevent default + } + + const sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)); + this.debugService.openOrRevealEditor(breakpoint.source, breakpoint.lineNumber, preserveFocus, sideBySide).done(null, errors.onUnexpectedError); + } + })); + + this.toDispose.push(this.debugService.getViewModel().addListener2(debug.ViewModelEvents.SELECTED_FUNCTION_BREAKPOINT_UPDATED, (fbp: debug.IFunctionBreakpoint) => { + if (!fbp || !(fbp instanceof model.FunctionBreakpoint)) { + return; + } + + this.tree.refresh(fbp, false).then(() => { + this.tree.setHighlight(fbp); + this.tree.addOneTimeListener(events.EventType.HIGHLIGHT, (e: tree.IHighlightEvent) => { + if (!e.highlight) { + this.debugService.getViewModel().setSelectedFunctionBreakpoint(null); + } + }); + }).done(null, errors.onUnexpectedError); + })); + } + + public getActions(): actions.IAction[] { + return [ + this.instantiationService.createInstance(debugactions.AddFunctionBreakpointAction, debugactions.AddFunctionBreakpointAction.ID, debugactions.AddFunctionBreakpointAction.LABEL), + this.instantiationService.createInstance(debugactions.ToggleBreakpointsActivatedAction, debugactions.ToggleBreakpointsActivatedAction.ID, debugactions.ToggleBreakpointsActivatedAction.LABEL), + this.instantiationService.createInstance(debugactions.RemoveAllBreakpointsAction, debugactions.RemoveAllBreakpointsAction.ID, debugactions.RemoveAllBreakpointsAction.LABEL) + ]; + } + + private onBreakpointsChange(): void { + const model = this.debugService.getModel(); + this.expandedBodySize = BreakpointsView.getExpandedBodySize( + model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length); + + if (this.tree) { + this.tree.refresh(); + } + } + + private static getExpandedBodySize(length: number): number { + return Math.min(BreakpointsView.MAX_VISIBLE_FILES, length) * 22; + } + + public shutdown(): void { + this.settings[BreakpointsView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); + super.shutdown(); + } +} diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 601f2a37912..55a20664e19 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -7,7 +7,9 @@ import uri from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import ee = require('vs/base/common/eventEmitter'); import severity from 'vs/base/common/severity'; +import { AdaptiveCollapsibleViewletView, CollapsibleViewletView } from 'vs/workbench/browser/viewlet'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import editor = require('vs/editor/common/editorCommon'); import editorbrowser = require('vs/editor/browser/editorBrowser'); import { Source } from 'vs/workbench/parts/debug/common/debugSource'; @@ -346,6 +348,41 @@ export interface IDebugEditorContribution extends editor.IEditorContribution { showHover(range: editor.IEditorRange, hoveringOver: string, focus: boolean): TPromise; } +// Debug view descriptors and registration + +export class DebugViewDescriptor extends SyncDescriptor { + // constructor(ctorName: string, public id: string, public name: string, public cssClass: string) { + // super(ctorName); + // } +} + +export interface IDebugViewRegistry { + registerDebugView(descriptor: DebugViewDescriptor): void; + getDebugViews(): DebugViewDescriptor[]; +} + +class DebugViewRegistryImpl implements IDebugViewRegistry { + private debugViews: DebugViewDescriptor[]; + + constructor() { + this.debugViews = []; + } + + public registerDebugView(descriptor: DebugViewDescriptor): void { + if (this.debugViews.some(dsc => dsc.equals(descriptor))) { + return; + } + + this.debugViews.push(descriptor); + } + + public getDebugViews(): DebugViewDescriptor[] { + return this.debugViews; + } +} + +export var DebugViewRegistry = new DebugViewRegistryImpl(); + // utils const _formatPIIRegexp = /{([^}]+)}/g; diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index dd9bc81398e..a0c9e83ef5a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -18,6 +18,7 @@ import { EditorBrowserRegistry } from 'vs/editor/browser/editorBrowserExtensions import wbaregistry = require('vs/workbench/common/actionRegistry'); import viewlet = require('vs/workbench/browser/viewlet'); import panel = require('vs/workbench/browser/panel'); +import { VariablesView, WatchExpressionsView, CallStackView, BreakpointsView } from 'vs/workbench/parts/debug/browser/debugViews'; import wbext = require('vs/workbench/common/contributions'); import * as debug from 'vs/workbench/parts/debug/common/debug'; import { DebugEditorModelManager } from 'vs/workbench/parts/debug/browser/debugEditorModelManager'; @@ -83,6 +84,12 @@ const openViewletKb: IKeybindings = { )); (platform.Registry.as(panel.Extensions.Panels)).setDefaultPanelId(debug.REPL_ID); +// Register default debug views +debug.DebugViewRegistry.registerDebugView(new debug.DebugViewDescriptor(VariablesView)); +debug.DebugViewRegistry.registerDebugView(new debug.DebugViewDescriptor(WatchExpressionsView)); +debug.DebugViewRegistry.registerDebugView(new debug.DebugViewDescriptor(CallStackView)); +debug.DebugViewRegistry.registerDebugView(new debug.DebugViewDescriptor(BreakpointsView)); + // register action to open viewlet const registry = ( platform.Registry.as(wbaregistry.Extensions.WorkbenchActions)); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenDebugViewletAction, OpenDebugViewletAction.ID, OpenDebugViewletAction.LABEL, openViewletKb), nls.localize('view', "View")); diff --git a/src/vs/workbench/parts/debug/test/common/mockDebugService.ts b/src/vs/workbench/parts/debug/test/common/mockDebugService.ts index 18b7180b66d..35bec5d1853 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebugService.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebugService.ts @@ -126,7 +126,7 @@ export class MockDebugService extends ee.EventEmitter implements debug.IDebugSer } public getViewModel(): debug.IViewModel { - return null + return null; } public openOrRevealEditor(source: Source, lineNumber: number, preserveFocus: boolean, sideBySide: boolean): TPromise {