Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d97fee9

Browse files
authored
Support tracking dirty state of unclosed files (microsoft#7532)
* Make a 'dirty' copy of a notebook when closing VS so that on reopen it is still dirty * Refactor storage * Merge with master * Fix debugger logging too * Fix linter problems * Remove memento from the provider. Not necessary
1 parent ed38ea2 commit d97fee9

File tree

5 files changed

+140
-85
lines changed

5 files changed

+140
-85
lines changed

news/2 Fixes/7418.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement dirty file tracking for notebooks so that on reopening of VS code they are shown in the dirty state.
2+
Canceling the save will get them back to their on disk state.

src/client/datascience/interactive-ipynb/nativeEditor.ts

+119-77
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import '../../common/extensions';
55

66
import * as fastDeepEqual from 'fast-deep-equal';
7-
import { inject, injectable, multiInject } from 'inversify';
7+
import { inject, injectable, multiInject, named } from 'inversify';
88
import * as path from 'path';
9-
import { Event, EventEmitter, Uri, ViewColumn } from 'vscode';
9+
import { Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode';
1010

1111
import {
1212
IApplicationShell,
@@ -19,7 +19,7 @@ import {
1919
import { ContextKey } from '../../common/contextKey';
2020
import { traceError } from '../../common/logger';
2121
import { IFileSystem, TemporaryFile } from '../../common/platform/types';
22-
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
22+
import { IConfigurationService, IDisposableRegistry, IMemento, WORKSPACE_MEMENTO } from '../../common/types';
2323
import { createDeferred, Deferred } from '../../common/utils/async';
2424
import * as localize from '../../common/utils/localize';
2525
import { StopWatch } from '../../common/utils/stopWatch';
@@ -62,6 +62,12 @@ import {
6262
IThemeFinder
6363
} from '../types';
6464

65+
enum AskForSaveResult {
66+
Yes,
67+
No,
68+
Cancel
69+
}
70+
6571
@injectable()
6672
export class NativeEditor extends InteractiveBase implements INotebookEditor {
6773
private closedEvent: EventEmitter<INotebookEditor> = new EventEmitter<INotebookEditor>();
@@ -96,7 +102,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
96102
@inject(IJupyterVariables) jupyterVariables: IJupyterVariables,
97103
@inject(IJupyterDebugger) jupyterDebugger: IJupyterDebugger,
98104
@inject(INotebookImporter) private importer: INotebookImporter,
99-
@inject(IDataScienceErrorHandler) errorHandler: IDataScienceErrorHandler
105+
@inject(IDataScienceErrorHandler) errorHandler: IDataScienceErrorHandler,
106+
@inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceStorage: Memento
100107
) {
101108
super(
102109
listeners,
@@ -137,49 +144,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
137144
}
138145

139146
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();
183149
}
184150

185151
public async load(content: string, file: Uri): Promise<void> {
@@ -195,16 +161,22 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
195161
// Show ourselves
196162
await this.show();
197163

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;
205176

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+
}
208180
}
209181

210182
public get closed(): Event<INotebookEditor> {
@@ -268,7 +240,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
268240
// Update our title to match
269241
if (this._dirty) {
270242
this._dirty = false;
271-
this.setDirty();
243+
await this.setDirty();
272244
} else {
273245
this.setTitle(path.basename(this._file.fsPath));
274246
}
@@ -285,9 +257,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
285257
protected submitNewCell(info: ISubmitNewCell) {
286258
// If there's any payload, it has the code and the id
287259
if (info && info.code && info.id) {
288-
// Update dirtiness
289-
this.setDirty();
290-
291260
// Send to ourselves.
292261
this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id).ignoreErrors();
293262

@@ -303,9 +272,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
303272
protected async reexecuteCell(info: ISubmitNewCell): Promise<void> {
304273
// If there's any payload, it has the code and the id
305274
if (info && info.code && info.id) {
306-
// Update dirtiness
307-
this.setDirty();
308-
309275
// Clear the result if we've run before
310276
await this.clearResult(info.id);
311277

@@ -345,11 +311,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
345311

346312
// Also keep track of our visible cells. We use this to save to the file when we close
347313
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();
353315
}
354316
}
355317

@@ -370,6 +332,52 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
370332
return Promise.resolve();
371333
}
372334

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+
373381
private editCell(request: IEditCell) {
374382
// Apply the changes to the visible cell list. We won't get an update until
375383
// submission otherwise
@@ -387,26 +395,60 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
387395
const newContents = `${before}${normalized}${after}`;
388396
if (contents !== newContents) {
389397
cell.data.source = newContents;
390-
this.setDirty();
398+
this.setDirty().ignoreErrors();
391399
}
392400
}
393401
}
394402
}
395403

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> {
397437
if (!this._dirty) {
398438
this._dirty = true;
399439
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
401442
this.modifiedEvent.fire(this);
402443
}
403444
}
404445

405-
private setClean(): void {
446+
private async setClean(): Promise<void> {
406447
if (this._dirty) {
407448
this._dirty = false;
408449
this.setTitle(`${path.basename(this.file.fsPath)}`);
409-
this.postMessage(InteractiveWindowMessages.NotebookClean).ignoreErrors();
450+
await this.storeContents(undefined);
451+
await this.postMessage(InteractiveWindowMessages.NotebookClean);
410452
}
411453
}
412454

@@ -445,15 +487,15 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
445487
await this.documentManager.showTextDocument(doc, ViewColumn.One);
446488
}
447489

448-
private async saveContents(forceAsk: boolean, skipUI: boolean): Promise<void> {
490+
private async saveToDisk(): Promise<void> {
449491
try {
450492
let fileToSaveTo: Uri | undefined = this.file;
451493
let isDirty = this._dirty;
452494

453495
// Ask user for a save as dialog if no title
454496
const baseName = path.basename(this.file.fsPath);
455497
const isUntitled = baseName.includes(localize.DataScience.untitledNotebookFileName());
456-
if (!skipUI && (isUntitled || forceAsk)) {
498+
if (isUntitled) {
457499
const filtersKey = localize.DataScience.dirtyNotebookDialogFilter();
458500
const filtersObject: { [name: string]: string[] } = {};
459501
filtersObject[filtersKey] = ['ipynb'];
@@ -470,7 +512,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
470512
// Save our visible cells into the file
471513
const notebook = await this.jupyterExporter.translateToNotebook(this.visibleCells, undefined);
472514
await this.fileSystem.writeFile(fileToSaveTo.fsPath, JSON.stringify(notebook));
473-
this.setClean();
515+
await this.setClean();
474516
}
475517

476518
} catch (e) {
@@ -480,7 +522,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
480522

481523
private saveAll(args: ISaveAll) {
482524
this.visibleCells = args.cells;
483-
this.saveContents(false, false).ignoreErrors();
525+
this.saveToDisk().ignoreErrors();
484526
}
485527

486528
private logNativeCommand(args: INativeCommand) {

src/client/datascience/interactive-ipynb/nativeEditorProvider.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,22 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp
3939
if (findFilesPromise && findFilesPromise.then) {
4040
findFilesPromise.then(r => this.notebookCount += r.length);
4141
}
42+
43+
// // Reopen our list of files that were open during shutdown. Actually not doing this for now. The files
44+
// don't open until the extension loads and all they all steal focus.
45+
// const uriList = this.workspaceStorage.get<Uri[]>(NotebookUriListStorageKey);
46+
// if (uriList && uriList.length) {
47+
// uriList.forEach(u => {
48+
// this.fileSystem.readFile(u.fsPath).then(c => this.open(u, c).ignoreErrors()).ignoreErrors();
49+
// });
50+
// }
4251
}
4352

4453
public async dispose(): Promise<void> {
4554
// Send a bunch of telemetry
4655
sendTelemetryEvent(Telemetry.NotebookOpenCount, this.openedNotebookCount);
4756
sendTelemetryEvent(Telemetry.NotebookRunCount, this.executedEditors.size);
4857
sendTelemetryEvent(Telemetry.NotebookWorkspaceCount, this.notebookCount);
49-
50-
// Try to save all of the currently dirty editors
51-
await Promise.all(this.editors.map(e => e.save()));
5258
}
5359

5460
public get activeEditor(): INotebookEditor | undefined {
@@ -67,8 +73,9 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp
6773
let editor = this.activeEditors.get(file.fsPath);
6874
if (!editor) {
6975
editor = await this.create(file, contents);
70-
this.activeEditors.set(file.fsPath, editor);
71-
this.openedNotebookCount += 1;
76+
this.onOpenedEditor(editor);
77+
} else {
78+
await editor.show();
7279
}
7380
return editor;
7481
}
@@ -125,6 +132,11 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp
125132
this.executedEditors.add(e.file.fsPath);
126133
}
127134

135+
private onOpenedEditor(e: INotebookEditor) {
136+
this.activeEditors.set(e.file.fsPath, e);
137+
this.openedNotebookCount += 1;
138+
}
139+
128140
private async getNextNewNotebookUri(): Promise<Uri> {
129141
// Start in the root and look for files starting with untitled
130142
let number = 1;

src/client/datascience/jupyter/jupyterDebugger.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ export class JupyterDebugger implements IJupyterDebugger, ICellHashListener {
126126
private traceCellResults(prefix: string, results: ICell[]) {
127127
if (results.length > 0 && results[0].data.cell_type === 'code') {
128128
const cell = results[0].data as nbformat.ICodeCell;
129-
const error = cell.outputs[0].evalue;
129+
const error = cell.outputs && cell.outputs[0] ? cell.outputs[0].evalue : undefined;
130130
if (error) {
131131
traceError(`${prefix} Error : ${error}`);
132-
} else {
132+
} else if (cell.outputs && cell.outputs[0]) {
133133
const data = cell.outputs[0].data;
134134
const text = cell.outputs[0].text;
135135
traceInfo(`${prefix} Output: ${text || JSON.stringify(data)}`);

src/client/datascience/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ export interface INotebookEditor extends IInteractiveBase {
241241
readonly visible: boolean;
242242
readonly active: boolean;
243243
load(contents: string, file: Uri): Promise<void>;
244-
save(): Promise<void>;
245244
}
246245

247246
export const IInteractiveWindowListener = Symbol('IInteractiveWindowListener');

0 commit comments

Comments
 (0)