From e4d4b43f358177420336d42bad395fa4f568c253 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 12 Sep 2019 09:06:50 -0700 Subject: [PATCH] Allow debug session to share repl with its parent; fixes #62419 Introduced DebugSessionOptions passed to debug.startDebugging which encapsulated DebugConsoleMode to control this behavior. --- src/vs/vscode.proposed.d.ts | 50 ++++++++++++++++ .../api/browser/mainThreadDebugService.ts | 15 +++-- .../workbench/api/common/extHost.api.impl.ts | 9 ++- .../workbench/api/common/extHost.protocol.ts | 11 +++- .../api/common/extHostDebugService.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 16 +++++ .../workbench/api/node/extHostDebugService.ts | 11 +++- .../debug/browser/debugActionViewItems.ts | 19 +++++- .../contrib/debug/browser/debugService.ts | 18 +++--- .../contrib/debug/browser/debugSession.ts | 22 +++++-- .../workbench/contrib/debug/browser/repl.ts | 13 ++-- .../workbench/contrib/debug/common/debug.ts | 12 +++- .../contrib/debug/common/debugUtils.ts | 2 +- .../contrib/debug/common/replModel.ts | 10 ++-- .../debug/test/browser/debugModel.test.ts | 60 +++++++++++++++---- .../contrib/debug/test/common/mockDebug.ts | 8 ++- 16 files changed, 219 insertions(+), 59 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7dc2a800f64..57e978b230d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -616,6 +616,56 @@ declare module 'vscode' { debugAdapterExecutable?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult; } + /** + * Debug console mode used by debug session, see [options](#DebugSessionOptions). + */ + export enum DebugConsoleMode { + /** + * Debug session should have a separate debug console. + */ + Separate = 0, + + /** + * Debug session should share debug console with its parent session. + * This value has no effect for sessions which do not have a parent session. + */ + MergeWithParent = 1 + } + + /** + * Options for [starting a debug session](#debug.startDebugging). + */ + export interface DebugSessionOptions { + + /** + * When specified the newly created debug session is registered as a "child" session of this + * "parent" debug session. + */ + parentSession?: DebugSession; + + /** + * Controls whether this session should have a separate debug console or share it + * with the parent session. Has no effect for sessions which do not have a parent session. + * Defaults to Separate. + */ + consoleMode?: DebugConsoleMode; + } + + export namespace debug { + /** + * Start debugging by using either a named launch or named compound configuration, + * or by directly passing a [DebugConfiguration](#DebugConfiguration). + * The named configurations are looked up in '.vscode/launch.json' found in the given folder. + * Before debugging starts, all unsaved files are saved and the launch configurations are brought up-to-date. + * Folder specific variables used in the configuration (e.g. '${workspaceFolder}') are resolved against the given folder. + * @param folder The [workspace folder](#WorkspaceFolder) for looking up named configurations and resolving variables or `undefined` for a non-folder setup. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. + * @param parentSessionOrOptions Debug sesison options. When passed a parent [debug session](#DebugSession), assumes options with just this parent session. + * @return A thenable that resolves when debugging could be successfully started. + */ + export function startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSessionOrOptions?: DebugSession | DebugSessionOptions): Thenable; + } + //#endregion //#region Rob, Matt: logging diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index fcdfb0e4178..bb89ef0cb6c 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,10 +5,10 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI as uri } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, - IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto + IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import severity from 'vs/base/common/severity'; @@ -218,10 +218,15 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return undefined; } - public $startDebugging(_folderUri: uri | undefined, nameOrConfiguration: string | IConfig, parentSessionID: DebugSessionUUID | undefined): Promise { - const folderUri = _folderUri ? uri.revive(_folderUri) : undefined; + public $startDebugging(options: IStartDebuggingOptions): Promise { + const folderUri = options.folder ? uri.revive(options.folder) : undefined; const launch = this.debugService.getConfigurationManager().getLaunch(folderUri); - return this.debugService.startDebugging(launch, nameOrConfiguration, false, this.getSession(parentSessionID)).then(success => { + const debugOptions: IDebugSessionOptions = { + noDebug: false, + parentSession: this.getSession(options.parentSessionID), + repl: options.repl + }; + return this.debugService.startDebugging(launch, options.nameOrConfig, debugOptions).then(success => { return success; }, err => { return Promise.reject(new Error(err && err.message ? err.message : 'cannot start debugging')); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 417b85c2883..3125b8ce583 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -754,8 +754,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerDebugAdapterTrackerFactory(debugType: string, factory: vscode.DebugAdapterTrackerFactory) { return extHostDebugService.registerDebugAdapterTrackerFactory(debugType, factory); }, - startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, parentSession?: vscode.DebugSession) { - return extHostDebugService.startDebugging(folder, nameOrConfig, parentSession); + startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, parentSessionOrOptions?: vscode.DebugSession | vscode.DebugSessionOptions) { + if (!parentSessionOrOptions || (typeof parentSessionOrOptions === 'object' && 'configuration' in parentSessionOrOptions)) { + return extHostDebugService.startDebugging(folder, nameOrConfig, { parentSession: parentSessionOrOptions }); + } + checkProposedApiEnabled(extension); + return extHostDebugService.startDebugging(folder, nameOrConfig, parentSessionOrOptions || {}); }, addBreakpoints(breakpoints: vscode.Breakpoint[]) { return extHostDebugService.addBreakpoints(breakpoints); @@ -900,6 +904,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall, CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall, CallHierarchyItem: extHostTypes.CallHierarchyItem, + DebugConsoleMode: extHostTypes.DebugConsoleMode, Decoration: extHostTypes.Decoration, WebviewEditorState: extHostTypes.WebviewEditorState, UIKind: UIKind diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c62623e3929..19bc442a54b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -39,7 +39,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; -import { IAdapterDescriptor, IConfig } from 'vs/workbench/contrib/debug/common/debug'; +import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal'; import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; @@ -712,6 +712,13 @@ export interface IDebugConfiguration { [key: string]: any; } +export interface IStartDebuggingOptions { + folder: UriComponents | undefined; + nameOrConfig: string | IDebugConfiguration; + parentSessionID?: DebugSessionUUID; + repl?: IDebugSessionReplMode; +} + export interface MainThreadDebugServiceShape extends IDisposable { $registerDebugTypes(debugTypes: string[]): void; $sessionCached(sessionID: string): void; @@ -722,7 +729,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise; $unregisterDebugConfigurationProvider(handle: number): void; $unregisterDebugAdapterDescriptorFactory(handle: number): void; - $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, parentSessionID: string | undefined): Promise; + $startDebugging(options: IStartDebuggingOptions): Promise; $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; $appendDebugConsole(value: string): void; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 3e0f21ec8e1..f079be622ee 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -26,7 +26,7 @@ export interface IExtHostDebugService extends ExtHostDebugServiceShape { addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; removeBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise; - startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, parentSession?: vscode.DebugSession): Promise; + startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise; registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider): vscode.Disposable; registerDebugAdapterDescriptorFactory(extension: IExtensionDescription, type: string, factory: vscode.DebugAdapterDescriptorFactory): vscode.Disposable; registerDebugAdapterTrackerFactory(type: string, factory: vscode.DebugAdapterTrackerFactory): vscode.Disposable; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f75f09034e7..e32da709002 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2352,6 +2352,22 @@ export enum CommentMode { //#endregion +//#region debug +export enum DebugConsoleMode { + /** + * Debug session should have a separate debug console. + */ + Separate = 0, + + /** + * Debug session should share debug console with its parent session. + * This value has no effect for sessions which do not have a parent session. + */ + MergeWithParent = 1 +} + +//#endregion + @es5ClassCompat export class QuickInputButtons { diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 795b4b1598a..d954b28b766 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -14,7 +14,7 @@ import { IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode } from 'vs/workbench/api/common/extHostTypes'; import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; @@ -252,8 +252,13 @@ export class ExtHostDebugService implements IExtHostDebugService, ExtHostDebugSe return this._debugServiceProxy.$unregisterBreakpoints(ids, fids, dids); } - public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, parentSession?: vscode.DebugSession): Promise { - return this._debugServiceProxy.$startDebugging(folder ? folder.uri : undefined, nameOrConfig, parentSession ? parentSession.id : undefined); + public startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration, options: vscode.DebugSessionOptions): Promise { + return this._debugServiceProxy.$startDebugging({ + folder: folder ? folder.uri : undefined, + nameOrConfig, + parentSessionID: options.parentSession ? options.parentSession.id : undefined, + repl: options.consoleMode === DebugConsoleMode.MergeWithParent ? 'mergeWithParent' : 'separate' + }); } public registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider): vscode.Disposable { diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 1904bd302ad..7653bcd4d64 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -202,7 +202,7 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { this._register(attachSelectBoxStyler(this.selectBox, themeService)); this._register(this.debugService.getViewModel().onDidFocusSession(() => { - const session = this.debugService.getViewModel().focusedSession; + const session = this.getSelectedSession(); if (session) { const index = this.getSessions().indexOf(session); this.select(index); @@ -222,11 +222,11 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { } protected getActionContext(_: string, index: number): any { - return this.debugService.getModel().getSessions()[index]; + return this.getSessions()[index]; } private update() { - const session = this.debugService.getViewModel().focusedSession; + const session = this.getSelectedSession(); const sessions = this.getSessions(); const names = sessions.map(s => { const label = s.getLabel(); @@ -240,10 +240,23 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { this.setOptions(names.map(data => { text: data }), session ? sessions.indexOf(session) : undefined); } + private getSelectedSession(): IDebugSession | undefined { + const session = this.debugService.getViewModel().focusedSession; + return session ? this.mapFocusedSessionToSelected(session) : undefined; + } + protected getSessions(): ReadonlyArray { const showSubSessions = this.configurationService.getValue('debug').showSubSessionsInToolBar; const sessions = this.debugService.getModel().getSessions(); return showSubSessions ? sessions : sessions.filter(s => !s.parentSession); } + + protected mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession { + const showSubSessions = this.configurationService.getValue('debug').showSubSessionsInToolBar; + while (focusedSession.parentSession && !showSubSessions) { + focusedSession = focusedSession.parentSession; + } + return focusedSession; + } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 0bd3afba644..7acf41ee1dc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -41,7 +41,7 @@ import { IAction } from 'vs/base/common/actions'; import { deepClone, equals } from 'vs/base/common/objects'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { isExtensionHostDebugging } from 'vs/workbench/contrib/debug/common/debugUtils'; import { isErrorWithActions, createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -255,7 +255,7 @@ export class DebugService implements IDebugService { * main entry point * properly manages compounds, checks for errors and handles the initializing state. */ - startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, noDebug = false, parentSession?: IDebugSession): Promise { + startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { this.startInitializingState(); // make sure to save all files and that the configuration is up to date @@ -318,7 +318,7 @@ export class DebugService implements IDebugService { } } - return this.createSession(launchForName, launchForName!.getConfiguration(name), noDebug, parentSession); + return this.createSession(launchForName, launchForName!.getConfiguration(name), options); })).then(values => values.every(success => !!success)); // Compound launch is a success only if each configuration launched successfully } @@ -328,7 +328,7 @@ export class DebugService implements IDebugService { return Promise.reject(new Error(message)); } - return this.createSession(launch, config, noDebug, parentSession); + return this.createSession(launch, config, options); }); })); }).then(success => { @@ -344,7 +344,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, parentSession?: IDebugSession): Promise { + private createSession(launch: ILaunch | undefined, config: IConfig | undefined, options?: IDebugSessionOptions): 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; @@ -356,7 +356,7 @@ export class DebugService implements IDebugService { } const unresolvedConfig = deepClone(config); - if (noDebug) { + if (options && options.noDebug) { config!.noDebug = true; } @@ -390,7 +390,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 }, parentSession); + return this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); } return false; }); @@ -419,9 +419,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 }, parentSession?: IDebugSession): Promise { + private doCreateSession(root: IWorkspaceFolder | undefined, configuration: { resolved: IConfig, unresolved: IConfig | undefined }, options?: IDebugSessionOptions): Promise { - const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model, parentSession); + const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model, options); this.model.addSession(session); // register listeners as the very first thing! this.registerSessionListeners(session); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 928e9d322ed..d14bb42b27c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { CompletionItem, completionKindFromString } from 'vs/editor/common/modes'; import { Position, IPosition } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { mixin } from 'vs/base/common/objects'; import { Thread, ExpressionContainer, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -43,6 +43,7 @@ export class DebugSession implements IDebugSession { private _subId: string | undefined; private raw: RawDebugSession | undefined; private initialized = false; + private _options: IDebugSessionOptions; private sources = new Map(); private threads = new Map(); @@ -66,7 +67,7 @@ export class DebugSession implements IDebugSession { private _configuration: { resolved: IConfig, unresolved: IConfig | undefined }, public root: IWorkspaceFolder, private model: DebugModel, - private _parentSession: IDebugSession | undefined, + options: IDebugSessionOptions | undefined, @IDebugService private readonly debugService: IDebugService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWindowService private readonly windowService: IWindowService, @@ -79,7 +80,12 @@ export class DebugSession implements IDebugSession { @IOpenerService private readonly openerService: IOpenerService ) { this.id = generateUuid(); - this.repl = new ReplModel(this); + this._options = options || {}; + if (this.hasSeparateRepl()) { + this.repl = new ReplModel(); + } else { + this.repl = (this.parentSession as DebugSession).repl; + } } getId(): string { @@ -103,7 +109,7 @@ export class DebugSession implements IDebugSession { } get parentSession(): IDebugSession | undefined { - return this._parentSession; + return this._options.parentSession; } setConfiguration(configuration: { resolved: IConfig, unresolved: IConfig | undefined }) { @@ -954,13 +960,17 @@ export class DebugSession implements IDebugSession { return this.repl.getReplElements(); } + hasSeparateRepl(): boolean { + return !this.parentSession || this._options.repl !== 'mergeWithParent'; + } + removeReplExpressions(): void { this.repl.removeReplExpressions(); this._onDidChangeREPLElements.fire(); } async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { - const expressionEvaluated = this.repl.addReplExpression(stackFrame, name); + const expressionEvaluated = this.repl.addReplExpression(this, stackFrame, name); this._onDidChangeREPLElements.fire(); await expressionEvaluated; this._onDidChangeREPLElements.fire(); @@ -974,7 +984,7 @@ export class DebugSession implements IDebugSession { } logToRepl(sev: severity, args: any[], frame?: { uri: URI, line: number, column: number }) { - this.repl.logToRepl(sev, args, frame); + this.repl.logToRepl(this, sev, args, frame); this._onDidChangeREPLElements.fire(); } } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index ec21ca71f2c..0b23036ba9a 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -356,7 +356,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati getActions(): IAction[] { const result: IAction[] = []; - if (this.debugService.getModel().getSessions(true).filter(s => !sessionsToIgnore.has(s)).length > 1) { + if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { result.push(this.selectReplAction); } result.push(this.clearReplAction); @@ -968,12 +968,15 @@ registerEditorAction(FilterReplAction); class SelectReplActionViewItem extends FocusSessionActionViewItem { - protected getActionContext(_: string, index: number): any { - return this.debugService.getModel().getSessions(true)[index]; + protected getSessions(): ReadonlyArray { + return this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)); } - protected getSessions(): ReadonlyArray { - return this.debugService.getModel().getSessions(true).filter(s => !sessionsToIgnore.has(s)); + protected mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession { + while (focusedSession.parentSession && !focusedSession.hasSeparateRepl()) { + focusedSession = focusedSession.parentSession; + } + return focusedSession; } } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8d4055a1569..10690a11cb3 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -147,6 +147,14 @@ export interface LoadedSourceEvent { source: Source; } +export type IDebugSessionReplMode = 'separate' | 'mergeWithParent'; + +export interface IDebugSessionOptions { + noDebug?: boolean; + parentSession?: IDebugSession; + repl?: IDebugSessionReplMode; +} + export interface IDebugSession extends ITreeElement { readonly configuration: IConfig; @@ -173,7 +181,7 @@ export interface IDebugSession extends ITreeElement { clearThreads(removeThreads: boolean, reference?: number): void; getReplElements(): IReplElement[]; - + hasSeparateRepl(): boolean; removeReplExpressions(): void; addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise; appendToRepl(data: string | IExpression, severity: severity, source?: IReplElementSource): void; @@ -815,7 +823,7 @@ export interface IDebugService { * Returns true if the start debugging was successfull. For compound launches, all configurations have to start successfuly for it to return success. * On errors the startDebugging will throw an error, however some error and cancelations are handled and in that case will simply return false. */ - startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, noDebug?: boolean, parentSession?: IDebugSession): Promise; + startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise; /** * Restarts a session or creates a new one if there is no active session. diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 1c3e291b23c..8dbe2680204 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -27,7 +27,7 @@ export function startDebugging(debugService: IDebugService, historyService: IHis configurationManager.selectConfiguration(launch); } - return debugService.startDebugging(launch, undefined, noDebug); + return debugService.startDebugging(launch, undefined, { noDebug }); } export function formatPII(value: string, excludePII: boolean, args: { [key: string]: string }): string { diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 7808f9e6a0d..3d7e8ab5e09 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -108,16 +108,14 @@ export class ReplEvaluationResult extends ExpressionContainer implements IReplEl export class ReplModel { private replElements: IReplElement[] = []; - constructor(private session: IDebugSession) { } - getReplElements(): IReplElement[] { return this.replElements; } - async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { + async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise { this.addReplElement(new ReplEvaluationInput(name)); const result = new ReplEvaluationResult(); - await result.evaluateExpression(name, this.session, stackFrame, 'repl'); + await result.evaluateExpression(name, session, stackFrame, 'repl'); this.addReplElement(result); } @@ -153,14 +151,14 @@ export class ReplModel { } } - logToRepl(sev: severity, args: any[], frame?: { uri: URI, line: number, column: number }) { + logToRepl(session: IDebugSession, sev: severity, args: any[], frame?: { uri: URI, line: number, column: number }) { let source: IReplElementSource | undefined; if (frame) { source = { column: frame.column, lineNumber: frame.line, - source: this.session.getSource({ + source: session.getSource({ name: basenameOrAuthority(frame.uri), path: frame.uri.fsPath }) diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts index e69860a2fac..99204a39f27 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts @@ -12,11 +12,11 @@ import { MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; -import { IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { IBreakpointUpdateData, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { NullOpenerService } from 'vs/platform/opener/common/opener'; -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!, NullOpenerService); +function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); } suite('Debug - Model', () => { @@ -341,10 +341,10 @@ suite('Debug - Model', () => { session['raw'] = rawSession; const thread = new Thread(session, 'mockthread', 1); const stackFrame = new StackFrame(thread, 1, undefined, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - const replModel = new ReplModel(session); - replModel.addReplExpression(stackFrame, 'myVariable').then(); - replModel.addReplExpression(stackFrame, 'myVariable').then(); - replModel.addReplExpression(stackFrame, 'myVariable').then(); + const replModel = new ReplModel(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); assert.equal(replModel.getReplElements().length, 3); replModel.getReplElements().forEach(re => { @@ -405,13 +405,13 @@ suite('Debug - Model', () => { model.addSession(session); const secondSession = createMockSession(model, 'mockSession2'); model.addSession(secondSession); - const firstChild = createMockSession(model, 'firstChild', session); + const firstChild = createMockSession(model, 'firstChild', { parentSession: session }); model.addSession(firstChild); - const secondChild = createMockSession(model, 'secondChild', session); + const secondChild = createMockSession(model, 'secondChild', { parentSession: session }); model.addSession(secondChild); const thirdSession = createMockSession(model, 'mockSession3'); model.addSession(thirdSession); - const anotherChild = createMockSession(model, 'secondChild', secondSession); + const anotherChild = createMockSession(model, 'secondChild', { parentSession: secondSession }); model.addSession(anotherChild); const sessions = model.getSessions(); @@ -426,8 +426,7 @@ suite('Debug - Model', () => { // 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!, undefined!, NullOpenerService); - const repl = new ReplModel(session); + const repl = new ReplModel(); repl.appendToRepl('first line\n', severity.Error); repl.appendToRepl('second line ', severity.Error); repl.appendToRepl('third line ', severity.Error); @@ -466,4 +465,41 @@ suite('Debug - Model', () => { assert.equal(elements[1], '23\n45\n'); assert.equal(elements[2], '6'); }); + + test('repl merging', () => { + // 'mergeWithParent' should be ignored when there is no parent. + const parent = createMockSession(model, 'parent', { repl: 'mergeWithParent' }); + const child1 = createMockSession(model, 'child1', { parentSession: parent, repl: 'separate' }); + const child2 = createMockSession(model, 'child2', { parentSession: parent, repl: 'mergeWithParent' }); + const grandChild = createMockSession(model, 'grandChild', { parentSession: child2, repl: 'mergeWithParent' }); + const child3 = createMockSession(model, 'child3', { parentSession: parent }); + + parent.appendToRepl('1\n', severity.Info); + assert.equal(parent.getReplElements().length, 1); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 1); + assert.equal(grandChild.getReplElements().length, 1); + assert.equal(child3.getReplElements().length, 0); + + grandChild.appendToRepl('1\n', severity.Info); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 0); + + child3.appendToRepl('1\n', severity.Info); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 1); + + child1.appendToRepl('1\n', severity.Info); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 1); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 1); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 155f23002cf..9ce2b7f792e 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -7,7 +7,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Position, IPosition } from 'vs/editor/common/core/position'; -import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; +import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { CompletionItem } from 'vs/editor/common/modes'; import Severity from 'vs/base/common/severity'; @@ -102,7 +102,7 @@ export class MockDebugService implements IDebugService { public removeWatchExpressions(id?: string): void { } - public startDebugging(launch: ILaunch, configOrName?: IConfig | string, noDebug?: boolean): Promise { + public startDebugging(launch: ILaunch, configOrName?: IConfig | string, options?: IDebugSessionOptions): Promise { return Promise.resolve(true); } @@ -159,6 +159,10 @@ export class MockSession implements IDebugSession { return []; } + hasSeparateRepl(): boolean { + return true; + } + removeReplExpressions(): void { } get onDidChangeReplElements(): Event { throw new Error('not implemented'); -- GitLab