Skip to content

Fix save on close #13567

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/2 Fixes/11711.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix saving during close and auto backup to actually save a notebook.
4 changes: 2 additions & 2 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[DSCommands.SetJupyterKernel]: [KernelConnectionMetadata, Uri, undefined | Uri];
[DSCommands.SwitchJupyterKernel]: [ISwitchKernelOptions | undefined];
[DSCommands.SelectJupyterCommandLine]: [undefined | Uri];
[DSCommands.SaveNotebookNonCustomEditor]: [Uri];
[DSCommands.SaveAsNotebookNonCustomEditor]: [Uri, Uri];
[DSCommands.SaveNotebookNonCustomEditor]: [INotebookModel];
[DSCommands.SaveAsNotebookNonCustomEditor]: [INotebookModel, Uri];
[DSCommands.OpenNotebookNonCustomEditor]: [Uri];
[DSCommands.GatherQuality]: [string];
[DSCommands.LatestExtension]: [string];
Expand Down
8 changes: 6 additions & 2 deletions src/client/datascience/gather/gatherLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify';
import cloneDeep = require('lodash/cloneDeep');
import { extensions } from 'vscode';
import { concatMultilineString } from '../../../datascience-ui/common';
import { traceError } from '../../common/logger';
import { traceError, traceInfo } from '../../common/logger';
import { IConfigurationService } from '../../common/types';
import { noop } from '../../common/utils/misc';
import { sendTelemetryEvent } from '../../telemetry';
Expand Down Expand Up @@ -69,7 +69,11 @@ export class GatherLogger implements IGatherLogger {
}
}
const api = ext.exports;
this.gather = api.getGatherProvider();
try {
this.gather = api.getGatherProvider();
} catch {
traceInfo(`Gather not installed`);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export class NativeEditorOldWebView extends NativeEditor {
}
try {
if (!this.isUntitled) {
await this.commandManager.executeCommand(Commands.SaveNotebookNonCustomEditor, this.model?.file);
await this.commandManager.executeCommand(Commands.SaveNotebookNonCustomEditor, this.model);
this.savedEvent.fire(this);
return;
}
Expand All @@ -295,7 +295,7 @@ export class NativeEditorOldWebView extends NativeEditor {
if (fileToSaveTo) {
await this.commandManager.executeCommand(
Commands.SaveAsNotebookNonCustomEditor,
this.model.file,
this.model,
fileToSaveTo
);
this.savedEvent.fire(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class NativeEditorProviderOld extends NativeEditorProvider {
@inject(IWorkspaceService) workspace: IWorkspaceService,
@inject(IConfigurationService) configuration: IConfigurationService,
@inject(ICustomEditorService) customEditorService: ICustomEditorService,
@inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem,
@inject(IDataScienceFileSystem) fs: IDataScienceFileSystem,
@inject(IDocumentManager) private documentManager: IDocumentManager,
@inject(ICommandManager) private readonly cmdManager: ICommandManager,
@inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler,
Expand All @@ -98,7 +98,8 @@ export class NativeEditorProviderOld extends NativeEditorProvider {
configuration,
customEditorService,
storage,
notebookProvider
notebookProvider,
fs
);

// No live share sync required as open document from vscode will give us our contents.
Expand All @@ -107,21 +108,18 @@ export class NativeEditorProviderOld extends NativeEditorProvider {
this.documentManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditorHandler.bind(this))
);
this.disposables.push(
this.cmdManager.registerCommand(Commands.SaveNotebookNonCustomEditor, async (resource: Uri) => {
const customDocument = this.customDocuments.get(resource.fsPath);
if (customDocument) {
await this.saveCustomDocument(customDocument, new CancellationTokenSource().token);
}
this.cmdManager.registerCommand(Commands.SaveNotebookNonCustomEditor, async (model: INotebookModel) => {
await this.storage.save(model, new CancellationTokenSource().token);
})
);
this.disposables.push(
this.cmdManager.registerCommand(
Commands.SaveAsNotebookNonCustomEditor,
async (resource: Uri, targetResource: Uri) => {
const customDocument = this.customDocuments.get(resource.fsPath);
async (model: INotebookModel, targetResource: Uri) => {
await this.storage.saveAs(model, targetResource);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sure the correct model is used for saving.

const customDocument = this.customDocuments.get(model.file.fsPath);
if (customDocument) {
await this.saveCustomDocumentAs(customDocument, targetResource);
this.customDocuments.delete(resource.fsPath);
this.customDocuments.delete(model.file.fsPath);
this.customDocuments.set(targetResource.fsPath, { ...customDocument, uri: targetResource });
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/client/datascience/notebookStorage/nativeEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
@inject(IConfigurationService) protected readonly configuration: IConfigurationService,
@inject(ICustomEditorService) private customEditorService: ICustomEditorService,
@inject(INotebookStorageProvider) protected readonly storage: INotebookStorageProvider,
@inject(INotebookProvider) private readonly notebookProvider: INotebookProvider
@inject(INotebookProvider) private readonly notebookProvider: INotebookProvider,
@inject(IDataScienceFileSystem) protected readonly fs: IDataScienceFileSystem
) {
traceInfo(`id is ${this._id}`);

Expand Down Expand Up @@ -214,14 +215,18 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit
public async loadModel(file: Uri, contents?: string, backupId?: string): Promise<INotebookModel>;
// tslint:disable-next-line: no-any
public async loadModel(file: Uri, contents?: string, options?: any): Promise<INotebookModel> {
// Every time we load a new untitled file, up the counter past the max value for this counter
this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter);
// Get the model that may match this file
let model = [...this.models.values()].find((m) => this.fs.arePathsSame(m.file, file));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was necessary for custom editor to find the correct model.

if (!model) {
// Every time we load a new untitled file, up the counter past the max value for this counter
this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter);

// Load our model from our storage object.
const model = await this.storage.getOrCreateModel(file, contents, options);
// Load our model from our storage object.
model = await this.storage.getOrCreateModel(file, contents, options);

// Make sure to listen to events on the model
this.trackModel(model);
// Make sure to listen to events on the model
this.trackModel(model);
}
return model;
}

Expand Down
21 changes: 16 additions & 5 deletions src/client/datascience/notebookStorage/nativeEditorStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import detectIndent = require('detect-indent');
import { VSCodeNotebookModel } from './vscNotebookModel';

const KeyPrefix = 'notebook-storage-';
export const KeyPrefix = 'notebook-storage-';
const NotebookTransferKey = 'notebook-transfered';

export function getNextUntitledCounter(file: Uri | undefined, currentValue: number): number {
Expand Down Expand Up @@ -178,21 +178,29 @@ export class NativeEditorStorage implements INotebookStorage {
// Keep track of the time when this data was saved.
// This way when we retrieve the data we can compare it against last modified date of the file.
const specialContents = contents ? JSON.stringify({ contents, lastModifiedTimeMs: Date.now() }) : undefined;
return this.writeToStorage(filePath, specialContents, cancelToken);
return this.writeToStorage(model.file, filePath, specialContents, cancelToken);
}

private async clearHotExit(file: Uri, backupId?: string): Promise<void> {
const key = backupId || this.getStaticStorageKey(file);
const filePath = this.getHashedFileName(key);
await this.writeToStorage(filePath);
await this.writeToStorage(undefined, filePath);
}

private async writeToStorage(filePath: string, contents?: string, cancelToken?: CancellationToken): Promise<void> {
private async writeToStorage(
owningFile: Uri | undefined,
filePath: string,
contents?: string,
cancelToken?: CancellationToken
): Promise<void> {
try {
if (!cancelToken?.isCancellationRequested) {
if (contents) {
await this.fs.createLocalDirectory(path.dirname(filePath));
if (!cancelToken?.isCancellationRequested) {
if (owningFile) {
this.trustService.trustNotebook(owningFile, contents).ignoreErrors();
}
await this.fs.writeLocalFile(filePath, contents);
}
} else {
Expand Down Expand Up @@ -374,6 +382,8 @@ export class NativeEditorStorage implements INotebookStorage {
if (isNotebookTrusted) {
model.trust();
}
} else {
model.trust();
}

return model;
Expand Down Expand Up @@ -407,9 +417,10 @@ export class NativeEditorStorage implements INotebookStorage {
}

private async getStoredContentsFromFile(file: Uri, key: string): Promise<string | undefined> {
const filePath = this.getHashedFileName(key);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was the removed one. I believe it's the root cause of the problem.

try {
// Use this to read from the extension global location
const contents = await this.fs.readLocalFile(file.fsPath);
const contents = await this.fs.readLocalFile(filePath);
const data = JSON.parse(contents);
// Check whether the file has been modified since the last time the contents were saved.
if (data && data.lastModifiedTimeMs && file.scheme === 'file') {
Expand Down
69 changes: 40 additions & 29 deletions src/datascience-ui/interactive-common/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { forceLoad } from '../transforms';
import { isAllowedAction, isAllowedMessage, postActionToExtension } from './helpers';
import { generatePostOfficeSendReducer } from './postOffice';
import { generateMonacoReducer, IMonacoState } from './reducers/monaco';
import { CommonActionType } from './reducers/types';
import { generateVariableReducer, IVariableState } from './reducers/variables';

function generateDefaultState(
Expand Down Expand Up @@ -109,19 +110,21 @@ function createSendInfoMiddleware(): Redux.Middleware<{}, IStore> {
}

// If cell vm count changed or selected cell changed, send the message
const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (
prevState.main.cellVMs.length !== afterState.main.cellVMs.length ||
getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId ||
prevState.main.undoStack.length !== afterState.main.undoStack.length ||
prevState.main.redoStack.length !== afterState.main.redoStack.length
) {
postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, {
cellCount: afterState.main.cellVMs.length,
undoCount: afterState.main.undoStack.length,
redoCount: afterState.main.redoStack.length,
selectedCell: currentSelection.selectedCellId
});
if (!action.type || action.type !== CommonActionType.UNMOUNT) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was this change for?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was necessary to add the check for the mounted web view being used by dispose. We were sending messages after the webview had been destroyed.

const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (
prevState.main.cellVMs.length !== afterState.main.cellVMs.length ||
getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId ||
prevState.main.undoStack.length !== afterState.main.undoStack.length ||
prevState.main.redoStack.length !== afterState.main.redoStack.length
) {
postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, {
cellCount: afterState.main.cellVMs.length,
undoCount: afterState.main.undoStack.length,
redoCount: afterState.main.redoStack.length,
selectedCell: currentSelection.selectedCellId
});
}
}
return res;
};
Expand Down Expand Up @@ -159,21 +162,26 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> {
});
};

// Special case for focusing a cell
const previousSelection = getSelectedAndFocusedInfo(prevState.main);
const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId });
}
if (previousSelection.selectedCellId !== currentSelection.selectedCellId && currentSelection.selectedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId });
}
// Special case for unfocusing a cell
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.UnfocusedCellEditor);
if (!action.type || action.type !== CommonActionType.UNMOUNT) {
// Special case for focusing a cell
const previousSelection = getSelectedAndFocusedInfo(prevState.main);
const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId });
}
if (
previousSelection.selectedCellId !== currentSelection.selectedCellId &&
currentSelection.selectedCellId
) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId });
}
// Special case for unfocusing a cell
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.UnfocusedCellEditor);
}
}

// Indicate settings updates
Expand Down Expand Up @@ -218,7 +226,10 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> {
sendMessage(InteractiveWindowMessages.ExecutionRendered);
}

if (!action.type || action.type !== InteractiveWindowMessages.FinishCell) {
if (
!action.type ||
(action.type !== InteractiveWindowMessages.FinishCell && action.type !== CommonActionType.UNMOUNT)
) {
// Might be a non finish but still update cells (like an undo or a delete)
const prevFinished = prevState.main.cellVMs
.filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ suite('DataScience - Native Editor Storage', () => {
when(executionProvider.serverStarted).thenReturn(serverStartedEvent.event);

when(trustService.isNotebookTrusted(anything(), anything())).thenReturn(Promise.resolve(true));
when(trustService.trustNotebook(anything(), anything())).thenReturn(Promise.resolve());
when(trustService.trustNotebook(anything(), anything())).thenCall(() => {
return Promise.resolve();
});

testIndex += 1;
when(crypto.createHash(anything(), 'string')).thenReturn(`${testIndex}`);
Expand Down Expand Up @@ -351,7 +353,7 @@ suite('DataScience - Native Editor Storage', () => {
context.object,
globalMemento,
localMemento,
trustService,
instance(trustService),
new NotebookModelFactory(false)
);

Expand Down
5 changes: 4 additions & 1 deletion src/test/datascience/mountedWebView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
IWebPanelOptions,
WebPanelMessage
} from '../../client/common/application/types';
import { traceInfo } from '../../client/common/logger';
import { traceError, traceInfo } from '../../client/common/logger';
import { IDisposable } from '../../client/common/types';
import { createDeferred } from '../../client/common/utils/async';
import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes';
Expand Down Expand Up @@ -236,6 +236,9 @@ export class MountedWebView implements IMountedWebView, IDisposable {
}
}
private postMessageToWebPanel(msg: any) {
if (this.disposed && !msg.type.startsWith(`DISPATCHED`)) {
traceError(`Posting to disposed mount.`);
}
if (this.webPanelListener) {
this.webPanelListener.onMessage(msg.type, msg.payload);
} else {
Expand Down
Loading