cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Ask before a change too large to undo

Dami committed 7fbf097
A command or sort whose undo step would hold more than
sheet.MaxStepBytes asks on the context line: "This can't be undone: it
would take N MB of undo history." Enter runs it without undo, Esc
backs out. Macros don't ask.
commit
7fbf09775997ff2cfc4e0460b8cf86cb226492d6
parent
36b062300e19d900a2f22ec461c149e9a402e678
author
Dami <dami.etoile@gmail.com>
5 files changed, +102 -1
Minternal/ui/commands.go3++
Minternal/ui/model.go1+
Minternal/ui/sort.go3+-
Ainternal/ui/undocost.go57++++++++++++++++++++++++++++++
Ainternal/ui/undocost_test.go39+++++++++++++++++++++
M internal/ui/commands.go +3 -0
M internal/ui/model.go +1 -0
M internal/ui/sort.go +2 -1
A internal/ui/undocost.go +57 -0
@@ -0,0 +1,57 @@
1+package ui2+3+import (4+ "fmt"5+6+ tea "charm.land/bubbletea/v2"7+8+ "github.com/FelineStateMachine/012/internal/sheet"9+)10+11+// A change whose undo step would hold more than sheet.MaxStepBytes12+// (millions of formulas, or of distinct long texts, cleared or rewritten13+// at once) asks first on the context line rather than holding gigabytes14+// for undo: Enter runs it without undo, forgetting the history, and Esc15+// backs out. Commands are checked through what they edit, as protected16+// ranges are, and so is sorting. Macros that run don't ask.17+18+// maxStepBytes is sheet.MaxStepBytes; tests lower it.19+var maxStepBytes int64 = sheet.MaxStepBytes20+21+// askUndoCost reports whether changing r would take more undo history22+// than maxStepBytes, and if so asks whether to go on: Enter runs retry23+// without undo, and retry isn't asked again.24+func (m *Model) askUndoCost(r sheet.Rect, retry func(*Model) tea.Cmd) bool {25+ if m.undoOK || m.macros.run != nil {26+ return false27+ }28+ cost := m.sheet.UndoCost(r)29+ if cost <= maxStepBytes {30+ return false31+ }32+ protectOK := m.protectOK // agreed to before this asked33+ m.openOverlay(&choiceBar{34+ m: m, msg: "This can't be undone: it would take " + byteSize(cost) + " of undo history.", warn: true,35+ desc: "Going on runs it without undo and forgets the changes before it",36+ choices: []choice{37+ {key: "enter", label: "Go on without undo", run: func(m *Model) tea.Cmd {38+ m.undoOK, m.protectOK = true, protectOK39+ defer func() { m.undoOK, m.protectOK = false, false }()40+ var cmd tea.Cmd41+ m.book().WithoutUndo(func() { cmd = retry(m) })42+ m.syncChanged()43+ return cmd44+ }},45+ {key: "esc", label: "Cancel", run: func(*Model) tea.Cmd { return nil }},46+ },47+ })48+ return true49+}50+51+// byteSize is n bytes in MB, or in GB from a thousand MB.52+func byteSize(n int64) string {53+ if mb := n >> 20; mb < 1000 {54+ return fmt.Sprintf("%d MB", mb)55+ }56+ return fmt.Sprintf("%.1f GB", float64(n)/(1<<30))57+}
A internal/ui/undocost_test.go +39 -0
@@ -0,0 +1,39 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+7+ "github.com/FelineStateMachine/012/internal/sheet"8+)9+10+// A change whose undo step would pass the ceiling asks first: Esc backs11+// out, Enter runs it without undo and forgets the history.12+func TestUndoCostAsks(t *testing.T) {13+ defer func(n int64) { maxStepBytes = n }(maxStepBytes)14+ maxStepBytes = 10015+ m := newModel()16+ press(t, m, "1", "<enter>", "2", "<enter>", "3", "<enter>", "4", "<enter>", "5", "<enter>", "6", "<enter>", "7", "<enter>")17+ press(t, m, "<up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>", "<shift+up>")18+ m.runCommand("clear")19+ l := line(m, contextLine)20+ if _, ok := m.overlay.(*choiceBar); !ok || !strings.Contains(l, "This can't be undone: it would take 0 MB of undo history.") {21+ t.Fatalf("clear didn't ask: %q", l)22+ }23+ press(t, m, "<esc>")24+ if m.sheet.Value(addr("A1")).Num != 1 || !m.sheet.CanUndo() {25+ t.Fatalf("Esc went on: A1 %v", m.sheet.Value(addr("A1")))26+ }27+ m.runCommand("clear")28+ press(t, m, "<enter>")29+ if m.sheet.Value(addr("A1")).Kind != sheet.Empty || m.sheet.CanUndo() || !m.changed {30+ t.Fatalf("after Enter: A1 %v, can undo %v, changed %v", m.sheet.Value(addr("A1")), m.sheet.CanUndo(), m.changed)31+ }32+ // Under the ceiling nothing asks.33+ maxStepBytes = sheet.MaxStepBytes34+ press(t, m, "8", "<enter>", "<up>")35+ m.runCommand("clear")36+ if m.overlay != nil || m.sheet.Value(addr("A1")).Kind != sheet.Empty || !m.sheet.CanUndo() {37+ t.Errorf("small clear: overlay %T, A1 %v", m.overlay, m.sheet.Value(addr("A1")))38+ }39+}