From ec34b728d0fbcc503962f75c4b40f638dc3682e3 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Wed, 18 Aug 2021 12:45:02 -0700 Subject: [PATCH] Keybindings and telemetry (#7183) * -update install modal -add keybindings -add progress notification when run by line starts -disconnect when the kernel is disposed * add telemetry * oops * lint * don't check for cell when stopping run by line * update link --- package.json | 18 ++++ package.nls.json | 4 +- src/client/common/application/commands.ts | 1 + src/client/common/utils/localize.ts | 7 +- src/client/datascience/constants.ts | 1 + src/client/debugger/constants.ts | 11 +++ .../debugger/jupyter/debuggingManager.ts | 99 ++++++++++++++----- .../debugger/jupyter/kernelDebugAdapter.ts | 17 ++++ src/client/telemetry/index.ts | 13 +++ 9 files changed, 142 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 6b8c20f084c..e2c57afce1b 100644 --- a/package.json +++ b/package.json @@ -277,6 +277,24 @@ "command": "jupyter.runAndDebugCell", "key": "ctrl+alt+shift+enter", "mac": "ctrl+shift+enter" + }, + { + "command": "jupyter.runByLine", + "key": "f10", + "mac": "f10", + "when": "!jupyter.notebookeditor.debuggingInProgress && !jupyter.notebookeditor.runByLineInProgress" + }, + { + "command": "jupyter.runByLineContinue", + "key": "f10", + "mac": "f10", + "when": "jupyter.notebookeditor.runByLineInProgress" + }, + { + "command": "jupyter.runByLineStop", + "key": "ctrl+enter", + "mac": "ctrl+enter", + "when": "jupyter.notebookeditor.runByLineInProgress" } ], "commands": [ diff --git a/package.nls.json b/package.nls.json index d7ef473f3ab..9b27373927b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -480,7 +480,9 @@ "DataScience.interactiveWindowModeBannerSwitchAlways": "Always", "DataScience.interactiveWindowModeBannerSwitchNo": "No", "DataScience.ipykernelNotInstalled": "IPyKernel not installed into interpreter {0}", - "DataScience.needIpykernel6": "Ipykernel 6 is needed for debugging, click Install to continue. Or you can run 'pip install ipykernel==6.0.3/conda install ipykernel=6'", + "DataScience.needIpykernel6": "Ipykernel setup required for this feature", + "DataScience.setup": "Setup", + "DataScience.startingRunByLine": "Starting Run by Line", "DataScience.illegalEditorConfig": "CustomEditor and NativeNotebook experiments cannot be turned on together", "DataScience.pythonExtensionRequired": "The Python extension is required to perform that task. Click Yes to open Python extension installation page.", "DataScience.pythonExtensionRequiredToRunNotebook": "Python Extension required to run Python notebooks.", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 919763795e9..95aecbba085 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -172,4 +172,5 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.RunByLine]: [NotebookCell]; [DSCommands.RunAndDebugCell]: [NotebookCell]; [DSCommands.RunByLineContinue]: [NotebookCell]; + [DSCommands.RunByLineStop]: []; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 62fa899b2c9..7cd589ef863 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -940,10 +940,9 @@ export namespace DataScience { 'DataScience.ipykernelNotInstalled', 'IPyKernel not installed into interpreter {0}' ); - export const needIpykernel6 = localize( - 'DataScience.needIpykernel6', - "Ipykernel 6 is needed for debugging, click Install to continue. Or you can run 'pip install ipykernel==6.0.3/conda install ipykernel=6'" - ); + export const needIpykernel6 = localize('DataScience.needIpykernel6', 'Ipykernel setup required for this feature'); + export const setup = localize('DataScience.setup', 'Setup'); + export const startingRunByLine = localize('DataScience.startingRunByLine', 'Starting Run by Line'); export const showDataViewerFail = localize( 'DataScience.showDataViewerFail', 'Failed to create the Data Viewer. Check the Jupyter tab of the Output window for more info.' diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 9c023e03296..10e05c9760e 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -172,6 +172,7 @@ export namespace Commands { export const RunByLine = 'jupyter.runByLine'; export const RunAndDebugCell = 'jupyter.runAndDebugCell'; export const RunByLineContinue = 'jupyter.runByLineContinue'; + export const RunByLineStop = 'jupyter.runByLineStop'; } export namespace CodeLensCommands { diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts index 9a2bfbbb00e..8ccd00bc922 100644 --- a/src/client/debugger/constants.ts +++ b/src/client/debugger/constants.ts @@ -4,3 +4,14 @@ 'use strict'; export const pythonKernelDebugAdapter = 'Python Kernel Debug Adapter'; + +export enum DebuggingTelemetry { + clickedOnSetup = 'DATASCIENCE.DEBUGGING.CLICKED_ON_SETUP', + closedModal = 'DATASCIENCE.DEBUGGING.CLOSED_MODAL', + ipykernel6Status = 'DATASCIENCE.DEBUGGING.IPYKERNEL6_STATUS', + clickedRunByLine = 'DATASCIENCE.DEBUGGING.CLICKED_RUNBYLINE', + successfullyStartedRunByLine = 'DATASCIENCE.DEBUGGING.SUCCESSFULLY_STARTED_RUNBYLINE', + clickedRunAndDebugCell = 'DATASCIENCE.DEBUGGING.CLICKED_RUN_AND_DEBUG_CELL', + successfullyStartedRunAndDebugCell = 'DATASCIENCE.DEBUGGING.SUCCESSFULLY_STARTED_RUN_AND_DEBUG_CELL', + endedSession = 'DATASCIENCE.DEBUGGING.ENDED_SESSION' +} diff --git a/src/client/debugger/jupyter/debuggingManager.ts b/src/client/debugger/jupyter/debuggingManager.ts index ed22cf5a95c..810e560cc55 100644 --- a/src/client/debugger/jupyter/debuggingManager.ts +++ b/src/client/debugger/jupyter/debuggingManager.ts @@ -15,11 +15,12 @@ import { DebugSessionOptions, DebugConfiguration, EventEmitter, - DebugProtocolMessage + DebugProtocolMessage, + ProgressLocation } from 'vscode'; import * as path from 'path'; import { IKernel, IKernelProvider } from '../../datascience/jupyter/kernels/types'; -import { IDisposable, IInstaller, Product, ProductInstallStatus } from '../../common/types'; +import { IDisposable, Product, ProductInstallStatus } from '../../common/types'; import { IKernelDebugAdapterConfig, KernelDebugAdapter, KernelDebugMode } from './kernelDebugAdapter'; import { INotebookProvider } from '../../datascience/types'; import { IExtensionSingleActivationService } from '../../activation/types'; @@ -34,8 +35,9 @@ import { Commands as DSCommands } from '../../datascience/constants'; import { IFileSystem } from '../../common/platform/types'; import { IDebuggingManager } from '../types'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { pythonKernelDebugAdapter } from '../constants'; +import { DebuggingTelemetry, pythonKernelDebugAdapter } from '../constants'; import { IPythonInstaller } from '../../api/types'; +import { sendTelemetryEvent } from '../../telemetry'; class Debugger { private resolveFunc?: (value: DebugSession) => void; @@ -93,8 +95,7 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, @inject(IFileSystem) private fs: IFileSystem, - @inject(IPythonInstaller) private pythonInstaller: IPythonInstaller, - @inject(IInstaller) private readonly installer: IInstaller + @inject(IPythonInstaller) private pythonInstaller: IPythonInstaller ) { this.debuggingInProgress = new ContextKey(EditorContexts.DebuggingInProgress, this.commandManager); this.runByLineInProgress = new ContextKey(EditorContexts.RunByLineInProgress, this.commandManager); @@ -173,36 +174,77 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb this.updateToolbar(true); void this.startDebugging(editor.document); } else { - void this.installIpykernel6(editor.document); + void this.installIpykernel6(); } } else { void this.appShell.showErrorMessage(DataScience.noNotebookToDebug()); } }), - this.commandManager.registerCommand(DSCommands.RunByLine, async (cell: NotebookCell) => { + this.commandManager.registerCommand(DSCommands.RunByLine, async (cell: NotebookCell | undefined) => { + sendTelemetryEvent(DebuggingTelemetry.clickedRunByLine); + void this.appShell.withProgress( + { location: ProgressLocation.Notification, title: DataScience.startingRunByLine() }, + async () => { + const editor = this.vscNotebook.activeNotebookEditor; + if (!cell) { + const range = editor?.selections[0]; + if (range) { + cell = editor?.document.cellAt(range.start); + } + } + + if (!cell) { + return; + } + + if (editor) { + if (await this.checkForIpykernel6(editor.document)) { + this.updateToolbar(true); + this.updateCellToolbar(true); + await this.startDebuggingCell(editor.document, KernelDebugMode.RunByLine, cell); + } else { + void this.installIpykernel6(); + } + } else { + void this.appShell.showErrorMessage(DataScience.noNotebookToDebug()); + } + } + ); + }), + + this.commandManager.registerCommand(DSCommands.RunByLineContinue, (cell: NotebookCell | undefined) => { const editor = this.vscNotebook.activeNotebookEditor; - if (editor) { - if (await this.checkForIpykernel6(editor.document)) { - this.updateToolbar(true); - this.updateCellToolbar(true); - void this.startDebuggingCell(editor.document, KernelDebugMode.RunByLine, cell); - } else { - void this.installIpykernel6(editor.document); + if (!cell) { + const range = editor?.selections[0]; + if (range) { + cell = editor?.document.cellAt(range.start); } - } else { - void this.appShell.showErrorMessage(DataScience.noNotebookToDebug()); } - }), - this.commandManager.registerCommand(DSCommands.RunByLineContinue, (cell: NotebookCell) => { + if (!cell) { + return; + } + const adapter = this.notebookToDebugAdapter.get(cell.notebook); if (adapter && adapter.debugCellUri?.toString() === cell.document.uri.toString()) { adapter.runByLineContinue(); } }), + this.commandManager.registerCommand(DSCommands.RunByLineStop, () => { + const editor = this.vscNotebook.activeNotebookEditor; + if (editor) { + const adapter = this.notebookToDebugAdapter.get(editor.document); + if (adapter) { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'withKeybinding' }); + adapter.disconnect(); + } + } + }), + this.commandManager.registerCommand(DSCommands.RunAndDebugCell, async (cell: NotebookCell | undefined) => { + sendTelemetryEvent(DebuggingTelemetry.clickedRunAndDebugCell); const editor = this.vscNotebook.activeNotebookEditor; if (!cell) { const range = editor?.selections[0]; @@ -220,7 +262,7 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb this.updateToolbar(true); void this.startDebuggingCell(editor.document, KernelDebugMode.Cell, cell); } else { - void this.installIpykernel6(editor.document); + void this.installIpykernel6(); } } else { void this.appShell.showErrorMessage(DataScience.noNotebookToDebug()); @@ -345,22 +387,31 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb controller?.connection.interpreter ); + if (result === ProductInstallStatus.Installed) { + sendTelemetryEvent(DebuggingTelemetry.ipykernel6Status, undefined, { status: 'installed' }); + } else { + sendTelemetryEvent(DebuggingTelemetry.ipykernel6Status, undefined, { status: 'notInstalled' }); + } return result === ProductInstallStatus.Installed; } catch { return false; } } - private async installIpykernel6(doc: NotebookDocument) { + private async installIpykernel6() { const response = await this.appShell.showInformationMessage( DataScience.needIpykernel6(), { modal: true }, - DataScience.jupyterInstall() + DataScience.setup() ); - if (response === DataScience.jupyterInstall()) { - const controller = this.notebookControllerManager.getSelectedNotebookController(doc); - void this.installer.install(Product.ipykernel, controller?.connection.interpreter, undefined, true); + if (response === DataScience.setup()) { + sendTelemetryEvent(DebuggingTelemetry.clickedOnSetup); + this.appShell.openUrl( + 'https://github.com/microsoft/vscode-jupyter/wiki/Setting-Up-Run-by-Line-and-Debugging-for-Notebooks' + ); + } else { + sendTelemetryEvent(DebuggingTelemetry.closedModal); } } } diff --git a/src/client/debugger/jupyter/kernelDebugAdapter.ts b/src/client/debugger/jupyter/kernelDebugAdapter.ts index d2641dd69b6..a617481e064 100644 --- a/src/client/debugger/jupyter/kernelDebugAdapter.ts +++ b/src/client/debugger/jupyter/kernelDebugAdapter.ts @@ -30,6 +30,8 @@ import { IKernelDebugAdapter } from '../types'; import { IDisposable } from '../../common/types'; import { Commands } from '../../datascience/constants'; import { IKernel } from '../../datascience/jupyter/kernels/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { DebuggingTelemetry } from '../constants'; const debugRequest = (message: DebugProtocol.Request, jupyterSessionId: string): KernelMessage.IDebugRequestMsg => { return { @@ -152,6 +154,14 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID this.debugCellUri = notebookDocument.cellAt(configuration.__cellIndex!)?.document.uri; } + if (configuration.__mode === KernelDebugMode.Cell) { + sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunAndDebugCell); + } + + if (configuration.__mode === KernelDebugMode.RunByLine) { + sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunByLine); + } + this.jupyterSession.onIOPubMessage((msg: KernelMessage.IIOPubMessage) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const content = msg.content as any; @@ -166,9 +176,15 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID if (this.kernel) { this.kernel.onWillRestart(() => { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'onARestart' }); this.disconnect(); }); this.kernel.onWillInterrupt(() => { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'onAnInterrupt' }); + this.disconnect(); + }); + this.kernel.onDisposed(() => { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'onKernelDisposed' }); this.disconnect(); }); } @@ -177,6 +193,7 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID (cellStateChange: NotebookCellExecutionStateChangeEvent) => { // If a cell has moved to idle, stop the debug session if (cellStateChange.state === NotebookCellExecutionState.Idle) { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'normally' }); this.disconnect(); } }, diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 4f2fa7a5c8b..d79d0e8d14a 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -24,6 +24,7 @@ import { populateTelemetryWithErrorInfo } from '../common/errors'; import { ErrorCategory, TelemetryErrorProperties } from '../common/errors/types'; import { noop } from '../common/utils/misc'; import { isPromise } from 'rxjs/internal-compatibility'; +import { DebuggingTelemetry } from '../debugger/constants'; export const waitBeforeSending = 'waitBeforeSending'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -1416,4 +1417,16 @@ export interface IEventNamePropertyMapping { hasKernelSpecInMetadata: boolean; // Whether we have kernelspec info in the notebook metadata. kernelConnectionFound: boolean; // Whether a kernel connection was found or not. }; + [DebuggingTelemetry.clickedOnSetup]: never | undefined; + [DebuggingTelemetry.closedModal]: never | undefined; + [DebuggingTelemetry.ipykernel6Status]: { + status: 'installed' | 'notInstalled'; + }; + [DebuggingTelemetry.clickedRunByLine]: never | undefined; + [DebuggingTelemetry.successfullyStartedRunByLine]: never | undefined; + [DebuggingTelemetry.clickedRunAndDebugCell]: never | undefined; + [DebuggingTelemetry.successfullyStartedRunAndDebugCell]: never | undefined; + [DebuggingTelemetry.endedSession]: { + reason: 'normally' | 'onKernelDisposed' | 'onAnInterrupt' | 'onARestart' | 'withKeybinding'; + }; }