33'use strict' ;
44import '../../common/extensions' ;
55
6+ import { nbformat } from '@jupyterlab/coreutils/lib/nbformat' ;
7+ import * as detectIndent from 'detect-indent' ;
68import * as fastDeepEqual from 'fast-deep-equal' ;
79import { inject , injectable , multiInject , named } from 'inversify' ;
810import * as path from 'path' ;
911import * as uuid from 'uuid/v4' ;
1012import { Event , EventEmitter , Memento , Uri , ViewColumn } from 'vscode' ;
1113
12- import { IApplicationShell , ICommandManager , IDocumentManager , ILiveShareApi , IWebPanelProvider , IWorkspaceService } from '../../common/application/types' ;
14+ import {
15+ IApplicationShell ,
16+ ICommandManager ,
17+ IDocumentManager ,
18+ ILiveShareApi ,
19+ IWebPanelProvider ,
20+ IWorkspaceService
21+ } from '../../common/application/types' ;
1322import { ContextKey } from '../../common/contextKey' ;
1423import { traceError } from '../../common/logger' ;
1524import { IFileSystem , TemporaryFile } from '../../common/platform/types' ;
@@ -20,10 +29,23 @@ import { StopWatch } from '../../common/utils/stopWatch';
2029import { EXTENSION_ROOT_DIR } from '../../constants' ;
2130import { IInterpreterService } from '../../interpreter/contracts' ;
2231import { captureTelemetry , sendTelemetryEvent } from '../../telemetry' ;
23- import { concatMultilineString } from '../common' ;
24- import { EditorContexts , Identifiers , NativeKeyboardCommandTelemetryLookup , NativeMouseCommandTelemetryLookup , Telemetry } from '../constants' ;
32+ import { concatMultilineStringInput , splitMultilineString } from '../common' ;
33+ import {
34+ EditorContexts ,
35+ Identifiers ,
36+ NativeKeyboardCommandTelemetryLookup ,
37+ NativeMouseCommandTelemetryLookup ,
38+ Telemetry
39+ } from '../constants' ;
2540import { InteractiveBase } from '../interactive-common/interactiveBase' ;
26- import { IEditCell , INativeCommand , InteractiveWindowMessages , ISaveAll , ISubmitNewCell } from '../interactive-common/interactiveWindowTypes' ;
41+ import {
42+ IEditCell ,
43+ INativeCommand ,
44+ InteractiveWindowMessages ,
45+ ISaveAll ,
46+ ISubmitNewCell
47+ } from '../interactive-common/interactiveWindowTypes' ;
48+ import { InvalidNotebookFileError } from '../jupyter/invalidNotebookFileError' ;
2749import {
2850 CellState ,
2951 ICell ,
@@ -62,6 +84,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
6284 private visibleCells : ICell [ ] = [ ] ;
6385 private startupTimer : StopWatch = new StopWatch ( ) ;
6486 private loadedAllCells : boolean = false ;
87+ private indentAmount : string = ' ' ;
88+ private notebookJson : Partial < nbformat . INotebookContent > = { } ;
6589
6690 constructor (
6791 @multiInject ( IInteractiveWindowListener ) listeners : IInteractiveWindowListener [ ] ,
@@ -132,7 +156,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
132156 this . close ( ) . ignoreErrors ( ) ;
133157 }
134158
135- public async load ( content : string , file : Uri ) : Promise < void > {
159+ public async load ( contents : string , file : Uri ) : Promise < void > {
136160 // Save our uri
137161 this . _file = file ;
138162
@@ -149,12 +173,10 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
149173 const dirtyContents = this . getStoredContents ( ) ;
150174 if ( dirtyContents ) {
151175 // This means we're dirty. Indicate dirty and load from this content
152- const cells = await this . importer . importCells ( dirtyContents ) ;
153- return this . loadCells ( cells , true ) ;
176+ return this . loadContents ( dirtyContents , true ) ;
154177 } else {
155- // Load the contents of this notebook into our cells.
156- const cells = content ? await this . importer . importCells ( content ) : [ ] ;
157- return this . loadCells ( cells , false ) ;
178+ // Load without setting dirty
179+ return this . loadContents ( contents , false ) ;
158180 }
159181 }
160182
@@ -318,8 +340,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
318340 id : info . id ,
319341 file : Identifiers . EmptyFileName ,
320342 line : 0 ,
321- state : CellState . error ,
322- type : 'execute'
343+ state : CellState . error
323344 }
324345 ] ) ;
325346
@@ -391,6 +412,41 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
391412 // Actually don't close, just let the error bubble out
392413 }
393414
415+ private async loadContents ( contents : string | undefined , forceDirty : boolean ) : Promise < void > {
416+ // tslint:disable-next-line: no-any
417+ const json = contents ? JSON . parse ( contents ) as any : undefined ;
418+
419+ // Double check json (if we have any)
420+ if ( json && ! json . cells ) {
421+ throw new InvalidNotebookFileError ( this . file . fsPath ) ;
422+ }
423+
424+ // Then compute indent. It's computed from the contents
425+ if ( contents ) {
426+ this . indentAmount = detectIndent ( contents ) . indent ;
427+ }
428+
429+ // Then save the contents. We'll stick our cells back into this format when we save
430+ if ( json ) {
431+ this . notebookJson = json ;
432+ }
433+
434+ // Extract cells from the json
435+ const cells = contents ? json . cells as ( nbformat . ICodeCell | nbformat . IRawCell | nbformat . IMarkdownCell ) [ ] : [ ] ;
436+
437+ // Then parse the cells
438+ return this . loadCells ( cells . map ( ( c , index ) => {
439+ return {
440+ id : `NotebookImport#${ index } ` ,
441+ file : Identifiers . EmptyFileName ,
442+ line : 0 ,
443+ state : CellState . finished ,
444+ data : c
445+ } ;
446+ } ) , forceDirty ) ;
447+
448+ }
449+
394450 private async loadCells ( cells : ICell [ ] , forceDirty : boolean ) : Promise < void > {
395451 // Make sure cells have at least 1
396452 if ( cells . length === 0 ) {
@@ -399,7 +455,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
399455 line : 0 ,
400456 file : Identifiers . EmptyFileName ,
401457 state : CellState . finished ,
402- type : 'execute' ,
403458 data : {
404459 cell_type : 'code' ,
405460 outputs : [ ] ,
@@ -419,7 +474,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
419474 if ( forceDirty ) {
420475 await this . setDirty ( ) ;
421476 }
422- sendTelemetryEvent ( Telemetry . CellCount , undefined , { count : cells . length } ) ;
477+ sendTelemetryEvent ( Telemetry . CellCount , undefined , { count : cells . length } ) ;
423478 return this . postMessage ( InteractiveWindowMessages . LoadAllCells , { cells } ) ;
424479 }
425480
@@ -497,7 +552,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
497552 const cell = this . visibleCells . find ( c => c . id === request . id ) ;
498553 if ( cell ) {
499554 // This is an actual edit.
500- const contents = concatMultilineString ( cell . data . source ) ;
555+ const contents = concatMultilineStringInput ( cell . data . source ) ;
501556 const before = contents . substr ( 0 , change . rangeOffset ) ;
502557 const after = contents . substr ( change . rangeOffset + change . rangeLength ) ;
503558 const newContents = `${ before } ${ normalized } ${ after } ` ;
@@ -533,8 +588,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
533588 this . visibleCells = cells ;
534589
535590 // Save our dirty state in the storage for reopen later
536- const notebook = await this . jupyterExporter . translateToNotebook ( this . visibleCells , undefined ) ;
537- await this . storeContents ( JSON . stringify ( notebook ) ) ;
591+ await this . storeContents ( this . generateNotebookContent ( cells ) ) ;
538592
539593 // Indicate dirty
540594 await this . setDirty ( ) ;
@@ -569,10 +623,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
569623 tempFile = await this . fileSystem . createTemporaryFile ( '.ipynb' ) ;
570624
571625 // Translate the cells into a notebook
572- const notebook = await this . jupyterExporter . translateToNotebook ( cells , undefined ) ;
573-
574- // Write the cells to this file
575- await this . fileSystem . writeFile ( tempFile . filePath , JSON . stringify ( notebook ) , { encoding : 'utf-8' } ) ;
626+ await this . fileSystem . writeFile ( tempFile . filePath , this . generateNotebookContent ( cells ) , { encoding : 'utf-8' } ) ;
576627
577628 // Import this file and show it
578629 const contents = await this . importer . importFromFile ( tempFile . filePath ) ;
@@ -594,6 +645,23 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
594645 await this . documentManager . showTextDocument ( doc , ViewColumn . One ) ;
595646 }
596647
648+ private fixupCell ( cell : nbformat . ICell ) : nbformat . ICell {
649+ // Source is usually a single string on input. Convert back to an array
650+ return {
651+ ...cell ,
652+ source : splitMultilineString ( cell . source )
653+ } ;
654+ }
655+
656+ private generateNotebookContent ( cells : ICell [ ] ) : string {
657+ // Reuse our original json except for the cells.
658+ const json = {
659+ ...this . notebookJson ,
660+ cells : cells . map ( c => this . fixupCell ( c . data ) )
661+ } ;
662+ return JSON . stringify ( json , null , this . indentAmount ) ;
663+ }
664+
597665 @captureTelemetry ( Telemetry . Save , undefined , true )
598666 private async saveToDisk ( ) : Promise < void > {
599667 try {
@@ -617,9 +685,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
617685 }
618686
619687 if ( fileToSaveTo && isDirty ) {
620- // Save our visible cells into the file
621- const notebook = await this . jupyterExporter . translateToNotebook ( this . visibleCells , undefined ) ;
622- await this . fileSystem . writeFile ( fileToSaveTo . fsPath , JSON . stringify ( notebook ) ) ;
688+ // Write out our visible cells
689+ await this . fileSystem . writeFile ( fileToSaveTo . fsPath , this . generateNotebookContent ( this . visibleCells ) ) ;
623690
624691 // Update our file name and dirty state
625692 this . _file = fileToSaveTo ;
0 commit comments