1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
// 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"
"codeberg.org/turbo-editors/turbo-core/editor"
"codeberg.org/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()
if err := view.Buffer().SaveAs(path); err != nil {
a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error())
return false
}
a.afterSave(view, path)
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)
}
|