Each step carries a label such as "clear B3:B5" or "insert 2 rows"; Undo and Redo return it with the affected range, and Batch takes one.
- commit
- ae0bac0a26b061523892dbd843a7984065c5efb4
- parent
- 14fe69c2c37dd02aea07b6b235b6efe28ae218e1
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 37 | ++++++++++++++++----------- |
| M | internal/ | 8 | +++--- |
| M | internal/ | 42 | +++++++++++++++++++++++------- |
| M | internal/ | 7 | ++++- |
| M | internal/ | 6 | ++-- |
@@ -26,6 +26,7 @@ type history struct {
2626 2727 type step struct {2828 id int29+ label string // what the step did, e.g. "clear B3:B5"2930 focus Rect // what the UI selects when the step is undone or redone3031 cells map[Addr]*Cell // before the step; nil for blank3132 widths map[int]int // before the step; 0 for the default width@@ -41,22 +42,28 @@ func (c *Cell) clone() *Cell {
4142 return &cp4243 }4344 45+// Change describes an undo step: what it did, in lower case for use in a46+// sentence ("clear B3:B5"), and the range it affected.47+type Change struct {48+ Label string49+ Focus Rect50+}51+4452 // 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 {53+// together, and formulas are recalculated once at the end. Batches nest;54+// inner ones join the outermost.55+func (s *Sheet) Batch(c Change, fn func() error) error {4956 var err error50- s.change(focus, func() { err = fn() })57+ s.change(c.Label, c.Focus, func() { err = fn() })5158 return err5259 }5360 5461 // change opens a step (unless one is open), runs fn, and on the way out5562 // of the outermost call recalculates what changed and pushes the step.56-func (s *Sheet) change(focus Rect, fn func()) {63+func (s *Sheet) change(label string, focus Rect, fn func()) {5764 h := &s.hist5865 if h.depth == 0 {59- h.open = &step{focus: focus, cells: map[Addr]*Cell{}, widths: map[int]int{}}66+ h.open = &step{label: label, focus: focus, cells: map[Addr]*Cell{}, widths: map[int]int{}}6067 }6168 h.depth++6269 defer func() {@@ -152,13 +159,13 @@ func (h *history) top() *step {
152159 // undo step. The UI calls it between user actions.153160 func (s *Sheet) Seal() { s.hist.mergeWidths = false }154161 155-// Undo reverts the last step and returns the range it affected.156-func (s *Sheet) Undo() (Rect, bool) {162+// Undo reverts the last step and describes it.163+func (s *Sheet) Undo() (Change, bool) {157164 return s.swap(&s.hist.undo, &s.hist.redo)158165 }159166 160-// Redo reapplies the last undone step and returns the range it affected.161-func (s *Sheet) Redo() (Rect, bool) {167+// Redo reapplies the last undone step and describes it.168+func (s *Sheet) Redo() (Change, bool) {162169 return s.swap(&s.hist.redo, &s.hist.undo)163170 }164171 @@ -178,15 +185,15 @@ func (s *Sheet) StateID() int {
178185 179186 // swap pops a step from one stack, restores its before-image, and pushes180187 // the state it replaced onto the other stack.181-func (s *Sheet) swap(from, to *[]*step) (Rect, bool) {188+func (s *Sheet) swap(from, to *[]*step) (Change, bool) {182189 if len(*from) == 0 || s.hist.open != nil {183- return Rect{}, false190+ return Change{}, false184191 }185192 st := (*from)[len(*from)-1]186193 *from = (*from)[:len(*from)-1]187194 // The inverse keeps the step's ID: on the redo stack, an ID names the188195 // 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{}}196+ inv := &step{id: st.id, label: st.label, focus: st.focus, cells: make(map[Addr]*Cell, len(st.cells)), widths: map[int]int{}}190197 changed := make([]Addr, 0, len(st.cells))191198 for a, c := range st.cells {192199 inv.cells[a] = s.cells[a].clone()@@ -200,7 +207,7 @@ func (s *Sheet) swap(from, to *[]*step) (Rect, bool) {
200207 s.recalc(changed)201208 *to = append(*to, inv)202209 s.hist.mergeWidths = false203- return st.focus, true210+ return Change{st.label, st.focus}, true204211 }205212 206213 // ClearHistory forgets all undo and redo steps.@@ -37,9 +37,9 @@ func TestUndoRedo(t *testing.T) {
3737 if s.Len() != 0 || s.StateID() != 4 {3838 t.Fatalf("len %d state %d", s.Len(), s.StateID())3939 }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")))40+ c, ok := s.Undo()41+ if !ok || c != (Change{"clear A1:A2", NewRect(at("A1"), at("A2"))}) || s.Value(at("A2")).Num != 20 {42+ t.Fatalf("undo erase: %v %v A2=%+v", c, ok, s.Value(at("A2")))4343 }4444 s.Undo()4545 if s.Value(at("A2")).Num != 10 || s.StateID() != 2 {@@ -81,7 +81,7 @@ func TestUndoKeepsWholeCells(t *testing.T) {
8181 8282 func TestBatchGroupsAndHistoryCap(t *testing.T) {8383 s := New()84- s.Batch(NewRect(at("A1"), at("A3")), func() error {84+ s.Batch(Change{"type", NewRect(at("A1"), at("A3"))}, func() error {8585 for i := range 3 {8686 s.Set(Addr{Row: i}, "1")8787 }@@ -2,6 +2,7 @@ package sheet
22 33 import (44 "errors"5+ "fmt"56 "strconv"67 )78 @@ -18,14 +19,14 @@ const maxFill = 1 << 20
1819 1920 // remap moves every stored cell with cell (dropping those it rejects) and2021 // rewrites every formula with rw, as one undo step.21-func (s *Sheet) remap(focus Rect, cell func(Addr) (Addr, bool), rw refRewrite) {22+func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rw refRewrite) {2223 next := make(map[Addr]*Cell, len(s.cells))2324 for a, c := range s.cells {2425 if to, ok := cell(a); ok {2526 next[to] = c.rewritten(rw)2627 }2728 }28- s.change(focus, func() {29+ s.change(label, focus, func() {2930 for a, c := range s.cells {3031 if next[a] != c {3132 s.place(a, next[a])@@ -81,13 +82,24 @@ func (s *Sheet) insert(rows bool, at, n int) error {
8182 8283 func (s *Sheet) restructure(rows bool, sp span) {8384 cell, rng := axisRewrite(rows, sp)84- last := sp.at + max(sp.n, -sp.n) - 185+ last := sp.at + abs(sp.n) - 18586 focus := colRect(sp.at, min(last, MaxCols-1))8687 if rows {8788 focus = rowRect(sp.at, min(last, MaxRows-1))8889 }89- s.change(focus, func() {90- s.remap(focus, cell, relocate(cell, rng))90+ verb, noun := "insert", "column"91+ if sp.n < 0 {92+ verb = "delete"93+ }94+ if rows {95+ noun = "row"96+ }97+ label := fmt.Sprintf("%s %d %s", verb, abs(sp.n), noun)98+ if abs(sp.n) > 1 {99+ label += "s"100+ }101+ s.change(label, focus, func() {102+ s.remap(label, focus, cell, relocate(cell, rng))91103 if !rows {92104 s.shiftWidths(sp)93105 }@@ -140,7 +152,7 @@ func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {
140152 }141153 return r, true142154 }143- s.remap(dst, cell, relocate(cell, rng))155+ s.remap("move "+src.String()+" to "+dst.String(), dst, cell, relocate(cell, rng))144156 return dst, nil145157 }146158 @@ -198,7 +210,11 @@ func (s *Sheet) Paste(c *Clip, dst Rect, values bool) (Rect, error) {
198210 if w*h > maxFill {199211 return Rect{}, ErrFillTooBig200212 }201- s.change(dst, func() {213+ label := "paste into " + dst.String()214+ if values {215+ label = "paste values into " + dst.String()216+ }217+ s.change(label, dst, func() {202218 for r := range h {203219 for col := range w {204220 off := Addr{Col: col % cols, Row: r % rows}@@ -250,7 +266,9 @@ func (s *Sheet) FillDown(r Rect) (Rect, error) {
250266 } else {251267 r.From.Row++252268 }253- return s.Paste(s.Copy(src), r, false)269+ var err error270+ s.change("fill down "+r.String(), r, func() { r, err = s.Paste(s.Copy(src), r, false) })271+ return r, err254272 }255273 256274 // FillRight copies the left column of r into the rest of it (Ctrl+R). A@@ -265,7 +283,9 @@ func (s *Sheet) FillRight(r Rect) (Rect, error) {
265283 } else {266284 r.From.Col++267285 }268- return s.Paste(s.Copy(src), r, false)286+ var err error287+ s.change("fill right "+r.String(), r, func() { r, err = s.Paste(s.Copy(src), r, false) })288+ return r, err269289 }270290 271291 // FillEntry stores input in every cell of r as if it had been typed at@@ -278,7 +298,7 @@ func (s *Sheet) FillEntry(r Rect, origin Addr, input string) error {
278298 if _, err := classify(input); err != nil {279299 return err280300 }281- return s.Batch(r, func() error {301+ return s.Batch(Change{"fill " + r.String(), r}, func() error {282302 if err := s.put(origin, input); err != nil {283303 return err284304 }@@ -293,3 +313,5 @@ func (s *Sheet) FillEntry(r Rect, origin Addr, input string) error {
293313 return nil294314 })295315 }316+317+func abs(n int) int { return max(n, -n) }@@ -188,8 +188,11 @@ func TestInsertDeleteColsShiftWidths(t *testing.T) {
188188 if got := inputs(s)["D1"]; got != "=#REF!+C1" || s.ColWidth(2) != 15 {189189 t.Errorf("after delete: D1 %q, width C %d", got, s.ColWidth(2))190190 }191- s.Undo()192- s.Undo()191+ c1, _ := s.Undo()192+ c2, _ := s.Undo()193+ if c1.Label != "delete 1 column" || c2.Label != "insert 2 columns" || c2.Focus != colRect(1, 2) {194+ t.Errorf("undid %+v then %+v", c1, c2)195+ }193196 if got := inputs(s)["C1"]; got != "=A1+B1" || s.ColWidth(1) != 15 {194197 t.Errorf("after undo: C1 %q width B %d", got, s.ColWidth(1))195198 }@@ -95,7 +95,7 @@ func (s *Sheet) ColWidth(c int) int {
9595 9696 // SetColWidth sets column c's width; w <= 0 resets it to the default.9797 func (s *Sheet) SetColWidth(c, w int) {98- s.change(colRect(c, c), func() { s.setWidth(c, w) })98+ s.change("column width", colRect(c, c), func() { s.setWidth(c, w) })9999 }100100 101101 func (s *Sheet) setWidth(c, w int) {@@ -155,7 +155,7 @@ func classify(input string) (Node, error) {
155155 // *ParseError and the sheet is left unchanged.156156 func (s *Sheet) Set(a Addr, input string) error {157157 var err error158- s.change(Rect{a, a}, func() { err = s.put(a, input) })158+ s.change("edit "+a.String(), Rect{a, a}, func() { err = s.put(a, input) })159159 return err160160 }161161 @@ -215,7 +215,7 @@ func (s *Sheet) place(a Addr, c *Cell) {
215215 216216 // EraseRange blanks every cell in r.217217 func (s *Sheet) EraseRange(r Rect) {218- s.change(r, func() {218+ s.change("clear "+r.String(), r, func() {219219 for _, a := range s.cellsIn(r) {220220 s.place(a, nil)221221 }