diff --git a/src/client/debugger/jupyter/debuggingManager.ts b/src/client/debugger/jupyter/debuggingManager.ts index 20bd689d343..34772410cad 100644 --- a/src/client/debugger/jupyter/debuggingManager.ts +++ b/src/client/debugger/jupyter/debuggingManager.ts @@ -192,7 +192,7 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb } const controller = this.notebookToRunByLineController.get(cell.notebook); - if (controller && controller.debugCellUri?.toString() === cell.document.uri.toString()) { + if (controller && controller.debugCell.document.uri.toString() === cell.document.uri.toString()) { controller.continue(); } }), @@ -323,6 +323,7 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb if (this.vscNotebook.activeNotebookEditor) { const activeDoc = this.vscNotebook.activeNotebookEditor.document; + // TODO we apparently always have a kernel here, clean up typings const kernel = await this.ensureKernelIsRunning(activeDoc); const debug = this.getDebuggerByUri(activeDoc); @@ -334,19 +335,17 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb }); if (notebook && notebook.session) { debug.resolve(session); - const adapter = new KernelDebugAdapter( - session, - debug.document, - notebook.session, - this.commandManager, - this.fs, - kernel, - this.settings - ); + const adapter = new KernelDebugAdapter(session, debug.document, notebook.session, this.fs, kernel); if (config.__mode === KernelDebugMode.RunByLine && typeof config.__cellIndex === 'number') { - const cellUri = activeDoc.cellAt(config.__cellIndex).document.uri; - const controller = new RunByLineController(adapter, cellUri); + const cell = activeDoc.cellAt(config.__cellIndex); + const controller = new RunByLineController( + adapter, + cell, + this.commandManager, + kernel!, + this.settings + ); adapter.setDebuggingDelegate(controller); this.notebookToRunByLineController.set(debug.document, controller); } diff --git a/src/client/debugger/jupyter/kernelDebugAdapter.ts b/src/client/debugger/jupyter/kernelDebugAdapter.ts index 029a77ca9cb..bda9415719c 100644 --- a/src/client/debugger/jupyter/kernelDebugAdapter.ts +++ b/src/client/debugger/jupyter/kernelDebugAdapter.ts @@ -3,37 +3,33 @@ 'use strict'; +import { KernelMessage } from '@jupyterlab/services'; +import * as path from 'path'; import { - NotebookDocument, - DebugSession, + debug, DebugAdapter, - NotebookCell, + DebugConfiguration, + DebugProtocolMessage, + DebugSession, Event, EventEmitter, - DebugProtocolMessage, - notebooks, - NotebookCellExecutionStateChangeEvent, + NotebookCell, NotebookCellExecutionState, - DebugConfiguration, - Uri, + NotebookCellExecutionStateChangeEvent, NotebookCellKind, - debug + NotebookDocument, + notebooks, + Uri } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import * as path from 'path'; -import { IJupyterSession } from '../../datascience/types'; -import { KernelMessage } from '@jupyterlab/services'; -import { ICommandManager } from '../../common/application/types'; import { traceError, traceVerbose } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; -import { DebuggingDelegate, IKernelDebugAdapter } from '../types'; -import { IConfigurationService, IDisposable } from '../../common/types'; -import { Commands } from '../../datascience/constants'; +import { IDisposable } from '../../common/types'; import { IKernel } from '../../datascience/jupyter/kernels/types'; +import { IJupyterSession } from '../../datascience/types'; import { sendTelemetryEvent } from '../../telemetry'; import { DebuggingTelemetry } from '../constants'; -import { parseForComments } from '../../../datascience-ui/common'; -import { noop } from '../../common/utils/misc'; +import { DebuggingDelegate, IKernelDebugAdapter } from '../types'; interface dumpCellResponse { sourcePath: string; // filename for the dumped source @@ -95,10 +91,8 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID private session: DebugSession, private notebookDocument: NotebookDocument, private readonly jupyterSession: IJupyterSession, - private commandManager: ICommandManager, private fs: IFileSystem, - private readonly kernel: IKernel | undefined, - private settings: IConfigurationService + private readonly kernel: IKernel | undefined ) { void this.dumpAllCells(); @@ -114,10 +108,6 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunAndDebugCell); } - if (configuration.__mode === KernelDebugMode.RunByLine) { - sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunByLine); - } - this.disposables.push( this.jupyterSession.onIOPubMessage(async (msg: KernelMessage.IIOPubMessage) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -125,7 +115,7 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID if (anyMsg.header.msg_type === 'debug_event') { this.trace('event', JSON.stringify(msg)); - if (!(await this.delegate?.willSendMessage(anyMsg))) { + if (!(await this.delegate?.willSendEvent(anyMsg))) { this.sendMessage.fire(msg.content); } } @@ -185,7 +175,12 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID if (message.type === 'request' && (message as DebugProtocol.Request).command === 'setBreakpoints') { const args = (message as DebugProtocol.Request).arguments; if (args.source && args.source.path && args.source.path.indexOf('vscode-notebook-cell:') === 0) { - await this.dumpCell(args.source.path); + const cell = this.notebookDocument + .getCells() + .find((c) => c.document.uri.toString() === args.source.path); + if (cell) { + await this.dumpCell(cell.index); + } } } @@ -197,14 +192,8 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID await this.debugInfo(); } - // initialize Run By Line - if ( - (this.configuration.__mode === KernelDebugMode.RunByLine || - this.configuration.__mode === KernelDebugMode.Cell) && - message.type === 'request' && - (message as DebugProtocol.Request).command === 'configurationDone' - ) { - await this.initializeExecute(message.seq); + if (message.type === 'request') { + await this.delegate?.willSendRequest(message as DebugProtocol.Request); } this.sendRequestToJupyterSession(message); @@ -236,16 +225,14 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID }); } - public stackTrace(args?: { - threadId: number; - startFrame?: number; - levels?: number; - }): Thenable { - return this.session.customRequest('stackTrace', { - threadId: args?.threadId, - startFrame: args?.startFrame, - levels: args?.levels - }); + public stackTrace(args: DebugProtocol.StackTraceArguments): Thenable { + return this.session.customRequest('stackTrace', args); + } + + public setBreakpoints( + args: DebugProtocol.SetBreakpointsArguments + ): Thenable { + return this.session.customRequest('setBreakpoints', args); } private scopes(frameId: number): void { @@ -259,14 +246,14 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID private dumpAllCells() { this.notebookDocument.getCells().forEach((cell) => { if (cell.kind === NotebookCellKind.Code) { - void this.dumpCell(cell.document.uri.toString()); + void this.dumpCell(cell.index); } }); } // Dump content of given cell into a tmp file and return path to file. - private async dumpCell(uri: string): Promise { - const cell = this.notebookDocument.getCells().find((c) => c.document.uri.toString() === uri); + public async dumpCell(index: number): Promise { + const cell = this.notebookDocument.cellAt(index); if (cell) { try { const response = await this.session.customRequest('dumpCell', { code: cell.document.getText() }); @@ -455,70 +442,4 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID break; } } - - private async initializeExecute(seq: number) { - // remove this if when https://github.com/microsoft/debugpy/issues/706 is fixed and ipykernel ships it - // executing this code restarts debugpy and fixes https://github.com/microsoft/vscode-jupyter/issues/7251 - if (this.kernel) { - const code = 'import debugpy\ndebugpy.debug_this_thread()'; - await this.kernel.executeHidden(code, this.notebookDocument); - } - - // put breakpoint at the beginning of the cell - const cellIndex = Number(this.configuration.__cellIndex); - const cell = this.notebookDocument.cellAt(cellIndex); - - await this.dumpCell(cell.document.uri.toString()); - - if (this.configuration.__mode === KernelDebugMode.RunByLine) { - // This will save the code lines of the cell in lineList (so ignore comments and emtpy lines) - // Its done to set the Run by Line breakpoint on the first code line - const textLines = cell.document.getText().splitLines({ trim: false, removeEmptyEntries: false }); - const lineList: number[] = []; - parseForComments( - textLines, - () => noop(), - (s, i) => { - if (s.trim().length !== 0) { - lineList.push(i); - } - } - ); - lineList.sort(); - - // Don't send the SetBreakpointsRequest or open the variable view if there are no code lines - if (lineList.length !== 0) { - const initialBreakpoint: DebugProtocol.SourceBreakpoint = { - line: lineList[0] + 1 - }; - const message: DebugProtocol.SetBreakpointsRequest = { - seq: seq + 1, - type: 'request', - command: 'setBreakpoints', - arguments: { - source: { - name: path.basename(cell.notebook.uri.path), - path: cell.document.uri.toString() - }, - lines: [lineList[0] + 1], - breakpoints: [initialBreakpoint], - sourceModified: false - } - }; - this.sendRequestToJupyterSession(message); - - // Open variable view - const settings = this.settings.getSettings(); - if (settings.showVariableViewWhenDebugging) { - void this.commandManager.executeCommand(Commands.OpenVariableView); - } - } - } - - // Run cell - void this.commandManager.executeCommand('notebook.cell.execute', { - ranges: [{ start: cell.index, end: cell.index + 1 }], - document: cell.document.uri - }); - } } diff --git a/src/client/debugger/jupyter/runByLineController.ts b/src/client/debugger/jupyter/runByLineController.ts index 1d80f7579e3..e23caf0f4ba 100644 --- a/src/client/debugger/jupyter/runByLineController.ts +++ b/src/client/debugger/jupyter/runByLineController.ts @@ -1,14 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { DebugProtocolMessage, Uri } from 'vscode'; +import * as path from 'path'; +import { DebugProtocolMessage, NotebookCell } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { parseForComments } from '../../../datascience-ui/common'; +import { ICommandManager } from '../../common/application/types'; import { traceVerbose } from '../../common/logger'; +import { IConfigurationService } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { Commands } from '../../datascience/constants'; +import { IKernel } from '../../datascience/jupyter/kernels/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { DebuggingTelemetry } from '../constants'; import { DebuggingDelegate, IKernelDebugAdapter } from '../types'; export class RunByLineController implements DebuggingDelegate { private lastPausedThreadId: number | undefined; - constructor(private readonly debugAdapter: IKernelDebugAdapter, public readonly debugCellUri: Uri) {} + constructor( + private readonly debugAdapter: IKernelDebugAdapter, + public readonly debugCell: NotebookCell, + private readonly commandManager: ICommandManager, + private readonly kernel: IKernel, + private readonly settings: IConfigurationService + ) { + sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunByLine); + } public continue(): void { if (typeof this.lastPausedThreadId !== 'number') { @@ -23,7 +41,7 @@ export class RunByLineController implements DebuggingDelegate { this.debugAdapter.disconnect(); } - public async willSendMessage(msg: DebugProtocolMessage): Promise { + public async willSendEvent(msg: DebugProtocolMessage): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const anyMsg = msg as any; @@ -38,6 +56,12 @@ export class RunByLineController implements DebuggingDelegate { return false; } + public async willSendRequest(request: DebugProtocol.Request): Promise { + if (request.command === 'configurationDone') { + await this.initializeExecute(); + } + } + private async handleStoppedEvent(threadId: number): Promise { if (await this.shouldStepIn(threadId)) { void this.debugAdapter.stepIn(threadId); @@ -53,10 +77,64 @@ export class RunByLineController implements DebuggingDelegate { const stResponse = await this.debugAdapter.stackTrace({ threadId, startFrame: 0, levels: 1 }); const sf = stResponse.stackFrames[0]; - return !!sf.source && sf.source.path !== this.debugCellUri.toString(); + return !!sf.source && sf.source.path !== this.debugCell.document.uri.toString(); } private trace(tag: string, msg: string) { traceVerbose(`[Debug-RBL] ${tag}: ${msg}`); } + + private async initializeExecute() { + // remove this if when https://github.com/microsoft/debugpy/issues/706 is fixed and ipykernel ships it + // executing this code restarts debugpy and fixes https://github.com/microsoft/vscode-jupyter/issues/7251 + if (this.kernel) { + const code = 'import debugpy\ndebugpy.debug_this_thread()'; + await this.kernel.executeHidden(code, this.debugCell.notebook); + } + + // put breakpoint at the beginning of the cell + await this.debugAdapter.dumpCell(this.debugCell.index); + + // This will save the code lines of the cell in lineList (so ignore comments and emtpy lines) + // Its done to set the Run by Line breakpoint on the first code line + const textLines = this.debugCell.document.getText().splitLines({ trim: false, removeEmptyEntries: false }); + const lineList: number[] = []; + parseForComments( + textLines, + () => noop(), + (s, i) => { + if (s.trim().length !== 0) { + lineList.push(i); + } + } + ); + lineList.sort(); + + // Don't send the SetBreakpointsRequest or open the variable view if there are no code lines + if (lineList.length !== 0) { + const initialBreakpoint: DebugProtocol.SourceBreakpoint = { + line: lineList[0] + 1 + }; + await this.debugAdapter.setBreakpoints({ + source: { + name: path.basename(this.debugCell.notebook.uri.path), + path: this.debugCell.document.uri.toString() + }, + breakpoints: [initialBreakpoint], + sourceModified: false + }); + + // Open variable view + const settings = this.settings.getSettings(); + if (settings.showVariableViewWhenDebugging) { + void this.commandManager.executeCommand(Commands.OpenVariableView); + } + } + + // Run cell + void this.commandManager.executeCommand('notebook.cell.execute', { + ranges: [{ start: this.debugCell.index, end: this.debugCell.index + 1 }], + document: this.debugCell.document.uri + }); + } } diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 2e7ad7767a1..008f9b0bea9 100644 --- a/src/client/debugger/types.ts +++ b/src/client/debugger/types.ts @@ -9,13 +9,11 @@ export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTe export interface IKernelDebugAdapter { debugSession: DebugSession; stepIn(threadId: number): Thenable; - stackTrace(args?: { - threadId: number; - startFrame?: number; - levels?: number; - }): Thenable; + stackTrace(args: DebugProtocol.StackTraceArguments): Thenable; + setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Thenable; disconnect(): void; onDidEndSession: Event; + dumpCell(index: number): Promise; } export const IDebuggingManager = Symbol('IDebuggingManager'); @@ -26,7 +24,12 @@ export interface IDebuggingManager { export interface DebuggingDelegate { /** - * Returns true to signal that sending the message is vetoed. + * Called for every event sent from the debug adapter to the client. Returns true to signal that sending the message is vetoed. */ - willSendMessage(msg: DebugProtocolMessage): Promise; + willSendEvent(msg: DebugProtocolMessage): Promise; + + /** + * Called for every request sent from the client to the debug adapter. + */ + willSendRequest(request: DebugProtocol.Request): Promise; }