Skip to content

Commit 3300b83

Browse files
authored
Sync VSCode output Cell Output & add INotebookEditor/INotebookEditorProvider (#11849)
* Add ability to run code * Fixes * Add comments * Added comments * More changes * More comments * Fixes * Fixes * Fixes
1 parent 856dfae commit 3300b83

File tree

6 files changed

+353
-1
lines changed

6 files changed

+353
-1
lines changed

src/client/datascience/notebook/executionHelpers.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import type { nbformat } from '@jupyterlab/coreutils';
77
import type { KernelMessage } from '@jupyterlab/services';
88
import { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode';
99
import { createErrorOutput } from '../../../datascience-ui/common/cellFactory';
10+
import { IDisposable } from '../../common/types';
1011
import { INotebookModelModifyChange } from '../interactive-common/interactiveWindowTypes';
1112
import { ICell, INotebookModel } from '../types';
12-
import { cellOutputsToVSCCellOutputs, translateErrorOutput } from './helpers';
13+
import { cellOutputsToVSCCellOutputs, findMappedNotebookCellData, translateErrorOutput } from './helpers';
1314

1415
export function hasTransientOutputForAnotherCell(output?: nbformat.IOutput) {
1516
return (
@@ -22,6 +23,7 @@ export function hasTransientOutputForAnotherCell(output?: nbformat.IOutput) {
2223
Object.keys((output as any).transient).length > 0
2324
);
2425
}
26+
2527
/**
2628
* Updates the cell in notebook model as well as the notebook document.
2729
* Update notebook document so UI is updated accordingly.
@@ -76,6 +78,47 @@ export function updateCellWithErrorStatus(cell: NotebookCell, ex: Partial<Error>
7678
cell.metadata.runState = NotebookCellRunState.Error;
7779
}
7880

81+
/**
82+
* Responsible for syncing changes from our model into the VS Code cells.
83+
* Eg. when executing a cell, we update our model with the output, and here we react to those events and update the VS Code output.
84+
* This way, all updates to VSCode cells can happen in one place (here), and we can focus on updating just the Cell model with the data.
85+
* Here we only keep the outputs in sync. The assumption is that we won't be adding cells directly.
86+
* If adding cells and the like then please use VSC api to manipulate cells, else we have 2 ways of doing the same thing and that could lead to issues.
87+
*/
88+
export function monitorModelCellOutputChangesAndUpdateNotebookDocument(
89+
document: NotebookDocument,
90+
model: INotebookModel
91+
): IDisposable {
92+
return model.changed((change) => {
93+
// We're only interested in updates to cells.
94+
if (change.kind !== 'modify') {
95+
return;
96+
}
97+
for (const cell of change.newCells) {
98+
const uiCellToUpdate = findMappedNotebookCellData(cell, document.cells);
99+
if (!uiCellToUpdate) {
100+
continue;
101+
}
102+
const newOutput = Array.isArray(cell.data.outputs)
103+
? // tslint:disable-next-line: no-any
104+
cellOutputsToVSCCellOutputs(cell.data.outputs as any)
105+
: [];
106+
// If there were no cells and still no cells, nothing to update.
107+
if (newOutput.length === 0 && uiCellToUpdate.outputs.length === 0) {
108+
return;
109+
}
110+
// If no changes in output, then nothing to do.
111+
if (
112+
newOutput.length === uiCellToUpdate.outputs.length &&
113+
JSON.stringify(newOutput) === JSON.stringify(uiCellToUpdate.outputs)
114+
) {
115+
return;
116+
}
117+
uiCellToUpdate.outputs = newOutput;
118+
}
119+
});
120+
}
121+
79122
/**
80123
* Updates our Cell Model with the cell output.
81124
* As we execute a cell we get output from jupyter. This code will ensure the cell is updated with the output.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { Event, EventEmitter, notebook, Uri, WebviewPanel } from 'vscode';
7+
import { INotebook, INotebookEditor, INotebookModel } from '../types';
8+
9+
export class NotebookEditor implements INotebookEditor {
10+
public get onDidChangeViewState(): Event<void> {
11+
return this.changedViewState.event;
12+
}
13+
public get closed(): Event<INotebookEditor> {
14+
return this._closed.event;
15+
}
16+
public get modified(): Event<INotebookEditor> {
17+
return this._modified.event;
18+
}
19+
20+
public get executed(): Event<INotebookEditor> {
21+
return this._executed.event;
22+
}
23+
public get saved(): Event<INotebookEditor> {
24+
return this._saved.event;
25+
}
26+
public get isUntitled(): boolean {
27+
return this.model.isUntitled;
28+
}
29+
public get isDirty(): boolean {
30+
return this.model.isDirty;
31+
}
32+
public get file(): Uri {
33+
return this.model.file;
34+
}
35+
public get visible(): boolean {
36+
return !this.model.isDisposed;
37+
}
38+
public get active(): boolean {
39+
return notebook.activeNotebookEditor?.document.uri.toString() === this.model.file.toString();
40+
}
41+
public get onExecutedCode(): Event<string> {
42+
return this.executedCode.event;
43+
}
44+
public notebook?: INotebook | undefined;
45+
private changedViewState = new EventEmitter<void>();
46+
private _closed = new EventEmitter<INotebookEditor>();
47+
private _saved = new EventEmitter<INotebookEditor>();
48+
private _executed = new EventEmitter<INotebookEditor>();
49+
private _modified = new EventEmitter<INotebookEditor>();
50+
private executedCode = new EventEmitter<string>();
51+
constructor(public readonly model: INotebookModel) {
52+
model.onDidEdit(() => this._modified.fire(this));
53+
}
54+
public async load(_storage: INotebookModel, _webViewPanel?: WebviewPanel): Promise<void> {
55+
// Not used.
56+
}
57+
public runAllCells(): void {
58+
throw new Error('Method not implemented.');
59+
}
60+
public runSelectedCell(): void {
61+
throw new Error('Method not implemented.');
62+
}
63+
public addCellBelow(): void {
64+
throw new Error('Method not implemented.');
65+
}
66+
public show(): Promise<void> {
67+
throw new Error('Method not implemented.');
68+
}
69+
public startProgress(): void {
70+
throw new Error('Method not implemented.');
71+
}
72+
public stopProgress(): void {
73+
throw new Error('Method not implemented.');
74+
}
75+
public undoCells(): void {
76+
throw new Error('Method not implemented.');
77+
}
78+
public redoCells(): void {
79+
throw new Error('Method not implemented.');
80+
}
81+
public removeAllCells(): void {
82+
throw new Error('Method not implemented.');
83+
}
84+
public interruptKernel(): Promise<void> {
85+
throw new Error('Method not implemented.');
86+
}
87+
public restartKernel(): Promise<void> {
88+
throw new Error('Method not implemented.');
89+
}
90+
public dispose() {
91+
// Not required.
92+
}
93+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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 { Event, EventEmitter, notebook, NotebookDocument, Uri } from 'vscode';
8+
import { IExtensionSingleActivationService } from '../../activation/types';
9+
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
10+
import '../../common/extensions';
11+
import { IDisposableRegistry } from '../../common/types';
12+
import { createDeferred, Deferred } from '../../common/utils/async';
13+
import { isUri } from '../../common/utils/misc';
14+
import { sendTelemetryEvent } from '../../telemetry';
15+
import { Telemetry } from '../constants';
16+
import { INotebookStorageProvider } from '../interactive-ipynb/notebookStorageProvider';
17+
import { INotebookEditor, INotebookEditorProvider } from '../types';
18+
import { monitorModelCellOutputChangesAndUpdateNotebookDocument } from './executionHelpers';
19+
import { NotebookEditor } from './notebookEditor';
20+
21+
/**
22+
* Class responsbile for activating an registering the necessary event handlers in NotebookEditorProvider.
23+
*/
24+
@injectable()
25+
export class NotebookEditorProviderActivation implements IExtensionSingleActivationService {
26+
constructor(@inject(INotebookEditorProvider) private readonly provider: INotebookEditorProvider) {}
27+
public async activate(): Promise<void> {
28+
// The whole purpose is to ensure the NotebookEditorProvider class activates as soon as extension loads.
29+
// tslint:disable-next-line: no-use-before-declare
30+
if (this.provider instanceof NotebookEditorProvider) {
31+
this.provider.activate();
32+
}
33+
}
34+
}
35+
36+
/**
37+
* Notebook Editor provider used by other parts of DS code.
38+
* This is an adapter, that takes the VSCode api for editors (did notebook editors open, close save, etc) and
39+
* then exposes them in a manner we expect - i.e. INotebookEditorProvider.
40+
* This is also responsible for tracking all notebooks that open and then keeping the VS Code notebook models updated with changes we made to our underlying model.
41+
* E.g. when cells are executed the results in our model is updated, this tracks those changes and syncs VSC cells with those updates.
42+
*/
43+
@injectable()
44+
export class NotebookEditorProvider implements INotebookEditorProvider {
45+
public get onDidChangeActiveNotebookEditor(): Event<INotebookEditor | undefined> {
46+
return this._onDidChangeActiveNotebookEditor.event;
47+
}
48+
public get onDidCloseNotebookEditor(): Event<INotebookEditor> {
49+
return this._onDidCloseNotebookEditor.event;
50+
}
51+
public get onDidOpenNotebookEditor(): Event<INotebookEditor> {
52+
return this._onDidOpenNotebookEditor.event;
53+
}
54+
public get activeEditor(): INotebookEditor | undefined {
55+
return this.editors.find((e) => e.visible && e.active);
56+
}
57+
public get editors(): INotebookEditor[] {
58+
return [...this.openedEditors];
59+
}
60+
// Note, this constant has to match the value used in the package.json to register the webview custom editor.
61+
public static readonly customEditorViewType = 'NativeEditorProvider.ipynb';
62+
protected readonly _onDidChangeActiveNotebookEditor = new EventEmitter<INotebookEditor | undefined>();
63+
protected readonly _onDidOpenNotebookEditor = new EventEmitter<INotebookEditor>();
64+
private readonly _onDidCloseNotebookEditor = new EventEmitter<INotebookEditor>();
65+
private openedEditors: Set<INotebookEditor> = new Set<INotebookEditor>();
66+
private executedEditors: Set<string> = new Set<string>();
67+
private notebookCount: number = 0;
68+
private openedNotebookCount: number = 0;
69+
private readonly notebookEditors = new Map<NotebookDocument, INotebookEditor>();
70+
private readonly notebookEditorsByUri = new Map<string, INotebookEditor>();
71+
private readonly notebooksWaitingToBeOpenedByUri = new Map<string, Deferred<INotebookEditor>>();
72+
constructor(
73+
@inject(INotebookStorageProvider) private readonly storage: INotebookStorageProvider,
74+
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService,
75+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
76+
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry
77+
) {
78+
disposables.push(this);
79+
}
80+
public activate() {
81+
this.disposables.push(notebook.onDidOpenNotebookDocument(this.onDidOpenNotebookDocument, this));
82+
this.disposables.push(notebook.onDidCloseNotebookDocument(this.onDidCloseNotebookDocument, this));
83+
84+
// Look through the file system for ipynb files to see how many we have in the workspace. Don't wait
85+
// on this though.
86+
const findFilesPromise = this.workspace.findFiles('**/*.ipynb');
87+
if (findFilesPromise && findFilesPromise.then) {
88+
findFilesPromise.then((r) => (this.notebookCount += r.length));
89+
}
90+
}
91+
public dispose() {
92+
// Send a bunch of telemetry
93+
if (this.openedNotebookCount) {
94+
sendTelemetryEvent(Telemetry.NotebookOpenCount, undefined, { count: this.openedNotebookCount });
95+
}
96+
if (this.executedEditors.size) {
97+
sendTelemetryEvent(Telemetry.NotebookRunCount, undefined, { count: this.executedEditors.size });
98+
}
99+
if (this.notebookCount) {
100+
sendTelemetryEvent(Telemetry.NotebookWorkspaceCount, undefined, { count: this.notebookCount });
101+
}
102+
}
103+
104+
public async open(file: Uri): Promise<INotebookEditor> {
105+
// Wait for editor to get opened up, vscode will notify when it is opened.
106+
// Further below.
107+
const deferred = createDeferred<INotebookEditor>();
108+
this.notebooksWaitingToBeOpenedByUri.set(file.toString(), deferred);
109+
deferred.promise.then(() => this.notebooksWaitingToBeOpenedByUri.delete(file.toString())).ignoreErrors();
110+
await this.commandManager.executeCommand('vscode.open', file);
111+
return deferred.promise;
112+
}
113+
public async show(_file: Uri): Promise<INotebookEditor | undefined> {
114+
// We do not need this.
115+
throw new Error('Not supported');
116+
}
117+
public async createNew(_contents?: string): Promise<INotebookEditor> {
118+
// tslint:disable-next-line: no-suspicious-comment
119+
// TODO: In another branch.
120+
// const model = await this.storage.createNew(contents);
121+
// await this.onDidOpenNotebookDocument(model.file);
122+
// tslint:disable-next-line: no-suspicious-comment
123+
// TODO: Need to do this.
124+
// Update number of notebooks in the workspace
125+
// this.notebookCount += 1;
126+
// return this.open(model.file);
127+
// tslint:disable-next-line: no-any
128+
return undefined as any;
129+
}
130+
protected openedEditor(editor: INotebookEditor): void {
131+
this.openedNotebookCount += 1;
132+
if (!this.executedEditors.has(editor.file.fsPath)) {
133+
editor.executed(this.onExecuted.bind(this));
134+
}
135+
this.disposables.push(editor.onDidChangeViewState(this.onChangedViewState, this));
136+
this.openedEditors.add(editor);
137+
editor.closed(this.closedEditor.bind(this));
138+
this._onDidOpenNotebookEditor.fire(editor);
139+
}
140+
141+
private closedEditor(editor: INotebookEditor): void {
142+
this.openedEditors.delete(editor);
143+
this._onDidCloseNotebookEditor.fire(editor);
144+
}
145+
private onChangedViewState(): void {
146+
this._onDidChangeActiveNotebookEditor.fire(this.activeEditor);
147+
}
148+
149+
private onExecuted(editor: INotebookEditor): void {
150+
if (editor) {
151+
this.executedEditors.add(editor.file.fsPath);
152+
}
153+
}
154+
155+
private async onDidOpenNotebookDocument(doc: NotebookDocument | Uri): Promise<void> {
156+
const uri = isUri(doc) ? doc : doc.uri;
157+
const model = await this.storage.load(uri);
158+
// In open method we might be waiting.
159+
const editor = this.notebookEditorsByUri.get(uri.toString()) || new NotebookEditor(model);
160+
const deferred = this.notebooksWaitingToBeOpenedByUri.get(uri.toString());
161+
deferred?.resolve(editor); // NOSONAR
162+
if (!isUri(doc)) {
163+
// This is where we ensure changes to our models are propagated back to the VSCode model.
164+
this.disposables.push(monitorModelCellOutputChangesAndUpdateNotebookDocument(doc, model));
165+
this.notebookEditors.set(doc, editor);
166+
}
167+
this.notebookEditorsByUri.set(uri.toString(), editor);
168+
}
169+
private async onDidCloseNotebookDocument(doc: NotebookDocument | Uri): Promise<void> {
170+
const editor = isUri(doc) ? this.notebookEditorsByUri.get(doc.toString()) : this.notebookEditors.get(doc);
171+
if (editor) {
172+
editor.dispose();
173+
if (editor.model) {
174+
editor.model.dispose();
175+
}
176+
}
177+
if (isUri(doc)) {
178+
this.notebookEditorsByUri.delete(doc.toString());
179+
} else {
180+
this.notebookEditors.delete(doc);
181+
this.notebookEditorsByUri.delete(doc.uri.toString());
182+
}
183+
}
184+
}

src/client/datascience/notebook/serviceRegistry.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,27 @@
44
'use strict';
55

66
import { IExtensionSingleActivationService } from '../../activation/types';
7+
import { NativeNotebook } from '../../common/experimentGroups';
8+
import { IExperimentsManager } from '../../common/types';
79
import { IServiceManager } from '../../ioc/types';
10+
import { INotebookEditorProvider } from '../types';
811
import { NotebookContentProvider } from './contentProvider';
912
import { NotebookIntegration } from './integration';
13+
import { NotebookEditorProvider, NotebookEditorProviderActivation } from './notebookEditorProvider';
1014
import { NotebookKernel } from './notebookKernel';
1115

1216
export function registerTypes(serviceManager: IServiceManager) {
17+
// This condition is temporary.
18+
// If user belongs to the experiment, then make the necessary changes to package.json.
19+
// Once the API is final, we won't need to modify the package.json.
20+
if (!serviceManager.get<IExperimentsManager>(IExperimentsManager).inExperiment(NativeNotebook.experiment)) {
21+
return;
22+
}
23+
serviceManager.addSingleton<IExtensionSingleActivationService>(
24+
IExtensionSingleActivationService,
25+
NotebookEditorProviderActivation
26+
);
27+
serviceManager.rebindSingleton<INotebookEditorProvider>(INotebookEditorProvider, NotebookEditorProvider);
1328
serviceManager.addSingleton<NotebookKernel>(NotebookKernel, NotebookKernel);
1429
serviceManager.addSingleton<IExtensionSingleActivationService>(
1530
IExtensionSingleActivationService,

src/client/ioc/serviceManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ export class ServiceManager implements IServiceManager {
9191
}
9292
}
9393

94+
public rebindSingleton<T>(
95+
serviceIdentifier: interfaces.ServiceIdentifier<T>,
96+
constructor: ClassType<T>,
97+
name?: string | number | symbol
98+
): void {
99+
if (name) {
100+
this.container.rebind<T>(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name);
101+
} else {
102+
this.container.rebind<T>(serviceIdentifier).to(constructor).inSingletonScope();
103+
}
104+
}
105+
94106
public rebindInstance<T>(
95107
serviceIdentifier: interfaces.ServiceIdentifier<T>,
96108
instance: T,

0 commit comments

Comments
 (0)