Skip to content

Commit f7bf370

Browse files
authored
Delegate management of notebooks (creation and disposing) to I… (#10568)
* No need to dispose * Refactor * Remove nb * Oops * Add missing imports * Skip tests * Fix linter
1 parent cf64598 commit f7bf370

15 files changed

+195
-24
lines changed

src/client/datascience/interactive-common/interactiveBase.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
IMessageCell,
9090
INotebook,
9191
INotebookExporter,
92+
INotebookProvider,
9293
INotebookServer,
9394
INotebookServerOptions,
9495
InterruptResult,
@@ -152,7 +153,8 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
152153
@unmanaged() title: string,
153154
@unmanaged() viewColumn: ViewColumn,
154155
@unmanaged() experimentsManager: IExperimentsManager,
155-
@unmanaged() private switcher: KernelSwitcher
156+
@unmanaged() private switcher: KernelSwitcher,
157+
@unmanaged() private readonly notebookProvider: INotebookProvider
156158
) {
157159
super(
158160
configuration,
@@ -326,13 +328,6 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
326328
super.dispose();
327329
this.listeners.forEach(l => l.dispose());
328330
this.updateContexts(undefined);
329-
330-
// When closing an editor, dispose of the notebook associated with it.
331-
// This won't work when we have multiple views of the notebook though. Notebook ownership
332-
// should probably move to whatever owns the backing model.
333-
return this.notebook?.dispose().then(() => {
334-
this._notebook = undefined;
335-
});
336331
}
337332

338333
public startProgress() {
@@ -1093,11 +1088,7 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
10931088
this.getNotebookOptions()
10941089
]);
10951090
try {
1096-
// We could have multiple native editors opened for the same file/model.
1097-
notebook = uri ? await server.getNotebook(uri) : undefined;
1098-
if (!notebook) {
1099-
notebook = uri ? await server.createNotebook(resource, uri, options?.metadata) : undefined;
1100-
}
1091+
notebook = uri ? await this.notebookProvider.getNotebook(server, uri, options?.metadata) : undefined;
11011092
} catch (e) {
11021093
// If we get an invalid kernel error, make sure to ask the user to switch
11031094
if (e instanceof JupyterInvalidKernelError && server && server.getConnectionInfo()?.localLaunch) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { nbformat } from '@jupyterlab/coreutils';
7+
import { injectable } from 'inversify';
8+
import { Uri } from 'vscode';
9+
import { noop } from '../../common/utils/misc';
10+
import { INotebook, INotebookProvider, INotebookServer } from '../types';
11+
12+
@injectable()
13+
export class BaseNotebookProvider implements INotebookProvider {
14+
protected readonly notebooks = new Map<string, Promise<INotebook>>();
15+
public async getNotebook(
16+
server: INotebookServer,
17+
resource: Uri,
18+
metadata?: nbformat.INotebookMetadata | undefined
19+
): Promise<INotebook> {
20+
// We could have multiple native editors opened for the same file/model.
21+
const notebook = await server.getNotebook(resource);
22+
if (notebook) {
23+
return notebook;
24+
}
25+
26+
if (this.notebooks.get(resource.fsPath)) {
27+
return this.notebooks.get(resource.fsPath)!!;
28+
}
29+
30+
const promise = server.createNotebook(resource, resource, metadata);
31+
this.notebooks.set(resource.fsPath, promise);
32+
33+
// Remove promise from cache if the same promise still exists.
34+
const removeFromCache = () => {
35+
const cachedPromise = this.notebooks.get(resource.fsPath);
36+
if (cachedPromise === promise) {
37+
this.notebooks.delete(resource.fsPath);
38+
}
39+
};
40+
41+
// If the notebook is disposed, remove from cache.
42+
promise.then(nb => nb.onDisposed(removeFromCache)).catch(noop);
43+
44+
// If promise fails, then remove the promise from cache.
45+
promise.catch(removeFromCache);
46+
47+
return promise;
48+
}
49+
protected async disposeNotebook(resource: Uri) {
50+
// First find all notebooks associated with this editor (ipynb file).
51+
const notebookPromise = this.notebooks.get(resource.fsPath);
52+
if (!notebookPromise) {
53+
// Possible it was closed before a notebook could be created.
54+
return;
55+
}
56+
this.notebooks.delete(resource.fsPath);
57+
const notebook = await notebookPromise.catch(noop);
58+
if (!notebook) {
59+
return;
60+
}
61+
62+
await notebook.dispose().catch(noop);
63+
}
64+
}

src/client/datascience/interactive-ipynb/nativeEditor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
INotebookExporter,
7979
INotebookImporter,
8080
INotebookModel,
81+
INotebookProvider,
8182
INotebookServerOptions,
8283
IStatusProvider,
8384
IThemeFinder,
@@ -90,6 +91,7 @@ import { nbformat } from '@jupyterlab/coreutils';
9091
import cloneDeep = require('lodash/cloneDeep');
9192
import { concatMultilineStringInput } from '../../../datascience-ui/common';
9293
import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher';
94+
import { NativeNotebookProvider } from './notebookProvider';
9395

9496
const nativeEditorDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'notebook');
9597
@injectable()
@@ -176,7 +178,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
176178
@inject(ProgressReporter) progressReporter: ProgressReporter,
177179
@inject(IExperimentsManager) experimentsManager: IExperimentsManager,
178180
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
179-
@inject(KernelSwitcher) switcher: KernelSwitcher
181+
@inject(KernelSwitcher) switcher: KernelSwitcher,
182+
@inject(NativeNotebookProvider) notebookProvider: INotebookProvider
180183
) {
181184
super(
182185
progressReporter,
@@ -210,7 +213,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
210213
localize.DataScience.nativeEditorTitle(),
211214
ViewColumn.Active,
212215
experimentsManager,
213-
switcher
216+
switcher,
217+
notebookProvider
214218
);
215219
asyncRegistry.push(this);
216220

src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ import {
4545
INotebookExporter,
4646
INotebookImporter,
4747
INotebookModel,
48+
INotebookProvider,
4849
IStatusProvider,
4950
IThemeFinder
5051
} from '../types';
5152
import { NativeEditor } from './nativeEditor';
5253
import { NativeEditorStorage } from './nativeEditorStorage';
5354
import { NativeEditorSynchronizer } from './nativeEditorSynchronizer';
55+
import { NativeNotebookProvider } from './notebookProvider';
5456

5557
enum AskForSaveResult {
5658
Yes,
@@ -97,7 +99,8 @@ export class NativeEditorOldWebView extends NativeEditor {
9799
@inject(ProgressReporter) progressReporter: ProgressReporter,
98100
@inject(IExperimentsManager) experimentsManager: IExperimentsManager,
99101
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
100-
@inject(KernelSwitcher) switcher: KernelSwitcher
102+
@inject(KernelSwitcher) switcher: KernelSwitcher,
103+
@inject(NativeNotebookProvider) notebookProvider: INotebookProvider
101104
) {
102105
super(
103106
listeners,
@@ -127,7 +130,8 @@ export class NativeEditorOldWebView extends NativeEditor {
127130
progressReporter,
128131
experimentsManager,
129132
asyncRegistry,
130-
switcher
133+
switcher,
134+
notebookProvider
131135
);
132136
asyncRegistry.push(this);
133137
// No ui syncing in old notebooks.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { IFileSystem } from '../../common/platform/types';
8+
import { IDisposableRegistry } from '../../common/types';
9+
import { BaseNotebookProvider } from '../interactive-common/notebookProvider';
10+
import { INotebookEditor, INotebookEditorProvider } from '../types';
11+
12+
@injectable()
13+
export class NativeNotebookProvider extends BaseNotebookProvider {
14+
constructor(
15+
@inject(IFileSystem) private readonly fs: IFileSystem,
16+
@inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider,
17+
@inject(IDisposableRegistry) disposables: IDisposableRegistry
18+
) {
19+
super();
20+
disposables.push(editorProvider.onDidCloseNotebookEditor(this.onDidCloseNotebookEditor, this));
21+
}
22+
23+
protected async onDidCloseNotebookEditor(editor: INotebookEditor) {
24+
// First find all notebooks associated with this editor (ipynb file).
25+
const editors = this.editorProvider.editors.filter(
26+
e => this.fs.arePathsSame(e.file.fsPath, editor.file.fsPath) && e !== editor
27+
);
28+
29+
// If we have no editors for this file, then dispose the notebook.
30+
if (editors.length === 0) {
31+
await super.disposeNotebook(editor.file);
32+
}
33+
}
34+
}

src/client/datascience/interactive-window/interactiveWindow.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ import {
5353
IJupyterKernelSpec,
5454
IJupyterVariables,
5555
INotebookExporter,
56+
INotebookProvider,
5657
INotebookServerOptions,
5758
IStatusProvider,
5859
IThemeFinder,
5960
WebViewViewChangeEventArgs
6061
} from '../types';
62+
import { InteractiveWindowNotebookProvider } from './notebookProvider';
6163

6264
const historyReactDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'notebook');
6365

@@ -108,7 +110,8 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi
108110
@inject(IMemento) @named(GLOBAL_MEMENTO) globalStorage: Memento,
109111
@inject(ProgressReporter) progressReporter: ProgressReporter,
110112
@inject(IExperimentsManager) experimentsManager: IExperimentsManager,
111-
@inject(KernelSwitcher) switcher: KernelSwitcher
113+
@inject(KernelSwitcher) switcher: KernelSwitcher,
114+
@inject(InteractiveWindowNotebookProvider) notebookProvider: INotebookProvider
112115
) {
113116
super(
114117
progressReporter,
@@ -142,7 +145,8 @@ export class InteractiveWindow extends InteractiveBase implements IInteractiveWi
142145
localize.DataScience.historyTitle(),
143146
ViewColumn.Two,
144147
experimentsManager,
145-
switcher
148+
switcher,
149+
notebookProvider
146150
);
147151

148152
// Send a telemetry event to indicate window is opening
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { IDisposableRegistry } from '../../common/types';
8+
import { noop } from '../../common/utils/misc';
9+
import { BaseNotebookProvider } from '../interactive-common/notebookProvider';
10+
import { IInteractiveWindowProvider } from '../types';
11+
12+
@injectable()
13+
export class InteractiveWindowNotebookProvider extends BaseNotebookProvider {
14+
constructor(
15+
@inject(IInteractiveWindowProvider) private readonly interactiveWindowProvider: IInteractiveWindowProvider,
16+
@inject(IDisposableRegistry) disposables: IDisposableRegistry
17+
) {
18+
super();
19+
disposables.push(
20+
interactiveWindowProvider.onDidChangeActiveInteractiveWindow(this.checkAndDisposeNotebook, this)
21+
);
22+
}
23+
24+
/**
25+
* Interactive windows have just one window.
26+
* When that it closed, just close all of the notebooks associated with interactive windows.
27+
*/
28+
protected checkAndDisposeNotebook() {
29+
if (this.interactiveWindowProvider.getActive()) {
30+
return;
31+
}
32+
33+
Array.from(this.notebooks.values()).forEach(promise => {
34+
promise.then(notebook => notebook.dispose()).catch(noop);
35+
});
36+
37+
this.notebooks.clear();
38+
}
39+
}

src/client/datascience/jupyter/jupyterNotebook.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,14 @@ export class JupyterNotebookBase implements INotebook {
156156
private _workingDirectory: string | undefined;
157157
private _loggers: INotebookExecutionLogger[] = [];
158158
private onStatusChangedEvent: EventEmitter<ServerStatus> | undefined;
159+
public get onDisposed(): Event<void> {
160+
return this.disposed.event;
161+
}
159162
public get onKernelChanged(): Event<IJupyterKernelSpec | LiveKernelModel> {
160163
return this.kernelChanged.event;
161164
}
162165
private kernelChanged = new EventEmitter<IJupyterKernelSpec | LiveKernelModel>();
166+
private disposed = new EventEmitter<void>();
163167
private sessionStatusChanged: Disposable | undefined;
164168
private initializedMatplotlib = false;
165169

@@ -211,6 +215,7 @@ export class JupyterNotebookBase implements INotebook {
211215
const dispose = this.session ? this.session.dispose() : undefined;
212216
return dispose ? dispose : Promise.resolve();
213217
}
218+
this.disposed.fire();
214219
return Promise.resolve();
215220
}
216221

src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class GuestJupyterNotebook
3636
public onKernelChanged: Event<IJupyterKernelSpec | LiveKernelModel> = new EventEmitter<
3737
IJupyterKernelSpec | LiveKernelModel
3838
>().event;
39+
public onDisposed = new EventEmitter<void>().event;
3940
private responseQueue: ResponseQueue = new ResponseQueue();
4041
private onStatusChangedEvent: EventEmitter<ServerStatus> | undefined;
4142

src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class HostJupyterNotebook
4040
private localResponses: ResponseQueue = new ResponseQueue();
4141
private requestLog: Map<string, number> = new Map<string, number>();
4242
private catchupPendingCount: number = 0;
43-
private disposed = false;
43+
private isDisposed = false;
4444
constructor(
4545
liveShare: ILiveShareApi,
4646
session: IJupyterSession,
@@ -74,8 +74,8 @@ export class HostJupyterNotebook
7474
}
7575

7676
public dispose = async (): Promise<void> => {
77-
if (!this.disposed) {
78-
this.disposed = true;
77+
if (!this.isDisposed) {
78+
this.isDisposed = true;
7979
await super.dispose();
8080
const api = await this.api;
8181
return this.onDetach(api);
@@ -85,7 +85,7 @@ export class HostJupyterNotebook
8585
public async onAttach(api: vsls.LiveShare | null): Promise<void> {
8686
await super.onAttach(api);
8787

88-
if (api && !this.disposed) {
88+
if (api && !this.isDisposed) {
8989
const service = await this.waitForService();
9090

9191
// Attach event handlers to different requests

src/client/datascience/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ import { NativeEditorProvider } from './interactive-ipynb/nativeEditorProvider';
3939
import { NativeEditorProviderOld } from './interactive-ipynb/nativeEditorProviderOld';
4040
import { NativeEditorStorage } from './interactive-ipynb/nativeEditorStorage';
4141
import { NativeEditorSynchronizer } from './interactive-ipynb/nativeEditorSynchronizer';
42+
import { NativeNotebookProvider } from './interactive-ipynb/notebookProvider';
4243
import { InteractiveWindow } from './interactive-window/interactiveWindow';
4344
import { InteractiveWindowCommandListener } from './interactive-window/interactiveWindowCommandListener';
4445
import { InteractiveWindowProvider } from './interactive-window/interactiveWindowProvider';
46+
import { InteractiveWindowNotebookProvider } from './interactive-window/notebookProvider';
4547
import { JupyterCommandLineSelector } from './jupyter/commandLineSelector';
4648
import { JupyterCommandFactory } from './jupyter/interpreter/jupyterCommand';
4749
import { JupyterCommandFinder } from './jupyter/interpreter/jupyterCommandFinder';
@@ -108,6 +110,7 @@ import {
108110
INotebookExecutionLogger,
109111
INotebookExporter,
110112
INotebookImporter,
113+
INotebookProvider,
111114
INotebookServer,
112115
INotebookStorage,
113116
IPlotViewer,
@@ -188,6 +191,8 @@ export function registerTypes(serviceManager: IServiceManager) {
188191
serviceManager.addSingleton<NotebookStarter>(NotebookStarter, NotebookStarter);
189192
serviceManager.addSingleton<ProgressReporter>(ProgressReporter, ProgressReporter);
190193
serviceManager.addSingleton<NativeEditorSynchronizer>(NativeEditorSynchronizer, NativeEditorSynchronizer);
194+
serviceManager.addSingleton<INotebookProvider>(InteractiveWindowNotebookProvider, InteractiveWindowNotebookProvider);
195+
serviceManager.addSingleton<INotebookProvider>(NativeNotebookProvider, NativeNotebookProvider);
191196

192197
// Temporary code, to allow users to revert to the old behavior.
193198
const cfg = serviceManager.get<IWorkspaceService>(IWorkspaceService).getConfiguration('python.dataScience', undefined);

src/client/datascience/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export interface INotebook extends IAsyncDisposable {
113113
readonly server: INotebookServer;
114114
readonly status: ServerStatus;
115115
onSessionStatusChanged: Event<ServerStatus>;
116+
onDisposed: Event<void>;
116117
onKernelChanged: Event<IJupyterKernelSpec | LiveKernelModel>;
117118
clear(id: string): void;
118119
executeObservable(code: string, file: string, line: number, id: string, silent: boolean): Observable<ICell[]>;
@@ -848,3 +849,10 @@ type WebViewViewState = {
848849
readonly active: boolean;
849850
};
850851
export type WebViewViewChangeEventArgs = { current: WebViewViewState; previous: WebViewViewState };
852+
853+
export interface INotebookProvider {
854+
/**
855+
* Gets or creates a notebook, and manages the lifetime of notebooks.
856+
*/
857+
getNotebook(server: INotebookServer, resource: Uri, options?: nbformat.INotebookMetadata): Promise<INotebook>;
858+
}

0 commit comments

Comments
 (0)