44define ( [
55 'jquery' ,
66 'base/js/utils' ,
7+ 'base/js/i18n' ,
8+ 'base/js/dialog' ,
79 'codemirror/lib/codemirror' ,
810 'codemirror/mode/meta' ,
911 'codemirror/addon/comment/comment' ,
@@ -19,6 +21,8 @@ define([
1921function (
2022 $ ,
2123 utils ,
24+ i18n ,
25+ dialog ,
2226 CodeMirror
2327) {
2428 "use strict" ;
@@ -33,6 +37,8 @@ function(
3337 this . file_path = options . file_path ;
3438 this . config = options . config ;
3539 this . file_extension_modes = options . file_extension_modes || { } ;
40+ this . last_modified = null ;
41+ this . _changed_on_disk_dialog = null ;
3642
3743 this . codemirror = new CodeMirror ( $ ( this . selector ) [ 0 ] ) ;
3844 this . codemirror . on ( 'changes' , function ( cm , changes ) {
@@ -106,6 +112,7 @@ function(
106112 that . generation = cm . changeGeneration ( ) ;
107113 that . events . trigger ( "file_loaded.Editor" , model ) ;
108114 that . _clean_state ( ) ;
115+ that . last_modified = new Date ( model . last_modified ) ;
109116 } ) . catch (
110117 function ( error ) {
111118 that . events . trigger ( "file_load_failed.Editor" , error ) ;
@@ -197,6 +204,11 @@ function(
197204 }
198205 } ;
199206
207+ /**
208+ * Rename the file.
209+ * @param {string } new_name
210+ * @return {Promise } promise that resolves when the file is renamed.
211+ */
200212 Editor . prototype . rename = function ( new_name ) {
201213 /** rename the file */
202214 var that = this ;
@@ -206,32 +218,111 @@ function(
206218 function ( model ) {
207219 that . file_path = model . path ;
208220 that . events . trigger ( 'file_renamed.Editor' , model ) ;
221+ that . last_modified = new Date ( model . last_modified ) ;
209222 that . _set_mode_for_model ( model ) ;
210223 that . _clean_state ( ) ;
211224 }
212225 ) ;
213226 } ;
214227
215- Editor . prototype . save = function ( ) {
228+
229+ /**
230+ * Save this file on the server.
231+ *
232+ * @param {boolean } check_last_modified - checks if file has been modified on disk
233+ * @return {Promise } - promise that resolves when the notebook is saved.
234+ */
235+ Editor . prototype . save = function ( check_last_modified ) {
216236 /** save the file */
217237 if ( ! this . save_enabled ) {
218238 console . log ( "Not saving, save disabled" ) ;
219239 return ;
220240 }
241+
242+ // used to check for last modified saves
243+ if ( check_last_modified === undefined ) {
244+ check_last_modified = true ;
245+ }
246+
221247 var model = {
222248 path : this . file_path ,
223249 type : 'file' ,
224250 format : 'text' ,
225251 content : this . codemirror . getValue ( ) ,
226252 } ;
227253 var that = this ;
228- // record change generation for isClean
229- this . generation = this . codemirror . changeGeneration ( ) ;
230- that . events . trigger ( "file_saving.Editor" ) ;
231- return this . contents . save ( this . file_path , model ) . then ( function ( data ) {
232- that . events . trigger ( "file_saved.Editor" , data ) ;
233- that . _clean_state ( ) ;
234- } ) ;
254+
255+ var _save = function ( ) {
256+ that . events . trigger ( "file_saving.Editor" ) ;
257+ return that . contents . save ( that . file_path , model ) . then ( function ( data ) {
258+ // record change generation for isClean
259+ that . generation = that . codemirror . changeGeneration ( ) ;
260+ that . events . trigger ( "file_saved.Editor" , data ) ;
261+ that . last_modified = new Date ( data . last_modified ) ;
262+ that . _clean_state ( ) ;
263+ } ) ;
264+ } ;
265+
266+ /*
267+ * Gets the current working file, and checks if the file has been modified on disk. If so, it
268+ * creates & opens a modal that issues the user a warning and prompts them to overwrite the file.
269+ *
270+ * If it can't get the working file, it builds a new file and saves.
271+ */
272+ if ( check_last_modified ) {
273+ return this . contents . get ( that . file_path , { content : false } ) . then (
274+ function check_if_modified ( data ) {
275+ var last_modified = new Date ( data . last_modified ) ;
276+ // We want to check last_modified (disk) > that.last_modified (our last save)
277+ // In some cases the filesystem reports an inconsistent time,
278+ // so we allow 0.5 seconds difference before complaining.
279+ if ( ( last_modified . getTime ( ) - that . last_modified . getTime ( ) ) > 500 ) { // 500 ms
280+ console . warn ( "Last saving was done on `" + that . last_modified + "`(" + that . _last_modified + "), " +
281+ "while the current file seem to have been saved on `" + data . last_modified + "`" ) ;
282+ if ( that . _changed_on_disk_dialog !== null ) {
283+ // since the modal's event bindings are removed when destroyed, we reinstate
284+ // save & reload callbacks on the confirmation & reload buttons
285+ that . _changed_on_disk_dialog . find ( '.save-confirm-btn' ) . click ( _save ) ;
286+ that . _changed_on_disk_dialog . find ( '.btn-warning' ) . click ( function ( ) { window . location . reload ( ) } ) ;
287+
288+ // redisplay existing dialog
289+ that . _changed_on_disk_dialog . modal ( 'show' ) ;
290+ } else {
291+ // create new dialog
292+ that . _changed_on_disk_dialog = dialog . modal ( {
293+ keyboard_manager : that . keyboard_manager ,
294+ title : i18n . msg . _ ( "File changed" ) ,
295+ body : i18n . msg . _ ( "The file has changed on disk since the last time we opened or saved it. "
296+ + "Do you want to overwrite the file on disk with the version open here, or load "
297+ + "the version on disk (reload the page)?" ) ,
298+ buttons : {
299+ Reload : {
300+ class : 'btn-warning' ,
301+ click : function ( ) {
302+ window . location . reload ( ) ;
303+ }
304+ } ,
305+ Cancel : { } ,
306+ Overwrite : {
307+ class : 'btn-danger save-confirm-btn' ,
308+ click : function ( ) {
309+ _save ( ) ;
310+ }
311+ } ,
312+ }
313+ } ) ;
314+ }
315+ } else {
316+ return _save ( ) ;
317+ }
318+ } , function ( error ) {
319+ console . log ( error ) ;
320+ // maybe it has been deleted or renamed? Go ahead and save.
321+ return _save ( ) ;
322+ } )
323+ } else {
324+ return _save ( ) ;
325+ }
235326 } ;
236327
237328 Editor . prototype . _clean_state = function ( ) {
0 commit comments