diff --git a/news/1 Enhancements/6672.md b/news/1 Enhancements/6672.md new file mode 100644 index 000000000000..165e146af9b9 --- /dev/null +++ b/news/1 Enhancements/6672.md @@ -0,0 +1 @@ +Add debug command code lenses when in debug mode \ No newline at end of file diff --git a/package.json b/package.json index 31b6c33b62bc..6c7343566f98 100644 --- a/package.json +++ b/package.json @@ -351,6 +351,26 @@ "title": "%python.command.python.datascience.runcurrentcell.title%", "category": "Python" }, + { + "command": "python.datascience.debugcell", + "title": "%python.command.python.datascience.debugcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugstepover", + "title": "%python.command.python.datascience.debugstepover.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugstop", + "title": "%python.command.python.datascience.debugstop.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugcontinue", + "title": "%python.command.python.datascience.debugcontinue.title%", + "category": "Python" + }, { "command": "python.datascience.runcurrentcelladvance", "title": "%python.command.python.datascience.runcurrentcelladvance.title%", @@ -1430,6 +1450,12 @@ "description": "Set of commands to put as code lens above a cell. Defaults to 'python.datascience.runcell, python.datascience.runallcellsabove, python.datascience.debugcell'", "scope": "resource" }, + "python.dataScience.debugCodeLenses": { + "type": "string", + "default": "python.datascience.debugcontinue, python.datascience.debugstop, python.datascience.debugstepover", + "description": "Set of debug commands to put as code lens above a cell while debugging.", + "scope": "resource" + }, "python.dataScience.ptvsdDistPath": { "type": "string", "default": "", diff --git a/package.nls.json b/package.nls.json index e971c42a615d..947c587bef1d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -39,6 +39,9 @@ "python.command.python.datascience.runcurrentcellandallbelow.palette.title": "Run Current Cell and Below", "python.command.python.datascience.debugcurrentcell.palette.title": "Debug Current Cell", "python.command.python.datascience.debugcell.title": "Debug Cell", + "python.command.python.datascience.debugstepover.title": "Step Over", + "python.command.python.datascience.debugcontinue.title": "Continue", + "python.command.python.datascience.debugstop.title": "Stop", "python.command.python.datascience.runtoline.title": "Run To Line in Python Interactive Window", "python.command.python.datascience.runfromline.title": "Run From Line in Python Interactive Window", "python.command.python.datascience.runcurrentcell.title": "Run Current Cell", @@ -305,6 +308,9 @@ "DataScience.jupyterDataRateExceeded": "Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0", "DataScience.addCellBelowCommandTitle": "Add cell", "DataScience.debugCellCommandTitle": "Debug cell", + "DataScience.debugStepOverCommandTitle": "Step over", + "DataScience.debugContinueCommandTitle": "Continue", + "DataScience.debugStopCommandTitle": "Stop", "DataScience.runCurrentCellAndAddBelow": "Run current and add cell below", "DataScience.variableExplorerDisabledDuringDebugging": "Please see the Debug Side Bar's VARIABLES section.", "DataScience.jupyterDebuggerNotInstalledError": "Pip module ptvsd is required for debugging cells. You will need to install it to debug cells.", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 43b5a6a2884f..92b5a99ea290 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -25,6 +25,8 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.Set_ShebangInterpreter]: []; [Commands.Run_Linter]: []; [Commands.Enable_Linter]: []; + ['workbench.action.debug.continue']: []; + ['workbench.action.debug.stepOver']: []; ['workbench.action.debug.stop']: []; ['workbench.action.reloadWindow']: []; ['editor.action.formatDocument']: []; @@ -115,6 +117,9 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.RunFileInInteractiveWindows]: [string]; [DSCommands.DebugFileInInteractiveWindows]: [string]; [DSCommands.DebugCell]: [string, number, number, number, number]; + [DSCommands.DebugStepOver]: []; + [DSCommands.DebugStop]: []; + [DSCommands.DebugContinue]: []; [DSCommands.RunCurrentCellAndAddBelow]: [string]; [DSCommands.ScrollToCell]: [string, string]; } diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index 20c4fe4cb05f..ab19abd01f04 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -38,6 +38,10 @@ export class DebugService implements IDebugService { public registerDebugConfigurationProvider(debugType: string, provider: any): Disposable { return debug.registerDebugConfigurationProvider(debugType, provider); } + // tslint:disable-next-line:no-any + public registerDebugAdapterTrackerFactory(debugType: string, provider: any): Disposable { + return debug.registerDebugAdapterTrackerFactory(debugType, provider); + } public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSession?: DebugSession): Thenable { return debug.startDebugging(folder, nameOrConfiguration, parentSession); } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index e420b354e8c4..3a0ca169d9a9 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -7,6 +7,7 @@ import { CancellationToken, CompletionItemProvider, ConfigurationChangeEvent, + DebugAdapterTrackerFactory, DebugConfiguration, DebugConfigurationProvider, DebugConsole, @@ -764,6 +765,15 @@ export interface IDebugService { */ registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable; + /** + * Register a debug adapter tracker factory for the given debug type. + * + * @param debugType The debug type for which the factory is registered or '*' for matching all debug types. + * @param factory The [debug adapter tracker factory](#DebugAdapterTrackerFactory) to register. + * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + */ + registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable; + /** * Start debugging by using either a named launch or named compound configuration, * or by directly passing a [DebugConfiguration](#DebugConfiguration). diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 60266c259ac1..fc525b2dedc3 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -324,6 +324,7 @@ export interface IDataScienceSettings { askForKernelRestart?: boolean; enablePlotViewer?: boolean; codeLenses?: string; + debugCodeLenses?: string; ptvsdDistPath?: string; stopOnFirstLineWhileDebugging?: boolean; textOutputLimit?: number; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 077426ce4fbd..5caf35519256 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -229,6 +229,9 @@ export namespace DataScience { export const jupyterDataRateExceeded = localize('DataScience.jupyterDataRateExceeded', 'Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0'); export const addCellBelowCommandTitle = localize('DataScience.addCellBelowCommandTitle', 'Add cell'); export const debugCellCommandTitle = localize('DataScience.debugCellCommandTitle', 'Debug cell'); + export const debugStepOverCommandTitle = localize('DataScience.debugStepOverCommandTitle', 'Step over'); + export const debugContinueCommandTitle = localize('DataScience.debugContinueCommandTitle', 'Continue'); + export const debugStopCommandTitle = localize('DataScience.debugStopCommandTitle', 'Stop'); export const runCurrentCellAndAddBelow = localize('DataScience.runCurrentCellAndAddBelow', 'Run current and add cell below'); export const variableExplorerDisabledDuringDebugging = localize('DataScience.variableExplorerDisabledDuringDebugging', 'Please see the Debug Side Bar\'s VARIABLES section.'); export const jupyterDebuggerNotInstalledError = localize('DataScience.jupyterDebuggerNotInstalledError', 'Pip module ptvsd is required for debugging cells. You will need to install it to debug cells.'); diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 42a845820171..216f37fea021 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -36,10 +36,22 @@ export namespace Commands { export const AddCellBelow = 'python.datascience.addcellbelow'; export const DebugCurrentCellPalette = 'python.datascience.debugcurrentcell.palette'; export const DebugCell = 'python.datascience.debugcell'; + export const DebugStepOver = 'python.datascience.debugstepover'; + export const DebugContinue = 'python.datascience.debugcontinue'; + export const DebugStop = 'python.datascience.debugstop'; export const RunCurrentCellAndAddBelow = 'python.datascience.runcurrentcellandaddbelow'; export const ScrollToCell = 'python.datascience.scrolltocell'; } +export namespace CodeLensCommands { + // If not specified in the options this is the default set of commands in our design time code lenses + export const DefaultDesignLenses = [Commands.RunCurrentCell, Commands.RunAllCellsAbove, Commands.DebugCell]; + // If not specified in the options this is the default set of commands in our debug time code lenses + export const DefaultDebuggingLenses = [Commands.DebugContinue, Commands.DebugStop, Commands.DebugStepOver]; + // These are the commands that are allowed at debug time + export const DebuggerCommands = [Commands.DebugContinue, Commands.DebugStop, Commands.DebugStepOver]; +} + export namespace EditorContexts { export const HasCodeCells = 'python.datascience.hascodecells'; export const DataScienceEnabled = 'python.datascience.featureenabled'; @@ -152,7 +164,10 @@ export enum Telemetry { PtvsdPromptToInstall = 'DATASCIENCE.PTVSD_PROMPT_TO_INSTALL', PtvsdSuccessfullyInstalled = 'DATASCIENCE.PTVSD_SUCCESSFULLY_INSTALLED', PtvsdInstallFailed = 'DATASCIENCE.PTVSD_INSTALL_FAILED', - ScrolledToCell = 'DATASCIENCE.SCROLLED_TO_CELL' + ScrolledToCell = 'DATASCIENCE.SCROLLED_TO_CELL', + DebugStepOver = 'DATASCIENCE.DEBUG_STEP_OVER', + DebugContinue = 'DATASCIENCE.DEBUG_CONTINUE', + DebugStop = 'DATASCIENCE.DEBUG_STOP' } export namespace HelpLinks { diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts index a370e0ebde25..55e5579ad43a 100644 --- a/src/client/datascience/datascience.ts +++ b/src/client/datascience/datascience.ts @@ -8,7 +8,7 @@ import { inject, injectable } from 'inversify'; import { URL } from 'url'; import * as vscode from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IApplicationShell, ICommandManager, IDebugService, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; import { ContextKey } from '../common/contextKey'; import { traceError } from '../common/logger'; @@ -43,6 +43,7 @@ export class DataScience implements IDataScience { @inject(IConfigurationService) private configuration: IConfigurationService, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IDebugService) private debugService: IDebugService, @inject(IWorkspaceService) private workspace: IWorkspaceService ) { this.commandListeners = this.serviceContainer.getAll(IDataScienceCommandListener); @@ -243,6 +244,36 @@ export class DataScience implements IDataScience { } } + @captureTelemetry(Telemetry.DebugStepOver) + public async debugStepOver(): Promise { + this.dataScienceSurveyBanner.showBanner().ignoreErrors(); + + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.stepOver'); + } + } + + @captureTelemetry(Telemetry.DebugStop) + public async debugStop(): Promise { + this.dataScienceSurveyBanner.showBanner().ignoreErrors(); + + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.stop'); + } + } + + @captureTelemetry(Telemetry.DebugContinue) + public async debugContinue(): Promise { + this.dataScienceSurveyBanner.showBanner().ignoreErrors(); + + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.continue'); + } + } + @captureTelemetry(Telemetry.SetJupyterURIToLocal) private async setJupyterURIToLocal(): Promise { await this.configuration.updateSetting('dataScience.jupyterServerURI', Settings.JupyterServerLocalLaunch, undefined, vscode.ConfigurationTarget.Workspace); @@ -417,6 +448,12 @@ export class DataScience implements IDataScience { this.disposableRegistry.push(disposable); disposable = this.commandManager.registerCommand(Commands.DebugCell, this.debugCell, this); this.disposableRegistry.push(disposable); + disposable = this.commandManager.registerCommand(Commands.DebugStepOver, this.debugStepOver, this); + this.disposableRegistry.push(disposable); + disposable = this.commandManager.registerCommand(Commands.DebugContinue, this.debugContinue, this); + this.disposableRegistry.push(disposable); + disposable = this.commandManager.registerCommand(Commands.DebugStop, this.debugStop, this); + this.disposableRegistry.push(disposable); disposable = this.commandManager.registerCommand(Commands.DebugCurrentCellPalette, this.debugCurrentCellFromCursor, this); this.disposableRegistry.push(disposable); this.commandListeners.forEach((listener: IDataScienceCommandListener) => { diff --git a/src/client/datascience/debugLocationTracker.ts b/src/client/datascience/debugLocationTracker.ts new file mode 100644 index 000000000000..69bd83fab775 --- /dev/null +++ b/src/client/datascience/debugLocationTracker.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { DebugSession, Event, EventEmitter } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IDebugLocation, IDebugLocationTracker } from './types'; + +// When a python debugging session is active keep track of the current debug location +@injectable() +export class DebugLocationTracker implements IDebugLocationTracker { + private waitingForStackTrace: boolean = false; + private _debugLocation: IDebugLocation | undefined; + private debugLocationUpdatedEvent: EventEmitter = new EventEmitter(); + + public setDebugSession(_targetSession: DebugSession) { + this.DebugLocation = undefined; + this.waitingForStackTrace = false; + } + + public get debugLocationUpdated(): Event { + return this.debugLocationUpdatedEvent.event; + } + + public get debugLocation(): IDebugLocation | undefined { + return this._debugLocation; + } + + // tslint:disable-next-line:no-any + public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { + if (this.isStopEvent(message)) { + // Some type of stop, wait to see our next stack trace to find our location + this.waitingForStackTrace = true; + } + + if (this.isContinueEvent(message)) { + // Running, clear the location + this.DebugLocation = undefined; + this.waitingForStackTrace = false; + } + + if (this.waitingForStackTrace) { + // If we are waiting for a stack track, check our messages for one + const debugLoc = this.getStackTrace(message); + if (debugLoc) { + this.DebugLocation = debugLoc; + this.waitingForStackTrace = false; + } + } + + } + + // Set our new location and fire our debug event + private set DebugLocation(newLocation: IDebugLocation | undefined) { + const oldLocation = this._debugLocation; + this._debugLocation = newLocation; + + if (this._debugLocation !== oldLocation) { + this.debugLocationUpdatedEvent.fire(); + } + } + + // tslint:disable-next-line:no-any + private isStopEvent(message: DebugProtocol.ProtocolMessage) { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'stopped') { + return true; + } + } + + return false; + } + + // tslint:disable-next-line:no-any + private getStackTrace(message: DebugProtocol.ProtocolMessage): IDebugLocation | undefined { + if (message.type === 'response') { + const responseMessage = message as DebugProtocol.Response; + if (responseMessage.command === 'stackTrace') { + const messageBody = responseMessage.body; + if (messageBody.stackFrames.length > 0) { + const lineNumber = messageBody.stackFrames[0].line; + const fileName = messageBody.stackFrames[0].source.path; + const column = messageBody.stackFrames[0].column; + return { lineNumber, fileName, column }; + } + } + } + + return undefined; + } + + // tslint:disable-next-line:no-any + private isContinueEvent(message: DebugProtocol.ProtocolMessage): boolean { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'continue') { + return true; + } + } else if (message.type === 'response') { + const responseMessage = message as DebugProtocol.Response; + if (responseMessage.command === 'continue') { + return true; + } + } + + return false; + } +} diff --git a/src/client/datascience/debugLocationTrackerFactory.ts b/src/client/datascience/debugLocationTrackerFactory.ts new file mode 100644 index 000000000000..d698863dc819 --- /dev/null +++ b/src/client/datascience/debugLocationTrackerFactory.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { DebugAdapterTracker, DebugSession, ProviderResult } from 'vscode'; + +import { IDebugService } from '../common/application/types'; +import { IDisposableRegistry } from '../common/types'; +import { IDebugLocationTracker, IDebugLocationTrackerFactory } from './types'; + +// Hook up our IDebugLocationTracker to python debugging sessions +@injectable() +export class DebugLocationTrackerFactory implements IDebugLocationTrackerFactory { + constructor( + @inject(IDebugLocationTracker) private locationTracker: IDebugLocationTracker, + @inject(IDebugService) debugService: IDebugService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); + } + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + this.locationTracker.setDebugSession(session); + return this.locationTracker; + } +} diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts index 65a4b519667d..e9cee575e059 100644 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ b/src/client/datascience/editor-integration/codeLensFactory.ts @@ -9,7 +9,7 @@ import { IConfigurationService } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { generateCellRanges } from '../cellFactory'; -import { Commands } from '../constants'; +import { CodeLensCommands, Commands } from '../constants'; import { InteractiveWindowMessages } from '../interactive-window/interactiveWindowTypes'; import { ICell, ICellHashProvider, ICodeLensFactory, IFileHashes, IInteractiveWindowListener } from '../types'; @@ -79,11 +79,24 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList } private enumerateCommands(): string[] { + let fullCommandList: string[]; + // Add our non-debug commands const commands = this.configService.getSettings().datascience.codeLenses; if (commands) { - return commands.split(',').map(s => s.trim()); + fullCommandList = commands.split(',').map(s => s.trim()); + } else { + fullCommandList = CodeLensCommands.DefaultDesignLenses; } - return [Commands.RunCurrentCell, Commands.RunAllCellsAbove, Commands.DebugCell]; + + // Add our debug commands + const debugCommands = this.configService.getSettings().datascience.debugCodeLenses; + if (debugCommands) { + fullCommandList = fullCommandList.concat(debugCommands.split(',').map(s => s.trim())); + } else { + fullCommandList = fullCommandList.concat(CodeLensCommands.DefaultDebuggingLenses); + } + + return fullCommandList; } private createCodeLens(document: TextDocument, cellRange: { range: Range; cell_type: string }, commandName: string, isFirst: boolean): CodeLens | undefined { @@ -118,6 +131,30 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList localize.DataScience.debugCellCommandTitle(), [document.fileName, range.start.line, range.start.character, range.end.line, range.end.character]); + case Commands.DebugStepOver: + // Only code cells get debug actions + if (cell_type !== 'code') { break; } + return this.generateCodeLens( + range, + Commands.DebugStepOver, + localize.DataScience.debugStepOverCommandTitle()); + + case Commands.DebugContinue: + // Only code cells get debug actions + if (cell_type !== 'code') { break; } + return this.generateCodeLens( + range, + Commands.DebugContinue, + localize.DataScience.debugContinueCommandTitle()); + + case Commands.DebugStop: + // Only code cells get debug actions + if (cell_type !== 'code') { break; } + return this.generateCodeLens( + range, + Commands.DebugStop, + localize.DataScience.debugStopCommandTitle()); + case Commands.RunCurrentCell: case Commands.RunCell: return this.generateCodeLens( diff --git a/src/client/datascience/editor-integration/codelensprovider.ts b/src/client/datascience/editor-integration/codelensprovider.ts index 725962b3fb62..1443ce36d3df 100644 --- a/src/client/datascience/editor-integration/codelensprovider.ts +++ b/src/client/datascience/editor-integration/codelensprovider.ts @@ -6,12 +6,13 @@ import * as vscode from 'vscode'; import { ICommandManager, IDebugService, IDocumentManager } from '../../common/application/types'; import { ContextKey } from '../../common/contextKey'; +import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, IDataScienceSettings, IDisposable, IDisposableRegistry } from '../../common/types'; import { StopWatch } from '../../common/utils/stopWatch'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; -import { EditorContexts, Telemetry } from '../constants'; -import { ICodeWatcher, IDataScienceCodeLensProvider } from '../types'; +import { CodeLensCommands, EditorContexts, Telemetry } from '../constants'; +import { ICodeWatcher, IDataScienceCodeLensProvider, IDebugLocationTracker } from '../types'; @injectable() export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider, IDisposable { @@ -20,16 +21,18 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider private activeCodeWatchers: ICodeWatcher[] = []; private didChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IDebugLocationTracker) private debugLocationTracker: IDebugLocationTracker, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IConfigurationService) private configuration: IConfigurationService, @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) private debugService: IDebugService + @inject(IDebugService) private debugService: IDebugService, + @inject(IFileSystem) private fileSystem: IFileSystem ) { disposableRegistry.push(this); disposableRegistry.push(this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this))); disposableRegistry.push(this.documentManager.onDidCloseTextDocument(this.onDidCloseTextDocument.bind(this))); - + disposableRegistry.push(this.debugLocationTracker.debugLocationUpdated(this.onDebugLocationUpdated.bind(this))); } public dispose() { @@ -55,6 +58,10 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider return this.matchWatcher(document.fileName, document.version, this.configuration.getSettings().datascience); } + private onDebugLocationUpdated() { + this.didChangeCodeLenses.fire(); + } + private onChangeDebugSession(_e: vscode.DebugSession | undefined) { this.didChangeCodeLenses.fire(); } @@ -80,7 +87,7 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider // Don't provide any code lenses if we have not enabled data science const settings = this.configuration.getSettings(); - if (!settings.datascience.enabled || !settings.datascience.enableCellCodeLens || this.debugService.activeDebugSession) { + if (!settings.datascience.enabled || !settings.datascience.enableCellCodeLens) { // Clear out any existing code watchers, providecodelenses is called on settings change // so we don't need to watch the settings change specifically here if (this.activeCodeWatchers.length > 0) { @@ -89,7 +96,42 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider return []; } - return result; + return this.adjustDebuggingLenses(document, result); + } + + // Adjust what code lenses are visible or not given debug mode and debug context location + private adjustDebuggingLenses(document: vscode.TextDocument, lenses: vscode.CodeLens[]): vscode.CodeLens[] { + const debugCellList = CodeLensCommands.DebuggerCommands; + + if (this.debugService.activeDebugSession) { + const debugLocation = this.debugLocationTracker.debugLocation; + + if (debugLocation && this.fileSystem.arePathsSame(debugLocation.fileName, document.uri.fsPath)) { + // We are in the given debug file, so only return the code lens that contains the given line + const activeLenses = lenses.filter(lens => { + // -1 for difference between file system one based and debugger zero based + const pos = new vscode.Position(debugLocation.lineNumber - 1, debugLocation.column - 1); + return lens.range.contains(pos); + }); + + return activeLenses.filter(lens => { + if (lens.command) { + return debugCellList.includes(lens.command.command); + } + return false; + }); + } + } else { + return lenses.filter(lens => { + if (lens.command) { + return !(debugCellList.includes(lens.command.command)); + } + return false; + }); + } + + // Fall through case to return nothing + return []; } private getCodeLens(document: vscode.TextDocument): vscode.CodeLens[] { diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index c151c337647b..c64b6d78ba4b 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -11,6 +11,8 @@ import { Telemetry } from './constants'; import { DataViewer } from './data-viewing/dataViewer'; import { DataViewerProvider } from './data-viewing/dataViewerProvider'; import { DataScience } from './datascience'; +import { DebugLocationTracker } from './debugLocationTracker'; +import { DebugLocationTrackerFactory } from './debugLocationTrackerFactory'; import { CellHashProvider } from './editor-integration/cellhashprovider'; import { CodeLensFactory } from './editor-integration/codeLensFactory'; import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; @@ -50,6 +52,8 @@ import { IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, + IDebugLocationTracker, + IDebugLocationTrackerFactory, IInteractiveWindow, IInteractiveWindowListener, IInteractiveWindowProvider, @@ -122,4 +126,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addBinding(ICellHashProvider, INotebookExecutionLogger); serviceManager.addBinding(IJupyterDebugger, ICellHashListener); serviceManager.addBinding(ICodeLensFactory, IInteractiveWindowListener); + serviceManager.addSingleton(IDebugLocationTrackerFactory, wrapType(DebugLocationTrackerFactory)); + serviceManager.addSingleton(IDebugLocationTracker, wrapType(DebugLocationTracker)); } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 98658dd0c412..a8a62a624b5e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -5,7 +5,7 @@ import { nbformat } from '@jupyterlab/coreutils'; import { Kernel, KernelMessage } from '@jupyterlab/services/lib/kernel'; import { JSONObject } from '@phosphor/coreutils'; import { Observable } from 'rxjs/Observable'; -import { CancellationToken, CodeLens, CodeLensProvider, Disposable, Event, Range, TextDocument, TextEditor } from 'vscode'; +import { CancellationToken, CodeLens, CodeLensProvider, DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, Disposable, Event, Range, TextDocument, TextEditor } from 'vscode'; import { ICommandManager } from '../common/application/types'; import { ExecutionResult, ObservableExecutionResult, SpawnOptions } from '../common/process/types'; @@ -445,3 +445,20 @@ export interface ICellHashProvider { updated: Event; getHashes(): IFileHashes[]; } + +export const IDebugLocationTrackerFactory = Symbol('IDebugLocationTrackerFactory'); +export interface IDebugLocationTrackerFactory extends DebugAdapterTrackerFactory { +} + +export interface IDebugLocation { + fileName: string; + lineNumber: number; + column: number; +} + +export const IDebugLocationTracker = Symbol('IDebugLocationTracker'); +export interface IDebugLocationTracker extends DebugAdapterTracker { + debugLocationUpdated: Event; + debugLocation: IDebugLocation | undefined; + setDebugSession(targetSession: DebugSession): void; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index adf1dbfed115..09cc335531b1 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -65,7 +65,7 @@ import { createDeferred } from './common/utils/async'; import { Common } from './common/utils/localize'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; import { registerTypes as dataScienceRegisterTypes } from './datascience/serviceRegistry'; -import { IDataScience } from './datascience/types'; +import { IDataScience, IDebugLocationTrackerFactory } from './datascience/types'; import { DebuggerTypeName } from './debugger/constants'; import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; @@ -161,6 +161,9 @@ async function activateUnsafe(context: ExtensionContext): Promise const lintingEngine = serviceManager.get(ILintingEngine); lintingEngine.linkJupyterExtension(jupyterExtension).ignoreErrors(); + // Activate debug location tracker + serviceManager.get(IDebugLocationTrackerFactory); + // Activate data science features const dataScience = serviceManager.get(IDataScience); dataScience.activate().ignoreErrors(); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 296d74330541..ba21b427af1b 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -495,7 +495,10 @@ export interface IEventNamePropertyMapping { [Telemetry.CopySourceCode]: never | undefined; [Telemetry.DataScienceSettings]: JSONObject; [Telemetry.DataViewerFetchTime]: never | undefined; + [Telemetry.DebugContinue]: never | undefined; [Telemetry.DebugCurrentCell]: never | undefined; + [Telemetry.DebugStepOver]: never | undefined; + [Telemetry.DebugStop]: never | undefined; [Telemetry.DebugFileInteractive]: never | undefined; [Telemetry.DeleteAllCells]: never | undefined; [Telemetry.DeleteCell]: never | undefined; diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 39e86d50c455..b96db904ca5c 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -97,6 +97,8 @@ import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../ import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; import { DataViewer } from '../../client/datascience/data-viewing/dataViewer'; import { DataViewerProvider } from '../../client/datascience/data-viewing/dataViewerProvider'; +import { DebugLocationTracker } from '../../client/datascience/debugLocationTracker'; +import { DebugLocationTrackerFactory } from '../../client/datascience/debugLocationTrackerFactory'; import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; import { CodeLensFactory } from '../../client/datascience/editor-integration/codeLensFactory'; import { DataScienceCodeLensProvider } from '../../client/datascience/editor-integration/codelensprovider'; @@ -135,6 +137,8 @@ import { IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, + IDebugLocationTracker, + IDebugLocationTrackerFactory, IInteractiveWindow, IInteractiveWindowListener, IInteractiveWindowProvider, @@ -348,6 +352,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(IInstallationChannelManager, InstallationChannelManager); this.serviceManager.addSingleton(IJupyterVariables, JupyterVariables); this.serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger); + this.serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTracker); + this.serviceManager.addSingleton(IDebugLocationTrackerFactory, DebugLocationTrackerFactory); this.serviceManager.addSingleton(ITerminalHelper, TerminalHelper); this.serviceManager.addSingleton( diff --git a/src/test/datascience/debugLocationTracker.unit.test.ts b/src/test/datascience/debugLocationTracker.unit.test.ts new file mode 100644 index 000000000000..2dd6f1824be6 --- /dev/null +++ b/src/test/datascience/debugLocationTracker.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +//tslint:disable:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace +import { expect } from 'chai'; + +import { DebugLocationTracker } from '../../client/datascience/debugLocationTracker'; +import { IDebugLocation } from '../../client/datascience/types'; + +suite('Debug Location Tracker', () => { + let debugTracker: DebugLocationTracker; + + setup(() => { + debugTracker = new DebugLocationTracker(); + }); + + test('Check debug location', async () => { + expect(debugTracker.debugLocation).to.be.equal(undefined, 'Initial location is empty'); + + debugTracker.onDidSendMessage(makeStopMessage()); + + expect(debugTracker.debugLocation).to.be.equal(undefined, 'After stop location is empty'); + + debugTracker.onDidSendMessage(makeStackTraceMessage()); + + const testLocation: IDebugLocation = { lineNumber: 1, column: 1, fileName: 'testpath' }; + expect(debugTracker.debugLocation).to.be.deep.equal(testLocation, 'Source location is incorrect'); + + debugTracker.onDidSendMessage(makeContinueMessage()); + + expect(debugTracker.debugLocation).to.be.equal(undefined, 'After continue location is empty'); + }); +}); + +function makeStopMessage(): any { + return { type: 'event', event: 'stopped' }; +} + +function makeContinueMessage(): any { + return { type: 'event', event: 'continue' }; +} + +function makeStackTraceMessage(): any { + return { + type: 'response', + command: 'stackTrace', + body: { + stackFrames: [ + { line: 1, column: 1, source: { path: 'testpath' } } + ] + } + }; +} diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx index 6e13f24046ee..0d01cd9cd86d 100644 --- a/src/test/datascience/debugger.functional.test.tsx +++ b/src/test/datascience/debugger.functional.test.tsx @@ -7,7 +7,8 @@ import * as path from 'path'; import * as React from 'react'; import * as TypeMoq from 'typemoq'; import * as uuid from 'uuid/v4'; -import { Disposable, Position, Range, SourceBreakpoint, Uri } from 'vscode'; +import { CodeLens, Disposable, Position, Range, SourceBreakpoint, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; import * as vsls from 'vsls/vscode'; import { IApplicationShell, IDebugService, IDocumentManager } from '../../client/common/application/types'; @@ -18,7 +19,7 @@ import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-window/interactiveWindowMessageListener'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; +import { IDataScienceCodeLensProvider, IDebugLocationTrackerFactory, IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; import { noop } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; @@ -124,6 +125,7 @@ suite('DataScience Debugger tests', () => { // This is necessary to get the appropriate live share services up and running. result.get(IInteractiveWindowProvider); result.get(IJupyterExecution); + result.get(IDebugLocationTrackerFactory); return result; } @@ -180,8 +182,13 @@ suite('DataScience Debugger tests', () => { assert.ok(stackTrace, 'Stack trace not computable'); assert.ok(stackTrace!.body.stackFrames.length >= 1, 'Not enough frames'); assert.equal(stackTrace!.body.stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number'); + + verifyCodeLenses(expectedBreakLine); + // Verify break location await mockDebuggerService!.continue(); + + verifyCodeLenses(undefined); } }); @@ -198,6 +205,32 @@ suite('DataScience Debugger tests', () => { await history.dispose(); } + function verifyCodeLenses(expectedBreakLine: number | undefined) { + // We should have three debug code lenses which should all contain the break line + const codeLenses = getCodeLenses(); + + if (expectedBreakLine) { + assert.equal(codeLenses.length, 3, 'Incorrect number of debug code lenses stop'); + codeLenses.forEach(codeLens => { + assert.ok(codeLens.range.contains(new Position(expectedBreakLine - 1, 0))); + }); + } else { + assert.equal(codeLenses.length, 0, 'Incorrect number of debug code lenses continue'); + } + } + + function getCodeLenses(): CodeLens[] { + const documentManager = ioc.serviceManager.get(IDocumentManager) as MockDocumentManager; + const codeLensProvider = ioc.serviceManager.get(IDataScienceCodeLensProvider); + const doc = documentManager.textDocuments[0]; + const result = codeLensProvider.provideCodeLenses(doc, CancellationToken.None); + // tslint:disable-next-line:no-any + if ((result as any).length) { + return result as CodeLens[]; + } + return []; + } + test('Debug cell without breakpoint', async () => { await debugCell('#%%\nprint("bar")'); }); diff --git a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts b/src/test/datascience/editor-integration/codelensprovider.unit.test.ts index db6884e507d6..814a0023d6e5 100644 --- a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts +++ b/src/test/datascience/editor-integration/codelensprovider.unit.test.ts @@ -5,9 +5,10 @@ import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, Disposable, TextDocument } from 'vscode'; import { ICommandManager, IDebugService, IDocumentManager } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; -import { ICodeWatcher, IDataScienceCodeLensProvider } from '../../../client/datascience/types'; +import { ICodeWatcher, IDataScienceCodeLensProvider, IDebugLocationTracker } from '../../../client/datascience/types'; import { IServiceContainer } from '../../../client/ioc/types'; // tslint:disable-next-line: max-func-body-length @@ -20,7 +21,9 @@ suite('DataScienceCodeLensProvider Unit Tests', () => { let documentManager: TypeMoq.IMock; let commandManager: TypeMoq.IMock; let debugService: TypeMoq.IMock; - let tokenSource : CancellationTokenSource; + let debugLocationTracker: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + let tokenSource: CancellationTokenSource; const disposables: Disposable[] = []; setup(() => { @@ -30,16 +33,17 @@ suite('DataScienceCodeLensProvider Unit Tests', () => { documentManager = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); debugService = TypeMoq.Mock.ofType(); - + debugLocationTracker = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); dataScienceSettings = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); dataScienceSettings.setup(d => d.enabled).returns(() => true); pythonSettings.setup(p => p.datascience).returns(() => dataScienceSettings.object); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); commandManager.setup(c => c.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); debugService.setup(d => d.activeDebugSession).returns(() => undefined); - codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, documentManager.object, configurationService.object, commandManager.object, disposables, debugService.object); + codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, debugLocationTracker.object, documentManager.object, configurationService.object, commandManager.object, disposables, debugService.object, fileSystem.object); }); test('Initialize Code Lenses one document', () => { @@ -57,7 +61,7 @@ suite('DataScienceCodeLensProvider Unit Tests', () => { targetCodeWatcher.verifyAll(); serviceContainer.verifyAll(); - }); + }); test('Initialize Code Lenses same doc called', () => { // Create our document diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 014ca8c26bc4..716246c00b1c 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -19,6 +19,7 @@ import { ICellHashProvider, ICodeWatcher, IDataScienceErrorHandler, + IDebugLocationTracker, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; @@ -43,6 +44,7 @@ suite('DataScience Code Watcher Unit Tests', () => { let helper: TypeMoq.IMock; let tokenSource: CancellationTokenSource; let debugService: TypeMoq.IMock; + let debugLocationTracker: TypeMoq.IMock; let cellHashProvider: TypeMoq.IMock; const contexts: Map = new Map(); const pythonSettings = new class extends PythonSettings { @@ -60,6 +62,7 @@ suite('DataScience Code Watcher Unit Tests', () => { textEditor = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); configService = TypeMoq.Mock.ofType(); + debugLocationTracker = TypeMoq.Mock.ofType(); helper = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); debugService = TypeMoq.Mock.ofType(); @@ -153,6 +156,20 @@ suite('DataScience Code Watcher Unit Tests', () => { expect(codeLenses[startLensIndex + indexAdd].command!.command).to.be.equal(Commands.DebugCell, 'Debug command incorrect'); } expect(codeLenses[startLensIndex + indexAdd].range).to.be.deep.equal(targetRange, 'Debug code lens range incorrect'); + + // Debugger mode commands + if (codeLenses[startLensIndex + indexAdd + 1].command) { + expect(codeLenses[startLensIndex + indexAdd + 1].command!.command).to.be.equal(Commands.DebugContinue, 'Debug command incorrect'); + } + expect(codeLenses[startLensIndex + indexAdd + 1].range).to.be.deep.equal(targetRange, 'Debug code lens range incorrect'); + if (codeLenses[startLensIndex + indexAdd + 2].command) { + expect(codeLenses[startLensIndex + indexAdd + 2].command!.command).to.be.equal(Commands.DebugStop, 'Debug command incorrect'); + } + expect(codeLenses[startLensIndex + indexAdd + 2].range).to.be.deep.equal(targetRange, 'Debug code lens range incorrect'); + if (codeLenses[startLensIndex + indexAdd + 3].command) { + expect(codeLenses[startLensIndex + indexAdd + 3].command!.command).to.be.equal(Commands.DebugStepOver, 'Debug command incorrect'); + } + expect(codeLenses[startLensIndex + indexAdd + 3].range).to.be.deep.equal(targetRange, 'Debug code lens range incorrect'); } } @@ -170,7 +187,7 @@ suite('DataScience Code Watcher Unit Tests', () => { // Verify code lenses const codeLenses = codeWatcher.getCodeLenses(); - expect(codeLenses.length).to.be.equal(3, 'Incorrect count of code lenses'); + expect(codeLenses.length).to.be.equal(6, 'Incorrect count of code lenses'); verifyCodeLensesAtPosition(codeLenses, 0, new Range(0, 0, 0, 3), true); // Verify function calls @@ -219,10 +236,10 @@ fourth line`; // Verify code lenses const codeLenses = codeWatcher.getCodeLenses(); - expect(codeLenses.length).to.be.equal(6, 'Incorrect count of code lenses'); + expect(codeLenses.length).to.be.equal(12, 'Incorrect count of code lenses'); verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 3, new Range(6, 0, 7, 11)); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 7, 11)); // Verify function calls document.verifyAll(); @@ -256,11 +273,11 @@ fourth line // Verify code lenses const codeLenses = codeWatcher.getCodeLenses(); - expect(codeLenses.length).to.be.equal(8, 'Incorrect count of code lenses'); + expect(codeLenses.length).to.be.equal(14, 'Incorrect count of code lenses'); verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 3, new Range(6, 0, 8, 0)); - verifyCodeLensesAtPosition(codeLenses, 6, new Range(9, 0, 10, 12), false, true); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 8, 0)); + verifyCodeLensesAtPosition(codeLenses, 12, new Range(9, 0, 10, 12), false, true); // Verify function calls document.verifyAll(); @@ -294,11 +311,11 @@ fourth line // Verify code lenses const codeLenses = codeWatcher.getCodeLenses(); - expect(codeLenses.length).to.be.equal(8, 'Incorrect count of code lenses'); + expect(codeLenses.length).to.be.equal(14, 'Incorrect count of code lenses'); verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 3, new Range(6, 0, 8, 0)); - verifyCodeLensesAtPosition(codeLenses, 6, new Range(9, 0, 10, 12), false, true); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 8, 0)); + verifyCodeLensesAtPosition(codeLenses, 12, new Range(9, 0, 10, 12), false, true); // Verify function calls document.verifyAll(); @@ -718,7 +735,7 @@ testing2`; // Command tests override getText, so just need the ranges here const inputText = '#%% foobar'; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); documentManager.setup(d => d.textDocuments).returns(() => [document.object]); - const codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, documentManager.object, configService.object, commandManager.object, disposables, debugService.object); + const codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, debugLocationTracker.object, documentManager.object, configService.object, commandManager.object, disposables, debugService.object, fileSystem.object); let result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); expect(result, 'result not okay').to.be.ok; diff --git a/src/test/datascience/mockDebugService.ts b/src/test/datascience/mockDebugService.ts index 39e1771cc8f3..fce6f894cf76 100644 --- a/src/test/datascience/mockDebugService.ts +++ b/src/test/datascience/mockDebugService.ts @@ -8,6 +8,8 @@ import * as uuid from 'uuid/v4'; import { Breakpoint, BreakpointsChangeEvent, + DebugAdapterTracker, + DebugAdapterTrackerFactory, DebugConfiguration, DebugConfigurationProvider, DebugConsole, @@ -67,6 +69,8 @@ export class MockDebuggerService implements IDebugService, IDisposable { private session: DebugSession | undefined; private sequence: number = 1; private breakpointEmitter: EventEmitter = new EventEmitter(); + private debugAdapterTrackerFactory: DebugAdapterTrackerFactory | undefined; + private debugAdapterTracker: DebugAdapterTracker | undefined; private sessionChangedEvent: EventEmitter = new EventEmitter(); private sessionStartedEvent: EventEmitter = new EventEmitter(); private sessionTerminatedEvent: EventEmitter = new EventEmitter(); @@ -121,11 +125,22 @@ export class MockDebuggerService implements IDebugService, IDisposable { public registerDebugConfigurationProvider(_debugType: string, _provider: DebugConfigurationProvider): Disposable { throw new Error('Method not implemented.'); } + public registerDebugAdapterTrackerFactory(_debugType: string, _provider: DebugAdapterTrackerFactory): Disposable { + this.debugAdapterTrackerFactory = _provider; + return { dispose: () => { noop(); } }; + } + public startDebugging(_folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, _parentSession?: DebugSession | undefined): Thenable { // Should have a port number. We'll assume during the test it's local const config = nameOrConfiguration as DebugConfiguration; if (config.port) { this.session = new MockDebugSession(uuid(), config, this.sendCustomRequest.bind(this)); + + // Create our debug adapter tracker at session start + if (this.debugAdapterTrackerFactory) { + this.debugAdapterTracker = this.debugAdapterTrackerFactory.createDebugAdapterTracker(this.session) as DebugAdapterTracker; + } + this.socket = net.createConnection(config.port); this.protocolParser.connect(this.socket); this.protocolParser.on('event_stopped', this.onBreakpoint.bind(this)); @@ -146,13 +161,19 @@ export class MockDebuggerService implements IDebugService, IDisposable { return this.breakpointEmitter.event; } - public continue(): Promise { - return this.sendMessage('continue', { threadId: 0 }); + public async continue(): Promise { + await this.sendMessage('continue', { threadId: 0 }); + if (this.debugAdapterTracker && this.debugAdapterTracker.onDidSendMessage) { + this.debugAdapterTracker.onDidSendMessage({ type: 'event', event: 'continue' }); + } } public async getStackTrace(): Promise { const deferred = createDeferred(); this.protocolParser.once('response_stackTrace', (args: any) => { + if (this.debugAdapterTracker && this.debugAdapterTracker.onDidSendMessage) { + this.debugAdapterTracker.onDidSendMessage(args as DebugProtocol.StackTraceResponse); + } deferred.resolve(args as DebugProtocol.StackTraceResponse); }); await this.emitMessage('stackTrace', { @@ -258,6 +279,9 @@ export class MockDebuggerService implements IDebugService, IDisposable { const objString = JSON.stringify(obj); const message = `Content-Length: ${objString.length}\r\n\r\n${objString}`; this.socket.write(message, (_a: any) => { + if (this.debugAdapterTracker && this.debugAdapterTracker.onDidSendMessage) { + this.debugAdapterTracker.onDidSendMessage(obj); + } resolve(); }); } @@ -271,6 +295,10 @@ export class MockDebuggerService implements IDebugService, IDisposable { // Save the current thread id. We use this in our stack trace request this._stoppedThreadId = args.body.threadId; + if (this.debugAdapterTracker && this.debugAdapterTracker.onDidSendMessage) { + this.debugAdapterTracker.onDidSendMessage(args); + } + // Indicate we stopped at a breakpoint this.breakpointEmitter.fire(); }