Every cell and column width change now goes through place or setWidth, which record a before-image into the open step. Steps snapshot whole cells, so fields added to Cell later are covered. Batch groups a multi-cell operation into one step and recalculates once at the end. Undo and redo swap before-images, StateID tells the UI whether it's back at the saved state, and history keeps the last 100 steps. Consecutive column width changes merge until Seal, so a live width preview and its result or cancellation are one step (or none). Loading a file starts a fresh history.
- commit
- 0d885ce92063f9f2cc04e16a73640ae13ef27ddc
- parent
- f9df936e417d22c7e3f5cd90d50c25927521987f
- author
- Dami <dami.etoile@gmail.com>
| A | internal/ | 226 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 147 | ++++++++++++++++++++ |
| M | internal/ | 65 | ++++++-- |
@@ -0,0 +1,226 @@
1+package sheet2+3+// MaxUndo is how many steps of undo history a sheet keeps.4+const MaxUndo = 1005+6+// History is a command log of before-images. Each step records, for every7+// cell and column width it touched, the state before the step; undoing8+// swaps those back in and records the state it replaced as the redo step.9+//10+// Steps snapshot whole cells (a shallow copy of the struct), so fields11+// added to Cell later are covered without changes here. That relies on12+// one rule: a stored *Cell is never modified in place except for its13+// computed Value. To change a cell, build a new one and store it with14+// place.15+type history struct {16+ undo, redo []*step17+ open *step // the step being built; nil outside change18+ depth int // nesting of change calls19+ dirty []Addr20+ lastID int21+ // mergeWidths lets the next width-only step join the top one, so a22+ // live preview of a column width and its final value (or its23+ // cancellation) are one step. Seal ends the run.24+ mergeWidths bool25+}26+27+type step struct {28+ id int29+ focus Rect // what the UI selects when the step is undone or redone30+ cells map[Addr]*Cell // before the step; nil for blank31+ widths map[int]int // before the step; 0 for the default width32+}33+34+func (st *step) empty() bool { return len(st.cells) == 0 && len(st.widths) == 0 }35+36+func (c *Cell) clone() *Cell {37+ if c == nil {38+ return nil39+ }40+ cp := *c41+ return &cp42+}43+44+// Batch runs fn as a single undo step: everything it changes is undone45+// together, and formulas are recalculated once at the end. focus is the46+// range to select when the step is undone or redone. Batches nest; inner47+// ones join the outermost.48+func (s *Sheet) Batch(focus Rect, fn func() error) error {49+ var err error50+ s.change(focus, func() { err = fn() })51+ return err52+}53+54+// change opens a step (unless one is open), runs fn, and on the way out55+// of the outermost call recalculates what changed and pushes the step.56+func (s *Sheet) change(focus Rect, fn func()) {57+ h := &s.hist58+ if h.depth == 0 {59+ h.open = &step{focus: focus, cells: map[Addr]*Cell{}, widths: map[int]int{}}60+ }61+ h.depth++62+ defer func() {63+ h.depth--64+ if h.depth > 0 {65+ return66+ }67+ st, dirty := h.open, h.dirty68+ h.open, h.dirty = nil, nil69+ s.recalc(dirty)70+ s.push(st)71+ }()72+ fn()73+}74+75+// record saves the cell at a before its first change in the open step.76+func (s *Sheet) record(a Addr) {77+ st := s.hist.open78+ if st == nil {79+ return80+ }81+ if _, seen := st.cells[a]; !seen {82+ st.cells[a] = s.cells[a].clone()83+ }84+ s.hist.dirty = append(s.hist.dirty, a)85+}86+87+// recordWidth saves column c's width before its first change in the open88+// step.89+func (s *Sheet) recordWidth(c int) {90+ st := s.hist.open91+ if st == nil {92+ return93+ }94+ if _, seen := st.widths[c]; !seen {95+ st.widths[c] = s.widths[c]96+ }97+}98+99+// push adds a finished step to the undo stack and clears redo, dropping100+// no-op changes.101+func (s *Sheet) push(st *step) {102+ h := &s.hist103+ for c, w := range st.widths {104+ if s.widths[c] == w {105+ delete(st.widths, c)106+ }107+ }108+ for a, c := range st.cells {109+ if c == nil && s.cells[a] == nil {110+ delete(st.cells, a)111+ }112+ }113+ if st.empty() {114+ return115+ }116+ h.redo = nil117+ h.lastID++118+ widthOnly := len(st.cells) == 0119+ if top := h.top(); widthOnly && h.mergeWidths && top != nil && len(top.cells) == 0 {120+ for c, w := range st.widths {121+ if _, ok := top.widths[c]; !ok {122+ top.widths[c] = w123+ }124+ }125+ for c, w := range top.widths {126+ if s.widths[c] == w {127+ delete(top.widths, c)128+ }129+ }130+ top.id, top.focus = h.lastID, union(top.focus, st.focus)131+ if top.empty() {132+ h.undo = h.undo[:len(h.undo)-1]133+ }134+ return135+ }136+ st.id = h.lastID137+ h.undo = append(h.undo, st)138+ if len(h.undo) > MaxUndo {139+ h.undo = h.undo[len(h.undo)-MaxUndo:]140+ }141+ h.mergeWidths = widthOnly142+}143+144+func (h *history) top() *step {145+ if len(h.undo) == 0 {146+ return nil147+ }148+ return h.undo[len(h.undo)-1]149+}150+151+// Seal ends a run of column width changes, so the next one starts a new152+// undo step. The UI calls it between user actions.153+func (s *Sheet) Seal() { s.hist.mergeWidths = false }154+155+// Undo reverts the last step and returns the range it affected.156+func (s *Sheet) Undo() (Rect, bool) {157+ return s.swap(&s.hist.undo, &s.hist.redo)158+}159+160+// Redo reapplies the last undone step and returns the range it affected.161+func (s *Sheet) Redo() (Rect, bool) {162+ return s.swap(&s.hist.redo, &s.hist.undo)163+}164+165+// CanUndo and CanRedo report whether there is a step to undo or redo.166+func (s *Sheet) CanUndo() bool { return len(s.hist.undo) > 0 }167+func (s *Sheet) CanRedo() bool { return len(s.hist.redo) > 0 }168+169+// StateID identifies the sheet's contents in its undo history: undoing170+// back to a saved state returns the ID it had when saved, so the UI can171+// tell whether there are unsaved changes.172+func (s *Sheet) StateID() int {173+ if top := s.hist.top(); top != nil {174+ return top.id175+ }176+ return 0177+}178+179+// swap pops a step from one stack, restores its before-image, and pushes180+// the state it replaced onto the other stack.181+func (s *Sheet) swap(from, to *[]*step) (Rect, bool) {182+ if len(*from) == 0 || s.hist.open != nil {183+ return Rect{}, false184+ }185+ st := (*from)[len(*from)-1]186+ *from = (*from)[:len(*from)-1]187+ // The inverse keeps the step's ID: on the redo stack, an ID names the188+ // state the step leads back to.189+ inv := &step{id: st.id, focus: st.focus, cells: make(map[Addr]*Cell, len(st.cells)), widths: map[int]int{}}190+ changed := make([]Addr, 0, len(st.cells))191+ for a, c := range st.cells {192+ inv.cells[a] = s.cells[a].clone()193+ s.place(a, c.clone())194+ changed = append(changed, a)195+ }196+ for c, w := range st.widths {197+ inv.widths[c] = s.widths[c]198+ s.setWidth(c, w)199+ }200+ s.recalc(changed)201+ *to = append(*to, inv)202+ s.hist.mergeWidths = false203+ return st.focus, true204+}205+206+// ClearHistory forgets all undo and redo steps.207+func (s *Sheet) ClearHistory() {208+ s.hist = history{}209+}210+211+// colRect is the range covering whole columns from..to.212+func colRect(from, to int) Rect {213+ return Rect{Addr{Col: from}, Addr{Col: to, Row: MaxRows - 1}}214+}215+216+// rowRect is the range covering whole rows from..to.217+func rowRect(from, to int) Rect {218+ return Rect{Addr{Row: from}, Addr{Col: MaxCols - 1, Row: to}}219+}220+221+func union(a, b Rect) Rect {222+ return Rect{223+ From: Addr{Col: min(a.From.Col, b.From.Col), Row: min(a.From.Row, b.From.Row)},224+ To: Addr{Col: max(a.To.Col, b.To.Col), Row: max(a.To.Row, b.To.Row)},225+ }226+}@@ -0,0 +1,147 @@
1+package sheet2+3+import (4+ "reflect"5+ "strings"6+ "testing"7+)8+9+// sheetOf builds a sheet from cell inputs, without undo history.10+func sheetOf(t *testing.T, cells map[string]string) *Sheet {11+ t.Helper()12+ s := New()13+ for a, in := range cells {14+ if err := s.put(at(a), in); err != nil {15+ t.Fatalf("%s: %v", a, err)16+ }17+ }18+ s.RecalcAll()19+ return s20+}21+22+// inputs returns every cell's input by address.23+func inputs(s *Sheet) map[string]string {24+ out := map[string]string{}25+ for a, c := range s.cells {26+ out[a.String()] = c.Input27+ }28+ return out29+}30+31+func TestUndoRedo(t *testing.T) {32+ s := New()33+ s.Set(at("A1"), "1")34+ s.Set(at("A2"), "=A1*10")35+ s.Set(at("A1"), "2")36+ s.EraseRange(NewRect(at("A1"), at("A2")))37+ if s.Len() != 0 || s.StateID() != 4 {38+ t.Fatalf("len %d state %d", s.Len(), s.StateID())39+ }40+ r, ok := s.Undo()41+ if !ok || r.String() != "A1:A2" || s.Value(at("A2")).Num != 20 {42+ t.Fatalf("undo erase: %v %v A2=%+v", r, ok, s.Value(at("A2")))43+ }44+ s.Undo()45+ if s.Value(at("A2")).Num != 10 || s.StateID() != 2 {46+ t.Errorf("undo set: A2=%+v state %d", s.Value(at("A2")), s.StateID())47+ }48+ s.Redo()49+ if s.Value(at("A2")).Num != 20 || s.StateID() != 3 {50+ t.Errorf("redo: A2=%+v state %d", s.Value(at("A2")), s.StateID())51+ }52+ // A new change drops the redo stack.53+ s.Set(at("B1"), "x")54+ if s.CanRedo() {55+ t.Error("redo survived a new change")56+ }57+ for s.CanUndo() {58+ s.Undo()59+ }60+ if s.Len() != 0 || s.StateID() != 0 {61+ t.Errorf("undo all: %v", inputs(s))62+ }63+ // A parse error changes nothing and adds no step.64+ s.Set(at("A1"), "=1+")65+ if s.CanUndo() {66+ t.Error("failed Set added a step")67+ }68+}69+70+func TestUndoKeepsWholeCells(t *testing.T) {71+ s := New()72+ s.Set(at("A1"), "=1+1")73+ orig := s.Cell(at("A1"))74+ s.Set(at("A1"), "x")75+ s.Undo()76+ got := s.Cell(at("A1"))77+ if got == orig || !reflect.DeepEqual(*got, *orig) {78+ t.Errorf("restored %+v, want a copy of %+v", got, orig)79+ }80+}81+82+func TestBatchGroupsAndHistoryCap(t *testing.T) {83+ s := New()84+ s.Batch(NewRect(at("A1"), at("A3")), func() error {85+ for i := range 3 {86+ s.Set(Addr{Row: i}, "1")87+ }88+ s.SetColWidth(0, 20)89+ return nil90+ })91+ s.Undo()92+ if s.Len() != 0 || s.ColWidth(0) != DefaultWidth || s.CanUndo() {93+ t.Errorf("batch undo left %v width %d", inputs(s), s.ColWidth(0))94+ }95+ for i := range MaxUndo + 10 {96+ s.Set(at("A1"), strings.Repeat("x", i+1))97+ }98+ n := 099+ for s.CanUndo() {100+ s.Undo()101+ n++102+ }103+ if n != MaxUndo || inputs(s)["A1"] != strings.Repeat("x", 10) {104+ t.Errorf("undid %d steps, A1 %q", n, inputs(s)["A1"])105+ }106+}107+108+func TestWidthPreviewMerges(t *testing.T) {109+ s := New()110+ s.Set(at("A1"), "1")111+ // A live preview over two columns, then the final value: one step.112+ for w := 11; w <= 13; w++ {113+ s.SetColWidth(0, w)114+ s.SetColWidth(1, w)115+ }116+ s.Undo()117+ if s.ColWidth(0) != DefaultWidth || s.ColWidth(1) != DefaultWidth || s.Len() != 1 {118+ t.Errorf("widths %d %d len %d", s.ColWidth(0), s.ColWidth(1), s.Len())119+ }120+ // A preview that is cancelled leaves no step at all.121+ s.Redo()122+ id := s.StateID()123+ s.Seal()124+ s.SetColWidth(0, 20)125+ s.SetColWidth(0, 13)126+ if s.StateID() != id {127+ t.Errorf("cancelled preview left a step")128+ }129+ // Separate actions are separate steps.130+ s.Seal()131+ s.SetColWidth(2, 5)132+ s.Seal()133+ s.SetColWidth(3, 5)134+ s.Undo()135+ if s.ColWidth(2) != 5 || s.ColWidth(3) != DefaultWidth {136+ t.Errorf("sealed width changes merged")137+ }138+}139+140+func TestLoadIsNotUndoable(t *testing.T) {141+ s := sheetOf(t, map[string]string{"A1": "1"})142+ s.SetColWidth(0, 12)143+ s.RecalcAll()144+ if s.CanUndo() || s.StateID() != 0 {145+ t.Error("RecalcAll kept history")146+ }147+}@@ -42,6 +42,8 @@ type Sheet struct {
4242 4343 // Circular is set when the last recalculation found a cycle.4444 Circular bool45+46+ hist history // undo and redo, see history.go4547 }4648 4749 // New returns an empty worksheet.@@ -93,6 +95,11 @@ func (s *Sheet) ColWidth(c int) int {
9395 9496 // SetColWidth sets column c's width; w <= 0 resets it to the default.9597 func (s *Sheet) SetColWidth(c, w int) {98+ s.change(colRect(c, c), func() { s.setWidth(c, w) })99+}100+101+func (s *Sheet) setWidth(c, w int) {102+ s.recordWidth(c)96103 if w <= 0 {97104 delete(s.widths, c)98105 return@@ -147,11 +154,9 @@ func classify(input string) (Node, error) {
147154 // erases the cell. A formula that fails to parse is rejected with a148155 // *ParseError and the sheet is left unchanged.149156 func (s *Sheet) Set(a Addr, input string) error {150- if err := s.put(a, input); err != nil {151- return err152- }153- s.recalc([]Addr{a})154- return nil157+ var err error158+ s.change(Rect{a, a}, func() { err = s.put(a, input) })159+ return err155160 }156161 157162 // put stores an entry without recalculating. Control characters are@@ -164,20 +169,38 @@ func (s *Sheet) put(a Addr, input string) error {
164169 return r165170 }, input)166171 if input == "" {167- s.erase(a)172+ s.place(a, nil)168173 return nil169174 }170175 n, err := classify(input)171176 if err != nil {172177 return err173178 }174- c := &Cell{Input: input, expr: n}179+ c := &Cell{Input: input}180+ c.setExpr(n)181+ s.place(a, c)182+ return nil183+}184+185+// setExpr sets c's expression and the references indexed from it.186+func (c *Cell) setExpr(n Node) {187+ c.expr, c.refs, c.ranges = n, nil, nil175188 if n != nil {176189 walkRefs(n,177190 func(r Addr) { c.refs = append(c.refs, r) },178191 func(r Rect) { c.ranges = append(c.ranges, r) })179192 }180- s.erase(a)193+}194+195+// place stores c at a (nil blanks it), recording the old cell for undo and196+// keeping the dependency indexes current. Every cell mutation goes through197+// here.198+func (s *Sheet) place(a Addr, c *Cell) {199+ s.record(a)200+ s.unlink(a)201+ if c == nil {202+ return203+ }181204 s.cells[a] = c182205 for _, r := range c.refs {183206 if s.dependents[r] == nil {@@ -188,24 +211,30 @@ func (s *Sheet) put(a Addr, input string) error {
188211 if len(c.ranges) > 0 {189212 s.rangeUsers[a] = struct{}{}190213 }191- return nil192214 }193215 194216 // EraseRange blanks every cell in r.195217 func (s *Sheet) EraseRange(r Rect) {196- var changed []Addr218+ s.change(r, func() {219+ for _, a := range s.cellsIn(r) {220+ s.place(a, nil)221+ }222+ })223+}224+225+// cellsIn returns the non-blank cells in r.226+func (s *Sheet) cellsIn(r Rect) []Addr {227+ var out []Addr197228 for a := range s.cells {198229 if r.Contains(a) {199- changed = append(changed, a)230+ out = append(out, a)200231 }201232 }202- for _, a := range changed {203- s.erase(a)204- }205- s.recalc(changed)233+ return out206234 }207235 208-func (s *Sheet) erase(a Addr) {236+// unlink removes the cell at a and its dependency edges.237+func (s *Sheet) unlink(a Addr) {209238 old := s.cells[a]210239 if old == nil {211240 return@@ -220,9 +249,11 @@ func (s *Sheet) erase(a Addr) {
220249 delete(s.cells, a)221250 }222251 223-// RecalcAll recomputes every formula, e.g. after loading a file.252+// RecalcAll recomputes every formula. Loaders call it once the sheet is253+// built, so it also starts a fresh undo history: loading isn't undoable.224254 func (s *Sheet) RecalcAll() {225255 s.recalc(s.Addrs())256+ s.ClearHistory()226257 }227258 228259 // recalc recomputes the changed cells and everything that transitively