diff --git a/package.json b/package.json index b6bc4047b40..e2d35725263 100644 --- a/package.json +++ b/package.json @@ -2088,7 +2088,8 @@ "notebookCellExecutionState", "portsAttributes", "quickPickSortByLabel", - "notebookKernelSource" + "notebookKernelSource", + "interactiveWindow" ], "scripts": { "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-toolsai-jupyter-insiders.vsix", diff --git a/src/interactive-window/helpers.ts b/src/interactive-window/helpers.ts index a618c6be535..1ce9c160f22 100644 --- a/src/interactive-window/helpers.ts +++ b/src/interactive-window/helpers.ts @@ -4,9 +4,11 @@ import { NotebookCell } from 'vscode'; import { IJupyterSettings } from '../platform/common/types'; import { appendLineFeed, removeLinesFromFrontAndBackNoConcat } from '../platform/common/utils'; +import { isUri } from '../platform/common/utils/misc'; import { uncommentMagicCommands } from './editor-integration/cellFactory'; import { CellMatcher } from './editor-integration/cellMatcher'; import { InteractiveCellMetadata } from './editor-integration/types'; +import { InteractiveTab } from './types'; export function getInteractiveCellMetadata(cell: NotebookCell): InteractiveCellMetadata | undefined { if (cell.metadata.interactive !== undefined) { @@ -35,3 +37,13 @@ export function generateInteractiveCode(code: string, settings: IJupyterSettings return withMagicsAndLinefeeds.join(''); } + +export function isInteractiveInputTab(tab: unknown): tab is InteractiveTab { + let interactiveTab = tab as InteractiveTab; + return ( + interactiveTab && + interactiveTab.input && + isUri(interactiveTab.input.uri) && + isUri(interactiveTab.input.inputBoxUri) + ); +} diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index 9d0392fddfa..f1f299e5103 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -21,19 +21,14 @@ import { NotebookController, NotebookEdit } from 'vscode'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService -} from '../platform/common/application/types'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../platform/common/application/types'; import { Commands, defaultNotebookFormat, MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../platform/common/constants'; import '../platform/common/extensions'; import { traceError, traceInfoIfCI } from '../platform/logging'; import { IFileSystem } from '../platform/common/platform/types'; import * as uuid from 'uuid/v4'; -import { IConfigurationService, InteractiveWindowMode, Resource } from '../platform/common/types'; +import { IConfigurationService, InteractiveWindowMode, IsWebExtension, Resource } from '../platform/common/types'; import { noop } from '../platform/common/utils/misc'; import { IKernel, @@ -53,8 +48,13 @@ import { INotebookExporter } from '../kernels/jupyter/types'; import { IExportDialog, ExportFormat } from '../notebooks/export/types'; import { generateCellsFromNotebookDocument } from './editor-integration/cellFactory'; import { CellMatcher } from './editor-integration/cellMatcher'; -import { IInteractiveWindowLoadable, IInteractiveWindowDebugger, IInteractiveWindowDebuggingManager } from './types'; -import { generateInteractiveCode } from './helpers'; +import { + IInteractiveWindowLoadable, + IInteractiveWindowDebugger, + IInteractiveWindowDebuggingManager, + InteractiveTab +} from './types'; +import { generateInteractiveCode, isInteractiveInputTab } from './helpers'; import { IControllerSelection, IVSCodeNotebookController } from '../notebooks/controllers/types'; import { DisplayOptions } from '../kernels/displayOptions'; import { getInteractiveCellMetadata } from './helpers'; @@ -85,9 +85,6 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { public get submitters(): Uri[] { return this._submitters; } - public get notebookUri(): Uri { - return this.notebookEditor.notebook.uri; - } public get notebookDocument(): NotebookDocument { return this.notebookEditor.notebook; } @@ -98,7 +95,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { private closedEvent = new EventEmitter(); private _submitters: Uri[] = []; private fileInKernel: Uri | undefined; - private cellMatcher; + private cellMatcher: CellMatcher; private internalDisposables: Disposable[] = []; private kernelDisposables: Disposable[] = []; @@ -110,33 +107,69 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { } = {}; private pendingNotebookScrolls: NotebookRange[] = []; + private _notebookEditor!: NotebookEditor; + public get notebookEditor(): NotebookEditor { + return this._notebookEditor; + } + + private _notebookUri: Uri; + public get notebookUri(): Uri { + return this._notebookUri; + } + + private readonly documentManager: IDocumentManager; + private readonly fs: IFileSystem; + private readonly configuration: IConfigurationService; + private readonly jupyterExporter: INotebookExporter; + private readonly workspaceService: IWorkspaceService; + private readonly exportDialog: IExportDialog; + private readonly notebookControllerSelection: IControllerSelection; + private readonly interactiveWindowDebugger: IInteractiveWindowDebugger | undefined; + private readonly errorHandler: IDataScienceErrorHandler; + private readonly codeGeneratorFactory: ICodeGeneratorFactory; + private readonly storageFactory: IGeneratedCodeStorageFactory; + private readonly debuggingManager: IInteractiveWindowDebuggingManager; + private readonly isWebExtension: boolean; + private readonly commandManager: ICommandManager; constructor( - private readonly documentManager: IDocumentManager, - private readonly fs: IFileSystem, - private readonly configuration: IConfigurationService, - private readonly commandManager: ICommandManager, - private readonly jupyterExporter: INotebookExporter, - private readonly workspaceService: IWorkspaceService, - private _owner: Resource, - private mode: InteractiveWindowMode, - private readonly exportDialog: IExportDialog, - private readonly notebookControllerSelection: IControllerSelection, private readonly serviceContainer: IServiceContainer, - private readonly interactiveWindowDebugger: IInteractiveWindowDebugger | undefined, - private readonly errorHandler: IDataScienceErrorHandler, - preferredController: IVSCodeNotebookController | undefined, - public readonly notebookEditor: NotebookEditor, - public readonly inputUri: Uri, - public readonly appShell: IApplicationShell, - private readonly codeGeneratorFactory: ICodeGeneratorFactory, - private readonly storageFactory: IGeneratedCodeStorageFactory, - private readonly debuggingManager: IInteractiveWindowDebuggingManager, - private readonly isWebExtension: boolean + private _owner: Resource, + public mode: InteractiveWindowMode, + private preferredController: IVSCodeNotebookController | undefined, + private readonly notebookEditorOrTab: NotebookEditor | InteractiveTab, + public readonly inputUri: Uri ) { + this.documentManager = this.serviceContainer.get(IDocumentManager); + this.commandManager = this.serviceContainer.get(ICommandManager); + this.fs = this.serviceContainer.get(IFileSystem); + this.configuration = this.serviceContainer.get(IConfigurationService); + this.jupyterExporter = this.serviceContainer.get(INotebookExporter); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.exportDialog = this.serviceContainer.get(IExportDialog); + this.notebookControllerSelection = this.serviceContainer.get(IControllerSelection); + this.interactiveWindowDebugger = + this.serviceContainer.tryGet(IInteractiveWindowDebugger); + this.errorHandler = this.serviceContainer.get(IDataScienceErrorHandler); + this.codeGeneratorFactory = this.serviceContainer.get(ICodeGeneratorFactory); + this.storageFactory = this.serviceContainer.get(IGeneratedCodeStorageFactory); + this.debuggingManager = this.serviceContainer.get( + IInteractiveWindowDebuggingManager + ); + this.isWebExtension = this.serviceContainer.get(IsWebExtension); + this._notebookUri = isInteractiveInputTab(notebookEditorOrTab) + ? notebookEditorOrTab.input.uri + : notebookEditorOrTab.notebook.uri; + if (!isInteractiveInputTab(notebookEditorOrTab)) { + this._notebookEditor = notebookEditorOrTab; + } + // Set our owner and first submitter if (this._owner) { this._submitters.push(this._owner); } + } + + public start() { window.onDidChangeActiveNotebookEditor((e) => { if (e === this.notebookEditor) { this._onDidChangeViewState.fire(); @@ -154,14 +187,33 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { } this.listenForControllerSelection(); - if (preferredController) { + if (this.preferredController) { // Also start connecting to our kernel but don't wait for it to finish - this.startKernel(preferredController.controller, preferredController.connection).ignoreErrors(); + this.startKernel(this.preferredController.controller, this.preferredController.connection).ignoreErrors(); } else if (this.isWebExtension) { this.insertInfoMessage(DataScience.noKernelsSpecifyRemote()).ignoreErrors(); } } + public async restore(preferredController: IVSCodeNotebookController | undefined) { + if (preferredController) { + this.preferredController = preferredController; + } + if (!this.notebookEditor) { + if (isInteractiveInputTab(this.notebookEditorOrTab)) { + const document = await workspace.openNotebookDocument(this.notebookEditorOrTab.input.uri); + const editor = await window.showNotebookDocument(document, { + viewColumn: this.notebookEditorOrTab.group.viewColumn + }); + this._notebookEditor = editor; + } else { + this._notebookEditor = this.notebookEditorOrTab; + } + } + + this.start(); + } + private async startKernel( controller: NotebookController | undefined = this.currentKernelInfo.controller, metadata: KernelConnectionMetadata | undefined = this.currentKernelInfo.metadata diff --git a/src/interactive-window/interactiveWindowProvider.ts b/src/interactive-window/interactiveWindowProvider.ts index 1fe3572f6df..ddd3f7af9a1 100644 --- a/src/interactive-window/interactiveWindowProvider.ts +++ b/src/interactive-window/interactiveWindowProvider.ts @@ -14,12 +14,7 @@ import { window } from 'vscode'; -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService -} from '../platform/common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../platform/common/application/types'; import { traceInfo, traceVerbose } from '../platform/logging'; import { IFileSystem } from '../platform/common/platform/types'; @@ -31,8 +26,8 @@ import { IDisposableRegistry, IMemento, InteractiveWindowMode, - IsWebExtension, - Resource + Resource, + WORKSPACE_MEMENTO } from '../platform/common/types'; import { chainable } from '../platform/common/utils/decorators'; import * as localize from '../platform/common/utils/localize'; @@ -44,28 +39,25 @@ import { InteractiveWindow } from './interactiveWindow'; import { InteractiveWindowView, JVSC_EXTENSION_ID, NotebookCellScheme } from '../platform/common/constants'; import { IInteractiveWindow, - IInteractiveWindowDebugger, - IInteractiveWindowDebuggingManager, + IInteractiveWindowCache, IInteractiveWindowProvider, - INativeInteractiveWindow + INativeInteractiveWindow, + InteractiveTab } from './types'; import { getInteractiveWindowTitle } from './identity'; import { createDeferred } from '../platform/common/utils/async'; import { getDisplayPath } from '../platform/common/platform/fs-paths'; -import { INotebookExporter } from '../kernels/jupyter/types'; -import { IDataScienceErrorHandler } from '../kernels/errors/types'; -import { IExportDialog } from '../notebooks/export/types'; import { IControllerDefaultService, IControllerRegistration, - IControllerSelection, IVSCodeNotebookController } from '../notebooks/controllers/types'; -import { ICodeGeneratorFactory, IGeneratedCodeStorageFactory } from './editor-integration/types'; import { getResourceType } from '../platform/common/utils'; +import { isInteractiveInputTab } from './helpers'; // Export for testing export const AskedForPerFileSettingKey = 'ds_asked_per_file_interactive'; +export const InteractiveWindowCacheKey = 'ds_interactive_window_cache'; @injectable() export class InteractiveWindowProvider @@ -100,6 +92,7 @@ export class InteractiveWindowProvider @inject(IFileSystem) private readonly fs: IFileSystem, @inject(IConfigurationService) private readonly configService: IConfigurationService, @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalMemento: Memento, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceMemento: Memento, @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IControllerRegistration) private readonly controllerRegistration: IControllerRegistration, @@ -109,6 +102,37 @@ export class InteractiveWindowProvider asyncRegistry.push(this); this.notebookEditorProvider.registerEmbedNotebookProvider(this); + this.restoreWindows(); + } + + private restoreWindows() { + // VS Code controls if interactive windows are restored. + const interactiveWindowMapping = new Map(); + window.tabGroups.all.forEach((group) => { + group.tabs.forEach((tab) => { + if (isInteractiveInputTab(tab) && tab.input.uri) { + interactiveWindowMapping.set(tab.input.uri.toString(), tab); + } + }); + }); + + this.workspaceMemento.get(InteractiveWindowCacheKey, [] as IInteractiveWindowCache[]).forEach((iw) => { + if (!iw.uriString || !interactiveWindowMapping.get(iw.uriString)) { + return; + } + + const result = new InteractiveWindow( + this.serviceContainer, + iw.owner !== undefined ? Uri.from(iw.owner) : undefined, + iw.mode, + undefined, + interactiveWindowMapping.get(iw.uriString)!, + Uri.parse(iw.inputBoxUriString) + ); + this._windows.push(result); + }); + + this._updateWindowCache(); } @chainable() @@ -132,6 +156,14 @@ export class InteractiveWindowProvider if (!result) { // No match. Create a new item. result = await this.create(resource, mode, connection); + // start the kernel + result.start(); + } else { + const preferredController = connection + ? this.controllerRegistration.get(connection, InteractiveWindowView) + : await this.controllerDefaultService.computeDefaultController(resource, InteractiveWindowView); + + await result.restore(preferredController); } return result; @@ -169,32 +201,17 @@ export class InteractiveWindowProvider : await this.controllerDefaultService.computeDefaultController(resource, InteractiveWindowView); const commandManager = this.serviceContainer.get(ICommandManager); - const [inputUri, editor] = await this.createEditor(preferredController, resource, mode, commandManager); const result = new InteractiveWindow( - this.serviceContainer.get(IDocumentManager), - this.serviceContainer.get(IFileSystem), - this.serviceContainer.get(IConfigurationService), - commandManager, - this.serviceContainer.get(INotebookExporter), - this.serviceContainer.get(IWorkspaceService), + this.serviceContainer, resource, mode, - this.serviceContainer.get(IExportDialog), - this.serviceContainer.get(IControllerSelection), - this.serviceContainer, - this.serviceContainer.tryGet(IInteractiveWindowDebugger), - this.serviceContainer.get(IDataScienceErrorHandler), preferredController, editor, - inputUri, - this.appShell, - this.serviceContainer.get(ICodeGeneratorFactory), - this.serviceContainer.get(IGeneratedCodeStorageFactory), - this.serviceContainer.get(IInteractiveWindowDebuggingManager), - this.serviceContainer.get(IsWebExtension) + inputUri ); this._windows.push(result); + this._updateWindowCache(); // This is the last interactive window at the moment (as we're about to create it) this.lastActiveInteractiveWindow = result; @@ -273,6 +290,18 @@ export class InteractiveWindowProvider } return result; } + private _updateWindowCache() { + const windowCache = this._windows.map( + (iw) => + ({ + owner: iw.owner, + mode: iw.mode, + uriString: iw.notebookUri.toString(), + inputBoxUriString: iw.inputUri.toString() + } as IInteractiveWindowCache) + ); + this.workspaceMemento.update(InteractiveWindowCacheKey, windowCache).then(noop, noop); + } public getExisting( owner: Resource, @@ -316,6 +345,7 @@ export class InteractiveWindowProvider traceVerbose(`Closing interactive window: ${interactiveWindow.notebookUri?.toString()}`); interactiveWindow.dispose(); this._windows = this._windows.filter((w) => w !== interactiveWindow); + this._updateWindowCache(); if (this.lastActiveInteractiveWindow === interactiveWindow) { this.lastActiveInteractiveWindow = this._windows[0]; } diff --git a/src/interactive-window/types.ts b/src/interactive-window/types.ts index 8830c414a57..d3365d6f040 100644 --- a/src/interactive-window/types.ts +++ b/src/interactive-window/types.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable, Event, NotebookCell, NotebookDocument, NotebookEditor, Uri } from 'vscode'; +import { Disposable, Event, NotebookCell, NotebookDocument, NotebookEditor, Tab, Uri } from 'vscode'; import { IDebuggingManager } from '../kernels/debugger/types'; import { IKernel, KernelConnectionMetadata } from '../kernels/types'; +import { IVSCodeNotebookController } from '../notebooks/controllers/types'; import { Resource, InteractiveWindowMode, ICell } from '../platform/common/types'; import { IFileGeneratedCodes } from './editor-integration/types'; @@ -66,6 +67,8 @@ export interface IInteractiveWindow extends IInteractiveBase { readonly inputUri?: Uri; readonly notebookDocument?: NotebookDocument; closed: Event; + start(): void; + restore(preferredController: IVSCodeNotebookController | undefined): Promise; addCode(code: string, file: Uri, line: number): Promise; addErrorMessage(message: string, cell: NotebookCell): Promise; debugCode(code: string, file: Uri, line: number): Promise; @@ -76,6 +79,22 @@ export interface IInteractiveWindow extends IInteractiveBase { export(cells?: ICell[]): void; } +export interface IInteractiveWindowCache { + owner: Resource; + mode: InteractiveWindowMode; + uriString: string; + inputBoxUriString: string; +} + +export interface TabInputInteractiveWindow { + readonly uri: Uri; + readonly inputBoxUri: Uri; +} + +export interface InteractiveTab extends Tab { + readonly input: TabInputInteractiveWindow; +} + export interface IInteractiveWindowLoadable extends IInteractiveWindow { changeMode(newMode: InteractiveWindowMode): void; }