Skip to content

Commit 7c6bede

Browse files
authored
Prompt to open exported notebook in native editor (#8315)
* Prompt to open exported nb in editor * News entry file * Fix tests * Fixes to how tests are run
1 parent c50c778 commit 7c6bede

File tree

6 files changed

+55
-43
lines changed

6 files changed

+55
-43
lines changed

build/.mocha.unittests.ts.opts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
--require ts-node/register
2-
--require out/test/unittests.js
2+
--require src/test/unittests.ts
33
--reporter mocha-multi-reporters
44
--reporter-options configFile=build/.mocha-multi-reporters.config
55
--ui tdd
66
--recursive
77
--colors
8-
./src/test/**/*.unit.test.ts
8+
./src/test/**/*.unit.test.ts

news/1 Enhancements/8078.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prompt to open exported `Notebook` in the `Notebook Editor`.

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
IInteractiveBase,
2626
IInteractiveWindowProvider,
2727
IJupyterExecution,
28+
INotebookEditorProvider,
2829
INotebookExporter,
2930
INotebookImporter,
3031
INotebookServer,
@@ -45,7 +46,8 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
4546
@inject(IConfigurationService) private configuration: IConfigurationService,
4647
@inject(IStatusProvider) private statusProvider: IStatusProvider,
4748
@inject(INotebookImporter) private jupyterImporter: INotebookImporter,
48-
@inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler
49+
@inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler,
50+
@inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider
4951
) {
5052
}
5153

@@ -159,7 +161,6 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
159161
saveLabel: localize.DataScience.exportDialogTitle(),
160162
filters: filtersObject
161163
});
162-
163164
await this.waitForStatus(async () => {
164165
if (uri) {
165166
let directoryChange;
@@ -172,16 +173,19 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
172173
await this.fileSystem.writeFile(uri.fsPath, JSON.stringify(notebook));
173174
}
174175
}, localize.DataScience.exportingFormat(), file);
175-
176176
// When all done, show a notice that it completed.
177-
const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
178177
if (uri && uri.fsPath) {
179-
this.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => {
180-
if (str === openQuestion) {
181-
// If the user wants to, open the notebook they just generated.
182-
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
183-
}
184-
});
178+
const openQuestion1 = localize.DataScience.exportOpenQuestion1();
179+
const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
180+
const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])];
181+
const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), ...questions);
182+
if (selection === openQuestion1) {
183+
await this.ipynbProvider.open(uri, await this.fileSystem.readFile(uri.fsPath));
184+
}
185+
if (selection === openQuestion2) {
186+
// If the user wants to, open the notebook they just generated.
187+
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
188+
}
185189
}
186190
}
187191
}
@@ -220,13 +224,17 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
220224
});
221225

222226
// When all done, show a notice that it completed.
223-
const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
224-
this.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => {
225-
if (str === openQuestion && output) {
226-
// If the user wants to, open the notebook they just generated.
227-
this.jupyterExecution.spawnNotebook(output).ignoreErrors();
228-
}
229-
});
227+
const openQuestion1 = localize.DataScience.exportOpenQuestion1();
228+
const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
229+
const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])];
230+
const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), ...questions);
231+
if (selection === openQuestion1) {
232+
await this.ipynbProvider.open(Uri.file(output), await this.fileSystem.readFile(output));
233+
}
234+
if (selection === openQuestion2) {
235+
// If the user wants to, open the notebook they just generated.
236+
this.jupyterExecution.spawnNotebook(output).ignoreErrors();
237+
}
230238

231239
return Uri.file(output);
232240
}

src/test/datascience/interactiveWindowCommandListener.unit.test.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@
33
'use strict';
44
import { nbformat } from '@jupyterlab/coreutils/lib/nbformat';
55
import { assert } from 'chai';
6-
import { anything, instance, mock, when } from 'ts-mockito';
6+
import { anything, instance, mock, verify, when } from 'ts-mockito';
77
import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher';
88
import * as TypeMoq from 'typemoq';
99
import * as uuid from 'uuid/v4';
1010
import { EventEmitter, Uri } from 'vscode';
1111

1212
import { ApplicationShell } from '../../client/common/application/applicationShell';
13+
import { IApplicationShell } from '../../client/common/application/types';
1314
import { PythonSettings } from '../../client/common/configSettings';
1415
import { ConfigurationService } from '../../client/common/configuration/service';
1516
import { Logger } from '../../client/common/logger';
1617
import { FileSystem } from '../../client/common/platform/fileSystem';
1718
import { IFileSystem } from '../../client/common/platform/types';
1819
import { IConfigurationService, IDisposable, ILogger } from '../../client/common/types';
20+
import * as localize from '../../client/common/utils/localize';
1921
import { generateCells } from '../../client/datascience/cellFactory';
2022
import { Commands } from '../../client/datascience/constants';
2123
import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler';
24+
import { NativeEditorProvider } from '../../client/datascience/interactive-ipynb/nativeEditorProvider';
2225
import {
2326
InteractiveWindowCommandListener
2427
} from '../../client/datascience/interactive-window/interactiveWindowCommandListener';
@@ -28,15 +31,15 @@ import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporte
2831
import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter';
2932
import {
3033
IInteractiveWindow,
34+
IJupyterExecution,
3135
INotebook,
36+
INotebookEditorProvider,
3237
INotebookServer
3338
} from '../../client/datascience/types';
3439
import { InterpreterService } from '../../client/interpreter/interpreterService';
3540
import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService';
3641
import { ServiceContainer } from '../../client/ioc/container';
37-
import { noop } from '../core';
3842
import { MockAutoSelectionService } from '../mocks/autoSelector';
39-
import * as vscodeMocks from '../vscode-mock';
4043
import { MockCommandManager } from './mockCommandManager';
4144
import { MockDocumentManager } from './mockDocumentManager';
4245
import { MockStatusProvider } from './mockStatusProvider';
@@ -67,26 +70,16 @@ suite('Interactive window command listener', async () => {
6770
const dataScienceErrorHandler = mock(DataScienceErrorHandler);
6871
const notebookImporter = mock(JupyterImporter);
6972
const notebookExporter = mock(JupyterExporter);
70-
const applicationShell = mock(ApplicationShell);
71-
const jupyterExecution = mock(JupyterExecutionFactory);
73+
let applicationShell: IApplicationShell;
74+
let jupyterExecution: IJupyterExecution;
7275
const interactiveWindow = createTypeMoq<IInteractiveWindow>('Interactive Window');
7376
const documentManager = new MockDocumentManager();
7477
const statusProvider = new MockStatusProvider();
7578
const commandManager = new MockCommandManager();
79+
let notebookEditorProvider: INotebookEditorProvider;
7680
const server = createTypeMoq<INotebookServer>('jupyter server');
7781
let lastFileContents: any;
7882

79-
suiteSetup(() => {
80-
vscodeMocks.initialize();
81-
});
82-
suiteTeardown(() => {
83-
noop();
84-
});
85-
86-
setup(() => {
87-
noop();
88-
});
89-
9083
teardown(() => {
9184
documentManager.activeTextEditor = undefined;
9285
lastFileContents = undefined;
@@ -111,6 +104,10 @@ suite('Interactive window command listener', async () => {
111104
}
112105

113106
function createCommandListener(): InteractiveWindowCommandListener {
107+
notebookEditorProvider = mock(NativeEditorProvider);
108+
jupyterExecution = mock(JupyterExecutionFactory);
109+
applicationShell = mock(ApplicationShell);
110+
114111
// Setup defaults
115112
when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event);
116113
when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter');
@@ -190,9 +187,7 @@ suite('Interactive window command listener', async () => {
190187
}
191188
);
192189

193-
if (jupyterExecution.isNotebookSupported) {
194-
when(jupyterExecution.isNotebookSupported()).thenResolve(true);
195-
}
190+
when(jupyterExecution.isNotebookSupported()).thenResolve(true);
196191

197192
documentManager.addDocument('#%%\r\nprint("code")', 'bar.ipynb');
198193

@@ -211,7 +206,8 @@ suite('Interactive window command listener', async () => {
211206
instance(configService),
212207
statusProvider,
213208
instance(notebookImporter),
214-
instance(dataScienceErrorHandler));
209+
instance(dataScienceErrorHandler),
210+
instance(notebookEditorProvider));
215211
result.register(commandManager);
216212

217213
return result;
@@ -233,9 +229,14 @@ suite('Interactive window command listener', async () => {
233229
const doc = await documentManager.openTextDocument('bar.ipynb');
234230
await documentManager.showTextDocument(doc);
235231
when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo')));
232+
when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo'));
233+
when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(Promise.resolve('moo'));
234+
when(jupyterExecution.isSpawnSupported()).thenResolve(true);
236235

237236
await commandManager.executeCommand(Commands.ExportFileAsNotebook, Uri.file('bar.ipynb'), undefined);
237+
238238
assert.ok(lastFileContents, 'Export file was not written to');
239+
verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once();
239240
});
240241
test('Export File and output', async () => {
241242
createCommandListener();
@@ -250,9 +251,13 @@ suite('Interactive window command listener', async () => {
250251

251252
when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo')));
252253
when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo'));
254+
when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn(Promise.resolve('moo'));
255+
when(jupyterExecution.isSpawnSupported()).thenResolve(true);
253256

254257
await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb'));
258+
255259
assert.ok(lastFileContents, 'Export file was not written to');
260+
verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once();
256261
});
257262
test('Export skipped on no file', async () => {
258263
createCommandListener();
@@ -268,5 +273,4 @@ suite('Interactive window command listener', async () => {
268273
await commandManager.executeCommand(Commands.ExportFileAsNotebook, undefined, undefined);
269274
assert.ok(lastFileContents, 'Export file was not written to');
270275
});
271-
272276
});

src/test/mocks/vsc/telemetryReporter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
'use strict';
55

66
// tslint:disable:all
7-
import * as telemetry from 'vscode-extension-telemetry';
8-
export class vscMockTelemetryReporter implements telemetry.default {
7+
export class vscMockTelemetryReporter {
98
constructor() {
109
//
1110
}

src/test/vscode-mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function initialize() {
3838
return mockedVSCode;
3939
}
4040
if (request === 'vscode-extension-telemetry') {
41-
return { default: vscMockTelemetryReporter };
41+
return { default: vscMockTelemetryReporter as any };
4242
}
4343
// less files need to be in import statements to be converted to css
4444
// But we don't want to try to load them in the mock vscode

0 commit comments

Comments
 (0)