A workbook several people edit keeps one history in the order steps ran, each step with its author and each operation (made, undone, redone) numbered, the latest kept for OpsSince. With the history shared, undo and redo take back the author's own latest step from under others' later steps when none of them changed what it changed, and refuse otherwise, saying whose step is in the way. TestRandomSharedEdits interleaves three authors' random edits and has them undo in random turns, out of order, back to the start.
- commit
- 3242d809cf362430d0ed89c6b983012695345b28
- parent
- 8bced0f1bbc97b809fca72fe724a6188e6fd3385
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 2.3 of 3, Jev's estimate
| A | internal/ | 348 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 171 | +++++++++++++++ |
| M | internal/ | 73 | +++++-- |
| M | internal/ | 2 | +- |
| M | internal/ | 1 | + |
| A | internal/ | 130 | +++++++++++ |
| M | internal/ | 1 | + |
@@ -0,0 +1,348 @@
1+package sheet2+3+import "slices"4+5+// Authors: a workbook several people edit at once (012 serve's shared6+// rooms) is still changed through the one mutation path, Batch and the7+// history's steps, in the one order the server runs them in. Each step8+// records its author (SetAuthor names who is acting) and a sequence9+// number, and the steps made, undone and redone are a stream of10+// operations (Op) the room sends every participant.11+//12+// With the history shared (ShareHistory), Undo and Redo take back only13+// the author's own steps: Undo reverts the author's latest step even14+// when others' steps came after it, as long as none of those changed15+// what it changed (the same cells, widths, heights, line formats or16+// names; the same sheet's charts, pivot, rules, regions or view; the17+// sheet list, the settings or the macros), and none of them, nor it,18+// inserted or deleted rows or columns on a sheet the other changed. The19+// step then leaves the middle of the history, and the later steps stay20+// as they are, their before-images still true: they touched none of21+// what it restores. Otherwise Undo refuses, and UndoBlocked says whose22+// step is in the way. Redo takes back the author's latest undo on the23+// same terms, against the steps made since. Formulas that read what an24+// undone step changed recalculate; entries typed after it that don't25+// overlap it stay as typed.26+27+// OpKind is what an operation did to the history.28+type OpKind int29+30+const (31+ OpDo OpKind = iota // a step made32+ OpUndo // a step undone33+ OpRedo // a step redone34+)35+36+// Op is one operation in the order the workbook ran it: a step made,37+// undone or redone, by its author, with what the step says of itself.38+type Op struct {39+ Seq uint64 // its place in the order, from 140+ Author int41+ Kind OpKind42+ Change43+}44+45+// opLogSize is how many of the latest operations the workbook keeps for46+// OpsSince.47+const opLogSize = 25648+49+// Blocked is a later step that keeps an author's step from being50+// undone or redone: whose it is, and what it did.51+type Blocked struct {52+ Author int53+ Label string54+}55+56+// SetAuthor names who makes the steps that follow; 0 is nobody in57+// particular, as in a workbook only one person edits.58+func (w *Workbook) SetAuthor(a int) { w.author = a }59+60+// Author is who makes the steps that follow (SetAuthor).61+func (w *Workbook) Author() int { return w.author }62+63+// ShareHistory has undo and redo take back only each author's own64+// steps, from now on.65+func (w *Workbook) ShareHistory() { w.hist.shared = true }66+67+// Version changes whenever the workbook changes: a step made, undone or68+// redone, a recalculation, rows arriving at a region.69+func (w *Workbook) Version() uint64 { return w.gen + w.hist.seq }70+71+// OpsSince is the operations after seq, the last opLogSize at most, and72+// the sequence number of the latest.73+func (w *Workbook) OpsSince(seq uint64) ([]Op, uint64) {74+ h := &w.hist75+ i, _ := slices.BinarySearchFunc(h.log, seq+1, func(o Op, s uint64) int { return int(o.Seq) - int(s) })76+ return slices.Clone(h.log[i:]), h.seq77+}78+79+// InStep reports whether a step is open (Begin, a macro running), and80+// whose it is.81+func (w *Workbook) InStep() (bool, int) {82+ if w.hist.open == nil {83+ return false, 084+ }85+ return true, w.hist.open.author86+}87+88+// logOp notes an operation in the order.89+func (w *Workbook) logOp(kind OpKind, author int, c Change) uint64 {90+ h := &w.hist91+ h.seq++92+ h.log = append(h.log, Op{Seq: h.seq, Author: author, Kind: kind, Change: c})93+ if len(h.log) > opLogSize {94+ h.log = slices.Delete(h.log, 0, len(h.log)-opLogSize)95+ }96+ return h.seq97+}98+99+// mine is the index in the undo stack of the author's latest step, or100+// -1; with the history unshared, the top's.101+func (h *history) mine(author int) int {102+ if !h.shared {103+ return len(h.undo) - 1104+ }105+ for i := len(h.undo) - 1; i >= 0; i-- {106+ if h.undo[i].author == author {107+ return i108+ }109+ }110+ return -1111+}112+113+// mineRedo is the index in the redo stack of the author's latest undo.114+func (h *history) mineRedo(author int) int {115+ if !h.shared {116+ return len(h.redo) - 1117+ }118+ for i := len(h.redo) - 1; i >= 0; i-- {119+ if h.redo[i].author == author {120+ return i121+ }122+ }123+ return -1124+}125+126+// UndoBlocked reports whether the current author's latest step can't be127+// undone, and the first later step in the way.128+func (w *Workbook) UndoBlocked() (Blocked, bool) {129+ h := &w.hist130+ i := h.mine(w.author)131+ if i < 0 {132+ return Blocked{}, false133+ }134+ return h.blocking(h.undo[i], h.undo[i+1:], 0)135+}136+137+// RedoBlocked reports whether the current author's latest undo can't be138+// redone, and the first step made since that's in the way.139+func (w *Workbook) RedoBlocked() (Blocked, bool) {140+ h := &w.hist141+ i := h.mineRedo(w.author)142+ if i < 0 {143+ return Blocked{}, false144+ }145+ return h.blocking(h.redo[i], h.undo, h.redo[i].seq)146+}147+148+// blocking is the first of later, made after seq by another author,149+// that overlaps st. The author's own steps after one of theirs are150+// redos of their own undos, which came back in order; with the history151+// unshared nothing blocks.152+func (h *history) blocking(st *step, later []*step, seq uint64) (Blocked, bool) {153+ if !h.shared {154+ return Blocked{}, false155+ }156+ for _, o := range later {157+ if o.seq > seq && o.author != st.author && st.overlaps(o) {158+ return Blocked{Author: o.author, Label: o.label}, true159+ }160+ }161+ return Blocked{}, false162+}163+164+// takeUndo removes the author's latest step from the undo stack, when165+// nothing later is in the way, giving the steps after it new IDs: the166+// states they lead to are new.167+func (h *history) takeUndo(author int) *step {168+ i := h.mine(author)169+ if i < 0 {170+ return nil171+ }172+ if _, blocked := h.blocking(h.undo[i], h.undo[i+1:], 0); blocked {173+ return nil174+ }175+ st := h.undo[i]176+ h.undo = slices.Delete(h.undo, i, i+1)177+ h.bytes -= st.bytes178+ for _, later := range h.undo[i:] {179+ h.lastID++180+ later.id = h.lastID181+ }182+ return st183+}184+185+// takeRedo removes the author's latest undo from the redo stack, when186+// no step made since is in the way.187+func (h *history) takeRedo(author int) *step {188+ i := h.mineRedo(author)189+ if i < 0 {190+ return nil191+ }192+ if _, blocked := h.blocking(h.redo[i], h.undo, h.redo[i].seq); blocked {193+ return nil194+ }195+ st := h.redo[i]196+ h.redo = slices.Delete(h.redo, i, i+1)197+ return st198+}199+200+// dropRedo forgets the author's undos, which a new step of theirs ends,201+// or everyone's when the history isn't shared.202+func (h *history) dropRedo(author int) {203+ if !h.shared {204+ h.redo = nil205+ return206+ }207+ h.redo = slices.DeleteFunc(h.redo, func(st *step) bool { return st.author == author })208+}209+210+// recordShift notes that the open step moves the sheet's cells by211+// inserting or deleting lines, which conflicts with any other step on212+// the sheet.213+func (s *Sheet) recordShift() {214+ if st := s.wb.hist.open; st != nil {215+ if st.shifts == nil {216+ st.shifts = map[*Sheet]bool{}217+ }218+ st.shifts[s] = true219+ }220+}221+222+// overlaps reports whether st and o change any of the same things, so223+// one can't be undone past the other (see Authors above).224+func (st *step) overlaps(o *step) bool {225+ if st.sheets != nil || o.sheets != nil || st.settings != nil && o.settings != nil || st.macros != nil && o.macros != nil {226+ return true227+ }228+ for s := range st.shifts {229+ if o.touches(s) {230+ return true231+ }232+ }233+ for s := range o.shifts {234+ if st.touches(s) {235+ return true236+ }237+ }238+ return st.sameCells(o) || sharesKey(st.widths, o.widths) || sharesKey(st.heights, o.heights) ||239+ sharesKey(st.lines, o.lines) || sharesKey(st.names, o.names) || sharesKey(st.views, o.views) ||240+ sharesKey(st.charts, o.charts) || sharesKey(st.pivots, o.pivots) || sharesKey(st.rules, o.rules) ||241+ sharesKey(st.regions, o.regions)242+}243+244+// sameCells reports whether the steps changed any of the same cells.245+func (st *step) sameCells(o *step) bool {246+ for s, img := range st.cells {247+ other := o.cells[s]248+ if other == nil {249+ continue250+ }251+ if img.len() > other.len() {252+ img, other = other, img253+ }254+ if img.anyIn(other) {255+ return true256+ }257+ }258+ return false259+}260+261+// touches reports whether the step changed anything of sheet s.262+func (st *step) touches(s *Sheet) bool {263+ if st.cells[s] != nil || st.views[s] != nil || st.shifts[s] {264+ return true265+ }266+ for _, m := range []map[*Sheet]bool{keysOf(st.charts), keysOf(st.pivots), keysOf(st.rules), keysOf(st.regions)} {267+ if m[s] {268+ return true269+ }270+ }271+ for k := range st.widths {272+ if k.s == s {273+ return true274+ }275+ }276+ for k := range st.heights {277+ if k.s == s {278+ return true279+ }280+ }281+ for k := range st.lines {282+ if k.s == s {283+ return true284+ }285+ }286+ return false287+}288+289+// keysOf is a map's sheets as a set.290+func keysOf[V any](m map[*Sheet]V) map[*Sheet]bool {291+ out := make(map[*Sheet]bool, len(m))292+ for s := range m {293+ out[s] = true294+ }295+ return out296+}297+298+// sharesKey reports whether two maps have a key in common.299+func sharesKey[K comparable, V1, V2 any](a map[K]V1, b map[K]V2) bool {300+ if len(a) > len(b) {301+ for k := range b {302+ if _, ok := a[k]; ok {303+ return true304+ }305+ }306+ return false307+ }308+ for k := range a {309+ if _, ok := b[k]; ok {310+ return true311+ }312+ }313+ return false314+}315+316+// anyIn reports whether img holds a before-image of any cell other317+// holds one of.318+func (img *image) anyIn(other *image) bool {319+ found := false320+ img.addrs(func(a Addr) bool {321+ found = other.seen(a)322+ return !found323+ })324+ return found325+}326+327+// addrs calls fn with the address of every cell the image holds, until328+// fn returns false.329+func (img *image) addrs(fn func(Addr) bool) {330+ for _, ic := range img.small {331+ if !fn(ic.a) {332+ return333+ }334+ }335+ st := img.tables()336+ more := true337+ for _, occ := range []*occupancy{&st.stored, &img.blank} {338+ for _, col := range occ.colIDs {339+ occ.colScan(col, 0, MaxRows-1, func(row int) bool {340+ more = fn(Addr{Col: col, Row: row})341+ return more342+ })343+ if !more {344+ return345+ }346+ }347+ }348+}@@ -0,0 +1,171 @@
1+package sheet2+3+import (4+ "testing"5+)6+7+// sharedBook is a workbook with its history shared, as a room's is.8+func sharedBook() (*Workbook, *Sheet) {9+ s := New()10+ w := s.Book()11+ w.ShareHistory()12+ return w, s13+}14+15+// as makes one step as author a.16+func as(w *Workbook, a int, fn func()) {17+ w.SetAuthor(a)18+ fn()19+}20+21+// Operations are numbered in the order they run, whoever runs them, and22+// OpsSince hands them out in that order.23+func TestOpsInOrder(t *testing.T) {24+ w, s := sharedBook()25+ as(w, 1, func() { s.Set(at("A1"), "1") })26+ as(w, 2, func() { s.Set(at("B1"), "2") })27+ as(w, 1, func() { w.Undo() })28+ ops, last := w.OpsSince(0)29+ if last != 3 || len(ops) != 3 {30+ t.Fatalf("ops %+v, last %d", ops, last)31+ }32+ want := []struct {33+ author int34+ kind OpKind35+ label string36+ }{{1, OpDo, "edit A1"}, {2, OpDo, "edit B1"}, {1, OpUndo, "edit A1"}}37+ for i, o := range ops {38+ if o.Seq != uint64(i+1) || o.Author != want[i].author || o.Kind != want[i].kind || o.Label != want[i].label {39+ t.Errorf("op %d: %+v, want %+v", i, o, want[i])40+ }41+ }42+ if more, _ := w.OpsSince(2); len(more) != 1 || more[0].Kind != OpUndo {43+ t.Errorf("since 2: %+v", more)44+ }45+}46+47+// Undo takes back only the author's own latest step, from under later48+// steps of others that changed other cells; those stay.49+func TestUndoIsPerAuthor(t *testing.T) {50+ w, s := sharedBook()51+ as(w, 1, func() { s.Set(at("A1"), "mine") })52+ as(w, 2, func() { s.Set(at("B1"), "theirs") })53+ as(w, 2, func() { s.Set(at("B2"), "=A1") })54+ before := w.StateID()55+ w.SetAuthor(1)56+ if w.UndoLabel() != "edit A1" {57+ t.Errorf("author 1 would undo %q", w.UndoLabel())58+ }59+ if _, ok := w.Undo(); !ok {60+ t.Fatal("author 1's undo refused")61+ }62+ if s.Filled(at("A1")) || s.Value(at("B1")).Str != "theirs" {63+ t.Errorf("A1 %v, B1 %v", s.Value(at("A1")), s.Value(at("B1")))64+ }65+ if v := s.Value(at("B2")); v.Str == "mine" {66+ t.Errorf("B2, reading A1, is %v", v)67+ }68+ if w.StateID() == before {69+ t.Error("an undo from under others' steps kept the state's ID")70+ }71+ if w.CanUndo() {72+ t.Error("author 1 has nothing left to undo")73+ }74+ w.SetAuthor(2)75+ if !w.CanUndo() || w.UndoLabel() != "edit B2" {76+ t.Errorf("author 2 would undo %q", w.UndoLabel())77+ }78+ // Redo is per author too.79+ w.SetAuthor(1)80+ if !w.CanRedo() {81+ t.Fatal("author 1 can't redo")82+ }83+ w.Redo()84+ if s.Value(at("A1")).Str != "mine" || s.Value(at("B2")).Str != "mine" {85+ t.Errorf("after redo A1 %v, B2 %v", s.Value(at("A1")), s.Value(at("B2")))86+ }87+ w.SetAuthor(2)88+ if w.CanRedo() {89+ t.Error("author 2 can redo author 1's undo")90+ }91+}92+93+// Undo refuses when a later step of someone else's changed what the94+// step changed, and says whose; once that step is undone, it goes.95+func TestUndoRefusesOverlaps(t *testing.T) {96+ w, s := sharedBook()97+ as(w, 1, func() { s.Set(at("A1"), "first") })98+ as(w, 2, func() { s.Set(at("A1"), "second") })99+ w.SetAuthor(1)100+ b, blocked := w.UndoBlocked()101+ if !blocked || b.Author != 2 || b.Label != "edit A1" {102+ t.Fatalf("blocked %v by %+v", blocked, b)103+ }104+ if _, ok := w.Undo(); ok || s.Value(at("A1")).Str != "second" {105+ t.Fatalf("undo went ahead: A1 %v", s.Value(at("A1")))106+ }107+ as(w, 2, func() { w.Undo() })108+ w.SetAuthor(1)109+ if _, blocked := w.UndoBlocked(); blocked {110+ t.Fatal("still blocked")111+ }112+ w.Undo()113+ if s.Filled(at("A1")) {114+ t.Errorf("A1 %v", s.Value(at("A1")))115+ }116+ // Author 2's redo is now in the way of nothing, and redoes onto117+ // the blank cell what it undid.118+ w.SetAuthor(2)119+ if _, ok := w.Redo(); !ok || s.Value(at("A1")).Str != "second" {120+ t.Errorf("author 2's redo: A1 %v", s.Value(at("A1")))121+ }122+ // Author 1's redo would overwrite it: refused.123+ w.SetAuthor(1)124+ if b, blocked := w.RedoBlocked(); !blocked || b.Author != 2 {125+ t.Errorf("author 1's redo not blocked: %+v", b)126+ }127+}128+129+// Inserting or deleting lines conflicts with any other step on the same130+// sheet, and with none on another.131+func TestUndoRefusesAcrossShifts(t *testing.T) {132+ w, s := sharedBook()133+ s2, _ := w.AddSheet("S2", 1)134+ as(w, 1, func() { s.Set(at("A5"), "x") })135+ as(w, 1, func() { s.InsertRows(2, 1) })136+ as(w, 2, func() { s2.Set(at("A1"), "elsewhere") })137+ w.SetAuthor(1)138+ if _, blocked := w.UndoBlocked(); blocked {139+ t.Fatal("a step on another sheet blocks the insert")140+ }141+ as(w, 2, func() { s.Set(at("Z1"), "far away") })142+ w.SetAuthor(1)143+ if _, blocked := w.UndoBlocked(); !blocked {144+ t.Fatal("a step on the shifted sheet doesn't block the insert")145+ }146+}147+148+// The sheet list conflicts with everything after it.149+func TestUndoRefusesAfterSheetChanges(t *testing.T) {150+ w, s := sharedBook()151+ as(w, 1, func() { w.AddSheet("S2", 1) })152+ as(w, 2, func() { s.Set(at("A1"), "1") })153+ w.SetAuthor(1)154+ if _, blocked := w.UndoBlocked(); !blocked {155+ t.Error("adding a sheet undone past a later step")156+ }157+}158+159+// Unshared, undo takes back the latest step whoever made it, as a160+// workbook one person edits does.161+func TestUndoUnshared(t *testing.T) {162+ s := New()163+ w := s.Book()164+ as(w, 1, func() { s.Set(at("A1"), "1") })165+ as(w, 2, func() { s.Set(at("A1"), "2") })166+ w.SetAuthor(1)167+ w.Undo()168+ if s.Value(at("A1")).Num != 1 {169+ t.Errorf("A1 %v", s.Value(at("A1")))170+ }171+}@@ -35,11 +35,23 @@ type history struct {
3535 // live preview of a column width or row height and its final value3636 // (or its cancellation) are one step. Seal ends the run.3737 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 []Op3844 }3945 4046 type step struct {41- id int42- bytes int64 // estimated heap held, set when pushed on the undo stack47+ 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 stack4355 // cellBytes is the estimated heap held by cells, kept as they are4456 // recorded so sizing a step doesn't walk them again.4557 cellBytes int64@@ -143,6 +155,7 @@ func (w *Workbook) begin(s *Sheet, label string, focus Rect) {
143155 h := &w.hist144156 if h.depth == 0 {145157 h.open = newStep(label, s, focus)158+ h.open.author = w.author146159 w.structural = false147160 }148161 h.depth++@@ -188,10 +201,11 @@ func (w *Workbook) push(st *step) {
188201 return189202 }190203 h := &w.hist191- h.redo = nil204+ h.dropRedo(st.author)192205 h.lastID++206+ st.seq = w.logOp(OpDo, st.author, st.change())193207 widthOnly := st.widthOnly()194- if top := h.top(); widthOnly && h.mergeWidths && top != nil && top.widthOnly() {208+ if top := h.top(); widthOnly && h.mergeWidths && top != nil && top.widthOnly() && top.author == st.author {195209 h.joinWidths(top, st)196210 return197211 }@@ -200,6 +214,16 @@ func (w *Workbook) push(st *step) {
200214 h.mergeWidths = widthOnly201215 }202216 217+// change is what the step says of itself.218+func (st *step) change() Change {219+ return Change{Label: st.label, Focus: st.focus, Sheet: st.sheet, Tabs: st.sheets != nil, Macros: st.macrosOnly()}220+}221+222+// macrosOnly reports whether the step changed nothing but the macros.223+func (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+}226+203227 // dropUnchanged removes from st what ended the step as it began.204228 func (w *Workbook) dropUnchanged(st *step) {205229 st.dropUnchangedLines()@@ -330,15 +354,16 @@ func (w *Workbook) Redo() (Change, bool) { return w.swap(false) }
330354 // UndoLabel describes the step Undo would revert, e.g. "clear B3:B5",331355 // or "" when there is none.332356 func (w *Workbook) UndoLabel() string {333- if top := w.hist.top(); top != nil {334- return top.label357+ if i := w.hist.mine(w.author); i >= 0 {358+ return w.hist.undo[i].label335359 }336360 return ""337361 }338362 339-// CanUndo and CanRedo report whether there is a step to undo or redo.340-func (w *Workbook) CanUndo() bool { return len(w.hist.undo) > 0 }341-func (w *Workbook) CanRedo() bool { return len(w.hist.redo) > 0 }363+// CanUndo and CanRedo report whether there is a step to undo or redo:364+// with the history shared, one of the current author's.365+func (w *Workbook) CanUndo() bool { return w.hist.mine(w.author) >= 0 }366+func (w *Workbook) CanRedo() bool { return w.hist.mineRedo(w.author) >= 0 }342367 343368 // StateID identifies the workbook's contents in its undo history: undoing344369 // back to a saved state returns the ID it had when saved, so the UI can@@ -368,28 +393,36 @@ func (s *Sheet) ClearHistory() { s.wb.ClearHistory() }
368393 // before-image, and pushes the state it replaced onto the other stack.369394 func (w *Workbook) swap(undo bool) (Change, bool) {370395 h := &w.hist371- from := &h.redo372- if undo {373- from = &h.undo374- }375- if len(*from) == 0 || h.open != nil {396+ if h.open != nil {376397 return Change{}, false377398 }378399 var st *step379- if undo {400+ switch {401+ case undo && h.shared:402+ st = h.takeUndo(w.author)403+ case undo && len(h.undo) > 0:380404 st = h.popUndo()381- } else {382- st = h.redo[len(h.redo)-1]383- h.redo = h.redo[:len(h.redo)-1]405+ case !undo:406+ st = h.takeRedo(w.author)407+ }408+ if st == nil {409+ return Change{}, false384410 }385411 inv, changed := w.restore(st)386412 w.recalcSwapped(changed)413+ inv.author = st.author414+ c := st.change()387415 if undo {416+ inv.seq = w.logOp(OpUndo, st.author, c)388417 h.redo = append(h.redo, inv)389418 } 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+ }390424 w.pushUndo(inv)391425 }392426 h.mergeWidths = false393- macrosOnly := 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) == 0394- return Change{Label: st.label, Focus: st.focus, Sheet: st.sheet, Tabs: st.sheets != nil, Macros: macrosOnly}, true427+ return c, true395428 }@@ -9,7 +9,7 @@ import "slices"
99 // change taken back inside another's step (Try) restores its own.1010 func (w *Workbook) restore(st *step) (inv *step, changed []loc) {1111 inv = newStep(st.label, st.sheet, st.focus)12- inv.id = st.id12+ inv.id, inv.shifts = st.id, st.shifts1313 // The sheet list goes first, so cells land on attached sheets.1414 if st.sheets != nil {1515 inv.sheets = w.sheetList()@@ -138,6 +138,7 @@ func (s *Sheet) restructure(rows bool, sp formula.Span) {
138138 label += "s"139139 }140140 s.change(label, focus, func() {141+ s.recordShift()141142 s.shiftRegions(rows, sp) // first, so remap doesn't take cells moving where regions were for theirs142143 s.remap(label, focus, cell, rng)143144 s.remapNames(rng)@@ -0,0 +1,130 @@
1+package sheet2+3+import (4+ "bytes"5+ "fmt"6+ "maps"7+ "math/rand/v2"8+ "testing"9+)10+11+// Random edits by several participants: the edits of TestRandomEdits,12+// each made by one of three authors drawn from the seed, interleaved in13+// one workbook whose history is shared, as a room of 012 serve orders14+// them. Then the authors undo, in turns drawn from the seed, each only15+// their own steps, from under the others' where nothing later overlaps16+// them. What must hold:17+//18+// - someone can always undo (the latest step's author, at least), so19+// every step is undone in the end;20+// - an undo that goes ahead, redone at once by its author, gives back21+// exactly the file it undid;22+// - once every step is undone, whatever the order, the workbook is23+// back where it started: the file it saves and every value.24+func TestRandomSharedEdits(t *testing.T) {25+ seeds := max(*randSeeds/4, 1)26+ if testing.Short() {27+ seeds = min(seeds, 20)28+ }29+ for seed := range seeds {30+ rng := rand.New(rand.NewPCG(uint64(seed), 34))31+ data := make([]byte, 64+(*randSteps)*10)32+ for i := range data {33+ data[i] = byte(rng.Uint32())34+ }35+ t.Run(fmt.Sprint(seed), func(t *testing.T) {36+ msg, log := randomSharedEdits(data, *randSteps, rng)37+ if msg != "" {38+ t.Fatalf("%s\nafter %d edits:\n %s", msg, len(log), joinLog(log))39+ }40+ })41+ }42+}43+44+// randAuthors is how many participants edit.45+const randAuthors = 346+47+func randomSharedEdits(data []byte, steps int, rng *rand.Rand) (string, []string) {48+ e := &edits{data: data}49+ r := newRandBook(e)50+ r.wb.ShareHistory()51+ start, startVals, startFeeds := r.save(), bookValues(r.wb), maps.Clone(r.feeds)52+ var log []string53+ for range steps {54+ if e.done() {55+ break56+ }57+ a := 1 + e.n(randAuthors)58+ r.wb.SetAuthor(a)59+ log = append(log, fmt.Sprintf("%d: %s", a, r.edit(e)))60+ r.feedStale()61+ }62+ for undone := 0; ; undone++ {63+ a, ok := r.nextUndo(rng)64+ if !ok {65+ break66+ }67+ r.wb.SetAuthor(a)68+ label := r.wb.UndoLabel()69+ before := r.save()70+ if _, ok := r.wb.Undo(); !ok {71+ return fmt.Sprintf("author %d's undo of %q refused though nothing blocks it", a, label), log72+ }73+ r.feedStale()74+ log = append(log, fmt.Sprintf("%d undoes %q", a, label))75+ if rng.IntN(3) > 0 {76+ continue77+ }78+ if _, ok := r.wb.Redo(); !ok {79+ return fmt.Sprintf("author %d can't redo %q at once", a, label), log80+ }81+ r.feedStale()82+ if got := r.save(); !bytes.Equal(got, before) {83+ return fmt.Sprintf("author %d's redo of %q saves differently:\n%s", a, label, lineDiff(string(before), string(got))), log84+ }85+ r.wb.Undo()86+ r.feedStale()87+ }88+ feedAll(r.wb, startFeeds)89+ if got := r.save(); !bytes.Equal(got, start) {90+ return "undoing every step saves differently:\n" + lineDiff(string(start), string(got)), log91+ }92+ if d := diffValues(startVals, bookValues(r.wb)); d != "" {93+ return "undoing every step: " + d, log94+ }95+ return "", log96+}97+98+// nextUndo picks an author who can undo a step now, in a turn drawn99+// from rng, or reports false when nobody has a step left. It fails the100+// run when steps are left but nobody can undo one.101+func (r *randBook) nextUndo(rng *rand.Rand) (int, bool) {102+ first := rng.IntN(randAuthors)103+ left := false104+ for i := range randAuthors {105+ a := 1 + (first+i)%randAuthors106+ r.wb.SetAuthor(a)107+ if !r.wb.CanUndo() {108+ continue109+ }110+ left = true111+ if _, blocked := r.wb.UndoBlocked(); !blocked {112+ return a, true113+ }114+ }115+ if left {116+ panic("steps are left that nobody can undo")117+ }118+ return 0, false119+}120+121+func joinLog(log []string) string {122+ var b bytes.Buffer123+ for i, l := range log {124+ if i > 0 {125+ b.WriteString("\n ")126+ }127+ b.WriteString(l)128+ }129+ return b.String()130+}