From 09f672c98b474fb18691b95179d7e1f6cf8473ed Mon Sep 17 00:00:00 2001 From: isidor Date: Wed, 20 Mar 2019 11:29:59 +0100 Subject: [PATCH] debug: Add support for sub-sessions fixes #70695 --- .../contrib/debug/browser/callStackView.ts | 11 +++++- .../contrib/debug/browser/debugActionItems.ts | 10 ++++- .../workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/common/debugModel.ts | 14 ++++++- .../debug/electron-browser/debugService.ts | 12 +++--- .../debug/electron-browser/debugSession.ts | 5 +++ .../contrib/debug/test/common/mockDebug.ts | 5 +++ .../test/electron-browser/debugModel.test.ts | 39 ++++++++++++++++--- 8 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index e2df7ce6296..1cc8655f956 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -97,7 +97,7 @@ export class CallStackView extends ViewletPanel { dom.addClass(container, 'debug-call-stack'); const treeContainer = renderViewTree(container); - this.dataSource = new CallStackDataSource(); + this.dataSource = new CallStackDataSource(this.debugService); this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, treeContainer, new CallStackDelegate(), [ new SessionsRenderer(), new ThreadsRenderer(), @@ -562,6 +562,8 @@ function isDeemphasized(frame: IStackFrame): boolean { class CallStackDataSource implements IAsyncDataSource { deemphasizedStackFramesToShow: IStackFrame[]; + constructor(private debugService: IDebugService) { } + hasChildren(element: IDebugModel | CallStackItem): boolean { return isDebugModel(element) || isDebugSession(element) || (element instanceof Thread && element.stopped); } @@ -573,13 +575,18 @@ class CallStackDataSource implements IAsyncDataSource 1) { - return Promise.resolve(sessions); + return Promise.resolve(sessions.filter(s => !s.parentSession)); } const threads = sessions[0].getAllThreads(); // Only show the threads in the call stack if there is more than 1 thread. return threads.length === 1 ? this.getThreadChildren(threads[0]) : Promise.resolve(threads); } else if (isDebugSession(element)) { + const childSessions = this.debugService.getModel().getSessions().filter(s => s.parentSession === element); + if (childSessions.length) { + return Promise.resolve(childSessions); + } + return Promise.resolve(element.getAllThreads()); } else { return this.getThreadChildren(element); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts index 574fec93f5b..e261a69c27c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionItems.ts @@ -217,7 +217,15 @@ export class FocusSessionActionItem extends SelectActionItem { private update() { const session = this.debugService.getViewModel().focusedSession; const sessions = this.getSessions(); - const names = sessions.map(s => s.getLabel()); + const names = sessions.map(s => { + const label = s.getLabel(); + if (s.parentSession) { + // Indent child sessions so they look like children + return `\u00A0\u00A0${label}`; + } + + return label; + }); this.setOptions(names.map(data => { text: data }), session ? sessions.indexOf(session) : undefined); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 34d69c72946..e983ec98aaa 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -149,6 +149,7 @@ export interface IDebugSession extends ITreeElement { readonly unresolvedConfiguration: IConfig | undefined; readonly state: State; readonly root: IWorkspaceFolder; + readonly parentSession: IDebugSession | undefined; getLabel(): string; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 0fa43459bc4..934672a612f 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -12,7 +12,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { RunOnceScheduler } from 'vs/base/common/async'; import severity from 'vs/base/common/severity'; import { isObject, isString, isUndefinedOrNull } from 'vs/base/common/types'; -import { distinct } from 'vs/base/common/arrays'; +import { distinct, lastIndex } from 'vs/base/common/arrays'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, IReplElementSource, @@ -797,7 +797,17 @@ export class DebugModel implements IDebugModel { return true; }); - this.sessions.push(session); + + let index = -1; + if (session.parentSession) { + // Make sure that child sessions are placed after the parent session + index = lastIndex(this.sessions, s => s.parentSession === session.parentSession || s === session.parentSession); + } + if (index >= 0) { + this.sessions.splice(index + 1, 0, session); + } else { + this.sessions.push(session); + } this._onDidChangeCallStack.fire(undefined); } diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts index 10d5ea1b424..5e9ed4025b3 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugService.ts @@ -315,7 +315,7 @@ export class DebugService implements IDebugService { } } - return this.createSession(launchForName, launchForName!.getConfiguration(name), noDebug); + return this.createSession(launchForName, launchForName!.getConfiguration(name), noDebug, parentSession); })).then(values => values.every(success => !!success)); // Compound launch is a success only if each configuration launched successfully } @@ -325,7 +325,7 @@ export class DebugService implements IDebugService { return Promise.reject(new Error(message)); } - return this.createSession(launch, config, noDebug); + return this.createSession(launch, config, noDebug, parentSession); }); })); }).then(success => { @@ -341,7 +341,7 @@ export class DebugService implements IDebugService { /** * gets the debugger for the type, resolves configurations by providers, substitutes variables and runs prelaunch tasks */ - private createSession(launch: ILaunch | undefined, config: IConfig | undefined, noDebug: boolean): Promise { + private createSession(launch: ILaunch | undefined, config: IConfig | undefined, noDebug: boolean, parentSession?: IDebugSession): Promise { // We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes. // Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config. let type: string | undefined; @@ -386,7 +386,7 @@ export class DebugService implements IDebugService { const workspace = launch ? launch.workspace : undefined; return this.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask).then(result => { if (result === TaskRunResult.Success) { - return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }); + return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, parentSession); } return false; }); @@ -415,9 +415,9 @@ export class DebugService implements IDebugService { /** * instantiates the new session, initializes the session, registers session listeners and reports telemetry */ - private doCreateSession(root: IWorkspaceFolder | undefined, configuration: { resolved: IConfig, unresolved: IConfig | undefined }): Promise { + private doCreateSession(root: IWorkspaceFolder | undefined, configuration: { resolved: IConfig, unresolved: IConfig | undefined }, parentSession?: IDebugSession): Promise { - const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model); + const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model, parentSession); this.model.addSession(session); // register listeners as the very first thing! this.registerSessionListeners(session); diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts b/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts index f4695c0ac7b..c04220c3bc0 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugSession.ts @@ -57,6 +57,7 @@ export class DebugSession implements IDebugSession { private _configuration: { resolved: IConfig, unresolved: IConfig | undefined }, public root: IWorkspaceFolder, private model: DebugModel, + private _parentSession: IDebugSession | undefined, @IDebugService private readonly debugService: IDebugService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IOutputService private readonly outputService: IOutputService, @@ -83,6 +84,10 @@ export class DebugSession implements IDebugSession { return this._configuration.unresolved; } + get parentSession(): IDebugSession | undefined { + return this._parentSession; + } + setConfiguration(configuration: { resolved: IConfig, unresolved: IConfig | undefined }) { this._configuration = configuration; } diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index d7b4cc2ab76..ad79b418172 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -123,6 +123,11 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + + get parentSession(): IDebugSession | undefined { + return undefined; + } + getReplElements(): IReplElement[] { return []; } diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts index 4069d26f76a..eda0096f655 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugModel.test.ts @@ -13,6 +13,10 @@ import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/electron-browser/debugSession'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; +function createMockSession(model: DebugModel, name = 'mockSession', parentSession?: DebugSession | undefined): DebugSession { + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); +} + suite('Debug - Model', () => { let model: DebugModel; let rawSession: MockRawSession; @@ -109,7 +113,7 @@ suite('Debug - Model', () => { test('threads simple', () => { const threadId = 1; const threadName = 'firstThread'; - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = createMockSession(model); model.addSession(session); assert.equal(model.getSessions(true).length, 1); @@ -136,7 +140,7 @@ suite('Debug - Model', () => { const stoppedReason = 'breakpoint'; // Add the threads - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = createMockSession(model); model.addSession(session); session['raw'] = rawSession; @@ -224,7 +228,7 @@ suite('Debug - Model', () => { const runningThreadId = 2; const runningThreadName = 'runningThread'; const stoppedReason = 'breakpoint'; - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = createMockSession(model); model.addSession(session); session['raw'] = rawSession; @@ -338,7 +342,7 @@ suite('Debug - Model', () => { }); test('repl expressions', () => { - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = createMockSession(model); assert.equal(session.getReplElements().length, 0); model.addSession(session); @@ -362,7 +366,7 @@ suite('Debug - Model', () => { }); test('stack frame get specific source name', () => { - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = createMockSession(model); model.addSession(session); let firstStackFrame: StackFrame; @@ -390,10 +394,33 @@ suite('Debug - Model', () => { assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js'); }); + test('debug child sessions are added in correct order', () => { + const session = createMockSession(model); + model.addSession(session); + const secondSession = createMockSession(model, 'mockSession2'); + model.addSession(secondSession); + const firstChild = createMockSession(model, 'firstChild', session); + model.addSession(firstChild); + const secondChild = createMockSession(model, 'secondChild', session); + model.addSession(secondChild); + const thirdSession = createMockSession(model, 'mockSession3'); + model.addSession(thirdSession); + const anotherChild = createMockSession(model, 'secondChild', secondSession); + model.addSession(anotherChild); + + const sessions = model.getSessions(); + assert.equal(sessions[0].getId(), session.getId()); + assert.equal(sessions[1].getId(), firstChild.getId()); + assert.equal(sessions[2].getId(), secondChild.getId()); + assert.equal(sessions[3].getId(), secondSession.getId()); + assert.equal(sessions[4].getId(), anotherChild.getId()); + assert.equal(sessions[5].getId(), thirdSession.getId()); + }); + // Repl output test('repl output', () => { - const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); + const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); const repl = new ReplModel(session); repl.appendToRepl('first line\n', severity.Error); repl.appendToRepl('second line', severity.Error); -- GitLab