turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

autosave.go · 147 lines · 4.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1// Automatic saving: writing modified files a short while after the user stops
2// typing, when the project has asked for it.
3
4package app
5
6import (
7 "path/filepath"
8 "time"
9
10 "codeberg.org/turbo-editors/turbo-core/editor"
11 "codeberg.org/turbo-editors/turbo-core/ui"
12)
13
14// autosave is the state of the pending automatic save.
15//
16// There is one deadline for the whole editor rather than one per window. The
17// setting is "save a short while after you stop typing", and typing stops once
18// — a per-window deadline would save a file the user has already moved on from
19// at a different moment from the one they are looking at, for no gain anyone
20// could see.
21type autosave struct {
22 enabled bool
23 delay time.Duration
24
25 // due is when the pending save comes due; the zero time means none is.
26 due time.Time
27 // timer nudges the event loop when the deadline arrives. It is only a
28 // nudge: the deadline above is what decides, so a nudge that goes missing
29 // costs a late save, never a lost one.
30 timer *time.Timer
31}
32
33// SetAutosave turns automatic saving on or off, and says how long to wait
34// after the last keystroke.
35//
36// A delay of zero or less keeps whatever delay was already set, so a project
37// that turns autosave on without naming a delay gets the default rather than a
38// save on every keystroke.
39//
40// a.SetAutosave(s.Autosave, s.AutosaveDelay)
41func (a *App) SetAutosave(enabled bool, delay time.Duration) {
42 a.autosave.enabled = enabled
43 if delay > 0 {
44 a.autosave.delay = delay
45 }
46 if !enabled {
47 a.cancelAutosave()
48 }
49}
50
51// AutosaveEnabled reports whether files are being saved automatically.
52func (a *App) AutosaveEnabled() bool { return a.autosave.enabled }
53
54// noteEdit restarts the idle countdown. It runs after every edit.
55func (a *App) noteEdit() {
56 if !a.autosave.enabled {
57 return
58 }
59
60 a.autosave.due = a.now().Add(a.autosave.delay)
61 a.armAutosave()
62}
63
64// armAutosave asks the event loop to come round again once the delay has
65// passed, because it would otherwise sit blocked in PollEvent with no reason
66// to wake and no way to notice that the deadline arrived.
67func (a *App) armAutosave() {
68 if a.autosave.timer != nil {
69 a.autosave.timer.Stop()
70 }
71 a.autosave.timer = time.AfterFunc(a.autosave.delay, a.wake)
72}
73
74// cancelAutosave forgets the pending save and stops the timer behind it.
75func (a *App) cancelAutosave() {
76 a.autosave.due = time.Time{}
77 if a.autosave.timer != nil {
78 a.autosave.timer.Stop()
79 a.autosave.timer = nil
80 }
81}
82
83// saveDueDocuments writes every modified file whose idle delay has run out.
84//
85// It runs at the top of every turn of the event loop rather than from the
86// timer, for the same reason announceOpenDocuments does: the timer's wake-up
87// is a PostEvent, and PostEvent drops what does not fit in its queue. State
88// the loop checks for itself cannot go missing; a message can.
89func (a *App) saveDueDocuments() {
90 if a.autosave.due.IsZero() || a.now().Before(a.autosave.due) {
91 return
92 }
93
94 // Cleared before writing, not after: a file that cannot be written must
95 // not be retried every delay for as long as the editor is open.
96 a.cancelAutosave()
97 for _, window := range a.desktop.Windows() {
98 if view, ok := editorViewOf(window); ok {
99 a.autosaveOne(view)
100 }
101 }
102}
103
104// autosaveOne writes one view's file, if it is one automatic saving can write.
105//
106// A window with no file name is left alone: naming it is a question, and a
107// feature whose whole point is not to interrupt must not open a dialog.
108func (a *App) autosaveOne(view *editor.View) {
109 if !view.Buffer().Modified() || view.Buffer().Path() == "" {
110 return
111 }
112 a.writeQuietly(view)
113}
114
115// writeQuietly saves a view and reports on the status bar rather than in a
116// dialog, reporting whether it worked.
117//
118// A modal would be wrong here twice over: nobody asked for this save, and a
119// dialog that reappears every few seconds because a file is read-only is worse
120// than the problem it describes.
121func (a *App) writeQuietly(view *editor.View) bool {
122 path := view.Buffer().Path()
123 if err := view.Buffer().SaveAs(path); err != nil {
124 a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error())
125 return false
126 }
127
128 a.afterSave(view, path)
129 return true
130}
131
132// savedByAutosave writes a window's file so that closing it need not ask about
133// unsaved work, and reports whether there is nothing left to ask about.
134//
135// It is false whenever there is still a real question — autosave is off, the
136// file has never been named, or the write failed — so the caller falls back to
137// asking rather than closing over work it could not save.
138func (a *App) savedByAutosave(window *ui.Window) bool {
139 view, ok := editorViewOf(window)
140 if !ok || !a.autosave.enabled || !view.Buffer().Modified() {
141 return false
142 }
143 if view.Buffer().Path() == "" {
144 return false
145 }
146 return a.writeQuietly(view)
147}