cat / 012
clone

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

Describe undo steps so the UI can say what was undone

Dami committed ae0bac0
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>
5 files changed, +66 -34
Minternal/sheet/history.go37++++++++++++++++-----------
Minternal/sheet/history_test.go8+++---
Minternal/sheet/ops.go42+++++++++++++++++++++++-------
Minternal/sheet/refs_test.go7++++-
Minternal/sheet/sheet.go6++--
M internal/sheet/history.go +22 -15
@@ -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.
M internal/sheet/history_test.go +4 -4
M internal/sheet/ops.go +32 -10
@@ -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) }
M internal/sheet/refs_test.go +5 -2
M internal/sheet/sheet.go +3 -3