Skip to content
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/1 Enhancements/7831.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ability to auto-save chagnes made to the notebook.
6 changes: 4 additions & 2 deletions src/client/common/application/applicationShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
// tslint:disable:no-var-requires no-any unified-signatures

import { injectable } from 'inversify';
import { CancellationToken, Disposable, env, InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, ProgressOptions, QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, TreeView, TreeViewOptions, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode';
import { CancellationToken, Disposable, env, Event, InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, OutputChannel, Progress, ProgressOptions, QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, TreeView, TreeViewOptions, Uri, window, WindowState, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode';
import { IApplicationShell } from './types';

@injectable()
export class ApplicationShell implements IApplicationShell {
public get onDidChangeWindowState(): Event<WindowState> {
return window.onDidChangeWindowState;
}
public showInformationMessage(message: string, ...items: string[]): Thenable<string>;
public showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable<string>;
public showInformationMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T>;
Expand Down Expand Up @@ -81,5 +84,4 @@ export class ApplicationShell implements IApplicationShell {
public createOutputChannel(name: string): OutputChannel {
return window.createOutputChannel(name);
}

}
7 changes: 7 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
TreeViewOptions,
Uri,
ViewColumn,
WindowState,
WorkspaceConfiguration,
WorkspaceEdit,
WorkspaceFolder,
Expand All @@ -64,6 +65,12 @@ import { ICommandNameArgumentTypeMapping } from './commands';

export const IApplicationShell = Symbol('IApplicationShell');
export interface IApplicationShell {
/**
* An [event](#Event) which fires when the focus state of the current window
* changes. The value of the event represents whether the window is focused.
*/
readonly onDidChangeWindowState: Event<WindowState>;

showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined>;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export namespace InteractiveWindowMessages {
export const Interrupt = 'interrupt';
export const SubmitNewCell = 'submit_new_cell';
export const UpdateSettings = SharedMessages.UpdateSettings;
// Message sent to React component from extension asking it to save the notebook.
export const DoSave = 'DoSave';
export const SendInfo = 'send_info';
export const Started = SharedMessages.Started;
export const AddedSysInfo = 'added_sys_info';
Expand Down
142 changes: 142 additions & 0 deletions src/client/datascience/interactive-ipynb/autoSaveService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { ConfigurationChangeEvent, Event, EventEmitter, TextEditor, Uri } from 'vscode';
import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { traceError } from '../../common/logger';
import { IFileSystem } from '../../common/platform/types';
import { IDisposable } from '../../common/types';
import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
import { FileSettings, IInteractiveWindowListener, INotebookEditor, INotebookEditorProvider } from '../types';

// tslint:disable: no-any

/**
* Sends notifications to Notebooks to save the notebook.
* Based on auto save settings, this class will regularly check for changes and send a save requet.
* If window state changes or active editor changes, then notify notebooks (if auto save is configured to do so).
* Monitor save and modified events on editor to determine its current dirty state.
*
* @export
* @class AutoSaveService
* @implements {IInteractiveWindowListener}
*/
@injectable()
export class AutoSaveService implements IInteractiveWindowListener {
private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ message: string; payload: any }>();
private disposables: IDisposable[] = [];
private notebookUri?: Uri;
private timeout?: ReturnType<typeof setTimeout>;
constructor(
@inject(IApplicationShell) appShell: IApplicationShell,
@inject(IDocumentManager) documentManager: IDocumentManager,
@inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider,
@inject(IFileSystem) private readonly fileSystem: IFileSystem,
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService
) {
this.workspace.onDidChangeConfiguration(this.onSettingsChanded.bind(this), this, this.disposables);
this.disposables.push(appShell.onDidChangeWindowState(this.onDidChangeWindowState.bind(this)));
this.disposables.push(documentManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor.bind(this)));
}

public get postMessage(): Event<{ message: string; payload: any }> {
return this.postEmitter.event;
}

public onMessage(message: string, payload?: any): void {
if (message === InteractiveWindowMessages.NotebookIdentity) {
this.notebookUri = Uri.parse((payload as INotebookIdentity).resource);
}
if (message === InteractiveWindowMessages.LoadAllCellsComplete) {
const notebook = this.getNotebook();
if (!notebook) {
traceError(`Received message ${message}, but there is no notebook for ${this.notebookUri ? this.notebookUri.fsPath : undefined}`);
return;
}
this.disposables.push(notebook.modified(this.onNotebookModified, this, this.disposables));
this.disposables.push(notebook.saved(this.onNotebookSaved, this, this.disposables));
}
}
public dispose(): void | undefined {
this.disposables.filter(item => !!item).forEach(item => item.dispose());
this.clearTimeout();
}
private onNotebookModified(_: INotebookEditor) {
// If we haven't started a timer, then start if necessary.
if (!this.timeout) {
this.setTimer();
}
}
private onNotebookSaved(_: INotebookEditor) {
// If we haven't started a timer, then start if necessary.
if (!this.timeout) {
this.setTimer();
}
}
private getNotebook(): INotebookEditor | undefined {
const uri = this.notebookUri;
if (!uri) {
return;
}
return this.notebookProvider.editors.find(item => this.fileSystem.arePathsSame(item.file.fsPath, uri.fsPath));
}
private getAutoSaveSettings(): FileSettings {
const filesConfig = this.workspace.getConfiguration('files', this.notebookUri);
return {
autoSave: filesConfig.get('autoSave', 'off'),
autoSaveDelay: filesConfig.get('autoSaveDelay', 1000)
};
}
private onSettingsChanded(e: ConfigurationChangeEvent) {
if (e.affectsConfiguration('files.autoSave') || e.affectsConfiguration('files.autoSaveDelay')) {
// Reset the timer, as we may have increased it, turned it off or other.
this.clearTimeout();
this.setTimer();
}
}
private setTimer() {
const settings = this.getAutoSaveSettings();
if (!settings || settings.autoSave === 'off') {
return;
}
if (settings && settings.autoSave === 'afterDelay') {
// Add a timeout to save after n milli seconds.
// Do not use setInterval, as that will cause all handlers to queue up.
this.timeout = setTimeout(() => {
this.save();
}, settings.autoSaveDelay);
}
}
private clearTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
}
private save() {
this.clearTimeout();
const notebook = this.getNotebook();
if (notebook && notebook.isDirty) {
// Notify webview to perform a save.
this.postEmitter.fire({ message: InteractiveWindowMessages.DoSave, payload: undefined });
} else {
this.setTimer();
}
}
private onDidChangeWindowState() {
const settings = this.getAutoSaveSettings();
if (settings && settings.autoSave === 'onWindowChange') {
this.save();
}
}
private onDidChangeActiveTextEditor(_e?: TextEditor) {
const settings = this.getAutoSaveSettings();
if (settings && settings.autoSave === 'onFocusChange') {
this.save();
}
}
}
79 changes: 43 additions & 36 deletions src/client/datascience/interactive-ipynb/nativeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import * as path from 'path';
import * as uuid from 'uuid/v4';
import { Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode';

import {
IApplicationShell,
ICommandManager,
IDocumentManager,
ILiveShareApi,
IWebPanelProvider,
IWorkspaceService
} from '../../common/application/types';
import { IApplicationShell, ICommandManager, IDocumentManager, ILiveShareApi, IWebPanelProvider, IWorkspaceService } from '../../common/application/types';
import { ContextKey } from '../../common/contextKey';
import { traceError } from '../../common/logger';
import { IFileSystem, TemporaryFile } from '../../common/platform/types';
Expand All @@ -28,21 +21,9 @@ import { EXTENSION_ROOT_DIR } from '../../constants';
import { IInterpreterService } from '../../interpreter/contracts';
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
import { concatMultilineString } from '../common';
import {
EditorContexts,
Identifiers,
NativeKeyboardCommandTelemetryLookup,
NativeMouseCommandTelemetryLookup,
Telemetry
} from '../constants';
import { EditorContexts, Identifiers, NativeKeyboardCommandTelemetryLookup, NativeMouseCommandTelemetryLookup, Telemetry } from '../constants';
import { InteractiveBase } from '../interactive-common/interactiveBase';
import {
IEditCell,
INativeCommand,
InteractiveWindowMessages,
ISaveAll,
ISubmitNewCell
} from '../interactive-common/interactiveWindowTypes';
import { IEditCell, INativeCommand, InteractiveWindowMessages, ISaveAll, ISubmitNewCell } from '../interactive-common/interactiveWindowTypes';
import {
CellState,
ICell,
Expand Down Expand Up @@ -74,6 +55,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
private closedEvent: EventEmitter<INotebookEditor> = new EventEmitter<INotebookEditor>();
private executedEvent: EventEmitter<INotebookEditor> = new EventEmitter<INotebookEditor>();
private modifiedEvent: EventEmitter<INotebookEditor> = new EventEmitter<INotebookEditor>();
private savedEvent: EventEmitter<INotebookEditor> = new EventEmitter<INotebookEditor>();
private loadedPromise: Deferred<void> = createDeferred<void>();
private _file: Uri = Uri.file('');
private _dirty: boolean = false;
Expand Down Expand Up @@ -129,7 +111,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
errorHandler,
path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'native-editor', 'index_bundle.js'),
localize.DataScience.nativeEditorTitle(),
ViewColumn.Active);
ViewColumn.Active
);
}

public get visible(): boolean {
Expand Down Expand Up @@ -187,6 +170,14 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
return this.modifiedEvent.event;
}

public get saved(): Event<INotebookEditor> {
return this.savedEvent.event;
}

public get isDirty(): boolean {
return this._dirty;
}

// tslint:disable-next-line: no-any
public onMessage(message: string, payload: any) {
super.onMessage(message, payload);
Expand Down Expand Up @@ -269,9 +260,19 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id).ignoreErrors();

// Activate the other side, and send as if came from a file
this.ipynbProvider.show(this.file).then(_v => {
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id, debug: false });
}).ignoreErrors();
this.ipynbProvider
.show(this.file)
.then(_v => {
this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {
code: info.code,
file: Identifiers.EmptyFileName,
line: 0,
id: info.id,
originator: this.id,
debug: false
});
})
.ignoreErrors();
}
}

Expand All @@ -289,7 +290,14 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {

// Activate the other side, and send as if came from a file
await this.ipynbProvider.show(this.file);
this.shareMessage(InteractiveWindowMessages.RemoteReexecuteCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id, debug: false });
this.shareMessage(InteractiveWindowMessages.RemoteReexecuteCode, {
code: info.code,
file: Identifiers.EmptyFileName,
line: 0,
id: info.id,
originator: this.id,
debug: false
});
}
} catch (exc) {
// Make this error our cell output
Expand All @@ -298,10 +306,12 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
data: {
source: info.code,
cell_type: 'code',
outputs: [{
output_type: 'error',
evalue: exc.toString()
}],
outputs: [
{
output_type: 'error',
evalue: exc.toString()
}
],
metadata: {},
execution_count: null
},
Expand All @@ -318,7 +328,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {

// Handle an error
await this.errorHandler.handleError(exc);

}
}

Expand Down Expand Up @@ -395,8 +404,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
cell_type: 'code',
outputs: [],
source: [],
metadata: {
},
metadata: {},
execution_count: null
}
};
Expand Down Expand Up @@ -570,7 +578,6 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
if (contents) {
await this.viewDocument(contents);
}

} catch (e) {
await this.errorHandler.handleError(e);
} finally {
Expand Down Expand Up @@ -615,8 +622,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
// Update our file name and dirty state
this._file = fileToSaveTo;
await this.setClean();
this.savedEvent.fire(this);
}

} catch (e) {
traceError(e);
}
Expand Down
2 changes: 2 additions & 0 deletions src/client/datascience/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DotNetIntellisenseProvider } from './interactive-common/intellisense/do
import { JediIntellisenseProvider } from './interactive-common/intellisense/jediIntellisenseProvider';
import { LinkProvider } from './interactive-common/linkProvider';
import { ShowPlotListener } from './interactive-common/showPlotListener';
import { AutoSaveService } from './interactive-ipynb/autoSaveService';
import { NativeEditor } from './interactive-ipynb/nativeEditor';
import { NativeEditorCommandListener } from './interactive-ipynb/nativeEditorCommandListener';
import { NativeEditorProvider } from './interactive-ipynb/nativeEditorProvider';
Expand Down Expand Up @@ -123,6 +124,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, wrapType(ShowPlotListener));
serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, wrapType(DebugListener));
serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, wrapType(GatherListener));
serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, wrapType(AutoSaveService));
serviceManager.addSingleton<IPlotViewerProvider>(IPlotViewerProvider, wrapType(PlotViewerProvider));
serviceManager.add<IPlotViewer>(IPlotViewer, wrapType(PlotViewer));
serviceManager.addSingleton<IJupyterDebugger>(IJupyterDebugger, wrapType(JupyterDebugger));
Expand Down
Loading