From 1460507b6f1e23ece5007b41e8a06ac3cd96d6d1 Mon Sep 17 00:00:00 2001 From: aamunger Date: Wed, 16 Mar 2022 14:42:51 -0700 Subject: [PATCH] Revert "removed Notebook creation from extension since it is moving to built-in (#9311)" This reverts commit 502897a0d099f9214d26b50076e0b87b4c453348. --- news/2 Fixes/9250.md | 1 - package.json | 25 +++- package.nls.json | 2 + package.nls.zh-cn.json | 1 + package.nls.zh-tw.json | 1 + src/client/common/application/commands.ts | 1 + .../commands/commandRegistry.ts | 7 + src/notebooks/notebookCreator.ts | 58 ++++++++ src/notebooks/serviceRegistry.ts | 4 + .../creation/notebookCreation.vscode.test.ts | 134 ++++++++++++++++++ 10 files changed, 229 insertions(+), 5 deletions(-) delete mode 100644 news/2 Fixes/9250.md create mode 100644 src/notebooks/notebookCreator.ts create mode 100644 src/test/datascience/notebook/creation/notebookCreation.vscode.test.ts diff --git a/news/2 Fixes/9250.md b/news/2 Fixes/9250.md deleted file mode 100644 index d34ea735d99..00000000000 --- a/news/2 Fixes/9250.md +++ /dev/null @@ -1 +0,0 @@ -Removed the "Create new Jupyter notebook" Command since it has moved to the built-in ipynb extension in VS Code. diff --git a/package.json b/package.json index d0e5ec72539..f01fa250b36 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "theme": "light" }, "engines": { - "vscode": "^1.66.0-insider" + "vscode": "^1.64.0" }, "keywords": [ "jupyter", @@ -55,6 +55,7 @@ "onCommand:jupyter.exportToHTML", "onCommand:jupyter.exportToPDF", "onCommand:jupyter.notebookeditor.export", + "onCommand:jupyter.createnewnotebook", "onCommand:jupyter.createnewinteractive", "onCommand:jupyter.importnotebook", "onCommand:jupyter.importnotebookfile", @@ -88,15 +89,15 @@ "when": "false", "steps": [ { - "id": "ipynb.newUntitledIpynb", + "id": "jupyter.createNewNotebook", "title": "Create or open a Jupyter Notebook", - "description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Create: New Jupyter Notebook``.\n[Create New Jupyter Notebook](command:toSide:ipynb.newUntitledIpynb)\n If you have an existing project, you can also [open a folder](command:toSide:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:toSide:git.clone).", + "description": "Right click in the file explorer and create a new file with an .ipynb extension. Or, open the [Command Palette](command:workbench.action.showCommands) and run the command \n``Jupyter: Create New Jupyter Notebook``.\n[Create New Jupyter Notebook](command:toSide:jupyter.createnewnotebook)\n If you have an existing project, you can also [open a folder](command:toSide:workbench.action.files.openFolder) and/or clone a project from GitHub: [clone a Git repository](command:toSide:git.clone).", "media": { "svg": "resources/walkthroughs/opennotebook.svg", "altText": "Creating a new Jupyter notebook" }, "completionEvents": [ - "onCommand:ipynb.newUntitledIpynb", + "onCommand:jupyter.createnewnotebook", "onCommand:jupyter.createnewinteractive", "onCommand:workbench.action.files.openFolder", "onCommand:workbench.action.files.openFileFolder" @@ -701,6 +702,12 @@ "title": "%jupyter.command.jupyter.addcellbelow.title%", "category": "Jupyter" }, + { + "command": "jupyter.createnewnotebook", + "title": "%jupyter.command.jupyter.createnewnotebook.title%", + "shortTitle": "%jupyter.command.jupyter.createnewnotebook.shortTitle%", + "category": "Jupyter" + }, { "command": "jupyter.scrolltocell", "title": "%jupyter.command.jupyter.scrolltocell.title%", @@ -1030,6 +1037,11 @@ "group": "Jupyter" } ], + "file/newFile": [ + { + "command": "jupyter.createnewnotebook" + } + ], "commandPalette": [ { "command": "jupyter.replayPylanceLog", @@ -1373,6 +1385,11 @@ "category": "Jupyter", "when": "jupyter.hascodecells && jupyter.ispythonornativeactive" }, + { + "command": "jupyter.createnewnotebook", + "title": "%jupyter.command.jupyter.createnewnotebook.title%", + "category": "Jupyter" + }, { "command": "jupyter.runtoline", "category": "Jupyter", diff --git a/package.nls.json b/package.nls.json index 814f8ffff3a..8a9e38c64f1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -122,6 +122,8 @@ "jupyter.command.jupyter.collapseallcells.shorttitle": "Collapse", "jupyter.command.jupyter.addcellbelow.title": "Add Empty Cell to File", "jupyter.command.jupyter.scrolltocell.title": "Scroll Cell Into View", + "jupyter.command.jupyter.createnewnotebook.title": "Create New Jupyter Notebook", + "jupyter.command.jupyter.createnewnotebook.shortTitle": "Jupyter Notebook", "jupyter.command.jupyter.selectJupyterInterpreter.title": "Select Interpreter to start Jupyter server", "jupyter.command.jupyter.createGitHubIssue.title": "Create GitHub Issue", "jupyter.command.jupyter.submitGitHubIssue.title": "Submit GitHub Issue", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 019b41d58a9..9844b536ef5 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -99,6 +99,7 @@ "jupyter.command.jupyter.collapseallcells.shorttitle": "折叠", "jupyter.command.jupyter.addcellbelow.title": "添加空单元到文件", "jupyter.command.jupyter.scrolltocell.title": "滚动至单元", + "jupyter.command.jupyter.createnewnotebook.title": "创建新的空白 Jupyter 笔记本", "jupyter.command.jupyter.selectJupyterInterpreter.title": "选择解释器来启动 Jupyter 服务器", "jupyter.command.jupyter.createGitHubIssue.title": "创建 GitHub Issue", "jupyter.command.jupyter.submitGitHubIssue.title": "提交 GitHub Issue", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index 4243d61de22..d1dc67534f5 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -77,6 +77,7 @@ "jupyter.command.jupyter.runfromline.title": "在互動式視窗中執行到此行", "jupyter.command.jupyter.execSelectionInteractive.title": "在互動式視窗中執行選取內容/行", "jupyter.command.jupyter.clearSavedJupyterUris.title": "清除遠端 Jupyter 伺服器清單", + "jupyter.command.jupyter.createnewnotebook.title": "新增 Jupyter Notebook", "jupyter.command.jupyter.openOutlineView.title": "顯示目錄(大綱欄位)", "jupyter.command.jupyter.openOutlineView.shorttitle": "大綱", "jupyter.command.jupyter.debug.title": "偵錯", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index eac5bff5747..ff1333bb7d6 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -52,6 +52,7 @@ interface ICommandNameWithoutArgumentTypeMapping { [DSCommands.CollapseAllCells]: []; [DSCommands.ExportOutputAsNotebook]: []; [DSCommands.AddCellBelow]: []; + [DSCommands.CreateNewNotebook]: []; [DSCommands.EnableDebugLogging]: []; [DSCommands.ResetLoggingLevel]: []; [DSCommands.OpenVariableView]: []; diff --git a/src/interactive-window/commands/commandRegistry.ts b/src/interactive-window/commands/commandRegistry.ts index 229301772dc..e875cc43db6 100644 --- a/src/interactive-window/commands/commandRegistry.ts +++ b/src/interactive-window/commands/commandRegistry.ts @@ -9,6 +9,7 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { IShowDataViewerFromVariablePanel } from '../../extension/messageTypes'; import { IKernelProvider } from '../../kernels/types'; import { convertDebugProtocolVariableToIJupyterVariable } from '../../kernels/variables/debuggerVariables'; +import { NotebookCreator } from '../../notebooks/notebookCreator'; import { DataViewerChecker } from '../../webviews/dataviewer/dataViewerChecker'; import { ICommandNameArgumentTypeMapping } from '../../client/common/application/commands'; import { @@ -65,6 +66,7 @@ export class CommandRegistry implements IDisposable { @inject(IDataViewerFactory) private readonly dataViewerFactory: IDataViewerFactory, @inject(IJupyterServerUriStorage) private readonly serverUriStorage: IJupyterServerUriStorage, @inject(IJupyterVariables) @named(Identifiers.DEBUGGER_VARIABLES) private variableProvider: IJupyterVariables, + @inject(NotebookCreator) private readonly nativeNotebookCreator: NotebookCreator, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IInteractiveWindowProvider) private readonly interactiveWindowProvider: IInteractiveWindowProvider, @inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler, @@ -94,6 +96,7 @@ export class CommandRegistry implements IDisposable { this.registerCommand(Commands.GotoNextCellInFile, this.gotoNextCellInFile); this.registerCommand(Commands.GotoPrevCellInFile, this.gotoPrevCellInFile); this.registerCommand(Commands.AddCellBelow, this.addCellBelow); + this.registerCommand(Commands.CreateNewNotebook, this.createNewNotebook); this.registerCommand(Commands.ViewJupyterOutput, this.viewJupyterOutput); this.registerCommand(Commands.LatestExtension, this.openPythonExtensionPage); this.registerCommand(Commands.EnableDebugLogging, this.enableDebugLogging); @@ -479,6 +482,10 @@ export class CommandRegistry implements IDisposable { } } + private async createNewNotebook(): Promise { + await this.nativeNotebookCreator.createNewNotebook(); + } + private viewJupyterOutput() { this.jupyterOutput.show(true); } diff --git a/src/notebooks/notebookCreator.ts b/src/notebooks/notebookCreator.ts new file mode 100644 index 00000000000..13589a51a9a --- /dev/null +++ b/src/notebooks/notebookCreator.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { QuickPickItem } from 'vscode'; +import { IApplicationShell } from '../client/common/application/types'; +import { PYTHON_LANGUAGE, JVSC_EXTENSION_ID, JVSC_EXTENSION_DisplayName } from '../client/common/constants'; +import { DataScience } from '../client/common/utils/localize'; +import { INotebookEditorProvider } from '../client/datascience/types'; +import { sendTelemetryEvent } from '../client/telemetry'; +import { Telemetry } from '../datascience-ui/common/constants'; +import { CreationOptionService } from '../kernels/common/creationOptionsService'; + +@injectable() +export class NotebookCreator { + constructor( + @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(CreationOptionService) private readonly creationOptionsService: CreationOptionService + ) {} + + public async createNewNotebook() { + if (this.creationOptionsService.registrations.length === 0) { + await this.editorProvider.createNew(); + return; + } + + const items: (QuickPickItem & { + extensionId: string; + defaultCellLanguage: string; + })[] = this.creationOptionsService.registrations.map((item) => { + return { + label: item.displayName, + detail: item.extensionId, + extensionId: item.extensionId, + defaultCellLanguage: item.defaultCellLanguage + }; + }); + + // First item is the Jupyter extension. + items.splice(0, 0, { + defaultCellLanguage: PYTHON_LANGUAGE, + detail: JVSC_EXTENSION_ID, + extensionId: JVSC_EXTENSION_ID, + label: JVSC_EXTENSION_DisplayName + }); + const placeHolder = DataScience.placeHolderToSelectOptionForNotebookCreation(); + const item = await this.appShell.showQuickPick(items, { + matchOnDescription: true, + matchOnDetail: true, + placeHolder + }); + sendTelemetryEvent(Telemetry.OpenNotebookSelection, undefined, { extensionId: item?.extensionId }); + if (item) { + await this.editorProvider.createNew({ defaultCellLanguage: item.defaultCellLanguage }); + } + } +} diff --git a/src/notebooks/serviceRegistry.ts b/src/notebooks/serviceRegistry.ts index 312df472a50..24650005cb2 100644 --- a/src/notebooks/serviceRegistry.ts +++ b/src/notebooks/serviceRegistry.ts @@ -12,6 +12,7 @@ import { NotebookCellBangInstallDiagnosticsProvider } from '../intellisense/diag import { EmptyNotebookCellLanguageService } from '../intellisense/emptyNotebookCellLanguageService'; import { IntellisenseProvider } from '../intellisense/intellisenseProvider'; import { PythonKernelCompletionProvider } from '../intellisense/pythonKernelCompletionProvider'; +import { CreationOptionService } from '../kernels/common/creationOptionsService'; import { KernelProvider } from '../kernels/kernelProvider'; import { IKernelProvider } from '../kernels/types'; import { KernelFilterService } from './controllers/kernelFilter/kernelFilterService'; @@ -21,6 +22,7 @@ import { NotebookControllerManager } from './controllers/notebookControllerManag import { RemoteSwitcher } from './controllers/remoteSwitcher'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { NotebookCommandListener } from './notebookCommandListener'; +import { NotebookCreator } from './notebookCreator'; import { NotebookEditorProvider } from './notebookEditorProvider'; import { ErrorRendererCommunicationHandler } from './outputs/errorRendererComms'; import { PlotSaveHandler } from './outputs/plotSaveHandler'; @@ -55,6 +57,8 @@ export function registerTypes(serviceManager: IServiceManager) { ); serviceManager.addSingleton(INotebookLanguageClientProvider, IntellisenseProvider); serviceManager.addBinding(INotebookLanguageClientProvider, IExtensionSingleActivationService); + serviceManager.addSingleton(CreationOptionService, CreationOptionService); + serviceManager.addSingleton(NotebookCreator, NotebookCreator); serviceManager.addSingleton(INotebookControllerManager, NotebookControllerManager); serviceManager.addSingleton(PlotSaveHandler, PlotSaveHandler); serviceManager.addSingleton(PlotViewHandler, PlotViewHandler); diff --git a/src/test/datascience/notebook/creation/notebookCreation.vscode.test.ts b/src/test/datascience/notebook/creation/notebookCreation.vscode.test.ts new file mode 100644 index 00000000000..18859091d83 --- /dev/null +++ b/src/test/datascience/notebook/creation/notebookCreation.vscode.test.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { commands } from 'vscode'; +import { IApplicationShell, IVSCodeNotebook } from '../../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { traceInfo } from '../../../../client/common/logger'; +import { IDisposable } from '../../../../client/common/types'; +import { Commands } from '../../../../client/datascience/constants'; +import { CreationOptionService } from '../../../../kernels/common/creationOptionsService'; +import { IExtensionTestApi, waitForCondition } from '../../../common'; +import { IS_REMOTE_NATIVE_TEST } from '../../../constants'; +import { closeActiveWindows, initialize } from '../../../initialize'; +import { + closeNotebooksAndCleanUpAfterTests, + ensureNewNotebooksHavePythonCells, + workAroundVSCodeNotebookStartPages +} from '../helper'; + +/* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ +suite('DataScience - VSCode Notebook - (Creation Integration)', function () { + this.timeout(15_000); + let api: IExtensionTestApi; + let vscodeNotebook: IVSCodeNotebook; + let creationOptions: CreationOptionService; + const disposables: IDisposable[] = []; + suiteSetup(async function () { + api = await initialize(); + if (IS_REMOTE_NATIVE_TEST) { + return this.skip(); + } + creationOptions = api.serviceContainer.get(CreationOptionService); + vscodeNotebook = api.serviceContainer.get(IVSCodeNotebook); + creationOptions.clear(); + await workAroundVSCodeNotebookStartPages(); + await ensureNewNotebooksHavePythonCells(); + }); + teardown(async function () { + traceInfo(`Ended Test ${this.currentTest?.title}`); + sinon.restore(); + creationOptions.clear(); + await closeNotebooksAndCleanUpAfterTests(disposables); + traceInfo(`Ended Test (completed) ${this.currentTest?.title}`); + }); + setup(async function () { + traceInfo(`Start Test ${this.currentTest?.title}`); + sinon.restore(); + await closeNotebooksAndCleanUpAfterTests(disposables); + traceInfo(`Start Test (completed) ${this.currentTest?.title}`); + }); + suiteTeardown(() => { + try { + creationOptions.clear(); + } catch { + // + } + }); + teardown(async function () { + traceInfo(`End Test ${this.currentTest?.title}`); + await closeNotebooksAndCleanUpAfterTests(disposables); + traceInfo(`Ended Test (completed) ${this.currentTest?.title}`); + }); + async function createNotebookAndValidateLanguageOfFirstCell(expectedLanguage: string) { + await commands.executeCommand(Commands.CreateNewNotebook); + await waitForCondition(async () => !!vscodeNotebook.activeNotebookEditor, 10_000, 'New Notebook not created'); + assert.strictEqual( + vscodeNotebook.activeNotebookEditor!.document.cellAt(0).document.languageId.toLowerCase(), + expectedLanguage + ); + } + test('With 3rd party integration, display quick pick when selecting create blank notebook command', async function () { + await creationOptions.registerNewNotebookContent('javascript'); + assert.equal(creationOptions.registrations.length, 1); + assert.isUndefined(vscodeNotebook.activeNotebookEditor); + + const appShell = api.serviceContainer.get(IApplicationShell); + const stub = sinon.stub(appShell, 'showQuickPick').callsFake((items: any) => { + traceInfo(`Quick Pick displayed to user`); + assert.isAtLeast(items.length, 2); + + // If this is the first time this prompt was displayed, then select the second item (javascript). + if (stub.callCount === 1) { + return items[1]; + } + + // Pick the first item, that will be us. + return items[0]; + }); + disposables.push({ dispose: () => stub.restore() }); + + // Create a blank notebook & we should have a javascript cell. + await createNotebookAndValidateLanguageOfFirstCell('javascript'); + assert.equal(stub.callCount, 1); + + await closeActiveWindows(); + + // Try again & this time select the first item from the list & we should end up with a python notebook. + await createNotebookAndValidateLanguageOfFirstCell(PYTHON_LANGUAGE.toLowerCase()); + assert.equal(stub.callCount, 2); + }); + test('Without 3rd party integration, do not display quick pick when selecting create blank notebook command', async function () { + assert.equal(creationOptions.registrations.length, 0); + assert.isUndefined(vscodeNotebook.activeNotebookEditor); + + // Create a blank notebook & it should just work. + await createNotebookAndValidateLanguageOfFirstCell(PYTHON_LANGUAGE.toLowerCase()); + }); + test('Create javascript & powershell Notebook using API', async function () { + // See https://github.com/microsoft/vscode-jupyter/issues/9158 + this.skip(); + await api.createBlankNotebook({ defaultCellLanguage: 'javascript' }); + + await waitForCondition(async () => !!vscodeNotebook.activeNotebookEditor, 10_000, 'New Notebook not created'); + assert.strictEqual( + vscodeNotebook.activeNotebookEditor!.document.cellAt(0).document.languageId.toLowerCase(), + 'javascript' + ); + + await closeActiveWindows(); + + await api.createBlankNotebook({ defaultCellLanguage: 'powershell' }); + + await waitForCondition(async () => !!vscodeNotebook.activeNotebookEditor, 10_000, 'New Notebook not created'); + assert.strictEqual( + vscodeNotebook.activeNotebookEditor!.document.cellAt(0).document.languageId.toLowerCase(), + 'powershell' + ); + }); +});