Skip to content

Commit 66ef1b9

Browse files
authored
Place VS Code API behind an interface, add tests (#11882)
For #10496
1 parent f8e9f5b commit 66ef1b9

File tree

16 files changed

+734
-147
lines changed

16 files changed

+734
-147
lines changed

build/ci/templates/globals.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ variables:
1010
CI_BRANCH_NAME: ${Build.SourceBranchName}
1111
npm_config_cache: $(Pipeline.Workspace)/.npm
1212
vmImageMacOS: 'macOS-10.15'
13+
TS_NODE_FILES: true # Temporarily enabled to allow using types from vscode.proposed.d.ts from ts-node (for tests).
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { injectable } from 'inversify';
5+
import {
6+
Disposable,
7+
Event,
8+
GlobPattern,
9+
notebook,
10+
NotebookContentProvider,
11+
NotebookDocument,
12+
NotebookDocumentChangeEvent,
13+
NotebookEditor,
14+
NotebookKernel,
15+
NotebookOutputRenderer,
16+
NotebookOutputSelector
17+
} from 'vscode';
18+
import { IVSCodeNotebook } from './types';
19+
20+
@injectable()
21+
export class VSCodeNotebook implements IVSCodeNotebook {
22+
public registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable {
23+
return notebook.registerNotebookContentProvider(notebookType, provider);
24+
}
25+
public registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable {
26+
return notebook.registerNotebookKernel(id, selectors, kernel);
27+
}
28+
public registerNotebookOutputRenderer(
29+
id: string,
30+
outputSelector: NotebookOutputSelector,
31+
renderer: NotebookOutputRenderer
32+
): Disposable {
33+
return notebook.registerNotebookOutputRenderer(id, outputSelector, renderer);
34+
}
35+
public get onDidOpenNotebookDocument(): Event<NotebookDocument> {
36+
return notebook.onDidOpenNotebookDocument;
37+
}
38+
public get onDidCloseNotebookDocument(): Event<NotebookDocument> {
39+
return notebook.onDidCloseNotebookDocument;
40+
}
41+
42+
public get onDidChangeNotebookDocument(): Event<NotebookDocumentChangeEvent> {
43+
return notebook.onDidChangeNotebookDocument;
44+
}
45+
public get activeNotebookEditor(): NotebookEditor | undefined {
46+
return notebook.activeNotebookEditor;
47+
}
48+
}

src/client/common/application/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import {
2424
InputBoxOptions,
2525
MessageItem,
2626
MessageOptions,
27+
NotebookContentProvider,
28+
NotebookDocument,
29+
NotebookDocumentChangeEvent,
30+
NotebookEditor,
31+
NotebookKernel,
32+
NotebookOutputRenderer,
33+
NotebookOutputSelector,
2734
OpenDialogOptions,
2835
OutputChannel,
2936
Progress,
@@ -1442,3 +1449,21 @@ export interface IClipboard {
14421449
*/
14431450
writeText(value: string): Promise<void>;
14441451
}
1452+
1453+
export const IVSCodeNotebook = Symbol('IVSCodeNotebook');
1454+
export interface IVSCodeNotebook {
1455+
readonly onDidOpenNotebookDocument: Event<NotebookDocument>;
1456+
readonly onDidCloseNotebookDocument: Event<NotebookDocument>;
1457+
1458+
readonly onDidChangeNotebookDocument: Event<NotebookDocumentChangeEvent>;
1459+
readonly activeNotebookEditor: NotebookEditor | undefined;
1460+
registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider): Disposable;
1461+
1462+
registerNotebookKernel(id: string, selectors: GlobPattern[], kernel: NotebookKernel): Disposable;
1463+
1464+
registerNotebookOutputRenderer(
1465+
id: string,
1466+
outputSelector: NotebookOutputSelector,
1467+
renderer: NotebookOutputRenderer
1468+
): Disposable;
1469+
}

src/client/common/serviceRegistry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
1919
import { DocumentManager } from './application/documentManager';
2020
import { Extensions } from './application/extensions';
2121
import { LanguageService } from './application/languageService';
22+
import { VSCodeNotebook } from './application/notebook';
2223
import { TerminalManager } from './application/terminalManager';
2324
import {
2425
IActiveResourceService,
@@ -32,6 +33,7 @@ import {
3233
ILanguageService,
3334
ILiveShareApi,
3435
ITerminalManager,
36+
IVSCodeNotebook,
3537
IWorkspaceService
3638
} from './application/types';
3739
import { WorkspaceService } from './application/workspace';
@@ -121,6 +123,7 @@ export function registerTypes(serviceManager: IServiceManager) {
121123
serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory);
122124
serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
123125
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
126+
serviceManager.addSingleton<IVSCodeNotebook>(IVSCodeNotebook, VSCodeNotebook);
124127
serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService);
125128
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
126129
serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { NotebookCell, NotebookDocument } from 'vscode';
7+
import { IDisposable } from '../../common/types';
8+
import { traceError } from '../../logging';
9+
import { ICell, INotebookModel } from '../types';
10+
import { cellOutputsToVSCCellOutputs } from './helpers';
11+
12+
export function findMappedNotebookCellData(source: ICell, cells: NotebookCell[]): NotebookCell {
13+
// tslint:disable-next-line: no-suspicious-comment
14+
// TODO: Will metadata get copied across when copying/pasting cells (cloning a cell)?
15+
// If so, then we have a problem.
16+
const found = cells.filter((cell) => source.id === cell.metadata.custom?.cellId);
17+
18+
// tslint:disable-next-line: no-suspicious-comment
19+
// TODO: Once VSC provides API, throw error here.
20+
if (!found || !found.length) {
21+
traceError(`Unable to find matching cell for ${source}`);
22+
return cells[0];
23+
}
24+
25+
return found[0];
26+
}
27+
28+
export function findMappedNotebookCellModel(source: NotebookCell, cells: ICell[]): ICell {
29+
// tslint:disable-next-line: no-suspicious-comment
30+
// TODO: Will metadata get copied across when copying/pasting cells (cloning a cell)?
31+
// If so, then we have a problem.
32+
const found = cells.filter((cell) => cell.id === source.metadata.custom?.cellId);
33+
34+
// tslint:disable-next-line: no-suspicious-comment
35+
// TODO: Once VSC provides API, throw error here.
36+
if (!found || !found.length) {
37+
traceError(`Unable to find matching cell for ${source}`);
38+
return cells[0];
39+
}
40+
41+
return found[0];
42+
}
43+
44+
/**
45+
* Responsible for syncing changes from our model into the VS Code cells.
46+
* 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.
47+
* 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.
48+
* Here we only keep the outputs in sync. The assumption is that we won't be adding cells directly.
49+
* 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.
50+
*/
51+
export function monitorModelCellOutputChangesAndUpdateNotebookDocument(
52+
document: NotebookDocument,
53+
model: INotebookModel
54+
): IDisposable {
55+
let wasUntitledNotebook = model.isUntitled;
56+
let stopSyncingOutput = false;
57+
const disposable = model.changed((change) => {
58+
if (stopSyncingOutput) {
59+
return;
60+
}
61+
if (change.kind === 'saveAs') {
62+
if (wasUntitledNotebook) {
63+
wasUntitledNotebook = false;
64+
// User saved untitled file as a real file.
65+
return;
66+
} else {
67+
// Ok, user save a normal notebook as another name.
68+
// Stop monitoring changes.
69+
stopSyncingOutput = true;
70+
disposable.dispose();
71+
return;
72+
}
73+
}
74+
// We're only interested in updates to cells.
75+
if (change.kind !== 'modify') {
76+
return;
77+
}
78+
for (const cell of change.newCells) {
79+
const uiCellToUpdate = findMappedNotebookCellData(cell, document.cells);
80+
if (!uiCellToUpdate) {
81+
continue;
82+
}
83+
const newOutput = Array.isArray(cell.data.outputs)
84+
? // tslint:disable-next-line: no-any
85+
cellOutputsToVSCCellOutputs(cell.data.outputs as any)
86+
: [];
87+
// If there were no cells and still no cells, nothing to update.
88+
if (newOutput.length === 0 && uiCellToUpdate.outputs.length === 0) {
89+
return;
90+
}
91+
// If no changes in output, then nothing to do.
92+
if (
93+
newOutput.length === uiCellToUpdate.outputs.length &&
94+
JSON.stringify(newOutput) === JSON.stringify(uiCellToUpdate.outputs)
95+
) {
96+
return;
97+
}
98+
uiCellToUpdate.outputs = newOutput;
99+
}
100+
});
101+
102+
return disposable;
103+
}

src/client/datascience/notebook/executionHelpers.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ 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';
1110
import { INotebookModelModifyChange } from '../interactive-common/interactiveWindowTypes';
1211
import { ICell, INotebookModel } from '../types';
13-
import { cellOutputsToVSCCellOutputs, findMappedNotebookCellData, translateErrorOutput } from './helpers';
12+
import { cellOutputsToVSCCellOutputs, translateErrorOutput } from './helpers';
1413

1514
export function hasTransientOutputForAnotherCell(output?: nbformat.IOutput) {
1615
return (
@@ -78,67 +77,6 @@ export function updateCellWithErrorStatus(cell: NotebookCell, ex: Partial<Error>
7877
cell.metadata.runState = NotebookCellRunState.Error;
7978
}
8079

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-
let wasUntitledNotebook = model.isUntitled;
93-
let stopSyncingOutput = false;
94-
const disposable = model.changed((change) => {
95-
if (stopSyncingOutput) {
96-
return;
97-
}
98-
if (change.kind === 'saveAs') {
99-
if (wasUntitledNotebook) {
100-
wasUntitledNotebook = false;
101-
// User saved untitled file as a real file.
102-
return;
103-
} else {
104-
// Ok, user save a normal notebook as another name.
105-
// Stop monitoring changes.
106-
stopSyncingOutput = true;
107-
disposable.dispose();
108-
return;
109-
}
110-
}
111-
// We're only interested in updates to cells.
112-
if (change.kind !== 'modify') {
113-
return;
114-
}
115-
for (const cell of change.newCells) {
116-
const uiCellToUpdate = findMappedNotebookCellData(cell, document.cells);
117-
if (!uiCellToUpdate) {
118-
continue;
119-
}
120-
const newOutput = Array.isArray(cell.data.outputs)
121-
? // tslint:disable-next-line: no-any
122-
cellOutputsToVSCCellOutputs(cell.data.outputs as any)
123-
: [];
124-
// If there were no cells and still no cells, nothing to update.
125-
if (newOutput.length === 0 && uiCellToUpdate.outputs.length === 0) {
126-
return;
127-
}
128-
// If no changes in output, then nothing to do.
129-
if (
130-
newOutput.length === uiCellToUpdate.outputs.length &&
131-
JSON.stringify(newOutput) === JSON.stringify(uiCellToUpdate.outputs)
132-
) {
133-
return;
134-
}
135-
uiCellToUpdate.outputs = newOutput;
136-
}
137-
});
138-
139-
return disposable;
140-
}
141-
14280
/**
14381
* Updates our Cell Model with the cell output.
14482
* As we execute a cell we get output from jupyter. This code will ensure the cell is updated with the output.

0 commit comments

Comments
 (0)