123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- ServiceManager
- } from '@jupyterlab/services';
- import {
- IDisposable
- } from 'phosphor/lib/core/disposable';
- import {
- clearSignalData
- } from 'phosphor/lib/core/signaling';
- import {
- okButton, cancelButton, showDialog
- } from '../common/dialog';
- import {
- DocumentRegistry
- } from '../docregistry';
- /**
- * A class that manages the auto saving of a document.
- *
- * #### Notes
- * Implements https://github.com/ipython/ipython/wiki/IPEP-15:-Autosaving-the-IPython-Notebook.
- */
- export
- class SaveHandler implements IDisposable {
- /**
- * Construct a new save handler.
- */
- constructor(options: SaveHandler.IOptions) {
- this._manager = options.manager;
- this._context = options.context;
- this._minInterval = options.saveInterval * 1000 || 120000;
- this._interval = this._minInterval;
- // Restart the timer when the contents model is updated.
- this._context.fileChanged.connect(this._setTimer, this);
- this._context.disposed.connect(this.dispose, this);
- }
- /**
- * The save interval used by the timer (in seconds).
- */
- get saveInterval(): number {
- return this._interval / 1000;
- }
- set saveInterval(value: number) {
- this._minInterval = this._interval = value * 1000;
- if (this._isActive) {
- this._setTimer();
- }
- }
- /**
- * Get whether the handler is active.
- */
- get isActive(): boolean {
- return this._isActive;
- }
- /**
- * Get whether the save handler is disposed.
- */
- get isDisposed(): boolean {
- return this._context === null;
- }
- /**
- * Dispose of the resources used by the save handler.
- */
- dispose(): void {
- if (this._context === null) {
- return;
- }
- this._context = null;
- clearTimeout(this._autosaveTimer);
- clearSignalData(this);
- }
- /**
- * Start the autosaver.
- */
- start(): void {
- this._isActive = true;
- this._setTimer();
- }
- /**
- * Stop the autosaver.
- */
- stop(): void {
- this._isActive = false;
- clearTimeout(this._autosaveTimer);
- }
- /**
- * Set the timer.
- */
- private _setTimer(): void {
- clearTimeout(this._autosaveTimer);
- if (!this._isActive) {
- return;
- }
- this._autosaveTimer = window.setTimeout(() => {
- this._save();
- }, this._interval);
- }
- /**
- * Handle an autosave timeout.
- */
- private _save(): void {
- let context = this._context;
- // Trigger the next update.
- this._setTimer();
- if (!context) {
- return;
- }
- // Bail if the model is not dirty or it is read only, or the dialog
- // is already showing.
- if (!context.model.dirty || context.model.readOnly || this._inDialog) {
- return;
- }
- // Make sure the file has not changed on disk.
- let promise = this._manager.contents.get(context.path);
- promise.then(model => {
- if (!this.isDisposed && context.contentsModel &&
- model.last_modified !== context.contentsModel.last_modified) {
- return this._timeConflict(model.last_modified);
- }
- return this._finishSave();
- }, (err) => {
- return this._finishSave();
- }).catch(err => {
- console.error('Error in Auto-Save', err.message);
- });
- }
- /**
- * Handle a time conflict.
- */
- private _timeConflict(modified: string): Promise<void> {
- let localTime = new Date(this._context.contentsModel.last_modified);
- let remoteTime = new Date(modified);
- console.warn(`Last saving peformed ${localTime} ` +
- `while the current file seem to have been saved ` +
- `${remoteTime}`);
- let body = `The file has changed on disk since the last time we ` +
- `opened or saved it. ` +
- `Do you want to overwrite the file on disk with the version ` +
- ` open here, or load the version on disk (revert)?`;
- this._inDialog = true;
- return showDialog({
- title: 'File Changed', body, okText: 'OVERWRITE',
- buttons: [cancelButton, { text: 'REVERT' }, okButton]
- }).then(result => {
- if (this.isDisposed) {
- return;
- }
- this._inDialog = false;
- if (result.text === 'OVERWRITE') {
- return this._finishSave();
- } else if (result.text === 'REVERT') {
- return this._context.revert();
- }
- });
- }
- /**
- * Perform the save, adjusting the save interval as necessary.
- */
- private _finishSave(): Promise<void> {
- let start = new Date().getTime();
- return this._context.save().then(() => {
- if (this.isDisposed) {
- return;
- }
- let duration = new Date().getTime() - start;
- // New save interval: higher of 10x save duration or min interval.
- this._interval = Math.max(10 * duration, this._minInterval);
- // Restart the update to pick up the new interval.
- this._setTimer();
- });
- }
- private _autosaveTimer = -1;
- private _minInterval = -1;
- private _interval = -1;
- private _context: DocumentRegistry.Context = null;
- private _manager: ServiceManager.IManager = null;
- private _isActive = false;
- private _inDialog = false;
- }
- /**
- * A namespace for `SaveHandler` statics.
- */
- export
- namespace SaveHandler {
- /**
- * The options used to create a save handler.
- */
- export
- interface IOptions {
- /**
- * The context asssociated with the file.
- */
- context: DocumentRegistry.Context;
- /**
- * The service manager to use for checking last saved.
- */
- manager: ServiceManager.IManager;
- /**
- * The minimum save interval in seconds (default is two minutes).
- */
- saveInterval?: number;
- }
- }
|