Skip to content

Prompt to open exported notebook in native editor #8315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build/.mocha.unittests.ts.opts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
--require ts-node/register
--require out/test/unittests.js
--require src/test/unittests.ts
--reporter mocha-multi-reporters
--reporter-options configFile=build/.mocha-multi-reporters.config
--ui tdd
--recursive
--colors
./src/test/**/*.unit.test.ts
./src/test/**/*.unit.test.ts
1 change: 1 addition & 0 deletions news/1 Enhancements/8078.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prompt to open exported `Notebook` in the `Notebook Editor`.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
IInteractiveBase,
IInteractiveWindowProvider,
IJupyterExecution,
INotebookEditorProvider,
INotebookExporter,
INotebookImporter,
INotebookServer,
Expand All @@ -45,7 +46,8 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
@inject(IConfigurationService) private configuration: IConfigurationService,
@inject(IStatusProvider) private statusProvider: IStatusProvider,
@inject(INotebookImporter) private jupyterImporter: INotebookImporter,
@inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler
@inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler,
@inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider
) {
}

Expand Down Expand Up @@ -159,7 +161,6 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
saveLabel: localize.DataScience.exportDialogTitle(),
filters: filtersObject
});

await this.waitForStatus(async () => {
if (uri) {
let directoryChange;
Expand All @@ -172,16 +173,19 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
await this.fileSystem.writeFile(uri.fsPath, JSON.stringify(notebook));
}
}, localize.DataScience.exportingFormat(), file);

// When all done, show a notice that it completed.
const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
if (uri && uri.fsPath) {
this.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => {
if (str === openQuestion) {
// If the user wants to, open the notebook they just generated.
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
}
});
const openQuestion1 = localize.DataScience.exportOpenQuestion1();
const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])];
const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), ...questions);
if (selection === openQuestion1) {
await this.ipynbProvider.open(uri, await this.fileSystem.readFile(uri.fsPath));
}
if (selection === openQuestion2) {
// If the user wants to, open the notebook they just generated.
this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors();
}
}
}
}
Expand Down Expand Up @@ -220,13 +224,17 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList
});

// When all done, show a notice that it completed.
const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
this.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => {
if (str === openQuestion && output) {
// If the user wants to, open the notebook they just generated.
this.jupyterExecution.spawnNotebook(output).ignoreErrors();
}
});
const openQuestion1 = localize.DataScience.exportOpenQuestion1();
const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined;
const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])];
const selection = await this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), ...questions);
if (selection === openQuestion1) {
await this.ipynbProvider.open(Uri.file(output), await this.fileSystem.readFile(output));
}
if (selection === openQuestion2) {
// If the user wants to, open the notebook they just generated.
this.jupyterExecution.spawnNotebook(output).ignoreErrors();
}

return Uri.file(output);
}
Expand Down
46 changes: 25 additions & 21 deletions src/test/datascience/interactiveWindowCommandListener.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
'use strict';
import { nbformat } from '@jupyterlab/coreutils/lib/nbformat';
import { assert } from 'chai';
import { anything, instance, mock, when } from 'ts-mockito';
import { anything, instance, mock, verify, when } from 'ts-mockito';
import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher';
import * as TypeMoq from 'typemoq';
import * as uuid from 'uuid/v4';
import { EventEmitter, Uri } from 'vscode';

import { ApplicationShell } from '../../client/common/application/applicationShell';
import { IApplicationShell } from '../../client/common/application/types';
import { PythonSettings } from '../../client/common/configSettings';
import { ConfigurationService } from '../../client/common/configuration/service';
import { Logger } from '../../client/common/logger';
import { FileSystem } from '../../client/common/platform/fileSystem';
import { IFileSystem } from '../../client/common/platform/types';
import { IConfigurationService, IDisposable, ILogger } from '../../client/common/types';
import * as localize from '../../client/common/utils/localize';
import { generateCells } from '../../client/datascience/cellFactory';
import { Commands } from '../../client/datascience/constants';
import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler';
import { NativeEditorProvider } from '../../client/datascience/interactive-ipynb/nativeEditorProvider';
import {
InteractiveWindowCommandListener
} from '../../client/datascience/interactive-window/interactiveWindowCommandListener';
Expand All @@ -28,15 +31,15 @@ import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporte
import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter';
import {
IInteractiveWindow,
IJupyterExecution,
INotebook,
INotebookEditorProvider,
INotebookServer
} from '../../client/datascience/types';
import { InterpreterService } from '../../client/interpreter/interpreterService';
import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService';
import { ServiceContainer } from '../../client/ioc/container';
import { noop } from '../core';
import { MockAutoSelectionService } from '../mocks/autoSelector';
import * as vscodeMocks from '../vscode-mock';
import { MockCommandManager } from './mockCommandManager';
import { MockDocumentManager } from './mockDocumentManager';
import { MockStatusProvider } from './mockStatusProvider';
Expand Down Expand Up @@ -67,26 +70,16 @@ suite('Interactive window command listener', async () => {
const dataScienceErrorHandler = mock(DataScienceErrorHandler);
const notebookImporter = mock(JupyterImporter);
const notebookExporter = mock(JupyterExporter);
const applicationShell = mock(ApplicationShell);
const jupyterExecution = mock(JupyterExecutionFactory);
let applicationShell: IApplicationShell;
let jupyterExecution: IJupyterExecution;
const interactiveWindow = createTypeMoq<IInteractiveWindow>('Interactive Window');
const documentManager = new MockDocumentManager();
const statusProvider = new MockStatusProvider();
const commandManager = new MockCommandManager();
let notebookEditorProvider: INotebookEditorProvider;
const server = createTypeMoq<INotebookServer>('jupyter server');
let lastFileContents: any;

suiteSetup(() => {
vscodeMocks.initialize();
});
suiteTeardown(() => {
noop();
});

setup(() => {
noop();
});

teardown(() => {
documentManager.activeTextEditor = undefined;
lastFileContents = undefined;
Expand All @@ -111,6 +104,10 @@ suite('Interactive window command listener', async () => {
}

function createCommandListener(): InteractiveWindowCommandListener {
notebookEditorProvider = mock(NativeEditorProvider);
jupyterExecution = mock(JupyterExecutionFactory);
applicationShell = mock(ApplicationShell);

// Setup defaults
when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event);
when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter');
Expand Down Expand Up @@ -190,9 +187,7 @@ suite('Interactive window command listener', async () => {
}
);

if (jupyterExecution.isNotebookSupported) {
when(jupyterExecution.isNotebookSupported()).thenResolve(true);
}
when(jupyterExecution.isNotebookSupported()).thenResolve(true);

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

Expand All @@ -211,7 +206,8 @@ suite('Interactive window command listener', async () => {
instance(configService),
statusProvider,
instance(notebookImporter),
instance(dataScienceErrorHandler));
instance(dataScienceErrorHandler),
instance(notebookEditorProvider));
result.register(commandManager);

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

await commandManager.executeCommand(Commands.ExportFileAsNotebook, Uri.file('bar.ipynb'), undefined);

assert.ok(lastFileContents, 'Export file was not written to');
verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once();
});
test('Export File and output', async () => {
createCommandListener();
Expand All @@ -250,9 +251,13 @@ suite('Interactive window command listener', async () => {

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

await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb'));

assert.ok(lastFileContents, 'Export file was not written to');
verify(applicationShell.showInformationMessage(anything(), localize.DataScience.exportOpenQuestion1(), localize.DataScience.exportOpenQuestion())).once();
});
test('Export skipped on no file', async () => {
createCommandListener();
Expand All @@ -268,5 +273,4 @@ suite('Interactive window command listener', async () => {
await commandManager.executeCommand(Commands.ExportFileAsNotebook, undefined, undefined);
assert.ok(lastFileContents, 'Export file was not written to');
});

});
3 changes: 1 addition & 2 deletions src/test/mocks/vsc/telemetryReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
'use strict';

// tslint:disable:all
import * as telemetry from 'vscode-extension-telemetry';
export class vscMockTelemetryReporter implements telemetry.default {
export class vscMockTelemetryReporter {
constructor() {
//
}
Expand Down
2 changes: 1 addition & 1 deletion src/test/vscode-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function initialize() {
return mockedVSCode;
}
if (request === 'vscode-extension-telemetry') {
return { default: vscMockTelemetryReporter };
return { default: vscMockTelemetryReporter as any };
}
// less files need to be in import statements to be converted to css
// But we don't want to try to load them in the mock vscode
Expand Down