4
4
import '../../common/extensions' ;
5
5
6
6
import * as fastDeepEqual from 'fast-deep-equal' ;
7
- import { inject , injectable , multiInject } from 'inversify' ;
7
+ import { inject , injectable , multiInject , named } from 'inversify' ;
8
8
import * as path from 'path' ;
9
- import { Event , EventEmitter , Uri , ViewColumn } from 'vscode' ;
9
+ import { Event , EventEmitter , Memento , Uri , ViewColumn } from 'vscode' ;
10
10
11
11
import {
12
12
IApplicationShell ,
@@ -19,7 +19,7 @@ import {
19
19
import { ContextKey } from '../../common/contextKey' ;
20
20
import { traceError } from '../../common/logger' ;
21
21
import { IFileSystem , TemporaryFile } from '../../common/platform/types' ;
22
- import { IConfigurationService , IDisposableRegistry } from '../../common/types' ;
22
+ import { IConfigurationService , IDisposableRegistry , IMemento , WORKSPACE_MEMENTO } from '../../common/types' ;
23
23
import { createDeferred , Deferred } from '../../common/utils/async' ;
24
24
import * as localize from '../../common/utils/localize' ;
25
25
import { StopWatch } from '../../common/utils/stopWatch' ;
@@ -62,6 +62,12 @@ import {
62
62
IThemeFinder
63
63
} from '../types' ;
64
64
65
+ enum AskForSaveResult {
66
+ Yes ,
67
+ No ,
68
+ Cancel
69
+ }
70
+
65
71
@injectable ( )
66
72
export class NativeEditor extends InteractiveBase implements INotebookEditor {
67
73
private closedEvent : EventEmitter < INotebookEditor > = new EventEmitter < INotebookEditor > ( ) ;
@@ -96,7 +102,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
96
102
@inject ( IJupyterVariables ) jupyterVariables : IJupyterVariables ,
97
103
@inject ( IJupyterDebugger ) jupyterDebugger : IJupyterDebugger ,
98
104
@inject ( INotebookImporter ) private importer : INotebookImporter ,
99
- @inject ( IDataScienceErrorHandler ) errorHandler : IDataScienceErrorHandler
105
+ @inject ( IDataScienceErrorHandler ) errorHandler : IDataScienceErrorHandler ,
106
+ @inject ( IMemento ) @named ( WORKSPACE_MEMENTO ) private workspaceStorage : Memento
100
107
) {
101
108
super (
102
109
listeners ,
@@ -137,49 +144,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
137
144
}
138
145
139
146
public dispose ( ) : void {
140
- let allowClose = true ;
141
- const close = ( ) => {
142
- super . dispose ( ) ;
143
- if ( this . closedEvent ) {
144
- this . closedEvent . fire ( this ) ;
145
- }
146
- } ;
147
-
148
- // Ask user if they want to save if hotExit is not enabled.
149
- if ( this . _dirty ) {
150
- const files = this . workspaceService . getConfiguration ( 'files' , undefined ) ;
151
- const hotExit = files ? files . get ( 'hotExit' ) : 'off' ;
152
- if ( hotExit === 'off' ) {
153
- const message1 = localize . DataScience . dirtyNotebookMessage1 ( ) . format ( `${ path . basename ( this . file . fsPath ) } ` ) ;
154
- const message2 = localize . DataScience . dirtyNotebookMessage2 ( ) ;
155
- const yes = localize . DataScience . dirtyNotebookYes ( ) ;
156
- const no = localize . DataScience . dirtyNotebookNo ( ) ;
157
- allowClose = false ;
158
- // tslint:disable-next-line: messages-must-be-localized
159
- this . applicationShell . showInformationMessage ( `${ message1 } \n${ message2 } ` , { modal : true } , yes , no ) . then ( v => {
160
- // Check message to see if we're really closing or not.
161
- allowClose = true ;
162
-
163
- if ( v === yes ) {
164
- this . saveContents ( false , false ) . ignoreErrors ( ) ;
165
- } else if ( v === undefined ) {
166
- // We don't want to close, reopen
167
- allowClose = false ;
168
- this . reopen ( this . visibleCells ) . ignoreErrors ( ) ;
169
- }
170
-
171
- // Reapply close since we waited for a promise.
172
- if ( allowClose ) {
173
- close ( ) ;
174
- }
175
- } ) ;
176
- } else {
177
- this . saveContents ( false , false ) . ignoreErrors ( ) ;
178
- }
179
- }
180
- if ( allowClose ) {
181
- close ( ) ;
182
- }
147
+ super . dispose ( ) ;
148
+ this . close ( ) . ignoreErrors ( ) ;
183
149
}
184
150
185
151
public async load ( content : string , file : Uri ) : Promise < void > {
@@ -195,16 +161,22 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
195
161
// Show ourselves
196
162
await this . show ( ) ;
197
163
198
- // Load the contents of this notebook into our cells.
199
- const cells = content ? await this . importer . importCells ( content ) : [ ] ;
200
- this . visibleCells = cells ;
201
-
202
- // If that works, send the cells to the web view
203
- return this . postMessage ( InteractiveWindowMessages . LoadAllCells , { cells } ) ;
204
- }
164
+ // See if this file was stored in storage prior to shutdown
165
+ const dirtyContents = this . getStoredContents ( ) ;
166
+ if ( dirtyContents ) {
167
+ // This means we're dirty. Indicate dirty and load from this content
168
+ const cells = await this . importer . importCells ( dirtyContents ) ;
169
+ this . visibleCells = cells ;
170
+ await this . setDirty ( ) ;
171
+ return this . postMessage ( InteractiveWindowMessages . LoadAllCells , { cells } ) ;
172
+ } else {
173
+ // Load the contents of this notebook into our cells.
174
+ const cells = content ? await this . importer . importCells ( content ) : [ ] ;
175
+ this . visibleCells = cells ;
205
176
206
- public save ( ) : Promise < void > {
207
- return this . saveContents ( false , true ) ;
177
+ // If that works, send the cells to the web view
178
+ return this . postMessage ( InteractiveWindowMessages . LoadAllCells , { cells } ) ;
179
+ }
208
180
}
209
181
210
182
public get closed ( ) : Event < INotebookEditor > {
@@ -268,7 +240,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
268
240
// Update our title to match
269
241
if ( this . _dirty ) {
270
242
this . _dirty = false ;
271
- this . setDirty ( ) ;
243
+ await this . setDirty ( ) ;
272
244
} else {
273
245
this . setTitle ( path . basename ( this . _file . fsPath ) ) ;
274
246
}
@@ -285,9 +257,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
285
257
protected submitNewCell ( info : ISubmitNewCell ) {
286
258
// If there's any payload, it has the code and the id
287
259
if ( info && info . code && info . id ) {
288
- // Update dirtiness
289
- this . setDirty ( ) ;
290
-
291
260
// Send to ourselves.
292
261
this . submitCode ( info . code , Identifiers . EmptyFileName , 0 , info . id ) . ignoreErrors ( ) ;
293
262
@@ -303,9 +272,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
303
272
protected async reexecuteCell ( info : ISubmitNewCell ) : Promise < void > {
304
273
// If there's any payload, it has the code and the id
305
274
if ( info && info . code && info . id ) {
306
- // Update dirtiness
307
- this . setDirty ( ) ;
308
-
309
275
// Clear the result if we've run before
310
276
await this . clearResult ( info . id ) ;
311
277
@@ -345,11 +311,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
345
311
346
312
// Also keep track of our visible cells. We use this to save to the file when we close
347
313
if ( info && 'visibleCells' in info && this . loadedAllCells ) {
348
- const isDirty = ! fastDeepEqual ( this . visibleCells , info . visibleCells ) ;
349
- this . visibleCells = info . visibleCells ;
350
- if ( isDirty ) {
351
- this . setDirty ( ) ;
352
- }
314
+ this . updateVisibleCells ( info . visibleCells ) . ignoreErrors ( ) ;
353
315
}
354
316
}
355
317
@@ -370,6 +332,52 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
370
332
return Promise . resolve ( ) ;
371
333
}
372
334
335
+ private getStorageKey ( ) : string {
336
+ return `notebook-storage-${ this . _file . toString ( ) } ` ;
337
+ }
338
+
339
+ private getStoredContents ( ) : string | undefined {
340
+ return this . workspaceStorage . get < string > ( this . getStorageKey ( ) ) ;
341
+ }
342
+
343
+ private async storeContents ( contents ?: string ) : Promise < void > {
344
+ const key = this . getStorageKey ( ) ;
345
+ await this . workspaceStorage . update ( key , contents ) ;
346
+ }
347
+
348
+ private async close ( ) : Promise < void > {
349
+ // Ask user if they want to save. It seems hotExit has no bearing on
350
+ // whether or not we should ask
351
+ if ( this . _dirty ) {
352
+ const askResult = await this . askForSave ( ) ;
353
+ switch ( askResult ) {
354
+ case AskForSaveResult . Yes :
355
+ // Save the file
356
+ await this . saveToDisk ( ) ;
357
+
358
+ // Close it
359
+ this . closedEvent . fire ( this ) ;
360
+ break ;
361
+
362
+ case AskForSaveResult . No :
363
+ // Mark as not dirty, so we update our storage
364
+ await this . setClean ( ) ;
365
+
366
+ // Close it
367
+ this . closedEvent . fire ( this ) ;
368
+ break ;
369
+
370
+ default :
371
+ // Reopen
372
+ await this . reopen ( this . visibleCells ) ;
373
+ break ;
374
+ }
375
+ } else {
376
+ // Not dirty, just close normally.
377
+ this . closedEvent . fire ( this ) ;
378
+ }
379
+ }
380
+
373
381
private editCell ( request : IEditCell ) {
374
382
// Apply the changes to the visible cell list. We won't get an update until
375
383
// submission otherwise
@@ -387,26 +395,60 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
387
395
const newContents = `${ before } ${ normalized } ${ after } ` ;
388
396
if ( contents !== newContents ) {
389
397
cell . data . source = newContents ;
390
- this . setDirty ( ) ;
398
+ this . setDirty ( ) . ignoreErrors ( ) ;
391
399
}
392
400
}
393
401
}
394
402
}
395
403
396
- private setDirty ( ) : void {
404
+ private async askForSave ( ) : Promise < AskForSaveResult > {
405
+ const message1 = localize . DataScience . dirtyNotebookMessage1 ( ) . format ( `${ path . basename ( this . file . fsPath ) } ` ) ;
406
+ const message2 = localize . DataScience . dirtyNotebookMessage2 ( ) ;
407
+ const yes = localize . DataScience . dirtyNotebookYes ( ) ;
408
+ const no = localize . DataScience . dirtyNotebookNo ( ) ;
409
+ // tslint:disable-next-line: messages-must-be-localized
410
+ const result = await this . applicationShell . showInformationMessage ( `${ message1 } \n${ message2 } ` , { modal : true } , yes , no ) ;
411
+ switch ( result ) {
412
+ case yes :
413
+ return AskForSaveResult . Yes ;
414
+
415
+ case no :
416
+ return AskForSaveResult . No ;
417
+
418
+ default :
419
+ return AskForSaveResult . Cancel ;
420
+ }
421
+ }
422
+
423
+ private async updateVisibleCells ( cells : ICell [ ] ) : Promise < void > {
424
+ if ( ! fastDeepEqual ( this . visibleCells , cells ) ) {
425
+ this . visibleCells = cells ;
426
+
427
+ // Save our dirty state in the storage for reopen later
428
+ const notebook = await this . jupyterExporter . translateToNotebook ( this . visibleCells , undefined ) ;
429
+ await this . storeContents ( JSON . stringify ( notebook ) ) ;
430
+
431
+ // Indicate dirty
432
+ await this . setDirty ( ) ;
433
+ }
434
+ }
435
+
436
+ private async setDirty ( ) : Promise < void > {
397
437
if ( ! this . _dirty ) {
398
438
this . _dirty = true ;
399
439
this . setTitle ( `${ path . basename ( this . file . fsPath ) } *` ) ;
400
- this . postMessage ( InteractiveWindowMessages . NotebookDirty ) . ignoreErrors ( ) ;
440
+ await this . postMessage ( InteractiveWindowMessages . NotebookDirty ) ;
441
+ // Tell listeners we're dirty
401
442
this . modifiedEvent . fire ( this ) ;
402
443
}
403
444
}
404
445
405
- private setClean ( ) : void {
446
+ private async setClean ( ) : Promise < void > {
406
447
if ( this . _dirty ) {
407
448
this . _dirty = false ;
408
449
this . setTitle ( `${ path . basename ( this . file . fsPath ) } ` ) ;
409
- this . postMessage ( InteractiveWindowMessages . NotebookClean ) . ignoreErrors ( ) ;
450
+ await this . storeContents ( undefined ) ;
451
+ await this . postMessage ( InteractiveWindowMessages . NotebookClean ) ;
410
452
}
411
453
}
412
454
@@ -445,15 +487,15 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
445
487
await this . documentManager . showTextDocument ( doc , ViewColumn . One ) ;
446
488
}
447
489
448
- private async saveContents ( forceAsk : boolean , skipUI : boolean ) : Promise < void > {
490
+ private async saveToDisk ( ) : Promise < void > {
449
491
try {
450
492
let fileToSaveTo : Uri | undefined = this . file ;
451
493
let isDirty = this . _dirty ;
452
494
453
495
// Ask user for a save as dialog if no title
454
496
const baseName = path . basename ( this . file . fsPath ) ;
455
497
const isUntitled = baseName . includes ( localize . DataScience . untitledNotebookFileName ( ) ) ;
456
- if ( ! skipUI && ( isUntitled || forceAsk ) ) {
498
+ if ( isUntitled ) {
457
499
const filtersKey = localize . DataScience . dirtyNotebookDialogFilter ( ) ;
458
500
const filtersObject : { [ name : string ] : string [ ] } = { } ;
459
501
filtersObject [ filtersKey ] = [ 'ipynb' ] ;
@@ -470,7 +512,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
470
512
// Save our visible cells into the file
471
513
const notebook = await this . jupyterExporter . translateToNotebook ( this . visibleCells , undefined ) ;
472
514
await this . fileSystem . writeFile ( fileToSaveTo . fsPath , JSON . stringify ( notebook ) ) ;
473
- this . setClean ( ) ;
515
+ await this . setClean ( ) ;
474
516
}
475
517
476
518
} catch ( e ) {
@@ -480,7 +522,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
480
522
481
523
private saveAll ( args : ISaveAll ) {
482
524
this . visibleCells = args . cells ;
483
- this . saveContents ( false , false ) . ignoreErrors ( ) ;
525
+ this . saveToDisk ( ) . ignoreErrors ( ) ;
484
526
}
485
527
486
528
private logNativeCommand ( args : INativeCommand ) {
0 commit comments