diff --git a/package.json b/package.json index ad67f0b52dd..07030613529 100644 --- a/package.json +++ b/package.json @@ -346,25 +346,25 @@ "command": "jupyter.export", "title": "%DataScience.notebookExportAs%", "category": "Jupyter", - "enablement": "isWorkspaceTrusted && !jupyter.webExtension" + "enablement": "isWorkspaceTrusted && !jupyter.webExtension || isWorkspaceTrusted && jupyter.webExtension && notebookKernel =~ /^ms-toolsai.jupyter\\//" }, { "command": "jupyter.exportAsPythonScript", "title": "%jupyter.command.jupyter.exportAsPythonScript.title%", "category": "Jupyter", - "enablement": "isWorkspaceTrusted && !jupyter.webExtension" + "enablement": "isWorkspaceTrusted && !jupyter.webExtension || isWorkspaceTrusted && jupyter.webExtension && notebookKernel =~ /^ms-toolsai.jupyter\\//" }, { "command": "jupyter.exportToHTML", "title": "%jupyter.command.jupyter.exportToHTML.title%", "category": "Jupyter", - "enablement": "isWorkspaceTrusted && !jupyter.webExtension" + "enablement": "isWorkspaceTrusted && !jupyter.webExtension || isWorkspaceTrusted && jupyter.webExtension && notebookKernel =~ /^ms-toolsai.jupyter\\//" }, { "command": "jupyter.exportToPDF", "title": "%jupyter.command.jupyter.exportToPDF.title%", "category": "Jupyter", - "enablement": "isWorkspaceTrusted && !jupyter.webExtension" + "enablement": "isWorkspaceTrusted && !jupyter.webExtension || isWorkspaceTrusted && jupyter.webExtension && notebookKernel =~ /^ms-toolsai.jupyter\\//" }, { "command": "jupyter.selectJupyterInterpreter", diff --git a/src/kernels/common/baseJupyterSession.ts b/src/kernels/common/baseJupyterSession.ts index c37aa45b1ef..881c210ec71 100644 --- a/src/kernels/common/baseJupyterSession.ts +++ b/src/kernels/common/baseJupyterSession.ts @@ -20,7 +20,13 @@ import { JupyterInvalidKernelError } from '../../platform/errors/jupyterInvalidK import { JupyterWaitForIdleError } from '../../platform/errors/jupyterWaitForIdleError'; import { KernelInterruptTimeoutError } from '../../platform/errors/kernelInterruptTimeoutError'; import { SessionDisposedError } from '../../platform/errors/sessionDisposedError'; -import { IJupyterSession, ISessionWithSocket, KernelConnectionMetadata, KernelSocketInformation } from '../types'; +import { + IJupyterServerSession, + IJupyterSession, + ISessionWithSocket, + KernelConnectionMetadata, + KernelSocketInformation +} from '../types'; import { ChainingExecuteRequester } from './chainingExecuteRequester'; import { getResourceType } from '../../platform/common/utils'; import { KernelProgressReporter } from '../../platform/progress/kernelProgressReporter'; @@ -127,6 +133,9 @@ export abstract class BaseJupyterSession implements IJupyterSession { traceInfo(`Unhandled message found: ${m.header.msg_type}`); }; } + public isServerSession(): this is IJupyterServerSession { + return false; + } public async dispose(): Promise { await this.shutdownImplementation(false); } diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts index 7e7c36fd3b0..13d204e016f 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts @@ -17,7 +17,7 @@ import { JupyterInstallError } from '../../../platform/errors/jupyterInstallErro import { ProductNames } from '../../installer/productNames'; import { Product, IInstaller, InstallerResponse } from '../../installer/types'; import { HelpLinks } from '../../../platform/common/constants'; -import { reportAction } from '../../../platform/progress/decorator.node'; +import { reportAction } from '../../../platform/progress/decorator'; import { ReportableAction } from '../../../platform/progress/types'; import { JupyterInterpreterDependencyResponse } from '../types'; import { IJupyterCommandFactory } from '../types.node'; diff --git a/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts b/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts index 04b86446c2a..9f41ea62578 100644 --- a/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts +++ b/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts @@ -7,7 +7,7 @@ import { inject, injectable } from 'inversify'; import { CancellationToken, Uri } from 'vscode'; import { traceError } from '../../../platform/logging'; import { IPythonExecutionFactory, IPythonDaemonExecutionService } from '../../../platform/common/process/types.node'; -import { reportAction } from '../../../platform/progress/decorator.node'; +import { reportAction } from '../../../platform/progress/decorator'; import { ReportableAction } from '../../../platform/progress/types'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { JupyterDaemonModule } from '../../../webviews/webview-side/common/constants'; diff --git a/src/kernels/jupyter/session/jupyterSession.ts b/src/kernels/jupyter/session/jupyterSession.ts index 4d98599ddce..375a85277d9 100644 --- a/src/kernels/jupyter/session/jupyterSession.ts +++ b/src/kernels/jupyter/session/jupyterSession.ts @@ -1,7 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import type { ContentsManager, Kernel, KernelSpecManager, Session, SessionManager } from '@jupyterlab/services'; +import type { + Contents, + ContentsManager, + Kernel, + KernelSpecManager, + Session, + SessionManager +} from '@jupyterlab/services'; import * as uuid from 'uuid/v4'; import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; import { Cancellation } from '../../../platform/common/cancellation'; @@ -21,15 +28,18 @@ import { isLocalConnection, IJupyterConnection, ISessionWithSocket, - KernelActionSource + KernelActionSource, + IJupyterServerSession } from '../../types'; import { DisplayOptions } from '../../displayOptions'; -import { IJupyterBackingFileCreator, IJupyterKernelService, IJupyterRequestCreator } from '../types'; +import { IBackupFile, IJupyterBackingFileCreator, IJupyterKernelService, IJupyterRequestCreator } from '../types'; import { Uri } from 'vscode'; import { generateBackingIPyNbFileName } from './backingFileCreator.base'; // function is -export class JupyterSession extends BaseJupyterSession { +export class JupyterSession extends BaseJupyterSession implements IJupyterServerSession { + public override readonly kind: 'remoteJupyter' | 'localJupyter'; + constructor( resource: Resource, private connInfo: IJupyterConnection, @@ -53,6 +63,12 @@ export class JupyterSession extends BaseJupyterSession { workingDirectory, interruptTimeout ); + + this.kind = connInfo.localLaunch ? 'localJupyter' : 'remoteJupyter'; + } + + public override isServerSession(): this is IJupyterServerSession { + return true; } @captureTelemetry(Telemetry.WaitForIdleJupyter, undefined, true) @@ -190,6 +206,53 @@ export class JupyterSession extends BaseJupyterSession { return promise; } + async invokeWithFileSynced(contents: string, handler: (file: IBackupFile) => Promise): Promise { + if (!this.resource) { + return; + } + + const backingFile = await this.backingFileCreator.createBackingFile( + this.resource, + this.workingDirectory, + this.kernelConnectionMetadata, + this.connInfo, + this.contentsManager + ); + + if (!backingFile) { + return; + } + + await this.contentsManager + .save(backingFile!.filePath, { + content: JSON.parse(contents), + type: 'notebook' + }) + .ignoreErrors(); + + await handler({ + filePath: backingFile.filePath, + dispose: backingFile.dispose.bind(backingFile) + }); + + await backingFile.dispose(); + await this.contentsManager.delete(backingFile.filePath).ignoreErrors(); + } + + async createTempfile(ext: string): Promise { + const tempFile = await this.contentsManager.newUntitled({ type: 'file', ext }); + return tempFile.path; + } + + async deleteTempfile(file: string): Promise { + await this.contentsManager.delete(file); + } + + async getContents(file: string, format: Contents.FileFormat): Promise { + const data = await this.contentsManager.get(file, { type: 'file', format: format, content: true }); + return data; + } + private async createSession(options: { token: CancellationToken; ui: IDisplayOptions; @@ -283,7 +346,7 @@ export class JupyterSession extends BaseJupyterSession { throw new JupyterSessionStartError(new Error(`No kernel created`)); }) .catch((ex) => Promise.reject(new JupyterSessionStartError(ex))) - .finally(() => { + .finally(async () => { if (this.connInfo && backingFile) { this.contentsManager.delete(backingFile.filePath).ignoreErrors(); } diff --git a/src/kernels/jupyter/types.ts b/src/kernels/jupyter/types.ts index 6f0f40236f0..4f51f7959e8 100644 --- a/src/kernels/jupyter/types.ts +++ b/src/kernels/jupyter/types.ts @@ -242,6 +242,11 @@ export interface IJupyterServerUriStorage { setUriToRemote(uri: string, displayName: string): Promise; } +export interface IBackupFile { + dispose: () => Promise; + filePath: string; +} + export const IJupyterBackingFileCreator = Symbol('IJupyterBackingFileCreator'); export interface IJupyterBackingFileCreator { createBackingFile( @@ -250,7 +255,7 @@ export interface IJupyterBackingFileCreator { kernel: KernelConnectionMetadata, connInfo: IJupyterConnection, contentsManager: ContentsManager - ): Promise<{ dispose: () => Promise; filePath: string } | undefined>; + ): Promise; } export const IJupyterKernelService = Symbol('IJupyterKernelService'); diff --git a/src/kernels/types.ts b/src/kernels/types.ts index ab738a7f26c..7cd37c4cd2f 100644 --- a/src/kernels/types.ts +++ b/src/kernels/types.ts @@ -3,7 +3,7 @@ 'use strict'; -import type { Kernel, KernelMessage, Session } from '@jupyterlab/services'; +import type { Contents, Kernel, KernelMessage, Session } from '@jupyterlab/services'; import type { Observable } from 'rxjs/Observable'; import type { JSONObject } from '@lumino/coreutils'; import type { @@ -19,7 +19,7 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { PythonEnvironment } from '../platform/pythonEnvironments/info'; import { IAsyncDisposable, IDisplayOptions, Resource } from '../platform/common/types'; import { WebSocketData } from '../platform/api/extension'; -import { IJupyterKernel } from './jupyter/types'; +import { IBackupFile, IJupyterKernel } from './jupyter/types'; import { PythonEnvironment_PythonApi } from '../platform/api/types'; export type LiveKernelModel = IJupyterKernel & @@ -262,6 +262,7 @@ export interface IJupyterSession extends IAsyncDisposable { readonly status: KernelMessage.Status; readonly kernelId: string; readonly kernelSocket: Observable; + isServerSession(): this is IJupyterServerSession; onSessionStatusChanged: Event; onDidDispose: Event; onIOPubMessage: Event; @@ -293,6 +294,14 @@ export interface IJupyterSession extends IAsyncDisposable { shutdown(): Promise; } +export interface IJupyterServerSession extends IJupyterSession { + readonly kind: 'remoteJupyter' | 'localJupyter'; + invokeWithFileSynced(contents: string, handler: (file: IBackupFile) => Promise): Promise; + createTempfile(ext: string): Promise; + deleteTempfile(file: string): Promise; + getContents(file: string, format: Contents.FileFormat): Promise; +} + export type ISessionWithSocket = Session.ISessionConnection & { /** * The resource associated with this session. diff --git a/src/platform/export/export.index.node.ts b/src/platform/export/export.index.node.ts deleted file mode 100644 index f075f8fca16..00000000000 --- a/src/platform/export/export.index.node.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export * from './exportBase.node'; -export * from './exportDialog'; -export * from './exportFileOpener'; -export * from './exportInterpreterFinder.node'; -export * from './exportToHTML.node'; -export * from './exportToPDF.node'; -export * from './exportToPython.node'; -export * from './exportToPythonPlain.node'; -export * from './exportUtil.node'; -export * from './fileConverter.node'; diff --git a/src/platform/export/exportBase.node.ts b/src/platform/export/exportBase.node.ts index 7d7ff89d1d7..ff76cc531e8 100644 --- a/src/platform/export/exportBase.node.ts +++ b/src/platform/export/exportBase.node.ts @@ -1,46 +1,77 @@ import { inject, injectable } from 'inversify'; import * as path from '../../platform/vscode-path/path'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, NotebookDocument, Uri } from 'vscode'; import { INotebookImporter } from '../../kernels/jupyter/types'; import { IJupyterSubCommandExecutionService } from '../../kernels/jupyter/types.node'; import { IFileSystemNode } from '../common/platform/types.node'; import { IPythonExecutionFactory, IPythonExecutionService } from '../common/process/types.node'; -import { reportAction } from '../progress/decorator.node'; +import { reportAction } from '../progress/decorator'; import { ReportableAction } from '../progress/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ExportFormat, INbConvertExport } from './types'; +import { ExportFormat, IExportBase, IExportDialog, INbConvertExport } from './types'; +import { ExportUtil } from './exportUtil.node'; +import { TemporaryDirectory } from '../common/platform/types'; +import { ExportInterpreterFinder } from './exportInterpreterFinder.node'; @injectable() -export class ExportBase implements INbConvertExport { +export class ExportBase implements INbConvertExport, IExportBase { constructor( @inject(IPythonExecutionFactory) protected readonly pythonExecutionFactory: IPythonExecutionFactory, @inject(IJupyterSubCommandExecutionService) protected jupyterService: IJupyterSubCommandExecutionService, @inject(IFileSystemNode) protected readonly fs: IFileSystemNode, - @inject(INotebookImporter) protected readonly importer: INotebookImporter + @inject(IExportDialog) protected readonly filePicker: IExportDialog, + @inject(ExportUtil) protected readonly exportUtil: ExportUtil, + @inject(INotebookImporter) protected readonly importer: INotebookImporter, + @inject(ExportInterpreterFinder) private exportInterpreterFinder: ExportInterpreterFinder ) {} public async export( - _source: Uri, + _sourceDocument: NotebookDocument, _target: Uri, _interpreter: PythonEnvironment, _token: CancellationToken - // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function - ): Promise {} + ): Promise { + return; + } @reportAction(ReportableAction.PerformingExport) public async executeCommand( - source: Uri, + sourceDocument: NotebookDocument, target: Uri, format: ExportFormat, - interpreter: PythonEnvironment, + interpreter: PythonEnvironment | undefined, token: CancellationToken ): Promise { if (token.isCancellationRequested) { return; } + interpreter = await this.exportInterpreterFinder.getExportInterpreter(interpreter); + + if (format === ExportFormat.python) { + const contents = await this.importer.importFromFile(sourceDocument.uri, interpreter); + await this.fs.writeFile(target, contents); + return; + } + + let contents = await this.exportUtil.getContent(sourceDocument); + + if (format === ExportFormat.pdf) { + // When exporting to PDF we need to remove any SVG output. This is due to an error + // with nbconvert and a dependency of its called InkScape. + contents = await this.exportUtil.removeSvgs(contents); + } + + /* Need to make a temp directory here, instead of just a temp file. This is because + we need to store the contents of the notebook in a file that is named the same + as what we want the title of the exported file to be. To ensure this file path will be unique + we store it in a temp directory. The name of the file matters because when + exporting to certain formats the filename is used within the exported document as the title. */ + const tempDir = await this.exportUtil.generateTempDir(); + const source = await this.makeSourceFile(target, contents, tempDir); + const service = await this.getExecutionService(source, interpreter); if (!service) { return; @@ -82,6 +113,15 @@ export class ExportBase implements INbConvertExport { } finally { tempTarget.dispose(); } + + return; + } + + private async makeSourceFile(target: Uri, contents: string, tempDir: TemporaryDirectory): Promise { + // Creates a temporary file with the same base name as the target file + const fileName = path.basename(target.fsPath, path.extname(target.fsPath)); + const sourceFilePath = await this.exportUtil.makeFileInDirectory(contents, `${fileName}.ipynb`, tempDir.path); + return Uri.file(sourceFilePath); } protected async getExecutionService( diff --git a/src/platform/export/exportBase.web.ts b/src/platform/export/exportBase.web.ts new file mode 100644 index 00000000000..edd4fef3c43 --- /dev/null +++ b/src/platform/export/exportBase.web.ts @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as nbformat from '@jupyterlab/nbformat'; +import { inject, injectable } from 'inversify'; +import { Uri, CancellationToken, NotebookDocument } from 'vscode'; +import * as path from '../../platform/vscode-path/path'; +import { DisplayOptions } from '../../kernels/displayOptions'; +import { executeSilently } from '../../kernels/helpers'; +import { IKernel, IKernelProvider } from '../../kernels/types'; +import { concatMultilineString } from '../../webviews/webview-side/common'; +import { IFileSystem } from '../common/platform/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ExportUtilBase } from './exportUtil'; +import { ExportFormat, IExportBase, IExportDialog, INbConvertExport } from './types'; +import { traceLog } from '../logging'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; + +@injectable() +export class ExportBase implements INbConvertExport, IExportBase { + constructor( + @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IExportDialog) protected readonly filePicker: IExportDialog, + @inject(ExportUtilBase) protected readonly exportUtil: ExportUtilBase + ) {} + + public async export( + _sourceDocument: NotebookDocument, + _target: Uri, + _interpreter: PythonEnvironment, + _token: CancellationToken + ): Promise { + return undefined; + } + + @reportAction(ReportableAction.PerformingExport) + async executeCommand( + sourceDocument: NotebookDocument, + target: Uri, + format: ExportFormat, + _interpreter: PythonEnvironment, + _token: CancellationToken + ): Promise { + const kernel = this.kernelProvider.get(sourceDocument.uri); + if (!kernel) { + // trace error + return; + } + + if (!kernel.session) { + await kernel.start(new DisplayOptions(false)); + } + + if (!kernel.session) { + return; + } + + if (kernel.session!.isServerSession()) { + const session = kernel.session!; + let contents = await this.exportUtil.getContent(sourceDocument); + + let fileExt = ''; + + switch (format) { + case ExportFormat.html: + fileExt = '.html'; + break; + case ExportFormat.pdf: + fileExt = '.pdf'; + break; + case ExportFormat.python: + fileExt = '.py'; + break; + } + + await kernel.session!.invokeWithFileSynced(contents, async (file) => { + const pwd = await this.getCWD(kernel); + const filePath = `${pwd}/${file.filePath}`; + const tempTarget = await session.createTempfile(fileExt); + const outputs = await executeSilently( + session, + `!jupyter nbconvert ${filePath} --to ${format} --output ${path.basename(tempTarget)}` + ); + + const text = this.parseStreamOutput(outputs); + if (this.isExportFailed(text)) { + throw new Error(text || `Failed to export to ${format}`); + } else if (text) { + // trace the output in case we didn't identify all errors + traceLog(text); + } + + if (format === ExportFormat.pdf) { + const content = await session.getContents(tempTarget, 'base64'); + const bytes = this.b64toBlob(content.content, 'application/pdf'); + const buffer = await bytes.arrayBuffer(); + await this.fs.writeFile(target!, Buffer.from(buffer)); + await session.deleteTempfile(tempTarget); + } else { + const content = await session.getContents(tempTarget, 'text'); + await this.fs.writeFile(target!, content.content as string); + await session.deleteTempfile(tempTarget); + } + }); + + return; + } else { + // no op + } + } + + private b64toBlob(b64Data: string, contentType: string | undefined) { + contentType = contentType || ''; + const sliceSize = 512; + b64Data = b64Data.replace(/^[^,]+,/, ''); + b64Data = b64Data.replace(/\s/g, ''); + const byteCharacters = atob(b64Data); + let byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + let byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; + } + + private isExportFailed(message: string | undefined) { + if (!message) { + return true; + } + + return /Traceback \(most recent call last\)/g.exec(message); + } + + private parseStreamOutput(outputs: nbformat.IOutput[]): string | undefined { + if (outputs.length === 0) { + return; + } + + const output: nbformat.IStream = outputs[0] as unknown as nbformat.IStream; + if (output.name !== 'stdout' && output.output_type !== 'stream') { + return; + } + + const text = concatMultilineString(output.text).trim(); + return text; + } + + private async getCWD(kernel: IKernel) { + const outputs = await executeSilently(kernel.session!, `import os;os.getcwd();`); + if (outputs.length === 0) { + return; + } + + const output: nbformat.IExecuteResult = outputs[0] as unknown as nbformat.IExecuteResult; + if (output.output_type !== 'execute_result') { + return undefined; + } + + return output.data['text/plain']; + } +} diff --git a/src/platform/export/exportToHTML.node.ts b/src/platform/export/exportToHTML.node.ts deleted file mode 100644 index a2a922fe212..00000000000 --- a/src/platform/export/exportToHTML.node.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { injectable } from 'inversify'; -import { CancellationToken, Uri } from 'vscode'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ExportBase } from './exportBase.node'; -import { ExportFormat } from './types'; - -@injectable() -export class ExportToHTML extends ExportBase { - public override async export( - source: Uri, - target: Uri, - interpreter: PythonEnvironment, - token: CancellationToken - ): Promise { - await this.executeCommand(source, target, ExportFormat.html, interpreter, token); - } -} diff --git a/src/platform/export/exportToHTML.ts b/src/platform/export/exportToHTML.ts new file mode 100644 index 00000000000..9514c326b77 --- /dev/null +++ b/src/platform/export/exportToHTML.ts @@ -0,0 +1,17 @@ +import { inject, injectable } from 'inversify'; +import { CancellationToken, NotebookDocument, Uri } from 'vscode'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ExportFormat, IExportBase, INbConvertExport } from './types'; + +@injectable() +export class ExportToHTML implements INbConvertExport { + constructor(@inject(IExportBase) protected readonly exportBase: IExportBase) {} + public async export( + sourceDocument: NotebookDocument, + target: Uri, + interpreter: PythonEnvironment, + token: CancellationToken + ): Promise { + await this.exportBase.executeCommand(sourceDocument, target, ExportFormat.html, interpreter, token); + } +} diff --git a/src/platform/export/exportToPDF.node.ts b/src/platform/export/exportToPDF.node.ts deleted file mode 100644 index 8d8ca858db7..00000000000 --- a/src/platform/export/exportToPDF.node.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { injectable } from 'inversify'; -import { CancellationToken, Uri } from 'vscode'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ExportBase } from './exportBase.node'; -import { ExportFormat } from './types'; - -@injectable() -export class ExportToPDF extends ExportBase { - public override async export( - source: Uri, - target: Uri, - interpreter: PythonEnvironment, - token: CancellationToken - ): Promise { - await this.executeCommand(source, target, ExportFormat.pdf, interpreter, token); - } -} diff --git a/src/platform/export/exportToPDF.ts b/src/platform/export/exportToPDF.ts new file mode 100644 index 00000000000..97c6464921a --- /dev/null +++ b/src/platform/export/exportToPDF.ts @@ -0,0 +1,18 @@ +import { inject, injectable } from 'inversify'; +import { CancellationToken, NotebookDocument, Uri } from 'vscode'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ExportFormat, IExportBase, INbConvertExport } from './types'; + +@injectable() +export class ExportToPDF implements INbConvertExport { + constructor(@inject(IExportBase) protected readonly exportBase: IExportBase) {} + + public async export( + sourceDocument: NotebookDocument, + target: Uri, + interpreter: PythonEnvironment, + token: CancellationToken + ): Promise { + await this.exportBase.executeCommand(sourceDocument, target, ExportFormat.pdf, interpreter, token); + } +} diff --git a/src/platform/export/exportToPython.node.ts b/src/platform/export/exportToPython.node.ts deleted file mode 100644 index 0ed12f6645e..00000000000 --- a/src/platform/export/exportToPython.node.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { injectable } from 'inversify'; -import { CancellationToken, Uri } from 'vscode'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ExportBase } from './exportBase.node'; - -@injectable() -export class ExportToPython extends ExportBase { - public override async export( - source: Uri, - target: Uri, - interpreter: PythonEnvironment, - token: CancellationToken - ): Promise { - if (token.isCancellationRequested) { - return; - } - const contents = await this.importer.importFromFile(source, interpreter); - await this.fs.writeFile(target, contents); - } -} diff --git a/src/platform/export/exportToPython.ts b/src/platform/export/exportToPython.ts new file mode 100644 index 00000000000..fdd57c4d2cf --- /dev/null +++ b/src/platform/export/exportToPython.ts @@ -0,0 +1,18 @@ +import { inject, injectable } from 'inversify'; +import { CancellationToken, NotebookDocument, Uri } from 'vscode'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ExportFormat, IExportBase, INbConvertExport } from './types'; + +@injectable() +export class ExportToPython implements INbConvertExport { + constructor(@inject(IExportBase) protected readonly exportBase: IExportBase) {} + + public async export( + sourceDocument: NotebookDocument, + target: Uri, + interpreter: PythonEnvironment, + token: CancellationToken + ): Promise { + await this.exportBase.executeCommand(sourceDocument, target, ExportFormat.python, interpreter, token); + } +} diff --git a/src/platform/export/exportToPythonPlain.node.ts b/src/platform/export/exportToPythonPlain.node.ts deleted file mode 100644 index 1f26e95f00e..00000000000 --- a/src/platform/export/exportToPythonPlain.node.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { ExportToPythonPlainBase } from './exportToPythonPlain'; - -// Handles exporting a NotebookDocument to python -@injectable() -export class ExportToPythonPlain extends ExportToPythonPlainBase { - public constructor( - @inject(IFileSystem) fs: IFileSystem, - @inject(IConfigurationService) configuration: IConfigurationService - ) { - super(fs, configuration); - } - - override getEOL(): string { - return os.EOL; - } -} diff --git a/src/platform/export/exportToPythonPlain.ts b/src/platform/export/exportToPythonPlain.ts index d8f4f61dca8..507649c87e4 100644 --- a/src/platform/export/exportToPythonPlain.ts +++ b/src/platform/export/exportToPythonPlain.ts @@ -3,18 +3,20 @@ 'use strict'; +import { inject, injectable } from 'inversify'; import { CancellationToken, NotebookCell, NotebookCellKind, NotebookDocument, Uri } from 'vscode'; import { appendLineFeed } from '../../webviews/webview-side/common'; -import { IFileSystem } from '../common/platform/types'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; import { IConfigurationService } from '../common/types'; import { IExport } from './types'; // Handles exporting a NotebookDocument to python -export class ExportToPythonPlainBase implements IExport { +@injectable() +export class ExportToPythonPlain implements IExport { public constructor( - private readonly fs: IFileSystem, - - protected readonly configuration: IConfigurationService + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IConfigurationService) private readonly configuration: IConfigurationService, + @inject(IPlatformService) private platform: IPlatformService ) {} async writeFile(target: Uri, contents: string): Promise { @@ -22,7 +24,7 @@ export class ExportToPythonPlainBase implements IExport { } getEOL(): string { - return '\n'; + return this.platform.isWindows ? '\r\n' : '\n'; } // Export the given document to the target source file diff --git a/src/platform/export/exportToPythonPlain.web.ts b/src/platform/export/exportToPythonPlain.web.ts deleted file mode 100644 index 8ab1b92251a..00000000000 --- a/src/platform/export/exportToPythonPlain.web.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { ExportToPythonPlainBase } from './exportToPythonPlain'; - -// Handles exporting a NotebookDocument to python -@injectable() -export class ExportToPythonPlain extends ExportToPythonPlainBase { - public constructor( - @inject(IFileSystem) fs: IFileSystem, - @inject(IConfigurationService) configuration: IConfigurationService - ) { - super(fs, configuration); - } -} diff --git a/src/platform/export/exportUtil.node.ts b/src/platform/export/exportUtil.node.ts index d86ac131a9d..365ed5dada2 100644 --- a/src/platform/export/exportUtil.node.ts +++ b/src/platform/export/exportUtil.node.ts @@ -3,14 +3,23 @@ import { inject, injectable } from 'inversify'; import * as os from 'os'; import * as path from '../../platform/vscode-path/path'; import * as uuid from 'uuid/v4'; -import { Uri } from 'vscode'; import { TemporaryDirectory } from '../common/platform/types'; import { IFileSystemNode } from '../common/platform/types.node'; import { sleep } from '../common/utils/async'; +import { ExportUtilBase } from './exportUtil'; +import { IExtensions } from '../common/types'; +import { ExportFormat, IExportDialog } from './types'; +import { Uri } from 'vscode'; @injectable() -export class ExportUtil { - constructor(@inject(IFileSystemNode) private fs: IFileSystemNode) {} +export class ExportUtil extends ExportUtilBase { + constructor( + @inject(IFileSystemNode) private fs: IFileSystemNode, + @inject(IExtensions) extensions: IExtensions, + @inject(IExportDialog) filePicker: IExportDialog + ) { + super(extensions, filePicker); + } public async generateTempDir(): Promise { const resultDir = path.join(os.tmpdir(), uuid()); @@ -36,6 +45,22 @@ export class ExportUtil { }; } + override async getTargetFile( + format: ExportFormat, + source: Uri, + defaultFileName?: string | undefined + ): Promise { + let target; + + if (format !== ExportFormat.python) { + target = await this.filePicker.showDialog(format, source, defaultFileName); + } else { + target = Uri.file((await this.fs.createTemporaryLocalFile('.py')).filePath); + } + + return target; + } + public async makeFileInDirectory(contents: string, fileName: string, dirPath: string): Promise { const newFilePath = path.join(dirPath, fileName); @@ -44,8 +69,7 @@ export class ExportUtil { return newFilePath; } - public async removeSvgs(source: Uri) { - const model = await this.fs.readFile(source); + public async removeSvgs(model: string) { const content = JSON.parse(model) as nbformat.INotebookContent; for (const cell of content.cells) { const outputs = 'outputs' in cell ? (cell.outputs as nbformat.IOutput[]) : undefined; @@ -53,7 +77,7 @@ export class ExportUtil { this.removeSvgFromOutputs(outputs); } } - await this.fs.writeFile(source, JSON.stringify(content, undefined, 4)); + return JSON.stringify(content, undefined, 4); } private removeSvgFromOutputs(outputs: nbformat.IOutput[]) { diff --git a/src/platform/export/exportUtil.ts b/src/platform/export/exportUtil.ts new file mode 100644 index 00000000000..dca3d2757b3 --- /dev/null +++ b/src/platform/export/exportUtil.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from 'inversify'; +import { NotebookCellData, NotebookData, NotebookDocument, Uri } from 'vscode'; +import { IExtensions } from '../common/types'; +import { ExportFormat, IExportDialog } from './types'; + +@injectable() +export class ExportUtilBase { + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IExportDialog) protected readonly filePicker: IExportDialog + ) {} + + async getContent(document: NotebookDocument): Promise { + const serializerApi = this.extensions.getExtension<{ exportNotebook: (notebook: NotebookData) => string }>( + 'vscode.ipynb' + ); + if (!serializerApi) { + throw new Error( + 'Unable to export notebook as the built-in vscode.ipynb extension is currently unavailable.' + ); + } + // Via the interactive window export this might not be activated + if (!serializerApi.isActive) { + await serializerApi.activate(); + } + + const cells = document.getCells(); + const cellData = cells.map((c) => { + const data = new NotebookCellData(c.kind, c.document.getText(), c.document.languageId); + data.metadata = c.metadata; + data.mime = c.mime; + data.outputs = [...c.outputs]; + return data; + }); + const notebookData = new NotebookData(cellData); + notebookData.metadata = document.metadata; + return serializerApi.exports.exportNotebook(notebookData); + } + + async getTargetFile( + format: ExportFormat, + source: Uri, + defaultFileName?: string | undefined + ): Promise { + let target = await this.filePicker.showDialog(format, source, defaultFileName); + + return target; + } +} diff --git a/src/platform/export/fileConverter.node.ts b/src/platform/export/fileConverter.node.ts index 05dccb722c8..fa8b680eb2b 100644 --- a/src/platform/export/fileConverter.node.ts +++ b/src/platform/export/fileConverter.node.ts @@ -1,104 +1,45 @@ import { inject, injectable, named } from 'inversify'; -import * as path from '../../platform/vscode-path/path'; -import { CancellationToken, NotebookCellData, NotebookData, NotebookDocument, Uri, workspace } from 'vscode'; +import { CancellationToken, NotebookDocument, Uri } from 'vscode'; import { IApplicationShell } from '../common/application/types'; -import { traceError } from '../logging'; -import { TemporaryDirectory } from '../common/platform/types'; -import { IConfigurationService, IExtensions } from '../common/types'; -import * as localize from '../common/utils/localize'; +import { IConfigurationService } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; -import { sendTelemetryEvent } from '../../telemetry'; -import { ProgressReporter } from '../progress/progressReporter.node'; +import { ProgressReporter } from '../progress/progressReporter'; import { ExportFileOpener } from './exportFileOpener'; -import { ExportInterpreterFinder } from './exportInterpreterFinder.node'; -import { ExportUtil } from './exportUtil.node'; import { ExportFormat, INbConvertExport, IExportDialog, IFileConverter, IExport } from './types'; -import { Telemetry } from '../common/constants'; import { IFileSystemNode } from '../common/platform/types.node'; +import { FileConverter as FileConverterBase } from './fileConverter'; +import { ExportUtil } from './exportUtil.node'; // Class is responsible for file conversions (ipynb, py, pdf, html) and managing nb convert for some of those conversions @injectable() -export class FileConverter implements IFileConverter { +export class FileConverter extends FileConverterBase implements IFileConverter { constructor( - @inject(INbConvertExport) @named(ExportFormat.pdf) private readonly exportToPDF: INbConvertExport, - @inject(INbConvertExport) @named(ExportFormat.html) private readonly exportToHTML: INbConvertExport, - @inject(INbConvertExport) @named(ExportFormat.python) private readonly exportToPython: INbConvertExport, - @inject(IExport) @named(ExportFormat.python) private readonly exportToPythonPlain: IExport, - @inject(IFileSystemNode) private readonly fs: IFileSystemNode, - @inject(IExportDialog) private readonly filePicker: IExportDialog, - @inject(ProgressReporter) private readonly progressReporter: ProgressReporter, - @inject(ExportUtil) private readonly exportUtil: ExportUtil, - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(ExportFileOpener) private readonly exportFileOpener: ExportFileOpener, - @inject(ExportInterpreterFinder) private exportInterpreterFinder: ExportInterpreterFinder, - @inject(IExtensions) private readonly extensions: IExtensions, - @inject(IConfigurationService) private readonly configuration: IConfigurationService - ) {} - - // Import a notebook file on disk to a .py file - public async importIpynb(source: Uri): Promise { - const reporter = this.progressReporter.createProgressIndicator(localize.DataScience.importingIpynb(), true); - let nbDoc; - try { - // Open the source as a NotebookDocument, note that this doesn't actually show an editor, and we don't need - // a specific close action as VS Code owns the lifetime - nbDoc = await workspace.openNotebookDocument(source); - await this.exportImpl(ExportFormat.python, nbDoc, reporter.token); - } finally { - reporter.dispose(); - } - } - - public async export( - format: ExportFormat, - sourceDocument: NotebookDocument, - defaultFileName?: string, - candidateInterpreter?: PythonEnvironment - ): Promise { - const reporter = this.progressReporter.createProgressIndicator( - localize.DataScience.exportingToFormat().format(format.toString()), - true + @inject(INbConvertExport) @named(ExportFormat.pdf) exportToPDF: INbConvertExport, + @inject(INbConvertExport) @named(ExportFormat.html) exportToHTML: INbConvertExport, + @inject(INbConvertExport) @named(ExportFormat.python) exportToPython: INbConvertExport, + @inject(IExport) @named(ExportFormat.python) exportToPythonPlain: IExport, + @inject(ExportUtil) override readonly exportUtil: ExportUtil, + @inject(IFileSystemNode) readonly fs: IFileSystemNode, + @inject(IExportDialog) filePicker: IExportDialog, + @inject(ProgressReporter) progressReporter: ProgressReporter, + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(ExportFileOpener) exportFileOpener: ExportFileOpener, + @inject(IConfigurationService) readonly configuration: IConfigurationService + ) { + super( + exportToPythonPlain, + exportToPDF, + exportToHTML, + exportToPython, + filePicker, + exportUtil, + progressReporter, + applicationShell, + exportFileOpener ); - - try { - await this.exportImpl(format, sourceDocument, reporter.token, defaultFileName, candidateInterpreter); - } finally { - reporter.dispose(); - } - - if (reporter.token.isCancellationRequested) { - sendTelemetryEvent(Telemetry.ExportNotebookAs, undefined, { format: format, cancelled: true }); - return; - } } - public async exportImpl( - format: ExportFormat, - sourceDocument: NotebookDocument, - token: CancellationToken, - defaultFileName?: string, - candidateInterpreter?: PythonEnvironment - ): Promise { - let target; - try { - target = await this.getTargetFile(format, sourceDocument.uri, defaultFileName); - if (!target) { - return; - } - await this.performExport(format, sourceDocument, target, token, candidateInterpreter); - } catch (e) { - traceError('Export failed', e); - sendTelemetryEvent(Telemetry.ExportNotebookAsFailed, undefined, { format: format }); - - if (format === ExportFormat.pdf) { - traceError(localize.DataScience.exportToPDFDependencyMessage()); - } - - this.showExportFailed(localize.DataScience.exportFailedGeneralMessage()); - } - } - - private async performExport( + override async performExport( format: ExportFormat, sourceDocument: NotebookDocument, target: Uri, @@ -111,129 +52,11 @@ export class FileConverter implements IFileConverter { // Unless selected by the setting use plain conversion for python script convert await this.performPlainExport(format, sourceDocument, target, token); } else { - // For all others (or if 'nbconvert' set for python export method) use nbconvert path - // Get the interpreter to use for the export, checking the candidate interpreter first - const exportInterpreter = await this.exportInterpreterFinder.getExportInterpreter(candidateInterpreter); - const contents = await this.getContent(sourceDocument); - await this.performNbConvertExport(format, contents, target, exportInterpreter, token); - } - - await this.exportFileOpener.openFile(format, target); - } - - private async performPlainExport( - format: ExportFormat, - sourceDocument: NotebookDocument, - target: Uri, - cancelToken: CancellationToken - ) { - switch (format) { - case ExportFormat.python: - await this.exportToPythonPlain.export(sourceDocument, target, cancelToken); - break; - } - } - - private async performNbConvertExport( - format: ExportFormat, - contents: string, - target: Uri, - interpreter: PythonEnvironment, - cancelToken: CancellationToken - ) { - /* Need to make a temp directory here, instead of just a temp file. This is because - we need to store the contents of the notebook in a file that is named the same - as what we want the title of the exported file to be. To ensure this file path will be unique - we store it in a temp directory. The name of the file matters because when - exporting to certain formats the filename is used within the exported document as the title. */ - const tempDir = await this.exportUtil.generateTempDir(); - const source = await this.makeSourceFile(target, contents, tempDir); - - try { - await this.exportToFormat(source, target, format, interpreter, cancelToken); - } finally { - tempDir.dispose(); - } - } - - private async getTargetFile(format: ExportFormat, source: Uri, defaultFileName?: string): Promise { - let target; - - if (format !== ExportFormat.python) { - target = await this.filePicker.showDialog(format, source, defaultFileName); - } else { - target = Uri.file((await this.fs.createTemporaryLocalFile('.py')).filePath); + await this.performNbConvertExport(sourceDocument, format, target, candidateInterpreter, token); } - return target; - } - - private async makeSourceFile(target: Uri, contents: string, tempDir: TemporaryDirectory): Promise { - // Creates a temporary file with the same base name as the target file - const fileName = path.basename(target.fsPath, path.extname(target.fsPath)); - const sourceFilePath = await this.exportUtil.makeFileInDirectory(contents, `${fileName}.ipynb`, tempDir.path); - return Uri.file(sourceFilePath); - } - - private showExportFailed(msg: string) { - // eslint-disable-next-line - this.applicationShell.showErrorMessage(`${localize.DataScience.failedExportMessage()} ${msg}`).then(); - } - - private async exportToFormat( - source: Uri, - target: Uri, - format: ExportFormat, - interpreter: PythonEnvironment, - cancelToken: CancellationToken - ) { - if (format === ExportFormat.pdf) { - // When exporting to PDF we need to remove any SVG output. This is due to an error - // with nbconvert and a dependency of its called InkScape. - await this.exportUtil.removeSvgs(source); + if (target) { + await this.exportFileOpener.openFile(format, target); } - - switch (format) { - case ExportFormat.python: - await this.exportToPython.export(source, target, interpreter, cancelToken); - break; - - case ExportFormat.pdf: - await this.exportToPDF.export(source, target, interpreter, cancelToken); - break; - - case ExportFormat.html: - await this.exportToHTML.export(source, target, interpreter, cancelToken); - break; - - default: - break; - } - } - private async getContent(document: NotebookDocument): Promise { - const serializerApi = this.extensions.getExtension<{ exportNotebook: (notebook: NotebookData) => string }>( - 'vscode.ipynb' - ); - if (!serializerApi) { - throw new Error( - 'Unable to export notebook as the built-in vscode.ipynb extension is currently unavailable.' - ); - } - // Via the interactive window export this might not be activated - if (!serializerApi.isActive) { - await serializerApi.activate(); - } - - const cells = document.getCells(); - const cellData = cells.map((c) => { - const data = new NotebookCellData(c.kind, c.document.getText(), c.document.languageId); - data.metadata = c.metadata; - data.mime = c.mime; - data.outputs = [...c.outputs]; - return data; - }); - const notebookData = new NotebookData(cellData); - notebookData.metadata = document.metadata; - return serializerApi.exports.exportNotebook(notebookData); } } diff --git a/src/platform/export/fileConverter.ts b/src/platform/export/fileConverter.ts new file mode 100644 index 00000000000..8964dfa95dd --- /dev/null +++ b/src/platform/export/fileConverter.ts @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, NotebookDocument, Uri, workspace } from 'vscode'; +import { sendTelemetryEvent } from '../../telemetry'; +import { IApplicationShell } from '../common/application/types'; +import { Telemetry } from '../common/constants'; +import * as localize from '../common/utils/localize'; +import { traceError } from '../logging'; +import { ProgressReporter } from '../progress/progressReporter'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ExportFileOpener } from './exportFileOpener'; +import { ExportUtilBase } from './exportUtil'; +import { ExportFormat, IExport, IExportDialog, IFileConverter, INbConvertExport } from './types'; + +@injectable() +export class FileConverter implements IFileConverter { + constructor( + @inject(IExport) @named(ExportFormat.python) private readonly exportToPythonPlain: IExport, + @inject(INbConvertExport) @named(ExportFormat.pdf) private readonly exportToPDF: INbConvertExport, + @inject(INbConvertExport) @named(ExportFormat.html) private readonly exportToHTML: INbConvertExport, + @inject(INbConvertExport) @named(ExportFormat.python) private readonly exportToPython: INbConvertExport, + @inject(IExportDialog) protected readonly filePicker: IExportDialog, + @inject(ExportUtilBase) protected readonly exportUtil: ExportUtilBase, + @inject(ProgressReporter) private readonly progressReporter: ProgressReporter, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ExportFileOpener) protected readonly exportFileOpener: ExportFileOpener + ) {} + + async importIpynb(source: Uri): Promise { + const reporter = this.progressReporter.createProgressIndicator(localize.DataScience.importingIpynb(), true); + let nbDoc; + try { + // Open the source as a NotebookDocument, note that this doesn't actually show an editor, and we don't need + // a specific close action as VS Code owns the lifetime + nbDoc = await workspace.openNotebookDocument(source); + await this.exportImpl(ExportFormat.python, nbDoc, undefined, reporter.token); + } finally { + reporter.dispose(); + } + } + + async export( + format: ExportFormat, + sourceDocument: NotebookDocument, + defaultFileName?: string | undefined, + candidateInterpreter?: PythonEnvironment + ): Promise { + const reporter = this.progressReporter.createProgressIndicator( + localize.DataScience.exportingToFormat().format(format.toString()), + true + ); + + try { + await this.exportImpl(format, sourceDocument, defaultFileName, reporter.token, candidateInterpreter); + } finally { + reporter.dispose(); + } + + if (reporter.token.isCancellationRequested) { + sendTelemetryEvent(Telemetry.ExportNotebookAs, undefined, { format: format, cancelled: true }); + return; + } + } + + public async exportImpl( + format: ExportFormat, + sourceDocument: NotebookDocument, + defaultFileName: string | undefined, + token: CancellationToken, + candidateInterpreter?: PythonEnvironment + ): Promise { + try { + let target = await this.exportUtil.getTargetFile(format, sourceDocument.uri, defaultFileName); + if (!target) { + return; + } + await this.performExport(format, sourceDocument, target, token, candidateInterpreter); + } catch (e) { + traceError('Export failed', e); + sendTelemetryEvent(Telemetry.ExportNotebookAsFailed, undefined, { format: format }); + + if (format === ExportFormat.pdf) { + traceError(localize.DataScience.exportToPDFDependencyMessage()); + } + + this.showExportFailed(localize.DataScience.exportFailedGeneralMessage()); + } + } + + protected async performExport( + format: ExportFormat, + sourceDocument: NotebookDocument, + target: Uri, + token: CancellationToken, + candidateInterpreter?: PythonEnvironment + ) { + // For web, we perform plain export for Python + if (format === ExportFormat.python) { + // Unless selected by the setting use plain conversion for python script convert + await this.performPlainExport(format, sourceDocument, target, token); + } else { + await this.performNbConvertExport(sourceDocument, format, target, candidateInterpreter, token); + } + + if (target) { + await this.exportFileOpener.openFile(format, target, true); + } + } + + protected async performPlainExport( + format: ExportFormat, + sourceDocument: NotebookDocument, + target: Uri, + cancelToken: CancellationToken + ): Promise { + if (target) { + switch (format) { + case ExportFormat.python: + await this.exportToPythonPlain.export(sourceDocument, target, cancelToken); + break; + } + } + + return target; + } + + protected async performNbConvertExport( + sourceDocument: NotebookDocument, + format: ExportFormat, + target: Uri, + interpreter: PythonEnvironment | undefined, + cancelToken: CancellationToken + ) { + try { + return await this.exportToFormat(sourceDocument, target, format, interpreter, cancelToken); + } finally { + } + } + + protected async exportToFormat( + sourceDocument: NotebookDocument, + target: Uri, + format: ExportFormat, + interpreter: PythonEnvironment | undefined, + cancelToken: CancellationToken + ) { + switch (format) { + case ExportFormat.python: + return await this.exportToPython.export(sourceDocument, target, interpreter, cancelToken); + + case ExportFormat.pdf: + return await this.exportToPDF.export(sourceDocument, target, interpreter, cancelToken); + + case ExportFormat.html: + return await this.exportToHTML.export(sourceDocument, target, interpreter, cancelToken); + + default: + break; + } + } + + private showExportFailed(msg: string) { + // eslint-disable-next-line + this.applicationShell.showErrorMessage(`${localize.DataScience.failedExportMessage()} ${msg}`).then(); + } +} diff --git a/src/platform/export/fileConverter.web.ts b/src/platform/export/fileConverter.web.ts deleted file mode 100644 index 6311061633f..00000000000 --- a/src/platform/export/fileConverter.web.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { CancellationToken, CancellationTokenSource, NotebookDocument, Uri } from 'vscode'; -import { traceError } from '../logging'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ExportFileOpener } from './exportFileOpener'; -import { ExportFormat, IExport, IExportDialog, IFileConverter } from './types'; - -@injectable() -export class FileConverter implements IFileConverter { - constructor( - @inject(IExport) @named(ExportFormat.python) private readonly exportToPythonPlain: IExport, - @inject(IExportDialog) private readonly filePicker: IExportDialog, - @inject(ExportFileOpener) private readonly exportFileOpener: ExportFileOpener - ) {} - - async export( - format: ExportFormat, - sourceDocument: NotebookDocument, - defaultFileName?: string | undefined, - candidateInterpreter?: PythonEnvironment - ): Promise { - try { - const cancellation = new CancellationTokenSource(); - await this.exportImpl(format, sourceDocument, cancellation.token, defaultFileName, candidateInterpreter); - cancellation.dispose(); - } finally { - } - - return; - } - - public async exportImpl( - format: ExportFormat, - sourceDocument: NotebookDocument, - token: CancellationToken, - defaultFileName?: string, - candidateInterpreter?: PythonEnvironment - ): Promise { - let target; - try { - target = await this.getTargetFile(format, sourceDocument.uri, defaultFileName); - if (!target) { - return; - } - await this.performExport(format, sourceDocument, target, token, candidateInterpreter); - } catch (e) { - traceError('Export failed', e); - } - } - - private async performExport( - format: ExportFormat, - sourceDocument: NotebookDocument, - target: Uri, - token: CancellationToken, - _candidateInterpreter?: PythonEnvironment - ) { - // For web, we perform plain export for Python - if (format === ExportFormat.python) { - // Unless selected by the setting use plain conversion for python script convert - await this.performPlainExport(format, sourceDocument, target, token); - await this.exportFileOpener.openFile(format, target, true); - } else { - throw new Error('Method not implemented.'); - } - } - - private async performPlainExport( - format: ExportFormat, - sourceDocument: NotebookDocument, - target: Uri, - cancelToken: CancellationToken - ) { - switch (format) { - case ExportFormat.python: - await this.exportToPythonPlain.export(sourceDocument, target, cancelToken); - break; - } - } - - private async getTargetFile(format: ExportFormat, source: Uri, defaultFileName?: string): Promise { - let target = await this.filePicker.showDialog(format, source, defaultFileName); - - return target; - } - - importIpynb(_source: Uri): Promise { - throw new Error('Method not implemented.'); - } -} diff --git a/src/platform/export/types.ts b/src/platform/export/types.ts index 8b43802918c..691048fd693 100644 --- a/src/platform/export/types.ts +++ b/src/platform/export/types.ts @@ -22,13 +22,24 @@ export interface IFileConverter { export const INbConvertExport = Symbol('INbConvertExport'); export interface INbConvertExport { export( - source: Uri, + sourceDocument: NotebookDocument, target: Uri, interpreter: PythonEnvironment | undefined, token: CancellationToken ): Promise; } +export const IExportBase = Symbol('IExportBase'); +export interface IExportBase { + executeCommand( + sourceDocument: NotebookDocument, + target: Uri, + format: ExportFormat, + interpreter: PythonEnvironment, + token: CancellationToken + ): Promise; +} + export const IExport = Symbol('IExport'); export interface IExport { export(sourceDocument: NotebookDocument, target: Uri, token: CancellationToken): Promise; diff --git a/src/platform/progress/decorator.node.ts b/src/platform/progress/decorator.ts similarity index 100% rename from src/platform/progress/decorator.node.ts rename to src/platform/progress/decorator.ts diff --git a/src/platform/progress/progressReporter.node.ts b/src/platform/progress/progressReporter.ts similarity index 98% rename from src/platform/progress/progressReporter.node.ts rename to src/platform/progress/progressReporter.ts index 519f5b0544c..90d82c0e53c 100644 --- a/src/platform/progress/progressReporter.node.ts +++ b/src/platform/progress/progressReporter.ts @@ -9,7 +9,7 @@ import { IApplicationShell } from '../common/application/types'; import { IDisposable } from '../common/types'; import { createDeferred } from '../common/utils/async'; import { noop } from '../common/utils/misc'; -import { registerReporter } from './decorator.node'; +import { registerReporter } from './decorator'; import { getUserMessageForAction } from './messages'; import { IProgressReporter, Progress, ReportableAction } from './types'; diff --git a/src/platform/serviceRegistry.node.ts b/src/platform/serviceRegistry.node.ts index a6d94403554..24b0b058283 100644 --- a/src/platform/serviceRegistry.node.ts +++ b/src/platform/serviceRegistry.node.ts @@ -37,22 +37,20 @@ import { DebuggingManager } from './debugger/jupyter/notebook/debuggingManager'; import { IDebugLocationTracker, IDebuggingManager } from './debugger/types'; import { DataScienceErrorHandler } from './errors/errorHandler'; import { IDataScienceErrorHandler } from './errors/types'; -import { - ExportBase, - ExportDialog, - ExportFileOpener, - ExportInterpreterFinder, - ExportToHTML, - ExportToPDF, - ExportToPython, - ExportToPythonPlain, - ExportUtil, - FileConverter -} from './export/export.index.node'; -import { IFileConverter, INbConvertExport, ExportFormat, IExport, IExportDialog } from './export/types'; +import { ExportBase } from './export/exportBase.node'; +import { ExportDialog } from './export/exportDialog'; +import { ExportFileOpener } from './export/exportFileOpener'; +import { ExportInterpreterFinder } from './export/exportInterpreterFinder.node'; +import { ExportToHTML } from './export/exportToHTML'; +import { ExportToPDF } from './export/exportToPDF'; +import { ExportToPython } from './export/exportToPython'; +import { ExportToPythonPlain } from './export/exportToPythonPlain'; +import { ExportUtil } from './export/exportUtil.node'; +import { FileConverter } from './export/fileConverter.node'; +import { IFileConverter, INbConvertExport, ExportFormat, IExport, IExportDialog, IExportBase } from './export/types'; import { GitHubIssueCommandListener } from './logging/gitHubIssueCommandListener.node'; import { KernelProgressReporter } from './progress/kernelProgressReporter'; -import { ProgressReporter } from './progress/progressReporter.node'; +import { ProgressReporter } from './progress/progressReporter'; import { StatusProvider } from './progress/statusProvider'; import { IStatusProvider } from './progress/types'; import { ApplicationShell } from './common/application/applicationShell'; @@ -65,6 +63,7 @@ import { FileSystem } from './common/platform/fileSystem.node'; import { WorkspaceService } from './common/application/workspace.node'; import { ExtensionSideRenderer, IExtensionSideRenderer } from '../webviews/extension-side/renderer'; import { OutputCommandListener } from './logging/outputCommandListener'; +import { ExportUtilBase } from './export/exportUtil'; export function registerTypes(context: IExtensionContext, serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton(FileSystem, FileSystem); @@ -110,11 +109,14 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IFileConverter, FileConverter); serviceManager.addSingleton(ExportInterpreterFinder, ExportInterpreterFinder); serviceManager.addSingleton(ExportFileOpener, ExportFileOpener); + + serviceManager.addSingleton(IExportBase, ExportBase); serviceManager.addSingleton(INbConvertExport, ExportToPDF, ExportFormat.pdf); serviceManager.addSingleton(INbConvertExport, ExportToHTML, ExportFormat.html); serviceManager.addSingleton(INbConvertExport, ExportToPython, ExportFormat.python); serviceManager.addSingleton(INbConvertExport, ExportBase, 'Export Base'); serviceManager.addSingleton(IExport, ExportToPythonPlain, ExportFormat.python); + serviceManager.addSingleton(ExportUtilBase, ExportUtilBase); serviceManager.addSingleton(ExportUtil, ExportUtil); serviceManager.addSingleton(IExportDialog, ExportDialog); serviceManager.addSingleton(INotebookWatcher, NotebookWatcher); diff --git a/src/platform/serviceRegistry.web.ts b/src/platform/serviceRegistry.web.ts index aee374d29ce..cf6d0176279 100644 --- a/src/platform/serviceRegistry.web.ts +++ b/src/platform/serviceRegistry.web.ts @@ -18,6 +18,7 @@ import { registerTypes as registerActivationTypes } from './activation/serviceRe import { registerTypes as registerDevToolTypes } from './devTools/serviceRegistry'; import { IConfigurationService, IDataScienceCommandListener, IExtensionContext } from './common/types'; import { IServiceManager } from './ioc/types'; +import { ProgressReporter } from './progress/progressReporter'; import { StatusProvider } from './progress/statusProvider'; import { IStatusProvider } from './progress/types'; import { WorkspaceService } from './common/application/workspace.web'; @@ -30,12 +31,17 @@ import { OutputCommandListener } from './logging/outputCommandListener'; import { IDebuggingManager } from './debugger/types'; import { DebuggingManager } from './debugger/jupyter/notebook/debuggingManager'; import { ExportDialog } from './export/exportDialog'; -import { ExportFormat, IExport, IExportDialog, IFileConverter } from './export/types'; -import { FileConverter } from './export/fileConverter.web'; +import { ExportFormat, IExport, IExportBase, IExportDialog, IFileConverter, INbConvertExport } from './export/types'; +import { FileConverter } from './export/fileConverter'; import { ExportFileOpener } from './export/exportFileOpener'; -import { ExportToPythonPlain } from './export/exportToPythonPlain.web'; +import { ExportToPythonPlain } from './export/exportToPythonPlain'; import { IFileSystem } from './common/platform/types'; import { FileSystem } from './common/platform/fileSystem'; +import { ExportBase } from './export/exportBase.web'; +import { ExportUtilBase } from './export/exportUtil'; +import { ExportToHTML } from './export/exportToHTML'; +import { ExportToPDF } from './export/exportToPDF'; +import { ExportToPython } from './export/exportToPython'; import { NotebookWatcher } from '../webviews/extension-side/variablesView/notebookWatcher'; import { DataViewerFactory } from '../webviews/extension-side/dataviewer/dataViewerFactory'; import { IDataViewerFactory } from '../webviews/extension-side/dataviewer/types'; @@ -54,9 +60,15 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingletonInstance(IExtensionSideRenderer, new ExtensionSideRenderer()); serviceManager.addSingleton(IDataScienceCommandListener, OutputCommandListener); serviceManager.addSingleton(ExportFileOpener, ExportFileOpener); + serviceManager.addSingleton(IExportBase, ExportBase); serviceManager.addSingleton(IExportDialog, ExportDialog); + serviceManager.addSingleton(ProgressReporter, ProgressReporter); serviceManager.addSingleton(IFileConverter, FileConverter); serviceManager.addSingleton(IExport, ExportToPythonPlain, ExportFormat.python); + serviceManager.addSingleton(INbConvertExport, ExportToHTML, ExportFormat.html); + serviceManager.addSingleton(INbConvertExport, ExportToPDF, ExportFormat.pdf); + serviceManager.addSingleton(INbConvertExport, ExportToPython, ExportFormat.python); + serviceManager.addSingleton(ExportUtilBase, ExportUtilBase); registerCommonTypes(serviceManager); registerApiTypes(serviceManager); diff --git a/src/test/datascience/export/exportFileOpener.unit.test.ts b/src/test/datascience/export/exportFileOpener.unit.test.ts index abc203b834a..9116befd5cc 100644 --- a/src/test/datascience/export/exportFileOpener.unit.test.ts +++ b/src/test/datascience/export/exportFileOpener.unit.test.ts @@ -10,7 +10,7 @@ import { IFileSystem } from '../../../platform/common/platform/types'; import { IBrowserService, IDisposable } from '../../../platform/common/types'; import { ExportFileOpener } from '../../../platform/export/exportFileOpener'; import { ExportFormat } from '../../../platform/export/types'; -import { ProgressReporter } from '../../../platform/progress/progressReporter.node'; +import { ProgressReporter } from '../../../platform/progress/progressReporter'; import { getLocString } from '../../../webviews/webview-side/react-common/locReactSide'; suite('DataScience - Export File Opener', () => { diff --git a/src/test/datascience/export/exportToHTML.vscode.test.ts b/src/test/datascience/export/exportToHTML.vscode.test.ts index dcebe165da6..1dba5e45fc1 100644 --- a/src/test/datascience/export/exportToHTML.vscode.test.ts +++ b/src/test/datascience/export/exportToHTML.vscode.test.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, no-invalid-this, @typescript-eslint/no-explicit-any */ import { assert } from 'chai'; import * as path from '../../../platform/vscode-path/path'; -import { CancellationTokenSource, Uri } from 'vscode'; +import { CancellationTokenSource, Uri, workspace } from 'vscode'; import { IFileSystemNode } from '../../../platform/common/platform/types.node'; import { ExportInterpreterFinder } from '../../../platform/export/exportInterpreterFinder.node'; import { INbConvertExport, ExportFormat } from '../../../platform/export/types'; @@ -29,15 +29,13 @@ suite('DataScience - Export HTML', function () { await file.dispose(); const token = new CancellationTokenSource(); const interpreter = await exportInterpreterFinder.getExportInterpreter(); - await exportToHTML.export( - Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')), - target, - interpreter, - token.token + const document = await workspace.openNotebookDocument( + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')) ); - - assert.equal(await fileSystem.localFileExists(target.fsPath), true); - const fileContents = await fileSystem.readLocalFile(target.fsPath); + await exportToHTML.export(document, target, interpreter, token.token); + assert.exists(target); + assert.equal(await fileSystem.localFileExists(target!.fsPath), true); + const fileContents = await fileSystem.readLocalFile(target!.fsPath); assert.include(fileContents, ''); // this is the content of a cell assert.include(fileContents, 'f6886df81f3d4023a2122cc3f55fdbec'); diff --git a/src/test/datascience/export/exportToPython.vscode.test.ts b/src/test/datascience/export/exportToPython.vscode.test.ts index ca0022fed0b..0972bf99d26 100644 --- a/src/test/datascience/export/exportToPython.vscode.test.ts +++ b/src/test/datascience/export/exportToPython.vscode.test.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, no-invalid-this, @typescript-eslint/no-explicit-any */ import { assert } from 'chai'; import * as path from '../../../platform/vscode-path/path'; -import { CancellationTokenSource, Uri } from 'vscode'; +import { CancellationTokenSource, Uri, workspace } from 'vscode'; import { IDocumentManager } from '../../../platform/common/application/types'; import { IFileSystemNode } from '../../../platform/common/platform/types.node'; import { ExportInterpreterFinder } from '../../../platform/export/exportInterpreterFinder.node'; @@ -28,15 +28,13 @@ suite('DataScience - Export Python', function () { const token = new CancellationTokenSource(); const exportInterpreterFinder = api.serviceContainer.get(ExportInterpreterFinder); const interpreter = await exportInterpreterFinder.getExportInterpreter(); - await exportToPython.export( - Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')), - target, - interpreter, - token.token + const document = await workspace.openNotebookDocument( + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')) ); - + await exportToPython.export(document, target, interpreter, token.token); + assert.exists(target); const documentManager = api.serviceContainer.get(IDocumentManager); - const document = await documentManager.openTextDocument(target); - assert.include(document.getText(), 'tim = 1'); + const targetDocument = await documentManager.openTextDocument(target!); + assert.include(targetDocument.getText(), 'tim = 1'); }); }); diff --git a/src/test/datascience/export/exportUtil.vscode.test.ts b/src/test/datascience/export/exportUtil.vscode.test.ts index d92e3b26a07..2789b32d3d9 100644 --- a/src/test/datascience/export/exportUtil.vscode.test.ts +++ b/src/test/datascience/export/exportUtil.vscode.test.ts @@ -32,8 +32,10 @@ suite('DataScience - Export Util', () => { suiteTeardown(() => closeActiveWindows(testDisposables)); test('Remove svgs from model', async () => { const exportUtil = api.serviceContainer.get(ExportUtil); + const contents = fs.readFileSync(testPdfIpynb.fsPath).toString(); - await exportUtil.removeSvgs(testPdfIpynb); + const contentsWithoutSvg = await exportUtil.removeSvgs(contents); + await fs.writeFile(testPdfIpynb.fsPath, contentsWithoutSvg); const model = JSON.parse(fs.readFileSync(testPdfIpynb.fsPath).toString()) as nbformat.INotebookContent; // make sure no svg exists in model diff --git a/src/test/datascience/export/fileConverter.vscode.test.ts b/src/test/datascience/export/fileConverter.vscode.test.ts index ec3951178bd..bcb944ee6a8 100644 --- a/src/test/datascience/export/fileConverter.vscode.test.ts +++ b/src/test/datascience/export/fileConverter.vscode.test.ts @@ -9,18 +9,13 @@ import * as sinon from 'sinon'; import { Uri } from 'vscode'; import { IApplicationShell } from '../../../platform/common/application/types'; import { IFileSystemNode } from '../../../platform/common/platform/types.node'; -import { - IConfigurationService, - IDisposable, - IExtensions, - IWatchableJupyterSettings -} from '../../../platform/common/types'; +import { IConfigurationService, IDisposable, IWatchableJupyterSettings } from '../../../platform/common/types'; import { ExportFileOpener } from '../../../platform/export/exportFileOpener'; import { ExportInterpreterFinder } from '../../../platform/export/exportInterpreterFinder.node'; import { ExportUtil } from '../../../platform/export/exportUtil.node'; import { FileConverter } from '../../../platform/export/fileConverter.node'; import { INbConvertExport, IExport, IExportDialog, ExportFormat } from '../../../platform/export/types'; -import { ProgressReporter } from '../../../platform/progress/progressReporter.node'; +import { ProgressReporter } from '../../../platform/progress/progressReporter'; suite('DataScience - File Converter', () => { let fileConverter: FileConverter; @@ -34,7 +29,6 @@ suite('DataScience - File Converter', () => { let appShell: IApplicationShell; let exportFileOpener: ExportFileOpener; let exportInterpreterFinder: ExportInterpreterFinder; - let extensions: IExtensions; let configuration: IConfigurationService; let settings: IWatchableJupyterSettings; setup(async () => { @@ -49,7 +43,6 @@ suite('DataScience - File Converter', () => { appShell = mock(); exportFileOpener = mock(); exportInterpreterFinder = mock(); - extensions = mock(); configuration = mock(); settings = mock(); when(configuration.getSettings(anything())).thenReturn(instance(settings)); @@ -63,6 +56,7 @@ suite('DataScience - File Converter', () => { // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function when(exportUtil.generateTempDir()).thenResolve({ path: 'test', dispose: () => {} }); when(exportUtil.makeFileInDirectory(anything(), anything(), anything())).thenResolve('foo'); + when(exportUtil.getTargetFile(anything(), anything(), anything())).thenResolve(Uri.file('bar')); // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function when(fileSystem.createTemporaryLocalFile(anything())).thenResolve({ filePath: 'test', dispose: () => {} }); when(exportPdf.export(anything(), anything(), anything(), anything())).thenResolve(); @@ -77,27 +71,21 @@ suite('DataScience - File Converter', () => { instance(exportHtml), instance(exportPython), instance(exportPythonPlain), + instance(exportUtil), instance(fileSystem), instance(filePicker), instance(reporter), - instance(exportUtil), instance(appShell), instance(exportFileOpener), - instance(exportInterpreterFinder), - instance(extensions), instance(configuration) ); // Stub out the getContent inner method of the ExportManager we don't care about the content returned - const getContentStub = sinon.stub(FileConverter.prototype, 'getContent' as any); + const getContentStub = sinon.stub(ExportUtil.prototype, 'getContent' as any); getContentStub.resolves('teststring'); }); teardown(() => sinon.restore()); - test('Remove svg is called when exporting to PDF', async () => { - await fileConverter.export(ExportFormat.pdf, {} as any); - verify(exportUtil.removeSvgs(anything())).once(); - }); test('Erorr message is shown if export fails', async () => { when(exportHtml.export(anything(), anything(), anything(), anything())).thenThrow(new Error('failed...')); await fileConverter.export(ExportFormat.html, {} as any); diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json index 008d3f5709a..e59cad34e39 100644 --- a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json @@ -65,4 +65,4 @@ "dependencies": { "uuid": "^8.2.0" } -} +} \ No newline at end of file diff --git a/src/test/datascience/notebook/exportFull.vscode.test.ts b/src/test/datascience/notebook/exportFull.vscode.test.ts index 475deb6f2cf..2ea571c42c7 100644 --- a/src/test/datascience/notebook/exportFull.vscode.test.ts +++ b/src/test/datascience/notebook/exportFull.vscode.test.ts @@ -20,6 +20,7 @@ import { hijackPrompt, insertCodeCell, insertMarkdownCell, + saveActiveNotebook, startJupyterServer, workAroundVSCodeNotebookStartPages } from './helper.node'; @@ -91,6 +92,7 @@ suite('DataScience - VSCode Notebook - (Export) (slow)', function () { await insertCodeCell('print("Hello World")', { index: 0 }); await insertMarkdownCell('# Markdown Header\nmarkdown string', { index: 1 }); await insertCodeCell('%whos', { index: 2 }); + await saveActiveNotebook(); const deferred = createDeferred(); const onDidChangeDispose = window.onDidChangeActiveTextEditor((te) => { @@ -121,6 +123,7 @@ suite('DataScience - VSCode Notebook - (Export) (slow)', function () { await insertCodeCell('print("Hello World")', { index: 0 }); await insertMarkdownCell('# Markdown Header\nmarkdown string', { index: 1 }); await insertCodeCell('%whos\n!shellcmd', { index: 2 }); + await saveActiveNotebook(); const deferred = createDeferred(); const onDidChangeDispose = window.onDidChangeActiveTextEditor((te) => { @@ -153,6 +156,7 @@ suite('DataScience - VSCode Notebook - (Export) (slow)', function () { await insertCodeCell('print("Hello World")', { index: 0 }); await insertMarkdownCell('# Markdown Header\nmarkdown string', { index: 1 }); await insertCodeCell('%whos\n!shellcmd', { index: 2 }); + await saveActiveNotebook(); const deferred = createDeferred(); const onDidChangeDispose = window.onDidChangeActiveTextEditor((te) => { diff --git a/src/test/datascience/progress/decorators.unit.test.ts b/src/test/datascience/progress/decorators.unit.test.ts index 2853c419fd6..7fbb35b48ad 100644 --- a/src/test/datascience/progress/decorators.unit.test.ts +++ b/src/test/datascience/progress/decorators.unit.test.ts @@ -5,8 +5,8 @@ import { anything, deepEqual, instance, mock, verify } from 'ts-mockito'; import { createDeferred } from '../../../platform/common/utils/async'; -import { reportAction, registerReporter, disposeRegisteredReporters } from '../../../platform/progress/decorator.node'; -import { ProgressReporter } from '../../../platform/progress/progressReporter.node'; +import { reportAction, registerReporter, disposeRegisteredReporters } from '../../../platform/progress/decorator'; +import { ProgressReporter } from '../../../platform/progress/progressReporter'; import { IProgressReporter, ReportableAction } from '../../../platform/progress/types'; import { noop } from '../../core'; diff --git a/src/test/datascience/progress/progressReporter.unit.test.ts b/src/test/datascience/progress/progressReporter.unit.test.ts index 8d7461c4b54..8b3179ec9ec 100644 --- a/src/test/datascience/progress/progressReporter.unit.test.ts +++ b/src/test/datascience/progress/progressReporter.unit.test.ts @@ -9,7 +9,7 @@ import { CancellationToken, CancellationTokenSource, Progress as VSCProgress } f import { ApplicationShell } from '../../../platform/common/application/applicationShell'; import { IApplicationShell } from '../../../platform/common/application/types'; import { getUserMessageForAction } from '../../../platform/progress/messages'; -import { ProgressReporter } from '../../../platform/progress/progressReporter.node'; +import { ProgressReporter } from '../../../platform/progress/progressReporter'; import { ReportableAction } from '../../../platform/progress/types'; import { noop, sleep } from '../../core'; type Task = (