diff --git a/src/vs/workbench/parts/debug/browser/debugActionItems.ts b/src/vs/workbench/parts/debug/browser/debugActionItems.ts index 1bd11a74f78bbe7aae2fbba961d7864189fdf37f..ab5c261e4feb4811aa24a87a35e6ef231dd50032 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionItems.ts @@ -8,8 +8,8 @@ import errors = require('vs/base/common/errors'); import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; import { SelectActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDebugService, State } from 'vs/workbench/parts/debug/common/debug'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDebugService, State } from 'vs/workbench/parts/debug/common/debug'; export class DebugSelectActionItem extends SelectActionItem { @@ -23,11 +23,11 @@ export class DebugSelectActionItem extends SelectActionItem { this.toDispose.push(configurationService.onDidUpdateConfiguration(e => { this.updateOptions(true).done(null, errors.onUnexpectedError); })); - this.toDispose.push(this.debugService.getConfigurationManager().onDidConfigurationChange(name => { + this.toDispose.push(this.debugService.getViewModel().onDidSelectConfigurationName(name => { this.updateOptions(false).done(null, errors.onUnexpectedError); })); - this.toDispose.push(this.debugService.onDidChangeState(state => { - this.enabled = state === State.Inactive; + this.toDispose.push(this.debugService.onDidChangeState(() => { + this.enabled = this.debugService.state === State.Inactive; })); } @@ -46,10 +46,9 @@ export class DebugSelectActionItem extends SelectActionItem { } const configurationNames = config.configurations.filter(cfg => !!cfg.name).map(cfg => cfg.name); - const configurationName = configurationManager.configuration ? configurationManager.configuration.name : null; - let selected = configurationNames.indexOf(configurationName); - + const selected = configurationNames.indexOf(this.debugService.getViewModel().selectedConfigurationName); this.setOptions(configurationNames, selected); + if (changeDebugConfiguration) { return this.actionRunner.run(this._action, this.getSelected()); } diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index e7df2ae1001db83271495f0106a919d69aa3aaa8..c292b009596b288ef82f006ac8b979226b3e3f28 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -39,10 +39,10 @@ export class AbstractDebugAction extends Action { super(id, label, cssClass, false); this.debugService = debugService; this.toDispose = []; - this.toDispose.push(this.debugService.onDidChangeState((state) => this.updateEnablement(state))); + this.toDispose.push(this.debugService.onDidChangeState(() => this.updateEnablement())); this.updateLabel(label); - this.updateEnablement(this.debugService.state); + this.updateEnablement(); } public run(e?: any): TPromise { @@ -53,8 +53,8 @@ export class AbstractDebugAction extends Action { this.label = newLabel; } - protected updateEnablement(state: debug.State): void { - this.enabled = this.isEnabled(state); + protected updateEnablement(): void { + this.enabled = this.isEnabled(this.debugService.state); } protected isEnabled(state: debug.State): boolean { @@ -75,7 +75,7 @@ export class ConfigureAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action configure', debugService, keybindingService); - this.toDispose.push(debugService.getConfigurationManager().onDidConfigurationChange((configurationName) => { + this.toDispose.push(debugService.getViewModel().onDidSelectConfigurationName(configurationName => { if (configurationName) { this.class = 'debug-action configure'; this.tooltip = ConfigureAction.LABEL; @@ -101,7 +101,8 @@ export class SelectConfigAction extends AbstractDebugAction { } public run(configName: string): TPromise { - return this.debugService.getConfigurationManager().setConfiguration(configName); + this.debugService.getViewModel().setSelectedConfigurationName(configName); + return TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -118,7 +119,7 @@ export class StartAction extends AbstractDebugAction { } public run(): TPromise { - return this.commandService.executeCommand('_workbench.startDebug'); + return this.commandService.executeCommand('_workbench.startDebug', this.debugService.getViewModel().selectedConfigurationName); } protected isEnabled(state: debug.State): boolean { @@ -133,16 +134,22 @@ export class RestartAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action restart', debugService, keybindingService); - this.setLabel(this.debugService.getConfigurationManager().configuration); - this.toDispose.push(this.debugService.getConfigurationManager().onDidConfigurationChange(config => this.setLabel(config))); + this.setLabel(this.debugService.getViewModel().focusedProcess); + this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.setLabel(this.debugService.getViewModel().focusedProcess))); } - private setLabel(config: debug.IConfig): void { - this.updateLabel(config && config.request === 'attach' ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); + private setLabel(process: debug.IProcess): void { + this.updateLabel(process && process.session.requestType === debug.SessionRequestType.ATTACH ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); } public run(): TPromise { - return this.debugService.restartSession(); + let process = this.debugService.getViewModel().focusedProcess; + if (!process) { + const processes = this.debugService.getModel().getProcesses(); + process = processes.length > 0 ? processes[0] : null; + } + + return this.debugService.restartProcess(process); } protected isEnabled(state: debug.State): boolean { @@ -159,10 +166,11 @@ export class StepOverAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.next(threadId); + return thread ? thread.next() : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -179,10 +187,11 @@ export class StepIntoAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.stepIn(threadId); + return thread ? thread.stepIn() : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -199,10 +208,11 @@ export class StepOutAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.stepOut(threadId); + return thread ? thread.stepOut() : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -219,16 +229,17 @@ export class StepBackAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.stepBack(threadId); + return thread.stepBack(); } protected isEnabled(state: debug.State): boolean { - const activeSession = this.debugService.activeSession; + const process = this.debugService.getViewModel().focusedProcess; return super.isEnabled(state) && state === debug.State.Stopped && - activeSession && activeSession.configuration.capabilities.supportsStepBack; + process && process.session.configuration.capabilities.supportsStepBack; } } @@ -241,8 +252,13 @@ export class StopAction extends AbstractDebugAction { } public run(): TPromise { - var session = this.debugService.activeSession; - return session ? session.disconnect(false, true) : TPromise.as(null); + let process = this.debugService.getViewModel().focusedProcess; + if (!process) { + const processes = this.debugService.getModel().getProcesses(); + process = processes.length > 0 ? processes[0] : null; + } + + return process ? process.session.disconnect(false, true) : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -259,8 +275,12 @@ export class DisconnectAction extends AbstractDebugAction { } public run(): TPromise { - const session = this.debugService.activeSession; - return session ? session.disconnect(false, true) : TPromise.as(null); + let process = this.debugService.getViewModel().focusedProcess; + if (!process) { + process = this.debugService.getModel().getProcesses().pop(); + } + + return process ? process.session.disconnect(false, true) : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -277,10 +297,11 @@ export class ContinueAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.continue(threadId); + return thread ? thread.continue() : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -297,10 +318,11 @@ export class PauseAction extends AbstractDebugAction { } public run(thread: debug.IThread): TPromise { - const threadId = thread && thread instanceof model.Thread ? thread.threadId - : this.debugService.getViewModel().getFocusedThreadId(); + if (!(thread instanceof model.Thread)) { + thread = this.debugService.getViewModel().focusedThread; + } - return this.debugService.pause(threadId); + return thread ? thread.pause() : TPromise.as(null); } protected isEnabled(state: debug.State): boolean { @@ -317,12 +339,11 @@ export class RestartFrameAction extends AbstractDebugAction { } public run(frame: debug.IStackFrame): TPromise { + if (!frame) { + frame = this.debugService.getViewModel().focusedStackFrame; + } - const frameId = (frame && frame instanceof model.StackFrame) - ? frame.frameId - : this.debugService.getViewModel().getFocusedStackFrame().frameId; - - return this.debugService.restartFrame(frameId); + return frame.restart(); } } @@ -346,7 +367,7 @@ export class RemoveAllBreakpointsAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action remove-all', debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); } public run(): TPromise { @@ -378,7 +399,7 @@ export class EnableAllBreakpointsAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action enable-all-breakpoints', debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); } public run(): TPromise { @@ -397,7 +418,7 @@ export class DisableAllBreakpointsAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action disable-all-breakpoints', debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); } public run(): TPromise { @@ -421,7 +442,7 @@ export class ToggleBreakpointsActivatedAction extends AbstractDebugAction { this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => { this.updateLabel(this.debugService.getModel().areBreakpointsActivated() ? ToggleBreakpointsActivatedAction.DEACTIVATE_LABEL : ToggleBreakpointsActivatedAction.ACTIVATE_LABEL); - this.updateEnablement(this.debugService.state); + this.updateEnablement(); })); } @@ -440,7 +461,7 @@ export class ReapplyBreakpointsAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, null, debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.updateEnablement())); } public run(): TPromise { @@ -592,8 +613,8 @@ export class SetValueAction extends AbstractDebugAction { } protected isEnabled(state: debug.State): boolean { - const session = this.debugService.activeSession; - return super.isEnabled(state) && state === debug.State.Stopped && session && session.configuration.capabilities.supportsSetVariable; + const process = this.debugService.getViewModel().focusedProcess; + return super.isEnabled(state) && state === debug.State.Stopped && process && process.session.configuration.capabilities.supportsSetVariable; } } @@ -622,7 +643,7 @@ class RunToCursorAction extends EditorAction { const lineNumber = editor.getPosition().lineNumber; const uri = editor.getModel().uri; - const oneTimeListener = debugService.activeSession.onDidEvent(event => { + const oneTimeListener = debugService.getViewModel().focusedProcess.session.onDidEvent(event => { if (event.event === 'stopped' || event.event === 'exit') { const toRemove = debugService.getModel().getBreakpoints() .filter(bp => bp.desiredLineNumber === lineNumber && bp.source.uri.toString() === uri.toString()).pop(); @@ -635,7 +656,7 @@ class RunToCursorAction extends EditorAction { const bpExists = !!(debugService.getModel().getBreakpoints().filter(bp => bp.lineNumber === lineNumber && bp.source.uri.toString() === uri.toString()).pop()); return (bpExists ? TPromise.as(null) : debugService.addBreakpoints([{ uri, lineNumber }])).then(() => { - debugService.continue(debugService.getViewModel().getFocusedThreadId()); + debugService.getViewModel().focusedThread.continue(); }); } } @@ -646,7 +667,7 @@ export class AddWatchExpressionAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action add-watch-expression', debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); } public run(): TPromise { @@ -747,7 +768,9 @@ export class AddToWatchExpressionsAction extends AbstractDebugAction { } public run(): TPromise { - return this.debugService.addWatchExpression(model.getFullExpressionName(this.expression, this.debugService.activeSession.configuration.type)); + const process = this.debugService.getViewModel().focusedProcess; + const type = process ? process.session.configuration.type : null; + return this.debugService.addWatchExpression(model.getFullExpressionName(this.expression, type)); } } @@ -785,7 +808,7 @@ export class RemoveAllWatchExpressionsAction extends AbstractDebugAction { constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { super(id, label, 'debug-action remove-all', debugService, keybindingService); - this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement(this.debugService.state))); + this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(() => this.updateEnablement())); } public run(): TPromise { @@ -885,7 +908,12 @@ export class RunAction extends AbstractDebugAction { } public run(): TPromise { - return this.debugService.createSession(true); + return this.debugService.getConfigurationManager().getConfiguration(this.debugService.getViewModel().selectedConfigurationName).then(configuration => { + if (configuration) { + configuration.noDebug = true; + return this.debugService.createProcess(configuration); + } + }); } protected isEnabled(state: debug.State): boolean { diff --git a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts index 66a30e104e94884f0684a5951756777b75a547ca..dd686efefb808ee9c6ec361f35414bdd15c82afa 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts @@ -70,8 +70,8 @@ export class DebugActionsWidget implements wbext.IWorkbenchContribution { } private registerListeners(): void { - this.toDispose.push(this.debugService.onDidChangeState(state => { - this.onDebugStateChange(state); + this.toDispose.push(this.debugService.onDidChangeState(() => { + this.onDebugStateChange(); })); this.toDispose.push(this.actionBar.actionRunner.addListener2(events.EventType.RUN, (e: any) => { // check for error @@ -131,7 +131,8 @@ export class DebugActionsWidget implements wbext.IWorkbenchContribution { return DebugActionsWidget.ID; } - private onDebugStateChange(state: debug.State): void { + private onDebugStateChange(): void { + const state = this.debugService.state; if (state === debug.State.Disabled || state === debug.State.Inactive) { return this.hide(); } @@ -183,11 +184,10 @@ export class DebugActionsWidget implements wbext.IWorkbenchContribution { } this.actions[0] = state === debug.State.Running ? this.pauseAction : this.continueAction; - const session = this.debugService.activeSession; - const configuration = this.debugService.getConfigurationManager().configuration; - this.actions[5] = configuration && configuration.request === 'attach' ? this.disconnectAction : this.stopAction; + const process = this.debugService.getViewModel().focusedProcess; + this.actions[5] = process && process.session.requestType === debug.SessionRequestType.ATTACH ? this.disconnectAction : this.stopAction; - if (session && session.configuration.capabilities.supportsStepBack) { + if (process && process.session.configuration.capabilities.supportsStepBack) { if (!this.stepBackAction) { this.stepBackAction = instantiationService.createInstance(StepBackAction, StepBackAction.ID, StepBackAction.LABEL); this.toDispose.push(this.stepBackAction); diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index a15f954ce082f213be11f2ff50d652b5c8ddaf19..b7e2a7573de07873ca0011d7f5caf9dddcbb39a5 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -83,8 +83,8 @@ export class DebugEditorModelManager implements IWorkbenchContribution { this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => this.onFocusStackFrame())); - this.toDispose.push(this.debugService.onDidChangeState(state => { - if (state === State.Inactive) { + this.toDispose.push(this.debugService.onDidChangeState(() => { + if (this.debugService.state === State.Inactive) { Object.keys(this.modelData).forEach(key => this.modelData[key].dirty = false); } })); @@ -132,27 +132,24 @@ export class DebugEditorModelManager implements IWorkbenchContribution { private createCallStackDecorations(modelUrlStr: string): editorcommon.IModelDeltaDecoration[] { const result: editorcommon.IModelDeltaDecoration[] = []; - const focusedStackFrame = this.debugService.getViewModel().getFocusedStackFrame(); - const focusedThreadId = this.debugService.getViewModel().getFocusedThreadId(); - const allThreads = this.debugService.getModel().getThreads(); - if (!focusedStackFrame || !allThreads[focusedThreadId] || !allThreads[focusedThreadId].getCachedCallStack()) { + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!focusedStackFrame || !focusedStackFrame.thread.getCachedCallStack()) { return result; } // only show decorations for the currently focussed thread. - const thread = allThreads[focusedThreadId]; - thread.getCachedCallStack().filter(sf => sf.source.uri.toString() === modelUrlStr).forEach(sf => { + focusedStackFrame.thread.getCachedCallStack().filter(sf => sf.source.uri.toString() === modelUrlStr).forEach(sf => { const wholeLineRange = createRange(sf.lineNumber, sf.column, sf.lineNumber, Number.MAX_VALUE); // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focussed stack frame, // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). - if (sf === thread.getCachedCallStack()[0]) { + if (sf === focusedStackFrame.thread.getCachedCallStack()[0]) { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_MARGIN, range: createRange(sf.lineNumber, sf.column, sf.lineNumber, sf.column + 1) }); - if (thread.stoppedDetails.reason === 'exception') { + if (focusedStackFrame.thread.stoppedDetails.reason === 'exception') { result.push({ options: DebugEditorModelManager.TOP_STACK_FRAME_EXCEPTION_DECORATION, range: wholeLineRange @@ -224,7 +221,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { }); } } - modelData.dirty = !!this.debugService.activeSession; + modelData.dirty = this.debugService.state !== State.Inactive && this.debugService.state !== State.Disabled; const toRemove = this.debugService.getModel().getBreakpoints() .filter(bp => bp.source.uri.toString() === modelUrl.toString()); @@ -277,7 +274,6 @@ export class DebugEditorModelManager implements IWorkbenchContribution { const state = this.debugService.state; const debugActive = state === State.Running || state === State.Stopped || state === State.Initializing; const modelData = this.modelData[breakpoint.source.uri.toString()]; - const session = this.debugService.activeSession; let result = (!breakpoint.enabled || !activated) ? DebugEditorModelManager.BREAKPOINT_DISABLED_DECORATION : debugActive && modelData && modelData.dirty && !breakpoint.verified ? DebugEditorModelManager.BREAKPOINT_DIRTY_DECORATION : @@ -293,7 +289,8 @@ export class DebugEditorModelManager implements IWorkbenchContribution { return result; } - if (session && !session.configuration.capabilities.supportsConditionalBreakpoints) { + const process = this.debugService.getViewModel().focusedProcess; + if (process && !process.session.configuration.capabilities.supportsConditionalBreakpoints) { return DebugEditorModelManager.BREAKPOINT_UNSUPPORTED_DECORATION; } diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index 26860486bb21dcc0ea482ca46a31e16e81f349a9..b73d291b8e943b948ee00a0a99d9d493f3c6fbca 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -53,8 +53,8 @@ export class DebugViewlet extends Viewlet { this.viewletSettings = this.getMemento(storageService, memento.Scope.WORKSPACE); this.toDispose = []; this.views = []; - this.toDispose.push(this.debugService.onDidChangeState((state) => { - this.onDebugServiceStateChange(state); + this.toDispose.push(this.debugService.onDidChangeState(() => { + this.onDebugServiceStateChange(); })); } @@ -146,12 +146,12 @@ export class DebugViewlet extends Viewlet { return null; } - private onDebugServiceStateChange(newState: debug.State): void { + private onDebugServiceStateChange(): void { if (this.progressRunner) { this.progressRunner.done(); } - if (newState === debug.State.Initializing) { + if (this.debugService.state === debug.State.Initializing) { this.progressRunner = this.progressService.show(true); } else { this.progressRunner = null; diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 03ec260c30406123e395173d10305bb74a2e7b89..273b656a2fd660573ed38e21f43e6bdf46fe5a28 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -31,6 +31,7 @@ export const DEBUG_SCHEME = 'debug'; export interface IRawModelUpdate { threadId: number; + sessionId: string; thread?: DebugProtocol.Thread; callStack?: DebugProtocol.StackFrame[]; stoppedDetails?: IRawStoppedDetails; @@ -53,6 +54,7 @@ export interface ITreeElement { export interface IExpressionContainer extends ITreeElement { reference: number; + stackFrame: IStackFrame; getChildren(debugService: IDebugService): TPromise; } @@ -63,7 +65,51 @@ export interface IExpression extends ITreeElement, IExpressionContainer { type?: string; } +export enum SessionRequestType { + LAUNCH, + ATTACH, + LAUNCH_NO_DEBUG +} + +export interface ISession { + requestType: SessionRequestType; + stackTrace(args: DebugProtocol.StackTraceArguments): TPromise; + scopes(args: DebugProtocol.ScopesArguments): TPromise; + variables(args: DebugProtocol.VariablesArguments): TPromise; + evaluate(args: DebugProtocol.EvaluateArguments): TPromise; + + configuration: { type: string, capabilities: DebugProtocol.Capabilities }; + disconnect(restart?: boolean, force?: boolean): TPromise; + custom(request: string, args: any): TPromise; + onDidEvent: Event; + restartFrame(args: DebugProtocol.RestartFrameArguments): TPromise; + + next(args: DebugProtocol.NextArguments): TPromise; + stepIn(args: DebugProtocol.StepInArguments): TPromise; + stepOut(args: DebugProtocol.StepOutArguments): TPromise; + stepBack(args: DebugProtocol.StepBackArguments): TPromise; + continue(args: DebugProtocol.ContinueArguments): TPromise; + pause(args: DebugProtocol.PauseArguments): TPromise; + + completions(args: DebugProtocol.CompletionsArguments): TPromise; + setVariable(args: DebugProtocol.SetVariableArguments): TPromise; + source(args: DebugProtocol.SourceArguments): TPromise; +} + +export interface IProcess extends ITreeElement { + name: string; + getThread(threadId: number): IThread; + getAllThreads(): IThread[]; + session: ISession; +} + export interface IThread extends ITreeElement { + + /** + * Process the thread belongs to + */ + process: IProcess; + /** * Id of the thread generated by the debug adapter backend. */ @@ -86,7 +132,7 @@ export interface IThread extends ITreeElement { * Only gets the first 20 stack frames. Calling this method consecutive times * with getAdditionalStackFrames = true gets the remainder of the call stack. */ - getCallStack(debugService: IDebugService, getAdditionalStackFrames?: boolean): TPromise; + getCallStack(getAdditionalStackFrames?: boolean): TPromise; /** * Gets the callstack if it has already been received from the debug @@ -104,6 +150,13 @@ export interface IThread extends ITreeElement { * threads can be retrieved from the debug adapter. */ stopped: boolean; + + next(): TPromise; + stepIn(): TPromise; + stepOut(): TPromise; + stepBack(): TPromise; + continue(): TPromise; + pause(): TPromise; } export interface IScope extends IExpressionContainer { @@ -112,13 +165,15 @@ export interface IScope extends IExpressionContainer { } export interface IStackFrame extends ITreeElement { - threadId: number; + thread: IThread; name: string; lineNumber: number; column: number; frameId: number; source: Source; - getScopes(debugService: IDebugService): TPromise; + getScopes(): TPromise; + restart(): TPromise; + completions(text: string, position: Position): TPromise; } export interface IEnablement extends ITreeElement { @@ -159,20 +214,39 @@ export interface IExceptionBreakpoint extends IEnablement { // model interfaces export interface IViewModel extends ITreeElement { - getFocusedStackFrame(): IStackFrame; + /** + * Returns the focused debug process or null if no process is stopped. + */ + focusedProcess: IProcess; + + /** + * Returns the focused thread or null if no thread is stopped. + */ + focusedThread: IThread; + + /** + * Returns the focused stack frame or null if there are no stack frames. + */ + focusedStackFrame: IStackFrame; getSelectedExpression(): IExpression; - getFocusedThreadId(): number; - setSelectedExpression(expression: IExpression); getSelectedFunctionBreakpoint(): IFunctionBreakpoint; + setSelectedExpression(expression: IExpression); setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint): void; + selectedConfigurationName: string; + setSelectedConfigurationName(name: string): void; + onDidFocusStackFrame: Event; onDidSelectExpression: Event; onDidSelectFunctionBreakpoint: Event; + /** + * Allows to register on change of selected debug configuration. + */ + onDidSelectConfigurationName: Event; } export interface IModel extends ITreeElement { - getThreads(): { [threadId: number]: IThread; }; + getProcesses(): IProcess[]; getBreakpoints(): IBreakpoint[]; areBreakpointsActivated(): boolean; getFunctionBreakpoints(): IFunctionBreakpoint[]; @@ -221,6 +295,7 @@ export interface IEnvConfig { debugServer?: number; noDebug?: boolean; silentlyAbort?: boolean; + configurationNames?: string[]; } export interface IExtHostConfig extends IEnvConfig { @@ -261,32 +336,11 @@ export interface IRawBreakpointContribution { language: string; } -export interface IRawDebugSession { - configuration: { type: string, capabilities: DebugProtocol.Capabilities }; - - disconnect(restart?: boolean, force?: boolean): TPromise; - - stackTrace(args: DebugProtocol.StackTraceArguments): TPromise; - scopes(args: DebugProtocol.ScopesArguments): TPromise; - variables(args: DebugProtocol.VariablesArguments): TPromise; - evaluate(args: DebugProtocol.EvaluateArguments): TPromise; - - custom(request: string, args: any): TPromise; - - onDidEvent: Event; -} - export interface IConfigurationManager { - configuration: IConfig; - setConfiguration(name: string): TPromise; + getConfiguration(nameOrConfig: string | IConfig): TPromise; openConfigFile(sideBySide: boolean): TPromise; loadLaunchConfig(): TPromise; canSetBreakpointsIn(model: editor.IModel): boolean; - - /** - * Allows to register on change of debug configuration. - */ - onDidConfigurationChange: Event; } export const IDebugService = createDecorator(DEBUG_SERVICE_ID); @@ -302,7 +356,7 @@ export interface IDebugService { /** * Allows to register on debug state changes. */ - onDidChangeState: Event; + onDidChangeState: Event; /** * Gets the current configuration manager. @@ -374,11 +428,6 @@ export interface IDebugService { */ appendReplOutput(value: string, severity?: severity): void; - /** - * Sets the value for the variable against the debug adapter. - */ - setVariable(variable: IExpression, value: string): TPromise; - /** * Adds a new watch expression and evaluates it against the debug adapter. */ @@ -395,19 +444,14 @@ export interface IDebugService { removeWatchExpressions(id?: string): void; /** - * Creates a new debug session. Depending on the configuration will either 'launch' or 'attach'. + * Creates a new debug process. Depending on the configuration will either 'launch' or 'attach'. */ - createSession(noDebug: boolean, configuration?: IConfig): TPromise; + createProcess(configurationOrName: IConfig | string): TPromise; /** - * Restarts an active debug session or creates a new one if there is no active session. + * Restarts a process or creates a new one if there is no active session. */ - restartSession(): TPromise; - - /** - * Returns the active debug session or null if debug is inactive. - */ - activeSession: IRawDebugSession; + restartProcess(process: IProcess): TPromise; /** * Gets the current debug model. @@ -423,15 +467,6 @@ export interface IDebugService { * Opens a new or reveals an already visible editor showing the source. */ openOrRevealSource(source: Source, lineNumber: number, preserveFocus: boolean, sideBySide: boolean): TPromise; - - next(threadId: number): TPromise; - stepIn(threadId: number): TPromise; - stepOut(threadId: number): TPromise; - stepBack(threadId: number): TPromise; - continue(threadId: number): TPromise; - pause(threadId: number): TPromise; - restartFrame(frameId: number): TPromise; - completions(text: string, position: Position): TPromise; } // Editor interfaces diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 27399ca2cca09ff142760eda3d5a70dfa996164e..0f3f88d6363ffdbeb9c0da5dbc163614030f6241 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -12,6 +12,8 @@ import objects = require('vs/base/common/objects'); import severity from 'vs/base/common/severity'; import types = require('vs/base/common/types'); import arrays = require('vs/base/common/arrays'); +import { ISuggestion } from 'vs/editor/common/modes'; +import { Position } from 'vs/editor/common/core/position'; import debug = require('vs/workbench/parts/debug/common/debug'); import { Source } from 'vs/workbench/parts/debug/common/debugSource'; @@ -22,15 +24,16 @@ function massageValue(value: string): string { return value ? value.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') : value; } -export function evaluateExpression(session: debug.IRawDebugSession, stackFrame: debug.IStackFrame, expression: Expression, context: string): TPromise { - if (!session) { +export function evaluateExpression(stackFrame: debug.IStackFrame, expression: Expression, context: string): TPromise { + if (!stackFrame || !stackFrame.thread.process) { expression.value = context === 'repl' ? nls.localize('startDebugFirst', "Please start a debug session to evaluate") : Expression.DEFAULT_VALUE; expression.available = false; expression.reference = 0; return TPromise.as(expression); } + expression.stackFrame = stackFrame; - return session.evaluate({ + return stackFrame.thread.process.session.evaluate({ expression: expression.name, frameId: stackFrame ? stackFrame.frameId : undefined, context @@ -83,74 +86,6 @@ export function getFullExpressionName(expression: debug.IExpression, sessionType return result; } -export class Thread implements debug.IThread { - private promisedCallStack: TPromise; - private cachedCallStack: debug.IStackFrame[]; - public stoppedDetails: debug.IRawStoppedDetails; - public stopped: boolean; - - constructor(public name: string, public threadId: number) { - this.promisedCallStack = undefined; - this.stoppedDetails = undefined; - this.cachedCallStack = undefined; - this.stopped = false; - } - - public getId(): string { - return `thread:${this.name}:${this.threadId}`; - } - - public clearCallStack(): void { - this.promisedCallStack = undefined; - this.cachedCallStack = undefined; - } - - public getCachedCallStack(): debug.IStackFrame[] { - return this.cachedCallStack; - } - - public getCallStack(debugService: debug.IDebugService, getAdditionalStackFrames = false): TPromise { - if (!this.stopped) { - return TPromise.as([]); - } - - if (!this.promisedCallStack) { - this.promisedCallStack = this.getCallStackImpl(debugService, 0).then(callStack => { - this.cachedCallStack = callStack; - return callStack; - }); - } else if (getAdditionalStackFrames) { - this.promisedCallStack = this.promisedCallStack.then(callStackFirstPart => this.getCallStackImpl(debugService, callStackFirstPart.length).then(callStackSecondPart => { - this.cachedCallStack = callStackFirstPart.concat(callStackSecondPart); - return this.cachedCallStack; - })); - } - - return this.promisedCallStack; - } - - private getCallStackImpl(debugService: debug.IDebugService, startFrame: number): TPromise { - let session = debugService.activeSession; - return session.stackTrace({ threadId: this.threadId, startFrame, levels: 20 }).then(response => { - if (!response || !response.body) { - return []; - } - - this.stoppedDetails.totalFrames = response.body.totalFrames; - return response.body.stackFrames.map((rsf, level) => { - if (!rsf) { - return new StackFrame(this.threadId, 0, new Source({ name: UNKNOWN_SOURCE_LABEL }, false), nls.localize('unknownStack', "Unknown stack location"), undefined, undefined); - } - - return new StackFrame(this.threadId, rsf.id, rsf.source ? new Source(rsf.source) : new Source({ name: UNKNOWN_SOURCE_LABEL }, false), rsf.name, rsf.line, rsf.column); - }); - }, (err: Error) => { - this.stoppedDetails.framesErrorMessage = err.message; - return []; - }); - } -} - export class OutputElement implements debug.ITreeElement { private static ID_COUNTER = 0; @@ -236,6 +171,7 @@ export abstract class ExpressionContainer implements debug.IExpressionContainer private _value: string; constructor( + public stackFrame: debug.IStackFrame, public reference: number, private id: string, private cacheChildren: boolean, @@ -246,39 +182,38 @@ export abstract class ExpressionContainer implements debug.IExpressionContainer // noop } - public getChildren(debugService: debug.IDebugService): TPromise { + public getChildren(): TPromise { if (!this.cacheChildren || !this.children) { - const session = debugService.activeSession; // only variables with reference > 0 have children. - if (!session || this.reference <= 0) { + if (this.reference <= 0) { this.children = TPromise.as([]); } else { - // Check if object has named variables, fetch them independent from indexed variables #9670 - this.children = (!!this.namedVariables ? this.fetchVariables(session, undefined, undefined, 'named') : TPromise.as([])).then(childrenArray => { - // Use a dynamic chunk size based on the number of elements #9774 - let chunkSize = ExpressionContainer.BASE_CHUNK_SIZE; - while (this.indexedVariables > chunkSize * ExpressionContainer.BASE_CHUNK_SIZE) { - chunkSize *= ExpressionContainer.BASE_CHUNK_SIZE; - } - - if (this.indexedVariables > chunkSize) { - // There are a lot of children, create fake intermediate values that represent chunks #9537 - const numberOfChunks = Math.ceil(this.indexedVariables / chunkSize); - for (let i = 0; i < numberOfChunks; i++) { - const start = this.startOfVariables + i * chunkSize; - const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - childrenArray.push(new Variable(this, this.reference, `[${start}..${start + count - 1}]`, '', null, count, null, true, start)); + this.children = (!!this.namedVariables ? this.fetchVariables(undefined, undefined, 'named') + : TPromise.as([])).then(childrenArray => { + // Use a dynamic chunk size based on the number of elements #9774 + let chunkSize = ExpressionContainer.BASE_CHUNK_SIZE; + while (this.indexedVariables > chunkSize * ExpressionContainer.BASE_CHUNK_SIZE) { + chunkSize *= ExpressionContainer.BASE_CHUNK_SIZE; } - return childrenArray; - } + if (this.indexedVariables > chunkSize) { + // There are a lot of children, create fake intermediate values that represent chunks #9537 + const numberOfChunks = Math.ceil(this.indexedVariables / chunkSize); + for (let i = 0; i < numberOfChunks; i++) { + const start = this.startOfVariables + i * chunkSize; + const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); + childrenArray.push(new Variable(this.stackFrame, this, this.reference, `[${start}..${start + count - 1}]`, '', null, count, null, true, start)); + } - const start = this.getChildrenInChunks ? this.startOfVariables : undefined; - const count = this.getChildrenInChunks ? this.indexedVariables : undefined; - return this.fetchVariables(session, start, count, 'indexed') - .then(variables => arrays.distinct(childrenArray.concat(variables), child => child.name)); - }); + return childrenArray; + } + + const start = this.getChildrenInChunks ? this.startOfVariables : undefined; + const count = this.getChildrenInChunks ? this.indexedVariables : undefined; + return this.fetchVariables(start, count, 'indexed') + .then(variables => arrays.distinct(childrenArray.concat(variables), child => child.name)); + }); } } @@ -293,17 +228,17 @@ export abstract class ExpressionContainer implements debug.IExpressionContainer return this._value; } - private fetchVariables(session: debug.IRawDebugSession, start: number, count: number, filter: 'indexed' | 'named'): TPromise { - return session.variables({ + private fetchVariables(start: number, count: number, filter: 'indexed' | 'named'): TPromise { + return this.stackFrame.thread.process.session.variables({ variablesReference: this.reference, start, count, filter }).then(response => { return response && response.body && response.body.variables ? arrays.distinct(response.body.variables.filter(v => !!v), v => v.name).map( - v => new Variable(this, v.variablesReference, v.name, v.value, v.namedVariables, v.indexedVariables, v.type) + v => new Variable(this.stackFrame, this, v.variablesReference, v.name, v.value, v.namedVariables, v.indexedVariables, v.type) ) : []; - }, (e: Error) => [new Variable(this, 0, null, e.message, 0, 0, null, false)]); + }, (e: Error) => [new Variable(this.stackFrame, this, 0, null, e.message, 0, 0, null, false)]); } // The adapter explicitly sents the children count of an expression only if there are lots of children which should be chunked. @@ -326,7 +261,7 @@ export class Expression extends ExpressionContainer implements debug.IExpression public type: string; constructor(public name: string, cacheChildren: boolean, id = uuid.generateUuid()) { - super(0, id, cacheChildren, 0, 0); + super(null, 0, id, cacheChildren, 0, 0); this.value = Expression.DEFAULT_VALUE; this.available = false; } @@ -338,6 +273,7 @@ export class Variable extends ExpressionContainer implements debug.IExpression { public errorMessage: string; constructor( + stackFrame: debug.IStackFrame, public parent: debug.IExpressionContainer, reference: number, public name: string, @@ -348,22 +284,37 @@ export class Variable extends ExpressionContainer implements debug.IExpression { public available = true, startOfVariables = 0 ) { - super(reference, `variable:${parent.getId()}:${name}:${reference}`, true, namedVariables, indexedVariables, startOfVariables); + super(stackFrame, reference, `variable:${stackFrame.getId()}:${parent.getId()}:${name}:${reference}`, true, namedVariables, indexedVariables, startOfVariables); this.value = massageValue(value); } + + public setVariable(value: string): TPromise { + return this.stackFrame.thread.process.session.setVariable({ + name: this.name, + value, + variablesReference: this.parent.reference + }).then(response => { + if (response && response.body) { + this.value = response.body.value; + } + // TODO@Isidor notify stackFrame that a change has happened so watch expressions get revelauted + }, err => { + this.errorMessage = err.message; + }); + } } export class Scope extends ExpressionContainer implements debug.IScope { constructor( - threadId: number, + stackFrame: debug.IStackFrame, public name: string, reference: number, public expensive: boolean, namedVariables: number, indexedVariables: number ) { - super(reference, `scope:${threadId}:${name}:${reference}`, true, namedVariables, indexedVariables); + super(stackFrame, reference, `scope:${stackFrame.getId()}:${name}:${reference}`, true, namedVariables, indexedVariables); } } @@ -372,7 +323,7 @@ export class StackFrame implements debug.IStackFrame { private scopes: TPromise; constructor( - public threadId: number, + public thread: debug.IThread, public frameId: number, public source: Source, public name: string, @@ -383,19 +334,223 @@ export class StackFrame implements debug.IStackFrame { } public getId(): string { - return `stackframe:${this.threadId}:${this.frameId}`; + return `stackframe:${this.thread.getId()}:${this.frameId}`; } - public getScopes(debugService: debug.IDebugService): TPromise { - if (!this.scopes && debugService.activeSession) { - this.scopes = debugService.activeSession.scopes({ frameId: this.frameId }).then(response => { + public getScopes(): TPromise { + if (!this.scopes) { + this.scopes = this.thread.process.session.scopes({ frameId: this.frameId }).then(response => { return response && response.body && response.body.scopes ? - response.body.scopes.map(rs => new Scope(this.threadId, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables)) : []; + response.body.scopes.map(rs => new Scope(this, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables)) : []; }, err => []); } return this.scopes; } + + public restart(): TPromise { + return this.thread.process.session.restartFrame({ frameId: this.frameId }); + } + + public completions(text: string, position: Position): TPromise { + if (!this.thread.process.session.configuration.capabilities.supportsCompletionsRequest) { + return TPromise.as([]); + } + + return this.thread.process.session.completions({ + frameId: this.frameId, + text, + column: position.column, + line: position.lineNumber + }).then(response => { + return response && response.body && response.body.targets ? response.body.targets.map(item => ({ + label: item.label, + insertText: item.text || item.label, + type: item.type + })) : []; + }, err => []); + } +} + +export class Thread implements debug.IThread { + private promisedCallStack: TPromise; + private cachedCallStack: debug.IStackFrame[]; + public stoppedDetails: debug.IRawStoppedDetails; + public stopped: boolean; + + constructor(public process: debug.IProcess, public name: string, public threadId: number) { + this.promisedCallStack = undefined; + this.stoppedDetails = undefined; + this.cachedCallStack = undefined; + this.stopped = false; + } + + public getId(): string { + return `thread:${this.process.getId()}:${this.name}:${this.threadId}`; + } + + public clearCallStack(): void { + this.promisedCallStack = undefined; + this.cachedCallStack = undefined; + } + + public getCachedCallStack(): debug.IStackFrame[] { + return this.cachedCallStack; + } + + public getCallStack(getAdditionalStackFrames = false): TPromise { + if (!this.stopped) { + return TPromise.as([]); + } + + if (!this.promisedCallStack) { + this.promisedCallStack = this.getCallStackImpl(0).then(callStack => { + this.cachedCallStack = callStack; + return callStack; + }); + } else if (getAdditionalStackFrames) { + this.promisedCallStack = this.promisedCallStack.then(callStackFirstPart => this.getCallStackImpl(callStackFirstPart.length).then(callStackSecondPart => { + this.cachedCallStack = callStackFirstPart.concat(callStackSecondPart); + return this.cachedCallStack; + })); + } + + return this.promisedCallStack; + } + + private getCallStackImpl(startFrame: number): TPromise { + return this.process.session.stackTrace({ threadId: this.threadId, startFrame, levels: 20 }).then(response => { + if (!response || !response.body) { + return []; + } + + this.stoppedDetails.totalFrames = response.body.totalFrames; + return response.body.stackFrames.map((rsf, level) => { + if (!rsf) { + return new StackFrame(this, 0, new Source({ name: UNKNOWN_SOURCE_LABEL }, false), nls.localize('unknownStack', "Unknown stack location"), undefined, undefined); + } + + return new StackFrame(this, rsf.id, rsf.source ? new Source(rsf.source) : new Source({ name: UNKNOWN_SOURCE_LABEL }, false), rsf.name, rsf.line, rsf.column); + }); + }, (err: Error) => { + this.stoppedDetails.framesErrorMessage = err.message; + return []; + }); + } + + public next(): TPromise { + return this.process.session.next({ threadId: this.threadId }); + } + + public stepIn(): TPromise { + return this.process.session.stepIn({ threadId: this.threadId }); + } + + public stepOut(): TPromise { + return this.process.session.stepOut({ threadId: this.threadId }); + } + + public stepBack(): TPromise { + return this.process.session.stepBack({ threadId: this.threadId }); + } + + public continue(): TPromise { + return this.process.session.continue({ threadId: this.threadId }); + } + + public pause(): TPromise { + return this.process.session.pause({ threadId: this.threadId }); + } +} + +export class Process implements debug.IProcess { + + private threads: { [reference: number]: debug.IThread; }; + + constructor(public name: string, private _session: debug.ISession & debug.ITreeElement) { + this.threads = {}; + } + + public get session(): debug.ISession { + return this._session; + } + + public getThread(threadId: number): debug.IThread { + return this.threads[threadId]; + } + + public getAllThreads(): debug.IThread[] { + return Object.keys(this.threads).map(key => this.threads[key]); + } + + public getId(): string { + return this._session.getId();; + } + + public rawUpdate(data: debug.IRawModelUpdate): void { + + if (data.thread && !this.threads[data.threadId]) { + // A new thread came in, initialize it. + this.threads[data.threadId] = new Thread(this, data.thread.name, data.thread.id); + } + + if (data.stoppedDetails) { + // Set the availability of the threads' callstacks depending on + // whether the thread is stopped or not + if (data.allThreadsStopped) { + Object.keys(this.threads).forEach(ref => { + // Only update the details if all the threads are stopped + // because we don't want to overwrite the details of other + // threads that have stopped for a different reason + this.threads[ref].stoppedDetails = objects.clone(data.stoppedDetails); + this.threads[ref].stopped = true; + this.threads[ref].clearCallStack(); + }); + } else { + // One thread is stopped, only update that thread. + this.threads[data.threadId].stoppedDetails = data.stoppedDetails; + this.threads[data.threadId].clearCallStack(); + this.threads[data.threadId].stopped = true; + } + } + } + + public clearThreads(removeThreads: boolean, reference: number = undefined): void { + if (reference) { + if (this.threads[reference]) { + this.threads[reference].clearCallStack(); + this.threads[reference].stoppedDetails = undefined; + this.threads[reference].stopped = false; + + if (removeThreads) { + delete this.threads[reference]; + } + } + } else { + Object.keys(this.threads).forEach(ref => { + this.threads[ref].clearCallStack(); + this.threads[ref].stoppedDetails = undefined; + this.threads[ref].stopped = false; + }); + + if (removeThreads) { + this.threads = {}; + ExpressionContainer.allValues = {}; + } + } + } + + public sourceIsUnavailable(source: Source): void { + Object.keys(this.threads).forEach(key => { + if (this.threads[key].getCachedCallStack()) { + this.threads[key].getCachedCallStack().forEach(stackFrame => { + if (stackFrame.source.uri.toString() === source.uri.toString()) { + stackFrame.source.available = false; + } + }); + } + }); + } } export class Breakpoint implements debug.IBreakpoint { @@ -457,7 +612,7 @@ export class ExceptionBreakpoint implements debug.IExceptionBreakpoint { export class Model implements debug.IModel { - private threads: { [reference: number]: debug.IThread; }; + private processes: Process[]; private toDispose: lifecycle.IDisposable[]; private replElements: debug.ITreeElement[]; private _onDidChangeBreakpoints: Emitter; @@ -472,7 +627,7 @@ export class Model implements debug.IModel { private exceptionBreakpoints: debug.IExceptionBreakpoint[], private watchExpressions: Expression[] ) { - this.threads = {}; + this.processes = []; this.replElements = []; this.toDispose = []; this._onDidChangeBreakpoints = new Emitter(); @@ -485,6 +640,19 @@ export class Model implements debug.IModel { return 'root'; } + public getProcesses(): Process[] { + return this.processes; + } + + public addProcess(name: string, session: debug.ISession & debug.ITreeElement): void { + this.processes.push(new Process(name, session)); + } + + public removeProcess(id: string): void { + this.processes = this.processes.filter(p => p.getId() !== id); + this._onDidChangeCallStack.fire(); + } + public get onDidChangeBreakpoints(): Event { return this._onDidChangeBreakpoints.event; } @@ -501,35 +669,20 @@ export class Model implements debug.IModel { return this._onDidChangeREPLElements.event; } - public getThreads(): { [reference: number]: debug.IThread; } { - return this.threads; + public rawUpdate(data: debug.IRawModelUpdate): void { + let process = this.processes.filter(p => p.getId() === data.sessionId).pop(); + if (process) { + process.rawUpdate(data); + this._onDidChangeCallStack.fire(); + } } - public clearThreads(removeThreads: boolean, reference: number = undefined): void { - if (reference) { - if (this.threads[reference]) { - this.threads[reference].clearCallStack(); - this.threads[reference].stoppedDetails = undefined; - this.threads[reference].stopped = false; - - if (removeThreads) { - delete this.threads[reference]; - } - } - } else { - Object.keys(this.threads).forEach(ref => { - this.threads[ref].clearCallStack(); - this.threads[ref].stoppedDetails = undefined; - this.threads[ref].stopped = false; - }); - - if (removeThreads) { - this.threads = {}; - ExpressionContainer.allValues = {}; - } + public clearThreads(id: string, removeThreads: boolean, reference: number = undefined): void { + const process = this.processes.filter(p => p.getId() === id).pop(); + if (process) { + process.clearThreads(removeThreads, reference); + this._onDidChangeCallStack.fire(); } - - this._onDidChangeCallStack.fire(); } public getBreakpoints(): debug.IBreakpoint[] { @@ -640,10 +793,10 @@ export class Model implements debug.IModel { return this.replElements; } - public addReplExpression(session: debug.IRawDebugSession, stackFrame: debug.IStackFrame, name: string): TPromise { + public addReplExpression(stackFrame: debug.IStackFrame, name: string): TPromise { const expression = new Expression(name, true); this.addReplElements([expression]); - return evaluateExpression(session, stackFrame, expression, 'repl') + return evaluateExpression(stackFrame, expression, 'repl') .then(() => this._onDidChangeREPLElements.fire()); } @@ -715,7 +868,7 @@ export class Model implements debug.IModel { return this.watchExpressions; } - public addWatchExpression(session: debug.IRawDebugSession, stackFrame: debug.IStackFrame, name: string): TPromise { + public addWatchExpression(stackFrame: debug.IStackFrame, name: string): TPromise { const we = new Expression(name, false); this.watchExpressions.push(we); if (!name) { @@ -723,14 +876,14 @@ export class Model implements debug.IModel { return TPromise.as(null); } - return this.evaluateWatchExpressions(session, stackFrame, we.getId()); + return this.evaluateWatchExpressions(stackFrame, we.getId()); } - public renameWatchExpression(session: debug.IRawDebugSession, stackFrame: debug.IStackFrame, id: string, newName: string): TPromise { + public renameWatchExpression(stackFrame: debug.IStackFrame, id: string, newName: string): TPromise { const filtered = this.watchExpressions.filter(we => we.getId() === id); if (filtered.length === 1) { filtered[0].name = newName; - return evaluateExpression(session, stackFrame, filtered[0], 'watch').then(() => { + return evaluateExpression(stackFrame, filtered[0], 'watch').then(() => { this._onDidChangeWatchExpressions.fire(filtered[0]); }); } @@ -738,19 +891,19 @@ export class Model implements debug.IModel { return TPromise.as(null); } - public evaluateWatchExpressions(session: debug.IRawDebugSession, stackFrame: debug.IStackFrame, id: string = null): TPromise { + public evaluateWatchExpressions(stackFrame: debug.IStackFrame, id: string = null): TPromise { if (id) { const filtered = this.watchExpressions.filter(we => we.getId() === id); if (filtered.length !== 1) { return TPromise.as(null); } - return evaluateExpression(session, stackFrame, filtered[0], 'watch').then(() => { + return evaluateExpression(stackFrame, filtered[0], 'watch').then(() => { this._onDidChangeWatchExpressions.fire(filtered[0]); }); } - return TPromise.join(this.watchExpressions.map(we => evaluateExpression(session, stackFrame, we, 'watch'))).then(() => { + return TPromise.join(this.watchExpressions.map(we => evaluateExpression(stackFrame, we, 'watch'))).then(() => { this._onDidChangeWatchExpressions.fire(); }); } @@ -771,45 +924,7 @@ export class Model implements debug.IModel { } public sourceIsUnavailable(source: Source): void { - Object.keys(this.threads).forEach(key => { - if (this.threads[key].getCachedCallStack()) { - this.threads[key].getCachedCallStack().forEach(stackFrame => { - if (stackFrame.source.uri.toString() === source.uri.toString()) { - stackFrame.source.available = false; - } - }); - } - }); - - this._onDidChangeCallStack.fire(); - } - - public rawUpdate(data: debug.IRawModelUpdate): void { - if (data.thread && !this.threads[data.threadId]) { - // A new thread came in, initialize it. - this.threads[data.threadId] = new Thread(data.thread.name, data.thread.id); - } - - if (data.stoppedDetails) { - // Set the availability of the threads' callstacks depending on - // whether the thread is stopped or not - if (data.allThreadsStopped) { - Object.keys(this.threads).forEach(ref => { - // Only update the details if all the threads are stopped - // because we don't want to overwrite the details of other - // threads that have stopped for a different reason - this.threads[ref].stoppedDetails = objects.clone(data.stoppedDetails); - this.threads[ref].stopped = true; - this.threads[ref].clearCallStack(); - }); - } else { - // One thread is stopped, only update that thread. - this.threads[data.threadId].stoppedDetails = data.stoppedDetails; - this.threads[data.threadId].clearCallStack(); - this.threads[data.threadId].stopped = true; - } - } - + this.processes.forEach(p => p.sourceIsUnavailable(source)); this._onDidChangeCallStack.fire(); } diff --git a/src/vs/workbench/parts/debug/common/debugSource.ts b/src/vs/workbench/parts/debug/common/debugSource.ts index 4deb6bacdcb0254bbfda0dd59b51a15721c3caa5..c487c81b00ab16a8776ad1f682b10b54b50f3890 100644 --- a/src/vs/workbench/parts/debug/common/debugSource.ts +++ b/src/vs/workbench/parts/debug/common/debugSource.ts @@ -38,12 +38,13 @@ export class Source { public static toRawSource(uri: uri, model: IModel): DebugProtocol.Source { if (model) { // first try to find the raw source amongst the stack frames - since that represenation has more data (source reference), - const threads = model.getThreads(); - for (let threadId in threads) { - if (threads.hasOwnProperty(threadId) && threads[threadId].getCachedCallStack()) { - const found = threads[threadId].getCachedCallStack().filter(sf => sf.source.uri.toString() === uri.toString()).pop(); - if (found) { - return found.source.raw; + for (let process of model.getProcesses()) { + for (let thread of process.getAllThreads()) { + if (thread.getCachedCallStack()) { + const found = thread.getCachedCallStack().filter(sf => sf.source.uri.toString() === uri.toString()).pop(); + if (found) { + return found.source.raw; + } } } } diff --git a/src/vs/workbench/parts/debug/common/debugViewModel.ts b/src/vs/workbench/parts/debug/common/debugViewModel.ts index 5de4b50287f4ac012eb267601c1845785baa1816..a219cefa1c0e93fdca2ace1fd7981cc03faae8ea 100644 --- a/src/vs/workbench/parts/debug/common/debugViewModel.ts +++ b/src/vs/workbench/parts/debug/common/debugViewModel.ts @@ -8,19 +8,20 @@ import debug = require('vs/workbench/parts/debug/common/debug'); export class ViewModel implements debug.IViewModel { - private focusedStackFrame: debug.IStackFrame; - private focusedThread: debug.IThread; + private _focusedStackFrame: debug.IStackFrame; private selectedExpression: debug.IExpression; private selectedFunctionBreakpoint: debug.IFunctionBreakpoint; private _onDidFocusStackFrame: Emitter; private _onDidSelectExpression: Emitter; private _onDidSelectFunctionBreakpoint: Emitter; + private _onDidSelectConfigurationName: Emitter; public changedWorkbenchViewState: boolean; - constructor() { + constructor(private _selectedConfigurationName: string) { this._onDidFocusStackFrame = new Emitter(); this._onDidSelectExpression = new Emitter(); this._onDidSelectFunctionBreakpoint = new Emitter(); + this._onDidSelectConfigurationName = new Emitter(); this.changedWorkbenchViewState = false; } @@ -28,13 +29,20 @@ export class ViewModel implements debug.IViewModel { return 'root'; } - public getFocusedStackFrame(): debug.IStackFrame { - return this.focusedStackFrame; + public get focusedProcess(): debug.IProcess { + return this._focusedStackFrame ? this._focusedStackFrame.thread.process : null; } - public setFocusedStackFrame(focusedStackFrame: debug.IStackFrame, focusedThread: debug.IThread): void { - this.focusedStackFrame = focusedStackFrame; - this.focusedThread = focusedThread; + public get focusedThread(): debug.IThread { + return this._focusedStackFrame ? this._focusedStackFrame.thread : null; + } + + public get focusedStackFrame(): debug.IStackFrame { + return this._focusedStackFrame; + } + + public setFocusedStackFrame(focusedStackFrame: debug.IStackFrame): void { + this._focusedStackFrame = focusedStackFrame; this._onDidFocusStackFrame.fire(focusedStackFrame); } @@ -42,10 +50,6 @@ export class ViewModel implements debug.IViewModel { return this._onDidFocusStackFrame.event; } - public getFocusedThreadId(): number { - return this.focusedThread ? this.focusedThread.threadId : 0; - } - public getSelectedExpression(): debug.IExpression { return this.selectedExpression; } @@ -71,4 +75,17 @@ export class ViewModel implements debug.IViewModel { public get onDidSelectFunctionBreakpoint(): Event { return this._onDidSelectFunctionBreakpoint.event; } + + public get selectedConfigurationName(): string { + return this._selectedConfigurationName; + } + + public setSelectedConfigurationName(configurationName: string): void { + this._selectedConfigurationName = configurationName; + this._onDidSelectConfigurationName.fire(configurationName); + } + + public get onDidSelectConfigurationName(): Event { + return this._onDidSelectConfigurationName.event; + } } 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 efdf4edbd2dd79eeaaa72a02ead26fe6a19df52b..97cbde992999ace6309a40ae96a6223997b90502 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -7,7 +7,6 @@ import 'vs/css!../browser/media/debug.contribution'; import 'vs/css!../browser/media/debugHover'; import nls = require('vs/nls'); import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { TPromise } from 'vs/base/common/winjs.base'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import platform = require('vs/platform/platform'); import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -137,19 +136,9 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(FocusReplAction, Focus KeybindingsRegistry.registerCommandAndKeybindingRule({ id: '_workbench.startDebug', weight: KeybindingsRegistry.WEIGHT.workbenchContrib(0), - handler(accessor: ServicesAccessor, configuration: any) { + handler(accessor: ServicesAccessor, configurationOrName: any) { const debugService = accessor.get(debug.IDebugService); - if (typeof configuration === 'string') { - const configurationManager = debugService.getConfigurationManager(); - return configurationManager.setConfiguration(configuration) - .then(() => { - return configurationManager.configuration ? debugService.createSession(false) - : TPromise.wrapError(new Error(nls.localize('launchConfigDoesNotExist', "Launch configuration '{0}' does not exist.", configuration))); - }); - } - - const noDebug = configuration && !!configuration.noDebug; - return debugService.createSession(noDebug, configuration); + return debugService.createProcess(configurationOrName); }, when: debug.CONTEXT_NOT_IN_DEBUG_MODE, primary: undefined diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 2d21d7791e309b96bb7df4fab181923238951601..4f809ddf4e88e5359e3a937e7862d5dd9149f455 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -117,7 +117,7 @@ export class DebugEditorContribution implements debug.IDebugEditorContribution { this.toDispose.push(this.editor.onMouseLeave((e: editorbrowser.IEditorMouseEvent) => { this.ensureBreakpointHintDecoration(-1); })); - this.toDispose.push(this.debugService.onDidChangeState(state => this.onDebugStateUpdate(state))); + this.toDispose.push(this.debugService.onDidChangeState(() => this.onDebugStateUpdate())); // hover listeners & hover widget this.toDispose.push(this.editor.onMouseDown((e: editorbrowser.IEditorMouseEvent) => this.onEditorMouseDown(e))); @@ -159,7 +159,8 @@ export class DebugEditorContribution implements debug.IDebugEditorContribution { this.breakpointHintDecoration = this.editor.deltaDecorations(this.breakpointHintDecoration, newDecoration); } - private onDebugStateUpdate(state: debug.State): void { + private onDebugStateUpdate(): void { + const state = this.debugService.state; if (state !== debug.State.Stopped) { this.hideHoverWidget(); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index 3f9ae15cf1488c212fa791201d599c50c23eb1ac..ddf424a8589b5b7ef04c003036a9ff98fdc38523 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -55,7 +55,7 @@ export class DebugHoverWidget implements editorbrowser.IContentWidget { this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree')); this.treeContainer.setAttribute('role', 'tree'); this.tree = new Tree(this.treeContainer, { - dataSource: new viewer.VariablesDataSource(this.debugService), + dataSource: new viewer.VariablesDataSource(), renderer: this.instantiationService.createInstance(VariablesHoverRenderer), controller: new DebugHoverController(editor) }, debugTreeOptions); @@ -148,19 +148,19 @@ export class DebugHoverWidget implements editorbrowser.IContentWidget { public showAt(range: Range, hoveringOver: string, focus: boolean): TPromise { const pos = range.getStartPosition(); - const focusedStackFrame = this.debugService.getViewModel().getFocusedStackFrame(); + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!hoveringOver || !focusedStackFrame || (focusedStackFrame.source.uri.toString() !== this.editor.getModel().uri.toString())) { return; } - const session = this.debugService.activeSession; + const process = this.debugService.getViewModel().focusedProcess; const lineContent = this.editor.getModel().getLineContent(pos.lineNumber); const expressionRange = this.getExactExpressionRange(lineContent, range); // use regex to extract the sub-expression #9821 const matchingExpression = lineContent.substring(expressionRange.startColumn - 1, expressionRange.endColumn); - const evaluatedExpression = session.configuration.capabilities.supportsEvaluateForHovers ? - evaluateExpression(session, focusedStackFrame, new Expression(matchingExpression, true), 'hover') : + const evaluatedExpression = process.session.configuration.capabilities.supportsEvaluateForHovers ? + evaluateExpression(focusedStackFrame, new Expression(matchingExpression, true), 'hover') : this.findExpressionInStackFrame(matchingExpression.split('.').map(word => word.trim()).filter(word => !!word)); return evaluatedExpression.then(expression => { @@ -198,7 +198,7 @@ export class DebugHoverWidget implements editorbrowser.IContentWidget { } private findExpressionInStackFrame(namesToFind: string[]): TPromise { - return this.debugService.getViewModel().getFocusedStackFrame().getScopes(this.debugService) + return this.debugService.getViewModel().focusedStackFrame.getScopes() // no expensive scopes .then(scopes => scopes.filter(scope => !scope.expensive)) .then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind)))) diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index ebdc236558edb9902b1566ba4e5182dc8283e128..834ada45019d42f50ecd65f16737f0f29431b2a4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -7,8 +7,9 @@ import nls = require('vs/nls'); import lifecycle = require('vs/base/common/lifecycle'); import { guessMimeTypes } from 'vs/base/common/mime'; import Event, { Emitter } from 'vs/base/common/event'; +import * as strings from 'vs/base/common/strings'; +import uuid = require('vs/base/common/uuid'); import uri from 'vs/base/common/uri'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { Action } from 'vs/base/common/actions'; import arrays = require('vs/base/common/arrays'); import types = require('vs/base/common/types'); @@ -16,9 +17,8 @@ import errors = require('vs/base/common/errors'); import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import aria = require('vs/base/browser/ui/aria/aria'); +import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; import editorbrowser = require('vs/editor/browser/editorBrowser'); -import { ISuggestion } from 'vs/editor/common/modes'; -import { Position } from 'vs/editor/common/core/position'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -54,7 +54,6 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IWindowService, IBroadcast } from 'vs/workbench/services/window/electron-browser/windowService'; import { ILogEntry, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/workbench/services/extensions/electron-browser/extensionHost'; import { ipcRenderer as ipc } from 'electron'; -import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -66,16 +65,15 @@ const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname'; export class DebugService implements debug.IDebugService { public _serviceBrand: any; - private _state: debug.State; - private _onDidChangeState: Emitter; - private session: RawDebugSession; + private sessionStates: { [id: string]: debug.State }; + private _onDidChangeState: Emitter; private model: model.Model; private viewModel: viewmodel.ViewModel; private configurationManager: ConfigurationManager; private customTelemetryService: ITelemetryService; private lastTaskEvent: TaskEvent; private toDispose: lifecycle.IDisposable[]; - private toDisposeOnSessionEnd: lifecycle.IDisposable[]; + private toDisposeOnSessionEnd: { [id: string]: lifecycle.IDisposable[] }; private inDebugMode: IContextKey; private breakpointsToSendOnResourceSaved: { [uri: string]: boolean }; @@ -102,22 +100,18 @@ export class DebugService implements debug.IDebugService { @IConfigurationService private configurationService: IConfigurationService ) { this.toDispose = []; - this.toDisposeOnSessionEnd = []; - this.session = null; + this.toDisposeOnSessionEnd = {}; this.breakpointsToSendOnResourceSaved = {}; - this._state = debug.State.Inactive; - this._onDidChangeState = new Emitter(); + this._onDidChangeState = new Emitter(); + this.sessionStates = {}; - if (!this.contextService.getWorkspace()) { - this._state = debug.State.Disabled; - } - this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE, 'null')); + this.configurationManager = this.instantiationService.createInstance(ConfigurationManager); this.inDebugMode = debug.CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.model = new model.Model(this.loadBreakpoints(), this.storageService.getBoolean(DEBUG_BREAKPOINTS_ACTIVATED_KEY, StorageScope.WORKSPACE, true), this.loadFunctionBreakpoints(), this.loadExceptionBreakpoints(), this.loadWatchExpressions()); this.toDispose.push(this.model); - this.viewModel = new viewmodel.ViewModel(); + this.viewModel = new viewmodel.ViewModel(this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE, 'null')); this.registerListeners(eventService, lifecycleService); } @@ -148,20 +142,23 @@ export class DebugService implements debug.IDebugService { private onBroadcast(broadcast: IBroadcast): void { // attach: PH is ready to be attached to + // TODO@Isidor this is a hack to just get any 'extensionHost' session. + // Optimally the broadcast would contain the id of the session + // We are only intersted if we have an active debug session for extensionHost + const session = this.model.getProcesses().map(p => p.session).filter(s => s.configuration.type === 'extensionHost').pop(); if (broadcast.channel === EXTENSION_ATTACH_BROADCAST_CHANNEL) { - this.rawAttach(broadcast.payload.port); + this.rawAttach(session, broadcast.payload.port); return; } if (broadcast.channel === EXTENSION_TERMINATE_BROADCAST_CHANNEL) { - this.onSessionEnd(); + this.onSessionEnd(session); return; } // from this point on we require an active session - let session = this.activeSession; - if (!session || session.configuration.type !== 'extensionHost') { - return; // we are only intersted if we have an active debug session for extensionHost + if (!session) { + return; } // a plugin logged output, show it inside the REPL @@ -237,8 +234,8 @@ export class DebugService implements debug.IDebugService { } private registerSessionListeners(session: RawDebugSession): void { - this.toDisposeOnSessionEnd.push(session); - this.toDisposeOnSessionEnd.push(session.onDidInitialize(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session); + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidInitialize(event => { aria.status(nls.localize('debuggingStarted', "Debugging started.")); const sendConfigurationDone = () => { if (session && session.configuration.capabilities.supportsConfigurationDoneRequest) { @@ -252,61 +249,78 @@ export class DebugService implements debug.IDebugService { } }; - this.sendAllBreakpoints().done(sendConfigurationDone, sendConfigurationDone); + this.sendAllBreakpoints(session).done(sendConfigurationDone, sendConfigurationDone); })); - this.toDisposeOnSessionEnd.push(session.onDidStop(event => { - this.setStateAndEmit(debug.State.Stopped); + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidStop(event => { + this.setStateAndEmit(session.getId(), debug.State.Stopped); const threadId = event.body.threadId; - this.getThreadData().done(() => { + session.threads().then(response => { + if (!response || !response.body || !response.body.threads) { + return; + } + + const thread = response.body.threads.filter(t => t.id === threadId).pop(); this.model.rawUpdate({ + sessionId: session.getId(), + thread, threadId, stoppedDetails: event.body, allThreadsStopped: event.body.allThreadsStopped }); - const thread = this.model.getThreads()[threadId]; - thread.getCallStack(this).then(callStack => { + const process = this.model.getProcesses().filter(p => p.getId() === session.getId()).pop(); + process.getThread(threadId).getCallStack().then(callStack => { if (callStack.length > 0) { // focus first stack frame from top that has source location const stackFrameToFocus = arrays.first(callStack, sf => sf.source && sf.source.available, callStack[0]); - this.setFocusedStackFrameAndEvaluate(stackFrameToFocus, thread).done(null, errors.onUnexpectedError); + this.setFocusedStackFrameAndEvaluate(stackFrameToFocus).done(null, errors.onUnexpectedError); this.windowService.getWindow().focus(); aria.alert(nls.localize('debuggingPaused', "Debugging paused, reason {0}, {1} {2}", event.body.reason, stackFrameToFocus.source ? stackFrameToFocus.source.name : '', stackFrameToFocus.lineNumber)); return this.openOrRevealSource(stackFrameToFocus.source, stackFrameToFocus.lineNumber, false, false); } else { - this.setFocusedStackFrameAndEvaluate(null, thread).done(null, errors.onUnexpectedError); + this.setFocusedStackFrameAndEvaluate(null).done(null, errors.onUnexpectedError); } }); }, errors.onUnexpectedError); })); - this.toDisposeOnSessionEnd.push(session.onDidThread(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidThread(event => { if (event.body.reason === 'started') { - this.getThreadData().done(null, errors.onUnexpectedError); + session.threads().done(response => { + if (response && response.body && response.body.threads) { + response.body.threads.forEach(thread => + this.model.rawUpdate({ + sessionId: session.getId(), + threadId: thread.id, + thread + })); + } + }, errors.onUnexpectedError); } else if (event.body.reason === 'exited') { - this.model.clearThreads(true, event.body.threadId); + this.model.clearThreads(session.getId(), true, event.body.threadId); } })); - this.toDisposeOnSessionEnd.push(session.onDidTerminateDebugee(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidTerminateDebugee(event => { aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); if (session && session.getId() === event.body.sessionId) { if (event.body && typeof event.body.restart === 'boolean' && event.body.restart) { - this.restartSession().done(null, err => this.messageService.show(severity.Error, err.message)); + const process = this.model.getProcesses().filter(p => p.getId() === session.getId()).pop(); + this.restartProcess(process).done(null, err => this.messageService.show(severity.Error, err.message)); } else { session.disconnect().done(null, errors.onUnexpectedError); } } })); - this.toDisposeOnSessionEnd.push(session.onDidContinued(event => { - this.lazyTransitionToRunningState(event.body.allThreadsContinued ? undefined : event.body.threadId); + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidContinued(event => { + this.transitionToRunningState(session, event.body.allThreadsContinued ? undefined : event.body.threadId); })); - this.toDisposeOnSessionEnd.push(session.onDidOutput(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidOutput(event => { if (event.body && event.body.category === 'telemetry') { // only log telemetry events from debug adapter if the adapter provided the telemetry key // and the user opted in telemetry @@ -318,7 +332,7 @@ export class DebugService implements debug.IDebugService { } })); - this.toDisposeOnSessionEnd.push(session.onDidBreakpoint(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidBreakpoint(event => { const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; const breakpoint = this.model.getBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); if (breakpoint) { @@ -331,13 +345,13 @@ export class DebugService implements debug.IDebugService { } })); - this.toDisposeOnSessionEnd.push(session.onDidExitAdapter(event => { + this.toDisposeOnSessionEnd[session.getId()].push(session.onDidExitAdapter(event => { // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 - if (session && session.configuration.type === 'extensionHost' && this._state === debug.State.RunningNoDebug) { + if (session && session.configuration.type === 'extensionHost' && this.sessionStates[session.getId()] === debug.State.RunningNoDebug) { ipc.send('vscode:closeExtensionHostWindow', this.contextService.getWorkspace().resource.fsPath); } if (session && session.getId() === event.body.sessionId) { - this.onSessionEnd(); + this.onSessionEnd(session); } })); } @@ -347,14 +361,6 @@ export class DebugService implements debug.IDebugService { this.appendReplOutput(event.body.output, outputSeverity); } - private getThreadData(): TPromise { - return this.session.threads().then(response => { - if (response && response.body && response.body.threads) { - response.body.threads.forEach(thread => this.model.rawUpdate({ threadId: thread.id, thread })); - } - }); - } - private loadBreakpoints(): debug.IBreakpoint[] { let result: debug.IBreakpoint[]; try { @@ -401,30 +407,39 @@ export class DebugService implements debug.IDebugService { } public get state(): debug.State { - return this._state; + if (!this.contextService.getWorkspace()) { + return debug.State.Disabled; + } + + const focusedProcess = this.viewModel.focusedProcess; + if (focusedProcess) { + return this.sessionStates[focusedProcess.getId()]; + } + const processes = this.model.getProcesses(); + if (processes.length > 0) { + return this.sessionStates[processes[0].getId()]; + } + + return debug.State.Inactive; } - public get onDidChangeState(): Event { + public get onDidChangeState(): Event { return this._onDidChangeState.event; } - private setStateAndEmit(newState: debug.State): void { - this._state = newState; - this._onDidChangeState.fire(newState); + private setStateAndEmit(sessionId: string, newState: debug.State): void { + this.sessionStates[sessionId] = newState; + this._onDidChangeState.fire(); } public get enabled(): boolean { return !!this.contextService.getWorkspace(); } - public setFocusedStackFrameAndEvaluate(focusedStackFrame: debug.IStackFrame, thread?: debug.IThread): TPromise { - if (!thread && focusedStackFrame) { - thread = this.model.getThreads()[focusedStackFrame.threadId]; - } - - this.viewModel.setFocusedStackFrame(focusedStackFrame, thread); + public setFocusedStackFrameAndEvaluate(focusedStackFrame: debug.IStackFrame): TPromise { + this.viewModel.setFocusedStackFrame(focusedStackFrame); if (focusedStackFrame) { - return this.model.evaluateWatchExpressions(this.session, focusedStackFrame); + return this.model.evaluateWatchExpressions(focusedStackFrame); } else { this.model.clearWatchExpressionValues(); return TPromise.as(null); @@ -485,10 +500,9 @@ export class DebugService implements debug.IDebugService { public addReplExpression(name: string): TPromise { this.telemetryService.publicLog('debugService/addReplExpression'); - const focussedStackFrame = this.viewModel.getFocusedStackFrame(); - return this.model.addReplExpression(this.session, focussedStackFrame, name) + return this.model.addReplExpression(this.viewModel.focusedStackFrame, name) // Evaluate all watch expressions again since repl evaluation might have changed some. - .then(() => this.setFocusedStackFrameAndEvaluate(focussedStackFrame)); + .then(() => this.setFocusedStackFrameAndEvaluate(this.viewModel.focusedStackFrame)); } public logToRepl(value: string | { [key: string]: any }, severity?: severity): void { @@ -503,48 +517,26 @@ export class DebugService implements debug.IDebugService { this.model.removeReplExpressions(); } - public setVariable(variable: debug.IExpression, value: string): TPromise { - if (!this.session || !(variable instanceof model.Variable)) { - return TPromise.as(null); - } - - return this.session.setVariable({ - name: variable.name, - value, - variablesReference: (variable).parent.reference - }).then(response => { - if (response && response.body) { - variable.value = response.body.value; - } - // Evaluate all watch expressions again since changing variable value might have changed some #8118. - return this.setFocusedStackFrameAndEvaluate(this.viewModel.getFocusedStackFrame()); - }, err => { - (variable).errorMessage = err.message; - }); - } - public addWatchExpression(name: string): TPromise { - return this.model.addWatchExpression(this.session, this.viewModel.getFocusedStackFrame(), name); + return this.model.addWatchExpression(this.viewModel.focusedStackFrame, name); } public renameWatchExpression(id: string, newName: string): TPromise { - return this.model.renameWatchExpression(this.session, this.viewModel.getFocusedStackFrame(), id, newName); + return this.model.renameWatchExpression(this.viewModel.focusedStackFrame, id, newName); } public removeWatchExpressions(id?: string): void { this.model.removeWatchExpressions(id); } - public createSession(noDebug: boolean, configuration?: debug.IConfig): TPromise { + public createProcess(configurationOrName: debug.IConfig | string): TPromise { this.removeReplExpressions(); return this.textFileService.saveAll() // make sure all dirty files are saved .then(() => this.configurationService.reloadConfiguration() // make sure configuration is up to date .then(() => this.extensionService.onReady() - .then(() => this.configurationManager.setConfiguration(configuration || this.configurationManager.configurationName) - .then(() => this.configurationManager.resolveInteractiveVariables()) - .then(resolvedConfiguration => { - configuration = resolvedConfiguration; + .then(() => this.configurationManager.getConfiguration(configurationOrName) + .then(configuration => { if (!configuration) { return this.configurationManager.openConfigFile(false).then(openend => { if (openend) { @@ -555,9 +547,11 @@ export class DebugService implements debug.IDebugService { if (configuration.silentlyAbort) { return; } + if (strings.equalsIgnoreCase(configuration.type, 'composite') && configuration.configurationNames) { + return TPromise.join(configuration.configurationNames.map(name => this.createProcess(name))); + } - configuration.noDebug = noDebug; - if (!this.configurationManager.adapter) { + if (!this.configurationManager.getAdapter(configuration.type)) { return configuration.type ? TPromise.wrapError(new Error(nls.localize('debugTypeNotSupported', "Configured debug type '{0}' is not supported.", configuration.type))) : TPromise.wrapError(errors.create(nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration."), { actions: [this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL), CloseAction] })); @@ -568,7 +562,7 @@ export class DebugService implements debug.IDebugService { const successExitCode = taskSummary && taskSummary.exitCode === 0; const failureExitCode = taskSummary && taskSummary.exitCode !== undefined && taskSummary.exitCode !== 0; if (successExitCode || (errorCount === 0 && !failureExitCode)) { - return this.doCreateSession(configuration); + return this.doCreateProcess(configuration); } this.messageService.show(severity.Error, { @@ -577,7 +571,7 @@ export class DebugService implements debug.IDebugService { nls.localize('preLaunchTaskExitCode', "The preLaunchTask '{0}' terminated with exit code {1}.", configuration.preLaunchTask, taskSummary.exitCode), actions: [new Action('debug.continue', nls.localize('debugAnyway', "Debug Anyway"), null, true, () => { this.messageService.hideAll(); - return this.doCreateSession(configuration); + return this.doCreateProcess(configuration); }), CloseAction] }); }, (err: TaskError) => { @@ -593,8 +587,9 @@ export class DebugService implements debug.IDebugService { })))); } - private doCreateSession(configuration: debug.IExtHostConfig): TPromise { - this.setStateAndEmit(debug.State.Initializing); + private doCreateProcess(configuration: debug.IExtHostConfig): TPromise { + const sessionId = uuid.generateUuid(); + this.setStateAndEmit(sessionId, debug.State.Initializing); return this.telemetryService.getTelemetryInfo().then(info => { const telemetryInfo: { [key: string]: string } = Object.create(null); @@ -602,12 +597,14 @@ export class DebugService implements debug.IDebugService { telemetryInfo['common.vscodesessionid'] = info.sessionId; return telemetryInfo; }).then(data => { - const { aiKey, type } = this.configurationManager.adapter; - const publisher = this.configurationManager.adapter.extensionDescription.publisher; + const adapter = this.configurationManager.getAdapter(configuration.type); + const { aiKey, type } = adapter; + const publisher = adapter.extensionDescription.publisher; this.customTelemetryService = null; + let client: TelemetryClient; if (aiKey) { - const client = new Client( + client = new TelemetryClient( uri.parse(require.toUrl('bootstrap')).fsPath, { serverName: 'Debug Telemetry', @@ -624,14 +621,18 @@ export class DebugService implements debug.IDebugService { const channel = client.getChannel('telemetryAppender'); const appender = new TelemetryAppenderClient(channel); - this.toDisposeOnSessionEnd.push(client); this.customTelemetryService = new TelemetryService({ appender }, this.configurationService); } - this.session = this.instantiationService.createInstance(RawDebugSession, configuration.debugServer, this.configurationManager.adapter, this.customTelemetryService); - this.registerSessionListeners(this.session); + const session = this.instantiationService.createInstance(RawDebugSession, sessionId, configuration.debugServer, adapter, this.customTelemetryService); + this.model.addProcess(configuration.name, session); + this.toDisposeOnSessionEnd[session.getId()] = []; + if (client) { + this.toDisposeOnSessionEnd[session.getId()].push(client); + } + this.registerSessionListeners(session); - return this.session.initialize({ + return session.initialize({ adapterID: configuration.type, pathFormat: 'path', linesStartAt1: true, @@ -640,14 +641,14 @@ export class DebugService implements debug.IDebugService { supportsVariablePaging: true, // #9537 supportsRunInTerminalRequest: true // #10574 }).then((result: DebugProtocol.InitializeResponse) => { - if (!this.session) { + if (session.disconnected) { return TPromise.wrapError(new Error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly"))); } - this.model.setExceptionBreakpoints(this.session.configuration.capabilities.exceptionBreakpointFilters); - return configuration.request === 'attach' ? this.session.attach(configuration) : this.session.launch(configuration); + this.model.setExceptionBreakpoints(session.configuration.capabilities.exceptionBreakpointFilters); + return configuration.request === 'attach' ? session.attach(configuration) : session.launch(configuration); }).then((result: DebugProtocol.Response) => { - if (!this.session) { + if (session.disconnected) { return TPromise.as(null); } @@ -667,15 +668,15 @@ export class DebugService implements debug.IDebugService { } this.extensionService.activateByEvent(`onDebug:${configuration.type}`).done(null, errors.onUnexpectedError); this.inDebugMode.set(true); - this.lazyTransitionToRunningState(); + this.transitionToRunningState(session); this.telemetryService.publicLog('debugSessionStart', { type: configuration.type, breakpointCount: this.model.getBreakpoints().length, exceptionBreakpoints: this.model.getExceptionBreakpoints(), watchExpressionsCount: this.model.getWatchExpressions().length, - extensionName: `${this.configurationManager.adapter.extensionDescription.publisher}.${this.configurationManager.adapter.extensionDescription.name}`, - isBuiltin: this.configurationManager.adapter.extensionDescription.isBuiltin + extensionName: `${adapter.extensionDescription.publisher}.${adapter.extensionDescription.name}`, + isBuiltin: adapter.extensionDescription.isBuiltin }); }).then(undefined, (error: any) => { if (error instanceof Error && error.message === 'Canceled') { @@ -684,9 +685,9 @@ export class DebugService implements debug.IDebugService { } this.telemetryService.publicLog('debugMisconfiguration', { type: configuration ? configuration.type : undefined }); - this.setStateAndEmit(debug.State.Inactive); - if (this.session) { - this.session.disconnect().done(null, errors.onUnexpectedError); + this.setStateAndEmit(session.getId(), debug.State.Inactive); + if (!session.disconnected) { + session.disconnect().done(null, errors.onUnexpectedError); } // Show the repl if some error got logged there #5870 if (this.model.getReplElements().length > 0) { @@ -744,61 +745,57 @@ export class DebugService implements debug.IDebugService { }); } - private rawAttach(port: number): TPromise { - if (this.session) { - return this.session.attach({ port }); + private rawAttach(session: RawDebugSession, port: number): TPromise { + if (session) { + return session.attach({ port }); } - this.setStateAndEmit(debug.State.Initializing); - const configuration = this.configurationManager.configuration; - return this.doCreateSession({ - type: configuration.type, - request: 'attach', - port, - sourceMaps: configuration.sourceMaps, - outDir: configuration.outDir, - debugServer: configuration.debugServer - }); + this.setStateAndEmit(session.getId(), debug.State.Initializing); + return this.configurationManager.getConfiguration(this.viewModel.selectedConfigurationName).then((configuration: debug.IExtHostConfig) => + this.doCreateProcess({ + type: configuration.type, + request: 'attach', + port, + sourceMaps: configuration.sourceMaps, + outDir: configuration.outDir, + debugServer: configuration.debugServer + }) + ); } - public restartSession(): TPromise { - return this.session ? this.session.disconnect(true).then(() => + public restartProcess(process: debug.IProcess): TPromise { + return process ? process.session.disconnect(true).then(() => new TPromise((c, e) => { setTimeout(() => { - this.createSession(false, null).then(() => c(null), err => e(err)); + this.createProcess(process.name).then(() => c(null), err => e(err)); }, 300); }) - ) : this.createSession(false, null); - } - - public get activeSession(): debug.IRawDebugSession { - return this.session; + ) : this.createProcess(this.viewModel.selectedConfigurationName); } - private onSessionEnd(): void { - if (this.session) { + private onSessionEnd(session: RawDebugSession): void { + if (session) { const bpsExist = this.model.getBreakpoints().length > 0; this.telemetryService.publicLog('debugSessionStop', { - type: this.session.configuration.type, - success: this.session.emittedStopped || !bpsExist, - sessionLengthInSeconds: this.session.getLengthInSeconds(), + type: session.configuration.type, + success: session.emittedStopped || !bpsExist, + sessionLengthInSeconds: session.getLengthInSeconds(), breakpointCount: this.model.getBreakpoints().length, watchExpressionsCount: this.model.getWatchExpressions().length }); } - this.session = null; try { - this.toDisposeOnSessionEnd = lifecycle.dispose(this.toDisposeOnSessionEnd); + this.toDisposeOnSessionEnd[session.getId()] = lifecycle.dispose(this.toDisposeOnSessionEnd[session.getId()]); } catch (e) { // an internal module might be open so the dispose can throw -> ignore and continue with stop session. } this.partService.removeClass('debugging'); - this.model.clearThreads(true); + this.model.removeProcess(session.getId()); this.setFocusedStackFrameAndEvaluate(null).done(null, errors.onUnexpectedError); - this.setStateAndEmit(debug.State.Inactive); + this.setStateAndEmit(session.getId(), debug.State.Inactive); // set breakpoints back to unverified since the session ended. // source reference changes across sessions, so we do not use it to persist the source. @@ -845,22 +842,23 @@ export class DebugService implements debug.IDebugService { } } + const process = this.viewModel.focusedProcess; if (source.inMemory) { // internal module - if (source.reference !== 0 && this.session && source.available) { - return this.session.source({ sourceReference: source.reference }).then(response => { + if (source.reference !== 0 && process && source.available) { + return process.session.source({ sourceReference: source.reference }).then(response => { const mime = response && response.body && response.body.mimeType ? response.body.mimeType : guessMimeTypes(source.name)[0]; const inputValue = response && response.body ? response.body.content : ''; - return this.getDebugStringEditorInput(source, inputValue, mime); + return this.getDebugStringEditorInput(process, source, inputValue, mime); }, (err: DebugProtocol.ErrorResponse) => { // Display the error from debug adapter using a temporary editor #8836 - return this.getDebugErrorEditorInput(source, err.message); + return this.getDebugErrorEditorInput(process, source, err.message); }).then(editorInput => { return this.editorService.openEditor(editorInput, { preserveFocus, selection: { startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 } }, sideBySide); }); } - return this.sourceIsUnavailable(source, sideBySide); + return this.sourceIsUnavailable(process, source, sideBySide); } return this.fileService.resolveFile(source.uri).then(() => @@ -875,13 +873,13 @@ export class DebugService implements debug.IDebugService { }, preserveFocus: preserveFocus } - }, sideBySide), err => this.sourceIsUnavailable(source, sideBySide) + }, sideBySide), err => this.sourceIsUnavailable(process, source, sideBySide) ); } - private sourceIsUnavailable(source: Source, sideBySide: boolean): TPromise { + private sourceIsUnavailable(process: debug.IProcess, source: Source, sideBySide: boolean): TPromise { this.model.sourceIsUnavailable(source); - const editorInput = this.getDebugErrorEditorInput(source, nls.localize('debugSourceNotAvailable', "Source {0} is not available.", source.name)); + const editorInput = this.getDebugErrorEditorInput(process, source, nls.localize('debugSourceNotAvailable', "Source {0} is not available.", source.name)); return this.editorService.openEditor(editorInput, { preserveFocus: true }, sideBySide); } @@ -890,209 +888,125 @@ export class DebugService implements debug.IDebugService { return this.configurationManager; } - public next(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.next({ threadId }).then(() => { - this.lazyTransitionToRunningState(threadId); - }); - } - - public stepIn(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.stepIn({ threadId }).then(() => { - this.lazyTransitionToRunningState(threadId); - }); - } - - public stepOut(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.stepOut({ threadId }).then(() => { - this.lazyTransitionToRunningState(threadId); - }); - } - - public stepBack(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.stepBack({ threadId }).then(() => { - this.lazyTransitionToRunningState(threadId); - }); - } - - public continue(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.continue({ threadId }).then(response => { - const allThreadsContinued = response && response.body ? response.body.allThreadsContinued !== false : true; - this.lazyTransitionToRunningState(allThreadsContinued ? undefined : threadId); - }); - } - - public pause(threadId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.pause({ threadId }); - } - - public restartFrame(frameId: number): TPromise { - if (!this.session) { - return TPromise.as(null); - } - - return this.session.restartFrame({ frameId }); - } - - public completions(text: string, position: Position): TPromise { - if (!this.session || !this.session.configuration.capabilities.supportsCompletionsRequest) { - return TPromise.as([]); - } - const focussedStackFrame = this.viewModel.getFocusedStackFrame(); - - return this.session.completions({ - frameId: focussedStackFrame ? focussedStackFrame.frameId : undefined, - text, - column: position.column, - line: position.lineNumber - }).then(response => { - return response && response.body && response.body.targets ? response.body.targets.map(item => ({ - label: item.label, - insertText: item.text || item.label, - type: item.type - })) : []; - }, err => []); - } - - private lazyTransitionToRunningState(threadId?: number): void { - let setNewFocusedStackFrameScheduler: RunOnceScheduler; - - const toDispose = this.session.onDidStop(e => { - if (e.body.threadId === threadId || e.body.allThreadsStopped || !threadId) { - setNewFocusedStackFrameScheduler.cancel(); - } - }); - - this.model.clearThreads(false, threadId); + private transitionToRunningState(session: RawDebugSession, threadId?: number): void { + this.model.clearThreads(session.getId(), false, threadId); // Get a top stack frame of a stopped thread if there is any. - const threads = this.model.getThreads(); - const stoppedReference = Object.keys(threads).filter(ref => threads[ref].stopped).pop(); - const stoppedThread = stoppedReference ? threads[parseInt(stoppedReference)] : null; + const process = this.model.getProcesses().filter(p => p.getId() === session.getId()).pop(); + const stoppedThread = process && process.getAllThreads().filter(t => t.stopped).pop(); const callStack = stoppedThread ? stoppedThread.getCachedCallStack() : null; const stackFrameToFocus = callStack && callStack.length > 0 ? callStack[0] : null; - if (!stoppedThread) { - this.setStateAndEmit(this.configurationManager.configuration.noDebug ? debug.State.RunningNoDebug : debug.State.Running); + if (!stoppedThread && process) { + this.setStateAndEmit(session.getId(), process.session.requestType === debug.SessionRequestType.LAUNCH_NO_DEBUG ? debug.State.RunningNoDebug : debug.State.Running); } - // Do not immediatly set a new focused stack frame since that might cause unnecessery flickering - // of the tree in the debug viewlet. Only set focused stack frame if no stopped event has arrived in 500ms. - setNewFocusedStackFrameScheduler = new RunOnceScheduler(() => { - toDispose.dispose(); - aria.status(nls.localize('debuggingContinued', "Debugging continued.")); - - this.setFocusedStackFrameAndEvaluate(stackFrameToFocus).done(null, errors.onUnexpectedError); - }, 500); - setNewFocusedStackFrameScheduler.schedule(); + this.setFocusedStackFrameAndEvaluate(stackFrameToFocus).done(null, errors.onUnexpectedError); } - private getDebugStringEditorInput(source: Source, value: string, mtype: string): DebugStringEditorInput { + private getDebugStringEditorInput(process: debug.IProcess, source: Source, value: string, mtype: string): DebugStringEditorInput { const result = this.instantiationService.createInstance(DebugStringEditorInput, source.name, source.uri, source.origin, value, mtype, void 0); - this.toDisposeOnSessionEnd.push(result); + this.toDisposeOnSessionEnd[process.getId()].push(result); return result; } - private getDebugErrorEditorInput(source: Source, value: string): DebugErrorEditorInput { + private getDebugErrorEditorInput(process: debug.IProcess, source: Source, value: string): DebugErrorEditorInput { const result = this.instantiationService.createInstance(DebugErrorEditorInput, source.name, value); - this.toDisposeOnSessionEnd.push(result); + this.toDisposeOnSessionEnd[process.getId()].push(result); return result; } - private sendAllBreakpoints(): TPromise { - return TPromise.join(arrays.distinct(this.model.getBreakpoints(), bp => bp.source.uri.toString()).map(bp => this.sendBreakpoints(bp.source.uri))) - .then(() => this.sendFunctionBreakpoints()) + private sendAllBreakpoints(session?: RawDebugSession): TPromise { + return TPromise.join(arrays.distinct(this.model.getBreakpoints(), bp => bp.source.uri.toString()).map(bp => this.sendBreakpoints(bp.source.uri, false, session))) + .then(() => this.sendFunctionBreakpoints(session)) // send exception breakpoints at the end since some debug adapters rely on the order - .then(() => this.sendExceptionBreakpoints()); + .then(() => this.sendExceptionBreakpoints(session)); } - private sendBreakpoints(modelUri: uri, sourceModified = false): TPromise { - if (!this.session || !this.session.readyForBreakpoints) { - return TPromise.as(null); - } - if (this.textFileService.isDirty(modelUri)) { - // Only send breakpoints for a file once it is not dirty #8077 - this.breakpointsToSendOnResourceSaved[modelUri.toString()] = true; - return TPromise.as(null); - } - - const breakpointsToSend = arrays.distinct( - this.model.getBreakpoints().filter(bp => this.model.areBreakpointsActivated() && bp.enabled && bp.source.uri.toString() === modelUri.toString()), - bp => `${bp.desiredLineNumber}` - ); - const rawSource = breakpointsToSend.length > 0 ? breakpointsToSend[0].source.raw : Source.toRawSource(modelUri, this.model); - - return this.session.setBreakpoints({ - source: rawSource, - lines: breakpointsToSend.map(bp => bp.desiredLineNumber), - breakpoints: breakpointsToSend.map(bp => ({ line: bp.desiredLineNumber, condition: bp.condition, hitCondition: bp.hitCondition })), - sourceModified - }).then(response => { - if (!response || !response.body) { - return; + private sendBreakpoints(modelUri: uri, sourceModified = false, targetSession?: RawDebugSession): TPromise { + const sendBreakpointsToSession = (session: RawDebugSession): TPromise => { + if (!session.readyForBreakpoints) { + return TPromise.as(null); } - - const data: { [id: string]: { line?: number, verified: boolean } } = {}; - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + if (this.textFileService.isDirty(modelUri)) { + // Only send breakpoints for a file once it is not dirty #8077 + this.breakpointsToSendOnResourceSaved[modelUri.toString()] = true; + return TPromise.as(null); } - this.model.updateBreakpoints(data); - }); - } + const breakpointsToSend = arrays.distinct( + this.model.getBreakpoints().filter(bp => this.model.areBreakpointsActivated() && bp.enabled && bp.source.uri.toString() === modelUri.toString()), + bp => `${bp.desiredLineNumber}` + ); + const rawSource = breakpointsToSend.length > 0 ? breakpointsToSend[0].source.raw : Source.toRawSource(modelUri, this.model); + + return session.setBreakpoints({ + source: rawSource, + lines: breakpointsToSend.map(bp => bp.desiredLineNumber), + breakpoints: breakpointsToSend.map(bp => ({ line: bp.desiredLineNumber, condition: bp.condition, hitCondition: bp.hitCondition })), + sourceModified + }).then(response => { + if (!response || !response.body) { + return; + } - private sendFunctionBreakpoints(): TPromise { - if (!this.session || !this.session.readyForBreakpoints || !this.session.configuration.capabilities.supportsFunctionBreakpoints) { - return TPromise.as(null); - } + const data: { [id: string]: { line?: number, verified: boolean } } = {}; + for (let i = 0; i < breakpointsToSend.length; i++) { + data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + } + + this.model.updateBreakpoints(data); + }); + }; + + return this.sendToOneOrAllSessions(targetSession, sendBreakpointsToSession); + } - const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); - return this.session.setFunctionBreakpoints({ breakpoints: breakpointsToSend }).then(response => { - if (!response || !response.body) { - return; + private sendFunctionBreakpoints(targetSession?: RawDebugSession): TPromise { + const sendFunctionBreakpointsToSession = (session: RawDebugSession): TPromise => { + if (!session.readyForBreakpoints || !session.configuration.capabilities.supportsFunctionBreakpoints) { + return TPromise.as(null); } - const data: { [id: string]: { name?: string, verified?: boolean } } = {}; - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); + return session.setFunctionBreakpoints({ breakpoints: breakpointsToSend }).then(response => { + if (!response || !response.body) { + return; + } + + const data: { [id: string]: { name?: string, verified?: boolean } } = {}; + for (let i = 0; i < breakpointsToSend.length; i++) { + data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + } + + this.model.updateFunctionBreakpoints(data); + }); + }; + + return this.sendToOneOrAllSessions(targetSession, sendFunctionBreakpointsToSession); + } + + private sendExceptionBreakpoints(targetSession?: RawDebugSession): TPromise { + const sendExceptionBreakpointsToSession = (session: RawDebugSession): TPromise => { + if (!session || !session.readyForBreakpoints || this.model.getExceptionBreakpoints().length === 0) { + return TPromise.as(null); } - this.model.updateFunctionBreakpoints(data); - }); + const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); + return session.setExceptionBreakpoints({ filters: enabledExceptionBps.map(exb => exb.filter) }); + }; + + return this.sendToOneOrAllSessions(targetSession, sendExceptionBreakpointsToSession); } - private sendExceptionBreakpoints(): TPromise { - if (!this.session || !this.session.readyForBreakpoints || this.model.getExceptionBreakpoints().length === 0) { - return TPromise.as(null); + private sendToOneOrAllSessions(session: RawDebugSession, send: (session: RawDebugSession) => TPromise): TPromise { + if (session) { + return send(session); } - const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); - return this.session.setExceptionBreakpoints({ filters: enabledExceptionBps.map(exb => exb.filter) }); + return TPromise.join(this.model.getProcesses().map(p => send(p.session))).then(() => void 0); } private onFileChanges(fileChangesEvent: FileChangesEvent): void { @@ -1113,12 +1027,12 @@ export class DebugService implements debug.IDebugService { this.storageService.store(DEBUG_BREAKPOINTS_ACTIVATED_KEY, this.model.areBreakpointsActivated() ? 'true' : 'false', StorageScope.WORKSPACE); this.storageService.store(DEBUG_FUNCTION_BREAKPOINTS_KEY, JSON.stringify(this.model.getFunctionBreakpoints()), StorageScope.WORKSPACE); this.storageService.store(DEBUG_EXCEPTION_BREAKPOINTS_KEY, JSON.stringify(this.model.getExceptionBreakpoints()), StorageScope.WORKSPACE); - this.storageService.store(DEBUG_SELECTED_CONFIG_NAME_KEY, this.configurationManager.configurationName, StorageScope.WORKSPACE); + this.storageService.store(DEBUG_SELECTED_CONFIG_NAME_KEY, this.viewModel.selectedConfigurationName, StorageScope.WORKSPACE); this.storageService.store(DEBUG_WATCH_EXPRESSIONS_KEY, JSON.stringify(this.model.getWatchExpressions().map(we => ({ name: we.name, id: we.getId() }))), StorageScope.WORKSPACE); } public dispose(): void { - this.toDisposeOnSessionEnd = lifecycle.dispose(this.toDisposeOnSessionEnd); + Object.keys(this.toDisposeOnSessionEnd).forEach(key => lifecycle.dispose(this.toDisposeOnSessionEnd[key])); this.toDispose = lifecycle.dispose(this.toDispose); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts index acfb1a6aabb73a364b8af12319cdf0161f255b89..bbc20a775ba5a2515fa4a44cb275c6154c229078 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts @@ -118,9 +118,9 @@ function renderRenameBox(debugService: debug.IDebugService, contextViewService: } else if (element instanceof model.FunctionBreakpoint && !element.name) { debugService.removeFunctionBreakpoints(element.getId()).done(null, errors.onUnexpectedError); } else if (element instanceof model.Variable) { - (element).errorMessage = null; + element.errorMessage = null; if (renamed && element.value !== inputBox.value) { - debugService.setVariable(element, inputBox.value) + element.setVariable(inputBox.value) // if everything went fine we need to refresh that tree element since his value updated .done(() => tree.refresh(element, false), errors.onUnexpectedError); } @@ -212,10 +212,14 @@ export class BaseDebugController extends treedefaults.DefaultController { // call stack +class ThreadAndProcessIds { + constructor(public processId: string, public threadId: number) { } +} + export class CallStackController extends BaseDebugController { protected onLeftClick(tree: tree.ITree, element: any, event: IMouseEvent): boolean { - if (typeof element === 'number') { + if (element instanceof ThreadAndProcessIds) { return this.showMoreStackFrames(tree, element); } if (element instanceof model.StackFrame) { @@ -227,7 +231,7 @@ export class CallStackController extends BaseDebugController { protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean { const element = tree.getFocus(); - if (typeof element === 'number') { + if (element instanceof ThreadAndProcessIds) { return this.showMoreStackFrames(tree, element); } if (element instanceof model.StackFrame) { @@ -266,10 +270,11 @@ export class CallStackController extends BaseDebugController { } // user clicked / pressed on 'Load More Stack Frames', get those stack frames and refresh the tree. - private showMoreStackFrames(tree: tree.ITree, threadId: number): boolean { - const thread = this.debugService.getModel().getThreads()[threadId]; + private showMoreStackFrames(tree: tree.ITree, threadAndProcessIds: ThreadAndProcessIds): boolean { + const process = this.debugService.getModel().getProcesses().filter(p => p.getId() === threadAndProcessIds.processId).pop(); + const thread = process && process.getThread(threadAndProcessIds.threadId); if (thread) { - thread.getCallStack(this.debugService, true) + thread.getCallStack(true) .done(() => tree.refresh(), errors.onUnexpectedError); } @@ -318,7 +323,7 @@ export class CallStackActionProvider implements renderer.IActionProvider { actions.push(this.instantiationService.createInstance(debugactions.PauseAction, debugactions.PauseAction.ID, debugactions.PauseAction.LABEL)); } } else if (element instanceof model.StackFrame) { - const capabilities = this.debugService.activeSession.configuration.capabilities; + const capabilities = this.debugService.getViewModel().focusedProcess.session.configuration.capabilities; if (typeof capabilities.supportsRestartFrame === 'boolean' && capabilities.supportsRestartFrame) { actions.push(this.instantiationService.createInstance(debugactions.RestartFrameAction, debugactions.RestartFrameAction.ID, debugactions.RestartFrameAction.LABEL)); } @@ -334,10 +339,6 @@ export class CallStackActionProvider implements renderer.IActionProvider { export class CallStackDataSource implements tree.IDataSource { - constructor( @debug.IDebugService private debugService: debug.IDebugService) { - // noop - } - public getId(tree: tree.ITree, element: any): string { if (typeof element === 'number') { return element.toString(); @@ -350,25 +351,28 @@ export class CallStackDataSource implements tree.IDataSource { } public hasChildren(tree: tree.ITree, element: any): boolean { - return element instanceof model.Model || (element instanceof model.Thread && (element).stopped); + return element instanceof model.Model || element instanceof model.Process || (element instanceof model.Thread && (element).stopped); } public getChildren(tree: tree.ITree, element: any): TPromise { if (element instanceof model.Thread) { return this.getThreadChildren(element); } + if (element instanceof model.Model) { + return TPromise.as(element.getProcesses()); + } - const threads = (element).getThreads(); - return TPromise.as(Object.keys(threads).map(ref => threads[ref])); + const process = element; + return TPromise.as(process.getAllThreads()); } private getThreadChildren(thread: debug.IThread): TPromise { - return thread.getCallStack(this.debugService).then((callStack: any[]) => { + return thread.getCallStack().then((callStack: any[]) => { if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) { return callStack.concat([thread.stoppedDetails.framesErrorMessage]); } if (thread.stoppedDetails && thread.stoppedDetails.totalFrames > callStack.length) { - return callStack.concat([thread.threadId]); + return callStack.concat([new ThreadAndProcessIds(thread.process.getId(), thread.threadId)]); } return callStack; @@ -387,6 +391,11 @@ interface IThreadTemplateData { stateLabel: HTMLSpanElement; } +interface IProcessTemplateData { + process: HTMLElement; + name: HTMLElement; +} + interface IErrorTemplateData { label: HTMLElement; } @@ -409,6 +418,7 @@ export class CallStackRenderer implements tree.IRenderer { private static STACK_FRAME_TEMPLATE_ID = 'stackFrame'; private static ERROR_TEMPLATE_ID = 'error'; private static LOAD_MORE_TEMPLATE_ID = 'loadMore'; + private static PROCESS_TEMPLATE_ID = 'process'; constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) { // noop @@ -419,6 +429,9 @@ export class CallStackRenderer implements tree.IRenderer { } public getTemplateId(tree: tree.ITree, element: any): string { + if (element instanceof model.Process) { + return CallStackRenderer.PROCESS_TEMPLATE_ID; + } if (element instanceof model.Thread) { return CallStackRenderer.THREAD_TEMPLATE_ID; } @@ -433,6 +446,14 @@ export class CallStackRenderer implements tree.IRenderer { } public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): any { + if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) { + let data: IProcessTemplateData = Object.create(null); + data.process = dom.append(container, $('.process')); + data.name = dom.append(data.process, $('.name')); + + return data; + } + if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) { let data: ILoadMoreTemplateData = Object.create(null); data.label = dom.append(container, $('.load-more')); @@ -466,17 +487,24 @@ export class CallStackRenderer implements tree.IRenderer { } public renderElement(tree: tree.ITree, element: any, templateId: string, templateData: any): void { - if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) { + if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) { + this.renderProcess(element, templateData); + } else if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) { this.renderThread(element, templateData); } else if (templateId === CallStackRenderer.STACK_FRAME_TEMPLATE_ID) { this.renderStackFrame(element, templateData); } else if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) { this.renderError(element, templateData); - } else { + } else if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) { this.renderLoadMore(element, templateData); } } + private renderProcess(process: debug.IProcess, data: IProcessTemplateData): void { + data.process.title = nls.localize('process', "Process"); + data.name.textContent = process.name; + } + private renderThread(thread: debug.IThread, data: IThreadTemplateData): void { data.thread.title = nls.localize('thread', "Thread"); data.name.textContent = thread.name; @@ -570,10 +598,6 @@ export class VariablesActionProvider implements renderer.IActionProvider { export class VariablesDataSource implements tree.IDataSource { - constructor(private debugService: debug.IDebugService) { - // noop - } - public getId(tree: tree.ITree, element: any): string { return element.getId(); } @@ -589,12 +613,12 @@ export class VariablesDataSource implements tree.IDataSource { public getChildren(tree: tree.ITree, element: any): TPromise { if (element instanceof viewmodel.ViewModel) { - let focusedStackFrame = (element).getFocusedStackFrame(); - return focusedStackFrame ? focusedStackFrame.getScopes(this.debugService) : TPromise.as([]); + const focusedStackFrame = (element).focusedStackFrame; + return focusedStackFrame ? focusedStackFrame.getScopes() : TPromise.as([]); } let scope = element; - return scope.getChildren(this.debugService); + return scope.getChildren(); } public getParent(tree: tree.ITree, element: any): TPromise { @@ -789,10 +813,6 @@ export class WatchExpressionsActionProvider implements renderer.IActionProvider export class WatchExpressionsDataSource implements tree.IDataSource { - constructor(private debugService: debug.IDebugService) { - // noop - } - public getId(tree: tree.ITree, element: any): string { return element.getId(); } @@ -812,7 +832,7 @@ export class WatchExpressionsDataSource implements tree.IDataSource { } let expression = element; - return expression.getChildren(this.debugService); + return expression.getChildren(); } public getParent(tree: tree.ITree, element: any): TPromise { @@ -1164,10 +1184,10 @@ export class BreakpointsRenderer implements tree.IRenderer { data.breakpoint.title = functionBreakpoint.name; // Mark function breakpoints as disabled if deactivated or if debug type does not support them #9099 - const session = this.debugService.activeSession; - if ((session && !session.configuration.capabilities.supportsFunctionBreakpoints) || !this.debugService.getModel().areBreakpointsActivated()) { + const process = this.debugService.getViewModel().focusedProcess; + if ((process && !process.session.configuration.capabilities.supportsFunctionBreakpoints) || !this.debugService.getModel().areBreakpointsActivated()) { tree.addTraits('disabled', [functionBreakpoint]); - if (session && !session.configuration.capabilities.supportsFunctionBreakpoints) { + if (process && !process.session.configuration.capabilities.supportsFunctionBreakpoints) { data.breakpoint.title = nls.localize('functionBreakpointsNotSupported', "Function breakpoints are not supported by this debug type"); } } else { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugViews.ts b/src/vs/workbench/parts/debug/electron-browser/debugViews.ts index 002dbe87decddb6e2ebfd38851459a074ed287b7..122e576bc0d4e2f2fbee374c669ba00cbcf47776 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugViews.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugViews.ts @@ -5,6 +5,7 @@ import nls = require('vs/nls'); import paths = require('vs/base/common/paths'); +import { RunOnceScheduler } from 'vs/base/common/async'; import dom = require('vs/base/browser/dom'); import builder = require('vs/base/browser/builder'); import { TPromise } from 'vs/base/common/winjs.base'; @@ -47,6 +48,7 @@ const $ = builder.$; export class VariablesView extends viewlet.CollapsibleViewletView { private static MEMENTO = 'variablesview.memento'; + private onFocusStackFrameScheduler: RunOnceScheduler; constructor( actionRunner: actions.IActionRunner, @@ -59,6 +61,22 @@ export class VariablesView extends viewlet.CollapsibleViewletView { @IInstantiationService private instantiationService: IInstantiationService ) { super(actionRunner, !!settings[VariablesView.MEMENTO], nls.localize('variablesSection', "Variables Section"), messageService, keybindingService, contextMenuService); + + // Use schedulre to prevent unnecessary flashing + this.onFocusStackFrameScheduler = new RunOnceScheduler(() => { + // Always clear tree highlight to avoid ending up in a broken state #12203 + this.tree.clearHighlight(); + this.tree.refresh().then(() => { + const stackFrame = this.debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + return stackFrame.getScopes().then(scopes => { + if (scopes.length > 0 && !scopes[0].expensive) { + return this.tree.expand(scopes[0]); + } + }); + } + }).done(null, errors.onUnexpectedError); + }, 700); } public renderHeader(container: HTMLElement): void { @@ -73,7 +91,7 @@ export class VariablesView extends viewlet.CollapsibleViewletView { this.treeContainer = renderViewTree(container); this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: new viewer.VariablesDataSource(this.debugService), + dataSource: new viewer.VariablesDataSource(), renderer: this.instantiationService.createInstance(viewer.VariablesRenderer), accessibilityProvider: new viewer.VariablesAccessibilityProvider(), controller: new viewer.VariablesController(this.debugService, this.contextMenuService, new viewer.VariablesActionProvider(this.instantiationService)) @@ -86,8 +104,17 @@ export class VariablesView extends viewlet.CollapsibleViewletView { const collapseAction = this.instantiationService.createInstance(viewlet.CollapseAction, this.tree, false, 'explorer-action collapse-explorer'); this.toolBar.setActions(actionbarregistry.prepareActions([collapseAction]))(); - this.toDispose.push(viewModel.onDidFocusStackFrame(sf => this.onFocusStackFrame(sf))); - this.toDispose.push(this.debugService.onDidChangeState(state => { + this.toDispose.push(viewModel.onDidFocusStackFrame(sf => { + // Only delay if the stack frames got cleared and there is no active stack frame + // Otherwise just update immediately + if (sf) { + this.onFocusStackFrameScheduler.schedule(0); + } else if (!this.onFocusStackFrameScheduler.isScheduled()) { + this.onFocusStackFrameScheduler.schedule(); + } + })); + this.toDispose.push(this.debugService.onDidChangeState(() => { + const state = this.debugService.state; collapseAction.enabled = state === debug.State.Running || state === debug.State.Stopped; })); @@ -116,20 +143,6 @@ export class VariablesView extends viewlet.CollapsibleViewletView { })); } - private onFocusStackFrame(stackFrame: debug.IStackFrame): void { - // Always clear tree highlight to avoid ending up in a broken state #12203 - this.tree.clearHighlight(); - this.tree.refresh().then(() => { - if (stackFrame) { - return stackFrame.getScopes(this.debugService).then(scopes => { - if (scopes.length > 0 && !scopes[0].expensive) { - return this.tree.expand(scopes[0]); - } - }); - } - }).done(null, errors.onUnexpectedError); - } - public shutdown(): void { this.settings[VariablesView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); super.shutdown(); @@ -139,6 +152,8 @@ export class VariablesView extends viewlet.CollapsibleViewletView { export class WatchExpressionsView extends viewlet.CollapsibleViewletView { private static MEMENTO = 'watchexpressionsview.memento'; + private onWatchExpressionsUpdatedScheduler: RunOnceScheduler; + private toReveal: debug.IExpression; constructor( actionRunner: actions.IActionRunner, @@ -150,12 +165,19 @@ export class WatchExpressionsView extends viewlet.CollapsibleViewletView { @IInstantiationService private instantiationService: IInstantiationService ) { super(actionRunner, !!settings[WatchExpressionsView.MEMENTO], nls.localize('expressionsSection', "Expressions Section"), messageService, keybindingService, contextMenuService); + this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(we => { // only expand when a new watch expression is added. if (we instanceof Expression) { this.expand(); } })); + + this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { + this.tree.refresh().done(() => { + return this.toReveal instanceof Expression ? this.tree.reveal(this.toReveal) : TPromise.as(true); + }, errors.onUnexpectedError); + }, 250); } public renderHeader(container: HTMLElement): void { @@ -171,7 +193,7 @@ export class WatchExpressionsView extends viewlet.CollapsibleViewletView { const actionProvider = new viewer.WatchExpressionsActionProvider(this.instantiationService); this.tree = new treeimpl.Tree(this.treeContainer, { - dataSource: new viewer.WatchExpressionsDataSource(this.debugService), + dataSource: new viewer.WatchExpressionsDataSource(), renderer: this.instantiationService.createInstance(viewer.WatchExpressionsRenderer, actionProvider, this.actionRunner), accessibilityProvider: new viewer.WatchExpressionsAccessibilityProvider(), controller: new viewer.WatchExpressionsController(this.debugService, this.contextMenuService, actionProvider) @@ -184,7 +206,13 @@ export class WatchExpressionsView extends viewlet.CollapsibleViewletView { 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().onDidChangeWatchExpressions(we => this.onWatchExpressionsUpdated(we))); + this.toDispose.push(this.debugService.getModel().onDidChangeWatchExpressions(we => { + if (!this.onWatchExpressionsUpdatedScheduler.isScheduled()) { + this.onWatchExpressionsUpdatedScheduler.schedule(); + } + this.toReveal = we; + })); + this.toDispose.push(this.debugService.getViewModel().onDidSelectExpression(expression => { if (!expression || !(expression instanceof Expression)) { return; @@ -201,12 +229,6 @@ export class WatchExpressionsView extends viewlet.CollapsibleViewletView { })); } - private onWatchExpressionsUpdated(expression: debug.IExpression): void { - this.tree.refresh().done(() => { - return expression instanceof Expression ? this.tree.reveal(expression) : TPromise.as(true); - }, errors.onUnexpectedError); - } - public shutdown(): void { this.settings[WatchExpressionsView.MEMENTO] = (this.state === splitview.CollapsibleState.COLLAPSED); super.shutdown(); @@ -218,6 +240,8 @@ export class CallStackView extends viewlet.CollapsibleViewletView { private static MEMENTO = 'callstackview.memento'; private pauseMessage: builder.Builder; private pauseMessageLabel: builder.Builder; + private onCallStackChangeScheduler: RunOnceScheduler; + private onStackFrameFocusScheduler: RunOnceScheduler; constructor( actionRunner: actions.IActionRunner, @@ -230,6 +254,49 @@ export class CallStackView extends viewlet.CollapsibleViewletView { @IInstantiationService private instantiationService: IInstantiationService ) { super(actionRunner, !!settings[CallStackView.MEMENTO], nls.localize('callstackSection', "Call Stack Section"), messageService, keybindingService, contextMenuService); + + // Create schedulers to prevent unnecessary flashing of tree when reacting to changes + this.onStackFrameFocusScheduler = new RunOnceScheduler(() => { + const stackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!stackFrame) { + this.pauseMessage.hide(); + return; + } + + const thread = stackFrame.thread; + this.tree.expandAll([thread.process, thread]).done(() => { + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + this.tree.setSelection([focusedStackFrame]); + if (thread.stoppedDetails && thread.stoppedDetails.reason) { + this.pauseMessageLabel.text(nls.localize('debugStopped', "Paused on {0}", thread.stoppedDetails.reason)); + if (thread.stoppedDetails.text) { + this.pauseMessageLabel.title(thread.stoppedDetails.text); + } + thread.stoppedDetails.reason === 'exception' ? this.pauseMessageLabel.addClass('exception') : this.pauseMessageLabel.removeClass('exception'); + this.pauseMessage.show(); + } else { + this.pauseMessage.hide(); + } + + return this.tree.reveal(focusedStackFrame); + }, errors.onUnexpectedError); + }, 100); + + this.onCallStackChangeScheduler = new RunOnceScheduler(() => { + let newTreeInput: any = this.debugService.getModel(); + const processes = this.debugService.getModel().getProcesses(); + if (processes.length === 1) { + const threads = processes[0].getAllThreads(); + // Only show the threads in the call stack if there is more than 1 thread. + newTreeInput = threads.length === 1 ? threads[0] : processes[0]; + } + + if (this.tree.getInput() === newTreeInput) { + this.tree.refresh().done(null, errors.onUnexpectedError); + } else { + this.tree.setInput(newTreeInput).done(null, errors.onUnexpectedError); + } + }, 50); } public renderHeader(container: HTMLElement): void { @@ -263,42 +330,15 @@ export class CallStackView extends viewlet.CollapsibleViewletView { } })); - const model = this.debugService.getModel(); this.toDispose.push(this.debugService.getViewModel().onDidFocusStackFrame(() => { - const focussedThread = model.getThreads()[this.debugService.getViewModel().getFocusedThreadId()]; - if (!focussedThread) { - this.pauseMessage.hide(); - return; + if (!this.onStackFrameFocusScheduler.isScheduled()) { + this.onStackFrameFocusScheduler.schedule(); } - - return this.tree.expand(focussedThread).then(() => { - const focusedStackFrame = this.debugService.getViewModel().getFocusedStackFrame(); - this.tree.setSelection([focusedStackFrame]); - 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(); - } - - return this.tree.reveal(focusedStackFrame); - }); })); - this.toDispose.push(model.onDidChangeCallStack(() => { - const threads = model.getThreads(); - const threadsArray = Object.keys(threads).map(ref => threads[ref]); - // Only show the threads in the call stack if there is more than 1 thread. - const newTreeInput = threadsArray.length === 1 ? threadsArray[0] : model; - - if (this.tree.getInput() === newTreeInput) { - this.tree.refresh().done(null, errors.onUnexpectedError); - } else { - this.tree.setInput(newTreeInput).done(null, errors.onUnexpectedError); + this.toDispose.push(this.debugService.getModel().onDidChangeCallStack(() => { + if (!this.onCallStackChangeScheduler.isScheduled()) { + this.onCallStackChangeScheduler.schedule(); } })); } diff --git a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts index 53f6a79d1c0b911e8b2678ad6946675a20c131d6..369d28cd75a59dc0529bbafa0ec3f438ec9af7ed 100644 --- a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts +++ b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts @@ -20,9 +20,9 @@ export class CopyValueAction extends actions.Action { public run(): TPromise { if (this.value instanceof Variable) { - const frameId = this.debugService.getViewModel().getFocusedStackFrame().frameId; - const session = this.debugService.activeSession; - return session.evaluate({ expression: getFullExpressionName(this.value, session.configuration.type), frameId }).then(result => { + const frameId = this.debugService.getViewModel().focusedStackFrame.frameId; + const process = this.debugService.getViewModel().focusedProcess; + return process.session.evaluate({ expression: getFullExpressionName(this.value, process.session.configuration.type), frameId }).then(result => { clipboard.writeText(result.body.result); }, err => clipboard.writeText(this.value.value)); } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 6c3ccf6cce0bcfd94f2135ca63bb8e7f03696ad3..36a8584f4c8dd80c1e40b7cdefd57dd5e8c5f804 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -27,7 +27,6 @@ import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/exte import { TerminalSupport } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - import { shell } from 'electron'; export interface SessionExitedEvent extends DebugProtocol.ExitedEvent { @@ -44,7 +43,7 @@ export interface SessionTerminatedEvent extends DebugProtocol.TerminatedEvent { }; } -export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSession { +export class RawDebugSession extends v8.V8Protocol implements debug.ISession { public restarted: boolean; public emittedStopped: boolean; @@ -54,7 +53,8 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes private socket: net.Socket = null; private cachedInitServer: TPromise; private startTime: number; - private stopServerPending: boolean; + public requestType: debug.SessionRequestType; + public disconnected: boolean; private sentPromises: TPromise[]; private capabilities: DebugProtocol.Capabilities; @@ -69,6 +69,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes private _onDidEvent: Emitter; constructor( + id: string, private debugServerPort: number, private adapter: Adapter, private customTelemetryService: ITelemetryService, @@ -79,7 +80,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes @IExternalTerminalService private nativeTerminalService: IExternalTerminalService, @IConfigurationService private configurationService: IConfigurationService ) { - super(); + super(id); this.emittedStopped = false; this.readyForBreakpoints = false; this.sentPromises = []; @@ -240,31 +241,48 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes } public launch(args: DebugProtocol.LaunchRequestArguments): TPromise { + this.requestType = args.noDebug ? debug.SessionRequestType.LAUNCH_NO_DEBUG : debug.SessionRequestType.LAUNCH; return this.send('launch', args).then(response => this.readCapabilities(response)); } public attach(args: DebugProtocol.AttachRequestArguments): TPromise { + this.requestType = debug.SessionRequestType.ATTACH; return this.send('attach', args).then(response => this.readCapabilities(response)); } public next(args: DebugProtocol.NextArguments): TPromise { - return this.send('next', args); + return this.send('next', args).then(response => { + this.fireFakeContinued(args.threadId); + return response; + }); } public stepIn(args: DebugProtocol.StepInArguments): TPromise { - return this.send('stepIn', args); + return this.send('stepIn', args).then(response => { + this.fireFakeContinued(args.threadId); + return response; + }); } public stepOut(args: DebugProtocol.StepOutArguments): TPromise { - return this.send('stepOut', args); + return this.send('stepOut', args).then(response => { + this.fireFakeContinued(args.threadId); + return response; + }); } public stepBack(args: DebugProtocol.StepBackArguments): TPromise { - return this.send('stepBack', args); + return this.send('stepBack', args).then(response => { + this.fireFakeContinued(args.threadId); + return response; + }); } public continue(args: DebugProtocol.ContinueArguments): TPromise { - return this.send('continue', args); + return this.send('continue', args).then(response => { + this.fireFakeContinued(args.threadId); + return response; + }); } public pause(args: DebugProtocol.PauseArguments): TPromise { @@ -284,7 +302,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes } public disconnect(restart = false, force = false): TPromise { - if (this.stopServerPending && force) { + if (this.disconnected && force) { return this.stopServer(); } @@ -295,9 +313,9 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes this.sentPromises = []; }, 1000); - if ((this.serverProcess || this.socket) && !this.stopServerPending) { + if ((this.serverProcess || this.socket) && !this.disconnected) { // point of no return: from now on don't report any errors - this.stopServerPending = true; + this.disconnected = true; this.restarted = restart; return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer()); } @@ -367,6 +385,17 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes } } + private fireFakeContinued(threadId: number): void { + this._onDidContinued.fire({ + type: 'event', + event: 'continued', + body: { + threadId + }, + seq: undefined + }); + } + private connectServer(port: number): TPromise { return new TPromise((c, e) => { this.socket = net.createConnection(port, '127.0.0.1', () => { @@ -436,7 +465,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes return TPromise.as(null); } - this.stopServerPending = true; + this.disconnected = true; let ret: TPromise; // when killing a process in windows its child @@ -492,7 +521,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.IRawDebugSes private onServerExit(): void { this.serverProcess = null; this.cachedInitServer = null; - if (!this.stopServerPending) { + if (!this.disconnected) { this.messageService.show(severity.Error, nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } this.onEvent({ event: 'exit', type: 'event', seq: 0 }); diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index f524470107c5de823ccbfd93ed6da480aa071b58..6a5e6fc5beba4dc8e6aaf840d122a3d7a5b1b33a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -183,7 +183,9 @@ export class Repl extends Panel implements IPrivateReplService { provideCompletionItems: (model: IReadOnlyModel, position: Position, token: CancellationToken): Thenable => { const word = this.replInput.getModel().getWordAtPosition(position); const text = this.replInput.getModel().getLineContent(position.lineNumber); - return wireCancellationToken(token, this.debugService.completions(text, position).then(suggestions => ({ + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + const completions = focusedStackFrame ? focusedStackFrame.completions(text, position) : TPromise.as([]); + return wireCancellationToken(token, completions.then(suggestions => ({ currentWord: word ? word.word : '', suggestions }))); diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index 041d560531773e5b1610753a757518cb70aebd0c..8dee37ee8967f06eab5ce071ba297f1326615016 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -130,7 +130,7 @@ export class Adapter { } const properties = attributes.properties; properties.type = { - enum: [this.type], + enum: [this.type, 'composite'], description: nls.localize('debugType', "Type of configuration.") }; properties.name = { @@ -142,6 +142,11 @@ export class Adapter { enum: [request], description: nls.localize('debugRequest', "Request type of configuration. Can be \"launch\" or \"attach\"."), }; + properties.configurationNames = { + type: 'array', + default: [], + description: nls.localize('debugConfigurationNames', "Configurations that will be launched as part of this \"composite\" configuration. Only respected if type of this configuration is \"composite\".") + }; properties.preLaunchTask = { type: ['string', 'null'], default: null, diff --git a/src/vs/workbench/parts/debug/node/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/node/debugConfigurationManager.ts index 7940b8e47313643c05cd127fd5a3be47be5f9891..527ff5e75b695984e0a6ca09ebbc6a4f73239bfd 100644 --- a/src/vs/workbench/parts/debug/node/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/node/debugConfigurationManager.ts @@ -167,13 +167,11 @@ const jsonRegistry = platfor jsonRegistry.registerSchema(schemaId, schema); export class ConfigurationManager implements debug.IConfigurationManager { - public configuration: debug.IConfig; private adapters: Adapter[]; private allModeIdsForBreakpoints: { [key: string]: boolean }; private _onDidConfigurationChange: Emitter; constructor( - configName: string, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IFileService private fileService: IFileService, @ITelemetryService private telemetryService: ITelemetryService, @@ -184,7 +182,6 @@ export class ConfigurationManager implements debug.IConfigurationManager { @IInstantiationService private instantiationService: IInstantiationService ) { this._onDidConfigurationChange = new Emitter(); - this.setConfiguration(configName); this.adapters = []; this.registerListeners(); this.allModeIdsForBreakpoints = {}; @@ -250,60 +247,58 @@ export class ConfigurationManager implements debug.IConfigurationManager { return this._onDidConfigurationChange.event; } - public get configurationName(): string { - return this.configuration ? this.configuration.name : null; + public getAdapter(type: string): Adapter { + return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop(); } - public get adapter(): Adapter { - if (!this.configuration || !this.configuration.type) { - return null; - } - - return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, this.configuration.type)).pop(); - } - - public setConfiguration(nameOrConfig: string | debug.IConfig): TPromise { + public getConfiguration(nameOrConfig: string | debug.IConfig): TPromise { return this.loadLaunchConfig().then(config => { + let result: debug.IConfig = null; if (types.isObject(nameOrConfig)) { - this.configuration = objects.deepClone(nameOrConfig) as debug.IConfig; + result = objects.deepClone(nameOrConfig) as debug.IConfig; } else { if (!config || !config.configurations) { - this.configuration = null; - return; + return result; } // if the configuration name is not set yet, take the first launch config (can happen if debug viewlet has not been opened yet). const filtered = nameOrConfig ? config.configurations.filter(cfg => cfg.name === nameOrConfig) : [config.configurations[0]]; - this.configuration = filtered.length === 1 ? objects.deepClone(filtered[0]) : null; - if (config && this.configuration) { - this.configuration.debugServer = config.debugServer; + result = filtered.length === 1 ? objects.deepClone(filtered[0]) : null; + if (config && result) { + result.debugServer = config.debugServer; } } - if (this.configuration) { + if (result) { // Set operating system specific properties #1873 - if (isWindows && this.configuration.windows) { - Object.keys(this.configuration.windows).forEach(key => { - this.configuration[key] = this.configuration.windows[key]; + if (isWindows && result.windows) { + Object.keys(result.windows).forEach(key => { + result[key] = result.windows[key]; }); } - if (isMacintosh && this.configuration.osx) { - Object.keys(this.configuration.osx).forEach(key => { - this.configuration[key] = this.configuration.osx[key]; + if (isMacintosh && result.osx) { + Object.keys(result.osx).forEach(key => { + result[key] = result.osx[key]; }); } - if (isLinux && this.configuration.linux) { - Object.keys(this.configuration.linux).forEach(key => { - this.configuration[key] = this.configuration.linux[key]; + if (isLinux && result.linux) { + Object.keys(result.linux).forEach(key => { + result[key] = result.linux[key]; }); } // massage configuration attributes - append workspace path to relatvie paths, substitute variables in paths. - Object.keys(this.configuration).forEach(key => { - this.configuration[key] = this.configurationResolverService.resolveAny(this.configuration[key]); + Object.keys(result).forEach(key => { + result[key] = this.configurationResolverService.resolveAny(result[key]); }); + + const adapter = this.getAdapter(result.type); + return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); } - }).then(() => this._onDidConfigurationChange.fire(this.configuration)); + }).then(result => { + this._onDidConfigurationChange.fire(result); + return result; + }); } public openConfigFile(sideBySide: boolean): TPromise { @@ -356,8 +351,4 @@ export class ConfigurationManager implements debug.IConfigurationManager { public loadLaunchConfig(): TPromise { return TPromise.as(this.configurationService.getConfiguration('launch')); } - - public resolveInteractiveVariables(): TPromise { - return this.configurationResolverService.resolveInteractiveVariables(this.configuration, this.adapter ? this.adapter.variables : null); - } } diff --git a/src/vs/workbench/parts/debug/node/v8Protocol.ts b/src/vs/workbench/parts/debug/node/v8Protocol.ts index 76c6452ce2a4bcd91d1bef6b93137d181d23df06..3dd75e0e8dbccc10d7ca2272183c5393810158c0 100644 --- a/src/vs/workbench/parts/debug/node/v8Protocol.ts +++ b/src/vs/workbench/parts/debug/node/v8Protocol.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import stream = require('stream'); -import uuid = require('vs/base/common/uuid'); import { TPromise } from 'vs/base/common/winjs.base'; import { canceled } from 'vs/base/common/errors'; @@ -16,15 +15,13 @@ export abstract class V8Protocol { private sequence: number; private pendingRequests: { [id: number]: (e: DebugProtocol.Response) => void; }; private rawData: Buffer; - private id: string; private contentLength: number; - constructor() { + constructor(private id: string) { this.sequence = 1; this.contentLength = -1; this.pendingRequests = {}; this.rawData = new Buffer(0); - this.id = uuid.generateUuid(); } public getId(): string { diff --git a/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts b/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts index 9f961b9db3e013161bd18f446bae9294f6f3b930..36d1f1c129c668eba2409f89b998d5cbe544d263 100644 --- a/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts +++ b/src/vs/workbench/parts/debug/test/common/debugViewModel.test.ts @@ -5,13 +5,14 @@ import assert = require('assert'); import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel'; -import { StackFrame, Expression, Thread } from 'vs/workbench/parts/debug/common/debugModel'; +import { StackFrame, Expression, Thread, Process } from 'vs/workbench/parts/debug/common/debugModel'; +import { MockSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; suite('Debug - View Model', () => { var model: ViewModel; setup(() => { - model = new ViewModel(); + model = new ViewModel('mockconfiguration'); }); teardown(() => { @@ -19,13 +20,16 @@ suite('Debug - View Model', () => { }); test('focused stack frame', () => { - assert.equal(model.getFocusedStackFrame(), null); - assert.equal(model.getFocusedThreadId(), 0); - const frame = new StackFrame(1, 1, null, 'app.js', 1, 1); - model.setFocusedStackFrame(frame, new Thread('myThread', 1)); - - assert.equal(model.getFocusedStackFrame(), frame); - assert.equal(model.getFocusedThreadId(), 1); + assert.equal(model.focusedStackFrame, null); + assert.equal(model.focusedThread, null); + const mockSession = new MockSession(); + const process = new Process('mockProcess', mockSession); + const thread = new Thread(process, 'myThread', 1); + const frame = new StackFrame(thread, 1, null, 'app.js', 1, 1); + model.setFocusedStackFrame(frame); + + assert.equal(model.focusedStackFrame, frame); + assert.equal(model.focusedThread.threadId, 1); }); test('selected expression', () => { diff --git a/src/vs/workbench/parts/debug/test/common/mockDebugService.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts similarity index 63% rename from src/vs/workbench/parts/debug/test/common/mockDebugService.ts rename to src/vs/workbench/parts/debug/test/common/mockDebug.ts index e283faee891ff9791bdc2d53084585bcdec17aca..32b0874b710f3ca4bacee82d5d8821010ba010f6 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebugService.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -12,18 +12,13 @@ import debug = require('vs/workbench/parts/debug/common/debug'); import { Source } from 'vs/workbench/parts/debug/common/debugSource'; export class MockDebugService implements debug.IDebugService { - private session: MockRawSession; public _serviceBrand: any; - constructor() { - this.session = new MockRawSession(); - } - public get state(): debug.State { return null; } - public get onDidChangeState(): Event { + public get onDidChangeState(): Event { return null; } @@ -81,18 +76,14 @@ export class MockDebugService implements debug.IDebugService { public removeWatchExpressions(id?: string): void { } - public createSession(noDebug: boolean): TPromise { + public createProcess(configurationOrName: debug.IConfig | string): TPromise { return TPromise.as(null); } - public restartSession(): TPromise { + public restartProcess(): TPromise { return TPromise.as(null); } - public get activeSession(): debug.IRawDebugSession { - return this.session; - } - public getModel(): debug.IModel { return null; } @@ -142,8 +133,45 @@ export class MockDebugService implements debug.IDebugService { } } +export class MockSession implements debug.ISession { + public readyForBreakpoints = true; + public emittedStopped = true; + + public getId() { + return 'mockrawsession'; + } -class MockRawSession implements debug.IRawDebugSession { + public get requestType() { + return debug.SessionRequestType.LAUNCH; + } + + public getLengthInSeconds(): number { + return 100; + } + + public stackTrace(args: DebugProtocol.StackTraceArguments): TPromise { + return TPromise.as({ + body: { + stackFrames: [] + } + }); + } + + public attach(args: DebugProtocol.AttachRequestArguments): TPromise { + return TPromise.as(null); + } + + public scopes(args: DebugProtocol.ScopesArguments): TPromise { + return TPromise.as(null); + } + + public variables(args: DebugProtocol.VariablesArguments): TPromise { + return TPromise.as(null); + } + + evaluate(args: DebugProtocol.EvaluateArguments): TPromise { + return TPromise.as(null); + } public get configuration(): { type: string, capabilities: DebugProtocol.Capabilities } { return { @@ -164,23 +192,61 @@ class MockRawSession implements debug.IRawDebugSession { return TPromise.as(null); } - public stackTrace(args: DebugProtocol.StackTraceArguments): TPromise { - return TPromise.as({ - body: { - stackFrames: [] - } - }); + public threads(): TPromise { + return TPromise.as(null); } - public scopes(args: DebugProtocol.ScopesArguments): TPromise { + public stepIn(args: DebugProtocol.StepInArguments): TPromise { return TPromise.as(null); } - public variables(args: DebugProtocol.VariablesArguments): TPromise { + public stepOut(args: DebugProtocol.StepOutArguments): TPromise { return TPromise.as(null); } - evaluate(args: DebugProtocol.EvaluateArguments): TPromise { + public stepBack(args: DebugProtocol.StepBackArguments): TPromise { + return TPromise.as(null); + } + + public continue(args: DebugProtocol.ContinueArguments): TPromise { + return TPromise.as(null); + } + + public pause(args: DebugProtocol.PauseArguments): TPromise { + return TPromise.as(null); + } + + public setVariable(args: DebugProtocol.SetVariableArguments): TPromise { + return TPromise.as(null); + } + + public restartFrame(args: DebugProtocol.RestartFrameArguments): TPromise { + return TPromise.as(null); + } + + public completions(args: DebugProtocol.CompletionsArguments): TPromise { + return TPromise.as(null); + } + + public next(args: DebugProtocol.NextArguments): TPromise { + return TPromise.as(null); + } + + public source(args: DebugProtocol.SourceArguments): TPromise { + return TPromise.as(null); + } + + public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise { + return TPromise.as(null); + } + + public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): TPromise { return TPromise.as(null); } + + public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): TPromise { + return TPromise.as(null); + } + + public onDidStop: Event = null; } diff --git a/src/vs/workbench/parts/debug/test/node/debugModel.test.ts b/src/vs/workbench/parts/debug/test/node/debugModel.test.ts index 79a60afcda1f38f927ae72fd5f34bb9257851c95..6f2e33aa93b2fb9977b22bf3def4fe522aa23e01 100644 --- a/src/vs/workbench/parts/debug/test/node/debugModel.test.ts +++ b/src/vs/workbench/parts/debug/test/node/debugModel.test.ts @@ -8,13 +8,15 @@ import uri from 'vs/base/common/uri'; import severity from 'vs/base/common/severity'; import debugmodel = require('vs/workbench/parts/debug/common/debugModel'); import * as sinon from 'sinon'; -import { MockDebugService } from 'vs/workbench/parts/debug/test/common/mockDebugService'; +import { MockSession } from 'vs/workbench/parts/debug/test/common/mockDebug'; suite('Debug - Model', () => { - var model: debugmodel.Model; + let model: debugmodel.Model; + let rawSession: MockSession; setup(() => { model = new debugmodel.Model([], true, [], [], []); + rawSession = new MockSession(); }); teardown(() => { @@ -78,24 +80,26 @@ suite('Debug - Model', () => { test('threads simple', () => { var threadId = 1; var threadName = 'firstThread'; + + model.addProcess('mockProcess', rawSession); model.rawUpdate({ + sessionId: rawSession.getId(), threadId: threadId, thread: { id: threadId, name: threadName } }); + const process = model.getProcesses().filter(p => p.getId() === rawSession.getId()).pop(); - var threads = model.getThreads(); - assert.equal(threads[threadId].name, threadName); + assert.equal(process.getThread(threadId).name, threadName); - model.clearThreads(true); - assert.equal(model.getThreads[threadId], null); + model.clearThreads(process.getId(), true); + assert.equal(process.getThread(threadId), null); }); test('threads multiple wtih allThreadsStopped', () => { - const mockDebugService = new MockDebugService(); - const sessionStub = sinon.spy(mockDebugService.activeSession, 'stackTrace'); + const sessionStub = sinon.spy(rawSession, 'stackTrace'); const threadId1 = 1; const threadName1 = 'firstThread'; @@ -104,7 +108,9 @@ suite('Debug - Model', () => { const stoppedReason = 'breakpoint'; // Add the threads + model.addProcess('mockProcess', rawSession); model.rawUpdate({ + sessionId: rawSession.getId(), threadId: threadId1, thread: { id: threadId1, @@ -113,6 +119,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ + sessionId: rawSession.getId(), threadId: threadId2, thread: { id: threadId2, @@ -122,6 +129,7 @@ suite('Debug - Model', () => { // Stopped event with all threads stopped model.rawUpdate({ + sessionId: rawSession.getId(), threadId: threadId1, stoppedDetails: { reason: stoppedReason, @@ -129,11 +137,13 @@ suite('Debug - Model', () => { }, allThreadsStopped: true }); + const process = model.getProcesses().filter(p => p.getId() === rawSession.getId()).pop(); - const thread1 = model.getThreads()[threadId1]; - const thread2 = model.getThreads()[threadId2]; + const thread1 = process.getThread(threadId1); + const thread2 = process.getThread(threadId2); // at the beginning, callstacks are obtainable but not available + assert.equal(process.getAllThreads().length, 2); assert.equal(thread1.name, threadName1); assert.equal(thread1.stopped, true); assert.equal(thread1.getCachedCallStack(), undefined); @@ -145,13 +155,13 @@ suite('Debug - Model', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - thread1.getCallStack(mockDebugService).then(() => { + thread1.getCallStack().then(() => { assert.notEqual(thread1.getCachedCallStack(), undefined); assert.equal(thread2.getCachedCallStack(), undefined); assert.equal(sessionStub.callCount, 1); }); - thread2.getCallStack(mockDebugService).then(() => { + thread2.getCallStack().then(() => { assert.notEqual(thread1.getCachedCallStack(), undefined); assert.notEqual(thread2.getCachedCallStack(), undefined); assert.equal(sessionStub.callCount, 2); @@ -159,8 +169,8 @@ suite('Debug - Model', () => { // calling multiple times getCallStack doesn't result in multiple calls // to the debug adapter - thread1.getCallStack(mockDebugService).then(() => { - return thread2.getCallStack(mockDebugService); + thread1.getCallStack().then(() => { + return thread2.getCallStack(); }).then(() => { assert.equal(sessionStub.callCount, 2); }); @@ -174,23 +184,24 @@ suite('Debug - Model', () => { assert.equal(thread2.stopped, true); assert.equal(thread2.getCachedCallStack(), undefined); - model.clearThreads(true); - assert.equal(model.getThreads[threadId1], null); - assert.equal(model.getThreads[threadId2], null); + model.clearThreads(process.getId(), true); + assert.equal(process.getThread(threadId1), null); + assert.equal(process.getThread(threadId2), null); + assert.equal(process.getAllThreads().length, 0); }); test('threads mutltiple without allThreadsStopped', () => { - const mockDebugService = new MockDebugService(); - const sessionStub = sinon.spy(mockDebugService.activeSession, 'stackTrace'); + const sessionStub = sinon.spy(rawSession, 'stackTrace'); const stoppedThreadId = 1; const stoppedThreadName = 'stoppedThread'; const runningThreadId = 2; const runningThreadName = 'runningThread'; const stoppedReason = 'breakpoint'; - + model.addProcess('mockProcess', rawSession); // Add the threads model.rawUpdate({ + sessionId: rawSession.getId(), threadId: stoppedThreadId, thread: { id: stoppedThreadId, @@ -199,6 +210,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ + sessionId: rawSession.getId(), threadId: runningThreadId, thread: { id: runningThreadId, @@ -208,6 +220,7 @@ suite('Debug - Model', () => { // Stopped event with only one thread stopped model.rawUpdate({ + sessionId: rawSession.getId(), threadId: stoppedThreadId, stoppedDetails: { reason: stoppedReason, @@ -215,14 +228,16 @@ suite('Debug - Model', () => { }, allThreadsStopped: false }); + const process = model.getProcesses().filter(p => p.getId() === rawSession.getId()).pop(); - const stoppedThread = model.getThreads()[stoppedThreadId]; - const runningThread = model.getThreads()[runningThreadId]; + const stoppedThread = process.getThread(stoppedThreadId); + const runningThread = process.getThread(runningThreadId); // the callstack for the stopped thread is obtainable but not available // the callstack for the running thread is not obtainable nor available assert.equal(stoppedThread.name, stoppedThreadName); assert.equal(stoppedThread.stopped, true); + assert.equal(process.getAllThreads().length, 2); assert.equal(stoppedThread.getCachedCallStack(), undefined); assert.equal(stoppedThread.stoppedDetails.reason, stoppedReason); assert.equal(runningThread.name, runningThreadName); @@ -232,7 +247,7 @@ suite('Debug - Model', () => { // after calling getCallStack, the callstack becomes available // and results in a request for the callstack in the debug adapter - stoppedThread.getCallStack(mockDebugService).then(() => { + stoppedThread.getCallStack().then(() => { assert.notEqual(stoppedThread.getCachedCallStack(), undefined); assert.equal(runningThread.getCachedCallStack(), undefined); assert.equal(sessionStub.callCount, 1); @@ -241,14 +256,14 @@ suite('Debug - Model', () => { // calling getCallStack on the running thread returns empty array // and does not return in a request for the callstack in the debug // adapter - runningThread.getCallStack(mockDebugService).then(callStack => { + runningThread.getCallStack().then(callStack => { assert.deepEqual(callStack, []); assert.equal(sessionStub.callCount, 1); }); // calling multiple times getCallStack doesn't result in multiple calls // to the debug adapter - stoppedThread.getCallStack(mockDebugService).then(() => { + stoppedThread.getCallStack().then(() => { assert.equal(sessionStub.callCount, 1); }); @@ -257,9 +272,10 @@ suite('Debug - Model', () => { assert.equal(stoppedThread.stopped, true); assert.equal(stoppedThread.getCachedCallStack(), undefined); - model.clearThreads(true); - assert.equal(model.getThreads[stoppedThreadId], null); - assert.equal(model.getThreads[runningThreadId], null); + model.clearThreads(process.getId(), true); + assert.equal(process.getThread(stoppedThreadId), null); + assert.equal(process.getThread(runningThreadId), null); + assert.equal(process.getAllThreads().length, 0 ); }); // Expressions @@ -275,14 +291,16 @@ suite('Debug - Model', () => { test('watch expressions', () => { assert.equal(model.getWatchExpressions().length, 0); - const stackFrame = new debugmodel.StackFrame(1, 1, null, 'app.js', 1, 1); - model.addWatchExpression(null, stackFrame, 'console').done(); - model.addWatchExpression(null, stackFrame, 'console').done(); + const process = new debugmodel.Process('mockProcess', rawSession); + const thread = new debugmodel.Thread(process, 'mockthread', 1); + const stackFrame = new debugmodel.StackFrame(thread, 1, null, 'app.js', 1, 1); + model.addWatchExpression(stackFrame, 'console').done(); + model.addWatchExpression(stackFrame, 'console').done(); const watchExpressions = model.getWatchExpressions(); assertWatchExpressions(watchExpressions, 'console'); - model.renameWatchExpression(null, stackFrame, watchExpressions[0].getId(), 'new_name').done(); - model.renameWatchExpression(null, stackFrame, watchExpressions[1].getId(), 'new_name').done(); + model.renameWatchExpression(stackFrame, watchExpressions[0].getId(), 'new_name').done(); + model.renameWatchExpression(stackFrame, watchExpressions[1].getId(), 'new_name').done(); assertWatchExpressions(model.getWatchExpressions(), 'new_name'); model.clearWatchExpressionValues(); @@ -294,10 +312,12 @@ suite('Debug - Model', () => { test('repl expressions', () => { assert.equal(model.getReplElements().length, 0); - const stackFrame = new debugmodel.StackFrame(1, 1, null, 'app.js', 1, 1); - model.addReplExpression(null, stackFrame, 'myVariable').done(); - model.addReplExpression(null, stackFrame, 'myVariable').done(); - model.addReplExpression(null, stackFrame, 'myVariable').done(); + const process = new debugmodel.Process('mockProcess', rawSession); + const thread = new debugmodel.Thread(process, 'mockthread', 1); + const stackFrame = new debugmodel.StackFrame(thread, 1, null, 'app.js', 1, 1); + model.addReplExpression(stackFrame, 'myVariable').done(); + model.addReplExpression(stackFrame, 'myVariable').done(); + model.addReplExpression(stackFrame, 'myVariable').done(); assert.equal(model.getReplElements().length, 3); model.getReplElements().forEach(re => { @@ -352,11 +372,14 @@ suite('Debug - Model', () => { assert.equal(debugmodel.getFullExpressionName(new debugmodel.Expression(null, false), type), null); assert.equal(debugmodel.getFullExpressionName(new debugmodel.Expression('son', false), type), 'son'); - const scope = new debugmodel.Scope(1, 'myscope', 1, false, 1, 0); - const son = new debugmodel.Variable(new debugmodel.Variable(new debugmodel.Variable(scope, 0, 'grandfather', '75', 1, 0), 0, 'father', '45', 1, 0), 0, 'son', '20', 1, 0); + const process = new debugmodel.Process('mockProcess', rawSession); + const thread = new debugmodel.Thread(process, 'mockthread', 1); + const stackFrame = new debugmodel.StackFrame(thread, 1, null, 'app.js', 1, 1); + const scope = new debugmodel.Scope(stackFrame, 'myscope', 1, false, 1, 0); + const son = new debugmodel.Variable(stackFrame, new debugmodel.Variable(stackFrame, new debugmodel.Variable(stackFrame, scope, 0, 'grandfather', '75', 1, 0), 0, 'father', '45', 1, 0), 0, 'son', '20', 1, 0); assert.equal(debugmodel.getFullExpressionName(son, type), 'grandfather.father.son'); - const grandson = new debugmodel.Variable(son, 0, '/weird_name', '1', 0, 0); + const grandson = new debugmodel.Variable(stackFrame, son, 0, '/weird_name', '1', 0, 0); assert.equal(debugmodel.getFullExpressionName(grandson, type), 'grandfather.father.son[\'/weird_name\']'); }); });