Persist edits immediately so a fast app-kill loses no state
Reproduced on device: open a file, scroll, long-press-highlight a word, then recents-wipe the app ~1s after the highlight. The session file held the file but lost the selection and cursor: the 1s rate-limit had not elapsed since the previous save, so the final changes never flushed before the process died (a recents-wipe is a kill, not a clean shutdown, so Shutdown's final save never runs). Two changes in saveSessionIfChanged: - Urgent path: save immediately when the file changes or a selection appears/vanishes. These are low-frequency, high-value changes (the user just opened a file or highlighted/cleared text), and they are exactly what 'where I left off' means. - sessionSaveInterval 1s -> 250ms: bounds how stale scroll/cursor can be at an unlucky kill. The file is a few hundred bytes, so a few small writes a second during active scrolling is negligible. TestRestore_UrgentSaveOnSelection drives the kill race: a selection made right after the file-open save must land in the saver without waiting out the interval (fails with the urgent path disabled).
This commit is contained in:
parent
03595af27d
commit
2eb18b5c70
|
|
@ -30,8 +30,12 @@ import (
|
|||
)
|
||||
|
||||
// sessionSaveInterval rate-limits the periodic session save. The file is
|
||||
// tiny (a few hundred bytes), so one write a second at most is negligible.
|
||||
const sessionSaveInterval = time.Second
|
||||
// tiny (a few hundred bytes), so a few writes a second at most is
|
||||
// negligible. The window bounds how stale the persisted state can be if
|
||||
// the process is killed (recents-wipe) right after a change: at 1s, a
|
||||
// kill less than a second after the last edit lost the cursor, selection
|
||||
// and final scroll. See saveSessionIfChanged for the urgent path.
|
||||
const sessionSaveInterval = 250 * time.Millisecond
|
||||
|
||||
// SessionState is the relaunch snapshot (see the file doc above). All
|
||||
// positions are absolute file byte offsets; Scroll is in Dp. It is a plain
|
||||
|
|
@ -101,13 +105,21 @@ func (l *Logic) saveSessionIfChanged() {
|
|||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Sub(l.lastSessionSave) < sessionSaveInterval {
|
||||
return
|
||||
}
|
||||
s := l.SnapshotSession()
|
||||
if s == l.lastSession {
|
||||
return
|
||||
}
|
||||
// Save immediately when the file changes or a selection appears or
|
||||
// vanishes — low-frequency, high-value changes (the user just opened a
|
||||
// file or highlighted/cleared text). Everything else (scroll ticks,
|
||||
// cursor moves, selection-handle drags) is high-frequency and stays
|
||||
// rate-limited; a kill within the window then loses at most a fraction
|
||||
// of a second of motion, not the edit state.
|
||||
urgent := s.File != l.lastSession.File ||
|
||||
(s.SelStart >= 0) != (l.lastSession.SelStart >= 0)
|
||||
if !urgent && now.Sub(l.lastSessionSave) < sessionSaveInterval {
|
||||
return
|
||||
}
|
||||
l.sessionSaver(s)
|
||||
l.lastSession = s
|
||||
l.lastSessionSave = now
|
||||
|
|
|
|||
|
|
@ -471,3 +471,84 @@ func TestRestore_ShutdownSavesFinalSnapshot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestore_UrgentSaveOnSelection verifies the kill-race fix: a selection
|
||||
// appearing (the user just highlighted text) saves immediately, even inside
|
||||
// the rate-limit window that the file-open save opened. Without the urgent
|
||||
// path, killing the app (recents-wipe) less than a second after highlighting
|
||||
// lost the selection and cursor.
|
||||
func TestRestore_UrgentSaveOnSelection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("abcdef"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saves := make(chan editor.SessionState, 16)
|
||||
l := editor.NewLogic(real.NewRealFileSystem(dir), "/", func(string) {})
|
||||
l.SetSessionSaver(func(s editor.SessionState) { saves <- s })
|
||||
go l.Run()
|
||||
defer l.Shutdown()
|
||||
|
||||
// Open the file: the file change is itself urgent, so a save lands as
|
||||
// the load settles. Drain everything up to and including the loaded
|
||||
// snapshot.
|
||||
if _, ok := l.Inspect(func(st *editor.State) any {
|
||||
editor.OpenFile("/a.txt")
|
||||
return nil
|
||||
}); !ok {
|
||||
t.Fatal("Inspect timed out")
|
||||
}
|
||||
loaded := false
|
||||
for i := 0; i < 100 && !loaded; i++ {
|
||||
v, ok := l.Inspect(func(st *editor.State) any {
|
||||
cb := st.Editor.ChunkedBuffer
|
||||
return cb != nil && cb.FileLen() == 6
|
||||
})
|
||||
if ok {
|
||||
loaded = v.(bool)
|
||||
}
|
||||
if loaded {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
if !loaded {
|
||||
t.Fatal("file did not load")
|
||||
}
|
||||
// Drain saves up to the first one describing the loaded file.
|
||||
deadline := time.After(3 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case s := <-saves:
|
||||
if s.File == "/a.txt" {
|
||||
goto drainDone
|
||||
}
|
||||
case <-deadline:
|
||||
t.Fatal("timed out: no save for the opened file")
|
||||
}
|
||||
}
|
||||
drainDone:
|
||||
// Immediately (well inside any rate-limit window after the saves above),
|
||||
// make a selection appear and force a frame: the urgent path must save it
|
||||
// now, not after the interval elapses.
|
||||
if _, ok := l.Inspect(func(st *editor.State) any {
|
||||
st.Editor.SelectionAnchor = 1
|
||||
st.Editor.SelectionStart = 1
|
||||
st.Editor.SelectionEnd = 4
|
||||
st.Editor.CursorPosition = 4
|
||||
st.ScrollOffset = 3
|
||||
return nil
|
||||
}); !ok {
|
||||
t.Fatal("Inspect timed out")
|
||||
}
|
||||
l.ConfigChan() <- editor.ConfigEvent{PixelWidth: 780, PixelHeight: 1688} // force emitFrame
|
||||
|
||||
select {
|
||||
case s := <-saves:
|
||||
if s.SelStart == 1 && s.SelEnd == 4 && s.Cursor == 4 {
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected snapshot: %+v", s)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out: selection appearance was not saved immediately (urgent path)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user