@@ -25,12 +25,22 @@ define(function (require, exports, module) {
2525 const NativeApp = brackets . getModule ( "utils/NativeApp" ) ,
2626 FileSystem = brackets . getModule ( "filesystem/FileSystem" ) ,
2727 ProjectManager = brackets . getModule ( "project/ProjectManager" ) ,
28+ MainViewManager = brackets . getModule ( "view/MainViewManager" ) ,
2829 FileSystemError = brackets . getModule ( "filesystem/FileSystemError" ) ,
2930 FileUtils = brackets . getModule ( "file/FileUtils" ) ,
30- DocumentManager = brackets . getModule ( "document/DocumentManager" ) ;
31+ DocumentManager = brackets . getModule ( "document/DocumentManager" ) ,
32+ NotificationUI = brackets . getModule ( "widgets/NotificationUI" ) ,
33+ Mustache = brackets . getModule ( "thirdparty/mustache/mustache" ) ,
34+ Strings = brackets . getModule ( "strings" ) ,
35+ FileViewController = brackets . getModule ( "project/FileViewController" ) ,
36+ recoveryTemplate = require ( "text!html/recovery-template.html" ) ,
37+ EventDispatcher = brackets . getModule ( "utils/EventDispatcher" ) ,
38+ EventManager = brackets . getModule ( "utils/EventManager" ) ;
3139
32- const BACKUP_INTERVAL_MS = 3000 ; // todo change to 20 secs
33- // todo large number of tracked files performance issues?
40+ EventDispatcher . makeEventDispatcher ( exports ) ;
41+ EventManager . registerEventHandler ( "ph-recovery" , exports ) ;
42+
43+ const BACKUP_INTERVAL_MS = 5000 ;
3444 const sessionRestoreDir = FileSystem . getDirectoryForPath (
3545 path . normalize ( NativeApp . getApplicationSupportDirectory ( ) + "/sessionRestore" ) ) ;
3646
@@ -131,6 +141,7 @@ define(function (require, exports, module) {
131141 let restoreFolder = project . restoreRoot ;
132142 await ensureFolderIsClean ( restoreFolder ) ;
133143 let allEntries = await FileSystem . getAllDirectoryContents ( restoreFolder ) ;
144+ let backupExists = false ;
134145 for ( let entry of allEntries ) {
135146 if ( entry . isDirectory ) {
136147 continue ;
@@ -145,9 +156,20 @@ define(function (require, exports, module) {
145156 return ;
146157 }
147158 project . lastBackedUpFileContents [ projectFilePath ] = text ;
148- console . log ( text ) ;
159+ backupExists = true ;
149160 }
150161 project . lastBackedupLoadInProgress = false ;
162+ if ( backupExists ) {
163+ let notificationHTML = Mustache . render ( recoveryTemplate , {
164+ Strings : Strings ,
165+ PROJECT_TO_RECOVER : projectRootPath
166+ } ) ;
167+ project . restoreNotification = NotificationUI . createToastFromTemplate ( Strings . RECOVER_UNSAVED_FILES_TITLE ,
168+ notificationHTML , {
169+ dismissOnClick : false ,
170+ toastStyle : NotificationUI . NOTIFICATION_STYLES_CSS_CLASS . SUCCESS
171+ } ) ;
172+ }
151173 }
152174
153175 function projectOpened ( _event , projectRoot ) {
@@ -156,14 +178,21 @@ define(function (require, exports, module) {
156178 return ;
157179 }
158180 if ( trackedProjects [ projectRoot . fullPath ] ) {
159- trackedProjects [ projectRoot . fullPath ] . projectLoadCount ++ ;
181+ if ( trackedProjects [ projectRoot . fullPath ] . restoreNotification ) {
182+ trackedProjects [ projectRoot . fullPath ] . restoreNotification . close ( ) ;
183+ trackedProjects [ projectRoot . fullPath ] . restoreNotification = null ;
184+ }
185+ trackedProjects [ projectRoot . fullPath ] . projectLoadCount ++ ; // we use this to prevent race conditions
186+ // on frequent project switch before all project backup files are loaded.
160187 trackedProjects [ projectRoot . fullPath ] . lastBackedUpFileContents = { } ;
161188 trackedProjects [ projectRoot . fullPath ] . firstEditHandled = false ;
162189 trackedProjects [ projectRoot . fullPath ] . lastBackedupLoadInProgress = true ;
163190 trackedProjects [ projectRoot . fullPath ] . trackedFileUpdateTimestamps = { } ;
164191 trackedProjects [ projectRoot . fullPath ] . trackedFileContents = { } ;
165- loadLastBackedUpFileContents ( projectRoot . fullPath ) ;
166- // todo race condition here frequent switch between projects
192+ trackedProjects [ projectRoot . fullPath ] . changeErrorReported = false ;
193+ loadLastBackedUpFileContents ( projectRoot . fullPath ) . catch ( err => {
194+ console . error ( "[recovery] loadLastBackedUpFileContents failed " , err ) ;
195+ } ) ;
167196 return ;
168197 }
169198 trackedProjects [ projectRoot . fullPath ] = {
@@ -176,9 +205,13 @@ define(function (require, exports, module) {
176205 lastBackedupLoadInProgress : true , // while the backup is loading, we need to prevent write over the existing
177206 // backup with backup info of the current session
178207 trackedFileUpdateTimestamps : { } ,
179- trackedFileContents : { }
208+ trackedFileContents : { } ,
209+ restoreNotification : null ,
210+ changeErrorReported : false // we only report change errors once to prevent too many Bugsnag reports
180211 } ;
181- loadLastBackedUpFileContents ( projectRoot . fullPath ) ;
212+ loadLastBackedUpFileContents ( projectRoot . fullPath ) . catch ( err => {
213+ console . error ( "[recovery] loadLastBackedUpFileContents failed " , err ) ;
214+ } ) ;
182215 }
183216
184217 async function writeFileIgnoreFailure ( filePath , contents ) {
@@ -189,7 +222,6 @@ define(function (require, exports, module) {
189222 await jsPromise ( FileUtils . writeText ( file , contents , true ) ) ;
190223 } catch ( e ) {
191224 console . error ( e ) ;
192- logger . reportError ( e ) ; // todo too many error reports prevent every 20 secs
193225 }
194226 }
195227
@@ -217,6 +249,12 @@ define(function (require, exports, module) {
217249 }
218250
219251 let backupInProgress = false ;
252+
253+ /**
254+ * This gets executed every 5 seconds and should be as light-weight as possible. If there are no changes to be
255+ * backed up, then this function should return as soon as possible without waiting for any async flows.
256+ * @return {Promise<void> }
257+ */
220258 async function changeScanner ( ) {
221259 let currentProjectRoot = ProjectManager . getProjectRoot ( ) ;
222260 const project = trackedProjects [ currentProjectRoot . fullPath ] ;
@@ -255,21 +293,71 @@ define(function (require, exports, module) {
255293 await cleanupUntrackedFiles ( docPathsToTrack , currentProjectRoot ) ;
256294 }
257295 } catch ( e ) {
258- console . error ( e ) ;
259- logger . reportError ( e ) ;
296+ console . error ( "[recovery] changeScanner error" , e ) ;
297+ if ( ! project . changeErrorReported ) {
298+ project . changeErrorReported = true ;
299+ // we only report change errors once to prevent too many Bugsnag reports
300+ logger . reportError ( e ) ;
301+ }
260302 }
261303 backupInProgress = false ;
262304 }
263305
264306 function beforeProjectClosed ( ) {
265- changeScanner ( ) ;
307+ let currentProjectRoot = ProjectManager . getProjectRoot ( ) ;
308+ const project = trackedProjects [ currentProjectRoot . fullPath ] ;
309+ if ( project . restoreNotification ) {
310+ project . restoreNotification . close ( ) ;
311+ }
312+ changeScanner ( ) . catch ( err => {
313+ console . error ( "[recovery] beforeProjectClosed failed which scanning for changes to backup" , err ) ;
314+ } ) ;
315+ }
316+
317+ async function ensureOpenEditors ( pathList ) {
318+ let allOpenFiles = MainViewManager . getAllOpenFiles ( ) ;
319+ let openFilePaths = { } ;
320+ for ( let file of allOpenFiles ) {
321+ openFilePaths [ file . fullPath ] = true ;
322+ }
323+ for ( let path of pathList ) {
324+ if ( ! openFilePaths [ path ] ) {
325+ let file = FileSystem . getFileForPath ( path ) ;
326+ await jsPromise ( FileViewController . openFileAndAddToWorkingSet ( file . fullPath ) ) ;
327+ }
328+ }
329+ }
330+
331+ async function restoreBtnClicked ( _event , projectToRestore ) {
332+ let currentProjectRoot = ProjectManager . getProjectRoot ( ) ;
333+ const project = trackedProjects [ currentProjectRoot . fullPath ] ;
334+ if ( ! project || projectToRestore !== currentProjectRoot . fullPath ) {
335+ console . error ( `[recovery] current project ${ currentProjectRoot . fullPath } != restore ${ projectToRestore } ` ) ;
336+ return ;
337+ }
338+ let pathsToRestore = Object . keys ( project . lastBackedUpFileContents ) ;
339+ await ensureOpenEditors ( pathsToRestore ) ;
340+ for ( let filePath of pathsToRestore ) {
341+ if ( ProjectManager . isWithinProject ( filePath ) ) {
342+ console . log ( "restoring" , filePath ) ;
343+ let document = await jsPromise ( DocumentManager . getDocumentForPath ( filePath ) ) ;
344+ document . setText ( project . lastBackedUpFileContents [ filePath ] ) ;
345+ } else {
346+ console . error ( "[recovery] Skipping restore of non project file: " , filePath ) ;
347+ }
348+ }
349+ if ( project . restoreNotification ) {
350+ project . restoreNotification . close ( ) ;
351+ project . restoreNotification = null ;
352+ }
266353 }
267354
268355 function init ( ) {
269- ProjectManager . on ( ProjectManager . EVENT_AFTER_PROJECT_OPEN , projectOpened ) ;
270- ProjectManager . on ( ProjectManager . EVENT_PROJECT_BEFORE_CLOSE , beforeProjectClosed ) ;
271- createDir ( sessionRestoreDir ) ;
272356 if ( ! window . testEnvironment ) {
357+ ProjectManager . on ( ProjectManager . EVENT_AFTER_PROJECT_OPEN , projectOpened ) ;
358+ ProjectManager . on ( ProjectManager . EVENT_PROJECT_BEFORE_CLOSE , beforeProjectClosed ) ;
359+ exports . on ( "restoreProject" , restoreBtnClicked ) ;
360+ createDir ( sessionRestoreDir ) ;
273361 setInterval ( changeScanner , BACKUP_INTERVAL_MS ) ;
274362 }
275363 }
0 commit comments