// Automatic saving: writing modified files a short while after the user stops // typing, when the project has asked for it. package app import ( "path/filepath" "time" "rickub.com/turbo-editors/turbo-core/editor" "rickub.com/turbo-editors/turbo-core/ui" ) // autosave is the state of the pending automatic save. // // There is one deadline for the whole editor rather than one per window. The // setting is "save a short while after you stop typing", and typing stops once // — a per-window deadline would save a file the user has already moved on from // at a different moment from the one they are looking at, for no gain anyone // could see. type autosave struct { enabled bool delay time.Duration // due is when the pending save comes due; the zero time means none is. due time.Time // timer nudges the event loop when the deadline arrives. It is only a // nudge: the deadline above is what decides, so a nudge that goes missing // costs a late save, never a lost one. timer *time.Timer } // SetAutosave turns automatic saving on or off, and says how long to wait // after the last keystroke. // // A delay of zero or less keeps whatever delay was already set, so a project // that turns autosave on without naming a delay gets the default rather than a // save on every keystroke. // // a.SetAutosave(s.Autosave, s.AutosaveDelay) func (a *App) SetAutosave(enabled bool, delay time.Duration) { a.autosave.enabled = enabled if delay > 0 { a.autosave.delay = delay } if !enabled { a.cancelAutosave() } } // AutosaveEnabled reports whether files are being saved automatically. func (a *App) AutosaveEnabled() bool { return a.autosave.enabled } // noteEdit restarts the idle countdown. It runs after every edit. func (a *App) noteEdit() { if !a.autosave.enabled { return } a.autosave.due = a.now().Add(a.autosave.delay) a.armAutosave() } // armAutosave asks the event loop to come round again once the delay has // passed, because it would otherwise sit blocked in PollEvent with no reason // to wake and no way to notice that the deadline arrived. func (a *App) armAutosave() { if a.autosave.timer != nil { a.autosave.timer.Stop() } a.autosave.timer = time.AfterFunc(a.autosave.delay, a.wake) } // cancelAutosave forgets the pending save and stops the timer behind it. func (a *App) cancelAutosave() { a.autosave.due = time.Time{} if a.autosave.timer != nil { a.autosave.timer.Stop() a.autosave.timer = nil } } // saveDueDocuments writes every modified file whose idle delay has run out. // // It runs at the top of every turn of the event loop rather than from the // timer, for the same reason announceOpenDocuments does: the timer's wake-up // is a PostEvent, and PostEvent drops what does not fit in its queue. State // the loop checks for itself cannot go missing; a message can. func (a *App) saveDueDocuments() { if a.autosave.due.IsZero() || a.now().Before(a.autosave.due) { return } // Cleared before writing, not after: a file that cannot be written must // not be retried every delay for as long as the editor is open. a.cancelAutosave() for _, window := range a.desktop.Windows() { if view, ok := editorViewOf(window); ok { a.autosaveOne(view) } } } // autosaveOne writes one view's file, if it is one automatic saving can write. // // A window with no file name is left alone: naming it is a question, and a // feature whose whole point is not to interrupt must not open a dialog. func (a *App) autosaveOne(view *editor.View) { if !view.Buffer().Modified() || view.Buffer().Path() == "" { return } a.writeQuietly(view) } // writeQuietly saves a view and reports on the status bar rather than in a // dialog, reporting whether it worked. // // A modal would be wrong here twice over: nobody asked for this save, and a // dialog that reappears every few seconds because a file is read-only is worse // than the problem it describes. func (a *App) writeQuietly(view *editor.View) bool { path := view.Buffer().Path() created := !fileExists(path) if err := view.Buffer().SaveAs(path); err != nil { a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error()) return false } a.afterSave(view, path, created) return true } // savedByAutosave writes a window's file so that closing it need not ask about // unsaved work, and reports whether there is nothing left to ask about. // // It is false whenever there is still a real question — autosave is off, the // file has never been named, or the write failed — so the caller falls back to // asking rather than closing over work it could not save. func (a *App) savedByAutosave(window *ui.Window) bool { view, ok := editorViewOf(window) if !ok || !a.autosave.enabled || !view.Buffer().Modified() { return false } if view.Buffer().Path() == "" { return false } return a.writeQuietly(view) }