From 9d1ba4b20a76ac5d01a66f6923dccf4b6943c739 Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Sat, 19 Aug 2017 15:53:57 -0400 Subject: [PATCH 1/7] added overwrite prevention to saving --- notebook/static/edit/js/editor.js | 92 ++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 4ab8249021..fce22acbef 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -4,11 +4,13 @@ define([ 'jquery', 'base/js/utils', + 'base/js/i18n', + 'base/js/dialog', 'codemirror/lib/codemirror', 'codemirror/mode/meta', 'codemirror/addon/comment/comment', - 'codemirror/addon/dialog/dialog', 'codemirror/addon/edit/closebrackets', + 'codemirror/addon/dialog/dialog', 'codemirror/addon/edit/matchbrackets', 'codemirror/addon/search/searchcursor', 'codemirror/addon/search/search', @@ -19,6 +21,8 @@ define([ function( $, utils, + i18n, + dialog, CodeMirror ) { "use strict"; @@ -33,6 +37,8 @@ function( this.file_path = options.file_path; this.config = options.config; this.file_extension_modes = options.file_extension_modes || {}; + this.last_modified = null; + this._changed_on_disk_dialog = null; this.codemirror = new CodeMirror($(this.selector)[0]); this.codemirror.on('changes', function(cm, changes){ @@ -100,6 +106,7 @@ function( that.generation = cm.changeGeneration(); that.events.trigger("file_loaded.Editor", model); that._clean_state(); + that.last_modified = new Date(model.last_modified); }).catch( function(error) { that.events.trigger("file_load_failed.Editor", error); @@ -198,6 +205,7 @@ function( var new_path = utils.url_path_join(parent, new_name); return this.contents.rename(this.file_path, new_path).then( function (model) { + that.last_modified = new Date(model.last_modified); that.file_path = model.path; that.events.trigger('file_renamed.Editor', model); that._set_mode_for_model(model); @@ -206,12 +214,18 @@ function( ); }; - Editor.prototype.save = function () { + Editor.prototype.save = function (check_last_modified) { /** save the file */ if (!this.save_enabled) { console.log("Not saving, save disabled"); return; } + + // used to check for last modified saves + if (check_last_modified === undefined) { + check_last_modified = true; + } + var model = { path: this.file_path, type: 'file', @@ -219,13 +233,77 @@ function( content: this.codemirror.getValue(), }; var that = this; + // record change generation for isClean + // (I don't know what this implies for the editor) this.generation = this.codemirror.changeGeneration(); - that.events.trigger("file_saving.Editor"); - return this.contents.save(this.file_path, model).then(function(data) { - that.events.trigger("file_saved.Editor", data); - that._clean_state(); - }); + + var _save = function () { + // What does this event do? Does this always need to happen, + // even if the file can't be saved? What is dependent on it? + that.events.trigger("file_saving.Editor"); + return that.contents.save(that.file_path, model).then(function(data) { + that.events.trigger("file_saved.Editor", data); + that.last_modified = new Date(data.last_modified); + that._clean_state(); + }); + } + + // check data after + if (check_last_modified) { + return this.contents.get(that.file_path, {content: false}).then( + function (data) { + var last_modified = new Date(data.last_modified); + // We want to check last_modified (disk) > that.last_modified (our last save) + // In some cases the filesystem reports an inconsistent time, + // so we allow 0.5 seconds difference before complaining. + console.log(that.last_modified); + if ((last_modified.getTime() - that.last_modified.getTime()) > 500) { // 500 ms + console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ + "while the current file seem to have been saved on `"+data.last_modified+"`"); + if (that._changed_on_disk_dialog !== null) { + console.log("Showing the modal"); + // update save callback on the confirmation button + that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); + // redisplay existing dialog + that._changed_on_disk_dialog.modal('show'); + } else { + // create new dialog + that._changed_on_disk_dialog = dialog.modal({ + keyboard_manager: that.keyboard_manager, + title: i18n.msg._("File changed"), + body: i18n.msg._("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 (reload the page)?"), + buttons: { + Reload: { + class: 'btn-warning', + click: function() { + window.location.reload(); + } + }, + Cancel: {}, + Overwrite: { + class: 'btn-danger save-confirm-btn', + click: function () { + _save(); + } + }, + } + }); + } + } else { + return _save(); + } + }, function (error) { + console.log(error); + // maybe it has been deleted or renamed? Go ahead and save. + // (need to test this) + return _save(); + }) + } else { + return _save(); + } }; Editor.prototype._clean_state = function(){ From 8c90419a31ad58fce5389bcbbf332749f61f648e Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Sat, 19 Aug 2017 16:13:05 -0400 Subject: [PATCH 2/7] rearranging order of require variables and edit to rename --- notebook/static/edit/js/editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index fce22acbef..b10499d529 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -9,8 +9,8 @@ define([ 'codemirror/lib/codemirror', 'codemirror/mode/meta', 'codemirror/addon/comment/comment', - 'codemirror/addon/edit/closebrackets', 'codemirror/addon/dialog/dialog', + 'codemirror/addon/edit/closebrackets', 'codemirror/addon/edit/matchbrackets', 'codemirror/addon/search/searchcursor', 'codemirror/addon/search/search', @@ -205,9 +205,9 @@ function( var new_path = utils.url_path_join(parent, new_name); return this.contents.rename(this.file_path, new_path).then( function (model) { - that.last_modified = new Date(model.last_modified); that.file_path = model.path; that.events.trigger('file_renamed.Editor', model); + that.last_modified = new Date(model.last_modified); that._set_mode_for_model(model); that._clean_state(); } From d468081fee485f8e76aafa07b800818b2c7af8bc Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Tue, 22 Aug 2017 15:22:17 -0400 Subject: [PATCH 3/7] added documentation, and fixed reload button --- notebook/static/edit/js/editor.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index b10499d529..6520e45bc7 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -198,6 +198,11 @@ function( } }; + /** + * Rename the file. + * @param {string} new_name + * @return {Promise} promise that resolves when the file is renamed. + */ Editor.prototype.rename = function (new_name) { /** rename the file */ var that = this; @@ -214,6 +219,13 @@ function( ); }; + + /** + * Save this file on the server. + * + * @param {boolean} check_last_modified - checks if file has been modified on disk + * @return {Promise} - promise that resolves when the notebook is saved. + */ Editor.prototype.save = function (check_last_modified) { /** save the file */ if (!this.save_enabled) { @@ -249,22 +261,28 @@ function( }); } - // check data after + /* + * Gets the current working file, and checks if the file has been modified on disk. If so, it + * creates & opens a modal that issues the user a warning and prompts them to overwrite the file. + * + * If it can't get the working file, it builds a new file and saves. + */ if (check_last_modified) { return this.contents.get(that.file_path, {content: false}).then( - function (data) { + function check_if_modified(data) { var last_modified = new Date(data.last_modified); // We want to check last_modified (disk) > that.last_modified (our last save) // In some cases the filesystem reports an inconsistent time, // so we allow 0.5 seconds difference before complaining. - console.log(that.last_modified); if ((last_modified.getTime() - that.last_modified.getTime()) > 500) { // 500 ms console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ "while the current file seem to have been saved on `"+data.last_modified+"`"); if (that._changed_on_disk_dialog !== null) { - console.log("Showing the modal"); - // update save callback on the confirmation button + // since the modal's event bindings are removed when destroyed, + // we reinstate save & reload callbacks on the confirmation & reload buttons that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); + that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()}); + // redisplay existing dialog that._changed_on_disk_dialog.modal('show'); } else { @@ -278,7 +296,7 @@ function( buttons: { Reload: { class: 'btn-warning', - click: function() { + click: function () { window.location.reload(); } }, @@ -298,7 +316,6 @@ function( }, function (error) { console.log(error); // maybe it has been deleted or renamed? Go ahead and save. - // (need to test this) return _save(); }) } else { From 4d45ec7c1b6da51d3e1d4140b174b6f237ea2133 Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Tue, 29 Aug 2017 13:54:57 -0400 Subject: [PATCH 4/7] followed suggestion by tom, started tests --- notebook/static/edit/js/editor.js | 12 ++-- notebook/tests/editor/save.js | 43 ++++++++++++++ notebook/tests/util.js | 97 +++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 notebook/tests/editor/save.js diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 6520e45bc7..95d7380797 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -246,15 +246,11 @@ function( }; var that = this; - // record change generation for isClean - // (I don't know what this implies for the editor) - this.generation = this.codemirror.changeGeneration(); - var _save = function () { - // What does this event do? Does this always need to happen, - // even if the file can't be saved? What is dependent on it? that.events.trigger("file_saving.Editor"); return that.contents.save(that.file_path, model).then(function(data) { + // record change generation for isClean + this.generation = this.codemirror.changeGeneration(); that.events.trigger("file_saved.Editor", data); that.last_modified = new Date(data.last_modified); that._clean_state(); @@ -278,8 +274,8 @@ function( console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ "while the current file seem to have been saved on `"+data.last_modified+"`"); if (that._changed_on_disk_dialog !== null) { - // since the modal's event bindings are removed when destroyed, - // we reinstate save & reload callbacks on the confirmation & reload buttons + // since the modal's event bindings are removed when destroyed, we reinstate + // save & reload callbacks on the confirmation & reload buttons that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()}); diff --git a/notebook/tests/editor/save.js b/notebook/tests/editor/save.js new file mode 100644 index 0000000000..6aee2c0a53 --- /dev/null +++ b/notebook/tests/editor/save.js @@ -0,0 +1,43 @@ +// +// Test prompt when overwriting a file that is modified on disk +// + +casper.editor_test(function () { + var fname = "has#hash and space and unicø∂e.py"; + + this.append_cell("s = '??'", 'code'); + + this.thenEvaluate(function (nbname) { + require(['base/js/events'], function (events) { + IPython.editor.set_notebook_name(nbname); + IPython._save_success = IPython._save_failed = false; + events.on('file_saved.Editor', function () { + IPython._save_success = true; + }); + IPython.notebook.save_notebook(); + }); + }, {nbname:nbname}); + + this.waitFor(function () { + return this.evaluate(function(){ + return IPython._save_failed || IPython._save_success; + }); + }); + + this.thenEvaluate(function(){ + IPython._checkpoint_created = false; + require(['base/js/events'], function (events) { + events.on('checkpoint_created.Notebook', function (evt, data) { + IPython._checkpoint_created = true; + }); + }); + IPython.notebook.save_checkpoint(); + }); + + this.waitFor(function() { + return this.evaluate(function () { + return IPython && IPython.notebook && true; + }); + }); + +}); diff --git a/notebook/tests/util.js b/notebook/tests/util.js index 75969a7cc6..d63f92672b 100644 --- a/notebook/tests/util.js +++ b/notebook/tests/util.js @@ -734,6 +734,103 @@ casper.dashboard_test = function (test) { }); }; +/** + * Editor Tests + * + * The functions below are utilities for setting up an editor and tearing it down + * after the test is over. + */ +caspser.open_new_file = function () { + // load up the jupyter notebook server (it's like running `jupyter notebook` in the shell) + var baseUrl = this.get_notebook_server(); + + // go to the base url, wait for it to load, then make a new file + this.start(baseUrl); + this.waitFor(this.page_loaded); + this.waitForSelector('#new-file a'); + this.thenClick('#new-file a'); + + // when the popup loads, go into that popup and wait until the main text box is loaded + this.withPopup(0, function () {this.waitForSelector('.CodeMirror-sizer');}); + + // now let's open the window where the file editor is displayed & load + this.then(function () { + this.open(this.popups[0].url); + }); + this.waitFor(this.page_loaded); + + // Hook the log and error methods of the console, forcing them to + // serialize their arguments before printing. This allows the + // Objects to cross into the phantom/slimer regime for display. + this.thenEvaluate(function(){ + var serialize_arguments = function(f, context) { + return function() { + var pretty_arguments = []; + for (var i = 0; i < arguments.length; i++) { + var value = arguments[i]; + if (value instanceof Object) { + var name = value.name || 'Object'; + // Print a JSON string representation of the object. + // If we don't do this, [Object object] gets printed + // by casper, which is useless. The long regular + // expression reduces the verbosity of the JSON. + pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ') + .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n') + .replace(/\n(\s+)?\n/g, '\n')); + } else { + pretty_arguments.push(value); + } + } + f.apply(context, pretty_arguments); + }; + }; + console.log = serialize_arguments(console.log, console); + console.error = serialize_arguments(console.error, console); + }); + + console.log('Editor loaded.') + +} + +casper.editor_test = function(test) { + // Wrap a notebook test to reduce boilerplate. + this.open_new_file(); + + // Echo whether or not we are running this test using SlimerJS + if (this.evaluate(function(){ + return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+ + })) { + console.log('This test is running in SlimerJS.'); + this.slimerjs = true; + } + + // Make sure to remove the onbeforeunload callback. This callback is + // responsible for the "Are you sure you want to quit?" type messages. + // PhantomJS ignores these prompts, SlimerJS does not which causes hangs. + this.then(function(){ + this.evaluate(function(){ + window.onbeforeunload = function(){}; + }); + }); + + this.then(test); + + // This is required to clean up the page we just finished with. If we don't call this + // casperjs will leak file descriptors of all the open WebSockets in that page. We + // have to set this.page=null so that next time casper.start runs, it will create a + // new page from scratch. + this.then(function () { + this.page.close(); + this.page = null; + }); + + // Run the browser automation. + this.run(function() { + this.test.done(); + }); +}; + + // note that this will only work for UNIQUE events -- if you want to // listen for the same event twice, this will not work! casper.event_test = function (name, events, action, timeout) { From 889f6c2670c3dd453132d91394495c079a57a87d Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Thu, 4 Jan 2018 17:08:24 -0500 Subject: [PATCH 5/7] Revert "followed suggestion by tom, started tests" This reverts commit 4d45ec7c1b6da51d3e1d4140b174b6f237ea2133. --- notebook/static/edit/js/editor.js | 12 ++-- notebook/tests/editor/save.js | 43 -------------- notebook/tests/util.js | 97 ------------------------------- 3 files changed, 8 insertions(+), 144 deletions(-) delete mode 100644 notebook/tests/editor/save.js diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 95d7380797..6520e45bc7 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -246,11 +246,15 @@ function( }; var that = this; + // record change generation for isClean + // (I don't know what this implies for the editor) + this.generation = this.codemirror.changeGeneration(); + var _save = function () { + // What does this event do? Does this always need to happen, + // even if the file can't be saved? What is dependent on it? that.events.trigger("file_saving.Editor"); return that.contents.save(that.file_path, model).then(function(data) { - // record change generation for isClean - this.generation = this.codemirror.changeGeneration(); that.events.trigger("file_saved.Editor", data); that.last_modified = new Date(data.last_modified); that._clean_state(); @@ -274,8 +278,8 @@ function( console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ "while the current file seem to have been saved on `"+data.last_modified+"`"); if (that._changed_on_disk_dialog !== null) { - // since the modal's event bindings are removed when destroyed, we reinstate - // save & reload callbacks on the confirmation & reload buttons + // since the modal's event bindings are removed when destroyed, + // we reinstate save & reload callbacks on the confirmation & reload buttons that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()}); diff --git a/notebook/tests/editor/save.js b/notebook/tests/editor/save.js deleted file mode 100644 index 6aee2c0a53..0000000000 --- a/notebook/tests/editor/save.js +++ /dev/null @@ -1,43 +0,0 @@ -// -// Test prompt when overwriting a file that is modified on disk -// - -casper.editor_test(function () { - var fname = "has#hash and space and unicø∂e.py"; - - this.append_cell("s = '??'", 'code'); - - this.thenEvaluate(function (nbname) { - require(['base/js/events'], function (events) { - IPython.editor.set_notebook_name(nbname); - IPython._save_success = IPython._save_failed = false; - events.on('file_saved.Editor', function () { - IPython._save_success = true; - }); - IPython.notebook.save_notebook(); - }); - }, {nbname:nbname}); - - this.waitFor(function () { - return this.evaluate(function(){ - return IPython._save_failed || IPython._save_success; - }); - }); - - this.thenEvaluate(function(){ - IPython._checkpoint_created = false; - require(['base/js/events'], function (events) { - events.on('checkpoint_created.Notebook', function (evt, data) { - IPython._checkpoint_created = true; - }); - }); - IPython.notebook.save_checkpoint(); - }); - - this.waitFor(function() { - return this.evaluate(function () { - return IPython && IPython.notebook && true; - }); - }); - -}); diff --git a/notebook/tests/util.js b/notebook/tests/util.js index d63f92672b..75969a7cc6 100644 --- a/notebook/tests/util.js +++ b/notebook/tests/util.js @@ -734,103 +734,6 @@ casper.dashboard_test = function (test) { }); }; -/** - * Editor Tests - * - * The functions below are utilities for setting up an editor and tearing it down - * after the test is over. - */ -caspser.open_new_file = function () { - // load up the jupyter notebook server (it's like running `jupyter notebook` in the shell) - var baseUrl = this.get_notebook_server(); - - // go to the base url, wait for it to load, then make a new file - this.start(baseUrl); - this.waitFor(this.page_loaded); - this.waitForSelector('#new-file a'); - this.thenClick('#new-file a'); - - // when the popup loads, go into that popup and wait until the main text box is loaded - this.withPopup(0, function () {this.waitForSelector('.CodeMirror-sizer');}); - - // now let's open the window where the file editor is displayed & load - this.then(function () { - this.open(this.popups[0].url); - }); - this.waitFor(this.page_loaded); - - // Hook the log and error methods of the console, forcing them to - // serialize their arguments before printing. This allows the - // Objects to cross into the phantom/slimer regime for display. - this.thenEvaluate(function(){ - var serialize_arguments = function(f, context) { - return function() { - var pretty_arguments = []; - for (var i = 0; i < arguments.length; i++) { - var value = arguments[i]; - if (value instanceof Object) { - var name = value.name || 'Object'; - // Print a JSON string representation of the object. - // If we don't do this, [Object object] gets printed - // by casper, which is useless. The long regular - // expression reduces the verbosity of the JSON. - pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ') - .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n') - .replace(/\n(\s+)?\n/g, '\n')); - } else { - pretty_arguments.push(value); - } - } - f.apply(context, pretty_arguments); - }; - }; - console.log = serialize_arguments(console.log, console); - console.error = serialize_arguments(console.error, console); - }); - - console.log('Editor loaded.') - -} - -casper.editor_test = function(test) { - // Wrap a notebook test to reduce boilerplate. - this.open_new_file(); - - // Echo whether or not we are running this test using SlimerJS - if (this.evaluate(function(){ - return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+ - })) { - console.log('This test is running in SlimerJS.'); - this.slimerjs = true; - } - - // Make sure to remove the onbeforeunload callback. This callback is - // responsible for the "Are you sure you want to quit?" type messages. - // PhantomJS ignores these prompts, SlimerJS does not which causes hangs. - this.then(function(){ - this.evaluate(function(){ - window.onbeforeunload = function(){}; - }); - }); - - this.then(test); - - // This is required to clean up the page we just finished with. If we don't call this - // casperjs will leak file descriptors of all the open WebSockets in that page. We - // have to set this.page=null so that next time casper.start runs, it will create a - // new page from scratch. - this.then(function () { - this.page.close(); - this.page = null; - }); - - // Run the browser automation. - this.run(function() { - this.test.done(); - }); -}; - - // note that this will only work for UNIQUE events -- if you want to // listen for the same event twice, this will not work! casper.event_test = function (name, events, action, timeout) { From b0dec205947af75ef9428ba92970a19b594fe59d Mon Sep 17 00:00:00 2001 From: Gabriel Ruiz Date: Thu, 4 Jan 2018 17:14:40 -0500 Subject: [PATCH 6/7] added back in reverted changes to editor.js --- notebook/static/edit/js/editor.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 6520e45bc7..2fd41660f3 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -246,15 +246,11 @@ function( }; var that = this; - // record change generation for isClean - // (I don't know what this implies for the editor) - this.generation = this.codemirror.changeGeneration(); - var _save = function () { - // What does this event do? Does this always need to happen, - // even if the file can't be saved? What is dependent on it? that.events.trigger("file_saving.Editor"); return that.contents.save(that.file_path, model).then(function(data) { + // record change generation for isClean + this.generation = this.codemirror.changeGeneration(); that.events.trigger("file_saved.Editor", data); that.last_modified = new Date(data.last_modified); that._clean_state(); @@ -278,8 +274,8 @@ function( console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+ "while the current file seem to have been saved on `"+data.last_modified+"`"); if (that._changed_on_disk_dialog !== null) { - // since the modal's event bindings are removed when destroyed, - // we reinstate save & reload callbacks on the confirmation & reload buttons + // since the modal's event bindings are removed when destroyed, we reinstate + // save & reload callbacks on the confirmation & reload buttons that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save); that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()}); @@ -365,4 +361,4 @@ function( }; return {Editor: Editor}; -}); +}); \ No newline at end of file From 7a772929923cd63687e7c461724c8ab4b8df2410 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 8 Feb 2018 14:27:10 +0000 Subject: [PATCH 7/7] Fix broken reference to 'this' --- notebook/static/edit/js/editor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/static/edit/js/editor.js b/notebook/static/edit/js/editor.js index 2fd41660f3..e7d310aebc 100644 --- a/notebook/static/edit/js/editor.js +++ b/notebook/static/edit/js/editor.js @@ -250,12 +250,12 @@ function( that.events.trigger("file_saving.Editor"); return that.contents.save(that.file_path, model).then(function(data) { // record change generation for isClean - this.generation = this.codemirror.changeGeneration(); + that.generation = that.codemirror.changeGeneration(); that.events.trigger("file_saved.Editor", data); that.last_modified = new Date(data.last_modified); that._clean_state(); }); - } + }; /* * Gets the current working file, and checks if the file has been modified on disk. If so, it @@ -361,4 +361,4 @@ function( }; return {Editor: Editor}; -}); \ No newline at end of file +});