cat / 012
clone

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

cat/module-path
012 / internal / sheet / history.go
history.go 12.8K
1package sheet23import "slices"45// MaxUndo is how many steps of undo history a workbook keeps, at most;6// MaxUndoBytes also bounds it.7const MaxUndo = 10089// History is a command log of before-images, shared by every sheet of a10// workbook as in Sheets. Each step records, for every cell, column width,11// named range, view, chart list and the sheet list it touched, the state12// before the step; undoing swaps those back in and records the state it13// replaced as the redo step.14//15// Steps keep cells' before-images in the compact form the sheet keeps16// its cells (see historyimage.go): plain cells as slots, rich ones as a17// shallow copy of their Cell, so fields added to Cell later are covered18// without changes here. That relies on one rule: a stored *Cell is never19// modified in place except for its computed Value. To change a cell,20// build a new one and store it with place.21type history struct {22 undo, redo []*step23 open *step // the step being built; nil outside change24 depth int // nesting of change calls25 dirty []loc26 lastID int27 bytes int64 // estimated heap held by the undo steps; see historysize.go28 // off is set while a change runs without undo (WithoutUndo): its29 // step records no before-images.30 off bool31 // base is the state ID with no step to undo: 0 until a change ran32 // without undo, then that change's.33 base int34 // mergeWidths lets the next width-only step join the top one, so a35 // live preview of a column width or row height and its final value36 // (or its cancellation) are one step. Seal ends the run.37 mergeWidths bool38 // shared has undo and redo take back each author's own steps, seq39 // counts the operations (steps made, undone, redone) and log keeps40 // the latest: authors.go.41 shared bool42 seq uint6443 log []Op44}4546type step struct {47 id int48 // author made the step; seq is its operation's place in the order,49 // or for a step on the redo stack, the order's when it was undone.50 author int51 seq uint6452 // shifts are the sheets whose lines the step inserted or deleted.53 shifts map[*Sheet]bool54 bytes int64 // estimated heap held, set when pushed on the undo stack55 // cellBytes is the estimated heap held by cells, kept as they are56 // recorded so sizing a step doesn't walk them again.57 cellBytes int6458 label string // what the step did, e.g. "clear B3:B5"59 sheet *Sheet // the sheet the UI shows when the step is undone or redone60 focus Rect // what the UI selects there61 cells map[*Sheet]*image // before the step, by sheet62 widths map[colKey]int // before the step; 0 for the default width63 heights map[rowKey]int // rows' heights before the step, 0 for none; nil until one changes64 lines map[lineKey]lineFmt // column and row formats before the step65 names map[string]*Name // before the step, by key; nil for undefined66 views map[*Sheet]*viewState67 // charts holds each touched sheet's charts before the step.68 charts map[*Sheet][]Chart69 // pivots holds each touched sheet's pivot definition before the step.70 pivots map[*Sheet]*Pivot71 // rules holds each touched sheet's conditional formats and data72 // validation before the step.73 rules map[*Sheet]rulesState74 // regions holds each touched sheet's regions before the step.75 regions map[*Sheet]regionState76 // sheets is the sheet list before the step, when it changed.77 sheets *sheetList78 // settings are the workbook's settings before the step, when they79 // changed.80 settings *settings81 // macros is the macro list before the step, when it changed.82 macros *[]Macro83}8485// colKey is a column of a sheet.86type colKey struct {87 s *Sheet88 col int89}9091func newStep(label string, s *Sheet, focus Rect) *step {92 return &step{label: label, sheet: s, focus: focus, cells: map[*Sheet]*image{}, widths: map[colKey]int{}, lines: map[lineKey]lineFmt{},93 names: map[string]*Name{}, views: map[*Sheet]*viewState{}, charts: map[*Sheet][]Chart{}, pivots: map[*Sheet]*Pivot{}, rules: map[*Sheet]rulesState{},94 regions: map[*Sheet]regionState{}}95}9697func (st *step) empty() bool {98 return len(st.widths) == 0 && len(st.heights) == 0 && st.widthOnly()99}100101// widthOnly reports whether the step changed nothing but column widths102// and row heights.103func (st *step) widthOnly() bool {104 return len(st.cells) == 0 && len(st.lines) == 0 && len(st.names) == 0 && len(st.views) == 0 && len(st.charts) == 0 && len(st.pivots) == 0 && len(st.rules) == 0 && st.sheets == nil && st.settings == nil &&105 st.macros == nil && len(st.regions) == 0106}107108func (c *Cell) clone() *Cell {109 if c == nil {110 return nil111 }112 cp := *c113 return &cp114}115116// Change describes an undo step: what it did, in lower case for use in a117// sentence ("clear B3:B5"), the sheet it happened on and the range it118// affected there.119type Change struct {120 Label string121 Focus Rect122 Sheet *Sheet123 // Tabs is set when the step added, deleted, renamed or moved sheets;124 // Focus means nothing then.125 Tabs bool126 // Macros is set when the step changed nothing but the macros; Focus127 // means nothing then either.128 Macros bool129}130131// Batch runs fn as a single undo step: everything it changes is undone132// together, and formulas are recalculated once at the end. Batches nest;133// inner ones join the outermost.134func (s *Sheet) Batch(c Change, fn func() error) error {135 var err error136 s.change(c.Label, c.Focus, func() { err = fn() })137 return err138}139140// change runs fn as an undo step on this sheet; see Workbook.change.141func (s *Sheet) change(label string, focus Rect, fn func()) {142 s.wb.change(s, label, focus, fn)143}144145// change opens a step (unless one is open), runs fn, and on the way out146// of the outermost call recalculates what changed and pushes the step.147func (w *Workbook) change(s *Sheet, label string, focus Rect, fn func()) {148 w.begin(s, label, focus)149 defer w.finish()150 fn()151}152153// begin opens a step unless one is open, and nests into it.154func (w *Workbook) begin(s *Sheet, label string, focus Rect) {155 h := &w.hist156 if h.depth == 0 {157 h.open = newStep(label, s, focus)158 h.open.author = w.author159 w.structural = false160 }161 h.depth++162}163164// finish leaves a nesting level; the outermost recalculates what changed165// and pushes the step.166func (w *Workbook) finish() {167 h := &w.hist168 h.depth--169 if h.depth > 0 {170 return171 }172 w.settleTables()173 st, dirty := h.open, h.dirty174 h.open, h.dirty = nil, nil175 if w.structural {176 w.structural = false177 w.recalcAll()178 } else {179 w.recalc(dirty)180 }181 w.push(st)182}183184// record saves the cell at a before its first change in the open step.185func (s *Sheet) record(a Addr) {186 st := s.wb.hist.open187 if st == nil {188 return189 }190 if !s.wb.hist.off {191 st.keep(s, a)192 }193 s.wb.hist.dirty = append(s.wb.hist.dirty, loc{s, a})194}195196// push adds a finished step to the undo stack and clears redo, dropping197// no-op changes.198func (w *Workbook) push(st *step) {199 w.dropUnchanged(st)200 if st.empty() {201 return202 }203 h := &w.hist204 h.dropRedo(st.author)205 h.lastID++206 st.seq = w.logOp(OpDo, st.author, st.change())207 widthOnly := st.widthOnly()208 if top := h.top(); widthOnly && h.mergeWidths && top != nil && top.widthOnly() && top.author == st.author {209 h.joinWidths(top, st)210 return211 }212 st.id = h.lastID213 w.pushUndo(st)214 h.mergeWidths = widthOnly215}216217// change is what the step says of itself.218func (st *step) change() Change {219 return Change{Label: st.label, Focus: st.focus, Sheet: st.sheet, Tabs: st.sheets != nil, Macros: st.macrosOnly()}220}221222// macrosOnly reports whether the step changed nothing but the macros.223func (st *step) macrosOnly() bool {224 return st.macros != nil && len(st.cells) == 0 && len(st.lines) == 0 && len(st.names) == 0 && len(st.charts) == 0 && len(st.pivots) == 0 && len(st.rules) == 0 && st.sheets == nil && len(st.regions) == 0225}226227// dropUnchanged removes from st what ended the step as it began.228func (w *Workbook) dropUnchanged(st *step) {229 st.dropUnchangedLines()230 st.dropUnchangedRules()231 for s, img := range st.cells {232 st.cellBytes -= img.dropBlanks(s)233 if img.len() == 0 {234 delete(st.cells, s)235 st.cellBytes -= imageBytes236 }237 }238 for k, n := range st.names {239 if sameName(n, w.namePtr(k)) {240 delete(st.names, k)241 }242 }243 for s, charts := range st.charts {244 if slices.Equal(charts, s.charts) {245 delete(st.charts, s)246 }247 }248 for s, v := range st.views {249 if v.equal(s.view) {250 delete(st.views, s)251 }252 }253 for s, p := range st.pivots {254 if samePivot(p, s.pivot.def) {255 delete(st.pivots, s)256 }257 }258 for s, r := range st.regions {259 if sameRegions(r, s.regions) {260 delete(st.regions, s)261 }262 }263 if st.sheets != nil && st.sheets.equal(w.sheetList()) {264 st.sheets = nil265 }266 if st.settings != nil && *st.settings == w.settings {267 st.settings = nil268 }269 if st.macros != nil && slices.Equal(*st.macros, w.macros) {270 st.macros = nil271 }272}273274// dropUnchangedLines removes the column widths, row heights and line275// formats that ended the step as they began.276func (st *step) dropUnchangedLines() {277 for k, width := range st.widths {278 if k.s.widths[k.col] == width {279 delete(st.widths, k)280 }281 }282 for k, h := range st.heights {283 if k.s.heights[k.row] == h {284 delete(st.heights, k)285 }286 }287 for k, l := range st.lines {288 if k.s.line(k.row, k.n) == l {289 delete(st.lines, k)290 }291 }292}293294// dropUnchangedRules removes the sheets' rules that ended the step as295// they began.296func (st *step) dropUnchangedRules() {297 for s, r := range st.rules {298 if r.equal(s.rules) {299 delete(st.rules, s)300 }301 }302}303304// sameName reports whether two named ranges (nil for undefined) are the305// same.306func sameName(a, b *Name) bool {307 if a == nil || b == nil {308 return a == b309 }310 return *a == *b311}312313// joinWidths folds the width-only step st into the width-only step on314// top, so a live preview of a column width or row height and its final315// value (or its cancellation) are one step.316func (h *history) joinWidths(top, st *step) {317 for k, width := range st.widths {318 if _, ok := top.widths[k]; !ok {319 top.widths[k] = width320 }321 }322 for k, h := range st.heights {323 if _, ok := top.heights[k]; !ok {324 top.keepHeight(k, h)325 }326 }327 top.dropUnchangedLines()328 top.id, top.focus = h.lastID, union(top.focus, st.focus)329 h.bytes -= top.bytes330 top.bytes = top.size()331 h.bytes += top.bytes332 if top.empty() {333 h.popUndo()334 }335}336337func (h *history) top() *step {338 if len(h.undo) == 0 {339 return nil340 }341 return h.undo[len(h.undo)-1]342}343344// Seal ends a run of column width changes, so the next one starts a new345// undo step. The UI calls it between user actions.346func (w *Workbook) Seal() { w.hist.mergeWidths = false }347348// Undo reverts the last step and describes it.349func (w *Workbook) Undo() (Change, bool) { return w.swap(true) }350351// Redo reapplies the last undone step and describes it.352func (w *Workbook) Redo() (Change, bool) { return w.swap(false) }353354// UndoLabel describes the step Undo would revert, e.g. "clear B3:B5",355// or "" when there is none.356func (w *Workbook) UndoLabel() string {357 if i := w.hist.mine(w.author); i >= 0 {358 return w.hist.undo[i].label359 }360 return ""361}362363// CanUndo and CanRedo report whether there is a step to undo or redo:364// with the history shared, one of the current author's.365func (w *Workbook) CanUndo() bool { return w.hist.mine(w.author) >= 0 }366func (w *Workbook) CanRedo() bool { return w.hist.mineRedo(w.author) >= 0 }367368// StateID identifies the workbook's contents in its undo history: undoing369// back to a saved state returns the ID it had when saved, so the UI can370// tell whether there are unsaved changes.371func (w *Workbook) StateID() int {372 if top := w.hist.top(); top != nil {373 return top.id374 }375 return w.hist.base376}377378// ClearHistory forgets all undo and redo steps.379func (w *Workbook) ClearHistory() { w.hist = history{} }380381// The sheet's history methods are the workbook's, for callers that work382// with one sheet at a time.383384func (s *Sheet) Seal() { s.wb.Seal() }385func (s *Sheet) Undo() (Change, bool) { return s.wb.Undo() }386func (s *Sheet) Redo() (Change, bool) { return s.wb.Redo() }387func (s *Sheet) CanUndo() bool { return s.wb.CanUndo() }388func (s *Sheet) CanRedo() bool { return s.wb.CanRedo() }389func (s *Sheet) StateID() int { return s.wb.StateID() }390func (s *Sheet) ClearHistory() { s.wb.ClearHistory() }391392// swap pops a step from the undo stack (or the redo stack), restores its393// before-image, and pushes the state it replaced onto the other stack.394func (w *Workbook) swap(undo bool) (Change, bool) {395 h := &w.hist396 if h.open != nil {397 return Change{}, false398 }399 var st *step400 switch {401 case undo && h.shared:402 st = h.takeUndo(w.author)403 case undo && len(h.undo) > 0:404 st = h.popUndo()405 case !undo:406 st = h.takeRedo(w.author)407 }408 if st == nil {409 return Change{}, false410 }411 inv, changed := w.restore(st)412 w.recalcSwapped(changed)413 inv.author = st.author414 c := st.change()415 if undo {416 inv.seq = w.logOp(OpUndo, st.author, c)417 h.redo = append(h.redo, inv)418 } else {419 inv.seq = w.logOp(OpRedo, st.author, c)420 if h.shared {421 h.lastID++422 inv.id = h.lastID // a new state, after whatever came since423 }424 w.pushUndo(inv)425 }426 h.mergeWidths = false427 return c, true428}