1package sheet23import (4 "errors"5 "fmt"6 "slices"78 "src.rest/cat/012/internal/formula"9)1011// Merged cells, as Sheets' Format > Merge cells: a merge joins a range12// into one cell that shows its top-left cell's contents and formatting.13// The merges are part of the sheet's view state (view.go): a list14// replaced whole on every change, so an undo step keeps it as it was,15// that follows inserted and deleted lines as range references do and is16// saved in the file. Finding the merge holding a cell goes through an17// index built when the list changes.1819// MergeKind is how Format > Merge cells joins a range.20type MergeKind uint82122const (23 MergeAll MergeKind = iota // the whole range, one cell24 MergeHorizontally // each row of it25 MergeVertically // each column of it26)2728// maxMerges caps the merges one change makes: merging a range's rows or29// columns makes one per line.30const maxMerges = 100003132var (33 // ErrMergeOne is why a single cell can't be merged.34 ErrMergeOne = errors.New("Select more than one cell to merge")35 // ErrMergeMany is why merging a whole column's rows is refused.36 ErrMergeMany = fmt.Errorf("That would make more than %d merged cells", maxMerges)37 // ErrSortMerged is why a range holding merged cells isn't sorted.38 ErrSortMerged = errors.New("Can't sort a range with merged cells; unmerge them first")39)4041// mergeIndex finds merges by the cells they cover, for the list it was42// built from (compared by identity: the list is replaced, never changed43// in place).44type mergeIndex struct {45 of []Rect46 idx rangeIndex47 at map[Addr]Rect // each merge by its top-left cell48}4950// sameList reports whether a and b are the same slice.51func sameList(a, b []Rect) bool {52 return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0])53}5455func (s *Sheet) merges() *mergeIndex {56 x := &s.mergeIdx57 if !sameList(x.of, s.view.merges) || x.at == nil {58 *x = mergeIndex{of: s.view.merges, at: make(map[Addr]Rect, len(s.view.merges))}59 for _, r := range s.view.merges {60 x.at[r.From] = r61 x.idx.add(r.From, []Rect{r})62 }63 }64 return x65}6667// Merges returns the merged ranges, in the order they were made.68func (s *Sheet) Merges() []Rect { return slices.Clone(s.view.merges) }6970// MergeAt returns the merged range holding a, and false when a isn't71// merged.72func (s *Sheet) MergeAt(a Addr) (Rect, bool) {73 if len(s.view.merges) == 0 {74 return Rect{}, false75 }76 x := s.merges()77 var out Rect78 found := false79 x.idx.readers(a, func(tl Addr) { out, found = x.at[tl], true })80 return out, found81}8283// MergesIn returns the merges that overlap r.84func (s *Sheet) MergesIn(r Rect) []Rect {85 var out []Rect86 for _, m := range s.view.merges {87 if _, ok := intersectRect(m, r); ok {88 out = append(out, m)89 }90 }91 return out92}9394// Grow returns r grown to hold every merge it overlaps, as a selection95// in Sheets takes in the merged cells it touches.96func (s *Sheet) Grow(r Rect) Rect {97 if len(s.view.merges) == 0 {98 return r99 }100 for grown := true; grown; {101 grown = false102 for _, m := range s.MergesIn(r) {103 if u := union(r, m); u != r {104 r, grown = u, true105 }106 }107 }108 return r109}110111// mergeAreas are the ranges merging r with k makes.112func mergeAreas(r Rect, k MergeKind) ([]Rect, error) {113 var out []Rect114 switch k {115 case MergeHorizontally:116 if r.To.Row-r.From.Row >= maxMerges {117 return nil, ErrMergeMany118 }119 for row := r.From.Row; row <= r.To.Row && r.From.Col < r.To.Col; row++ {120 out = append(out, Rect{From: Addr{Col: r.From.Col, Row: row}, To: Addr{Col: r.To.Col, Row: row}})121 }122 case MergeVertically:123 if r.To.Col-r.From.Col >= maxMerges {124 return nil, ErrMergeMany125 }126 for c := r.From.Col; c <= r.To.Col && r.From.Row < r.To.Row; c++ {127 out = append(out, Rect{From: Addr{Col: c, Row: r.From.Row}, To: Addr{Col: c, Row: r.To.Row}})128 }129 default:130 if r.From != r.To {131 out = []Rect{r}132 }133 }134 if len(out) == 0 {135 return nil, ErrMergeOne136 }137 return out, nil138}139140// MergeLoses returns the first cell merging r with k would clear: one141// with contents that isn't the top-left of its merge. Sheets warns that142// only the top-left value is kept.143func (s *Sheet) MergeLoses(r Rect, k MergeKind) (Addr, bool) {144 areas, err := mergeAreas(r, k)145 if err != nil {146 return Addr{}, false147 }148 var lost []Addr149 for a, c := range s.cells.anyInRange(r) {150 if !c.Blank() && !c.Spilled() && !isTopLeft(areas, a) {151 lost = append(lost, a)152 }153 }154 if len(lost) == 0 {155 return Addr{}, false156 }157 sortAddrs(lost)158 return lost[0], true159}160161// isTopLeft reports whether a is the top-left cell of one of areas, which162// all lie along r's rows or columns, or a isn't in any.163func isTopLeft(areas []Rect, a Addr) bool {164 for _, m := range areas {165 if m.Contains(a) {166 return a == m.From167 }168 }169 return true170}171172// Merge joins r into merged cells as k says, as one undo step: each173// keeps its top-left cell's contents and clears the rest, keeping their174// formatting and notes. Merges it overlaps are replaced.175func (s *Sheet) Merge(r Rect, k MergeKind) error {176 areas, err := mergeAreas(r, k)177 if err != nil {178 return err179 }180 s.change("merge "+r.String(), r, func() {181 for a, c := range s.cells.anyInRange(r) {182 if !c.Blank() && !isTopLeft(areas, a) {183 s.place(a, c.leftover())184 }185 }186 next, span := s.mergesOutside(r)187 s.setMerges(append(next, areas...), span)188 })189 return nil190}191192// MergeAcross returns a merge the line after the first n rows (or193// columns) would cut through, as freezing them would, and whether there194// is one.195func (s *Sheet) MergeAcross(rows bool, n int) (Rect, bool) {196 for _, m := range s.view.merges {197 from, to := m.From.Col, m.To.Col198 if rows {199 from, to = m.From.Row, m.To.Row200 }201 if n > 0 && from < n && to >= n {202 return m, true203 }204 }205 return Rect{}, false206}207208// Unmerge splits the merges overlapping r back into cells, as one undo209// step, and returns how many there were.210func (s *Sheet) Unmerge(r Rect) int {211 next, span := s.mergesOutside(r)212 n := len(s.view.merges) - len(next)213 if n > 0 {214 s.change("unmerge "+r.String(), r, func() { s.setMerges(next, span) })215 }216 return n217}218219// mergesOutside returns the merges that don't overlap r, and the range220// holding r and those that do: where arrays blocked by them may spill221// once they go.222func (s *Sheet) mergesOutside(r Rect) ([]Rect, Rect) {223 span := r224 next := slices.DeleteFunc(slices.Clone(s.view.merges), func(m Rect) bool {225 _, overlaps := intersectRect(m, r)226 if overlaps {227 span = union(span, m)228 }229 return overlaps230 })231 return next, span232}233234// setMerges replaces the merges, recording them for undo, and has the235// arrays spilling over the range that changed spill again: a merge236// blocks a spill.237func (s *Sheet) setMerges(next []Rect, changed Rect) {238 if len(next) == 0 {239 next = nil240 }241 s.recordView()242 s.view.merges = next243 s.version++244 s.respill(changed)245}246247// respill marks the anchors spilling, or blocked, over r to recalculate.248func (s *Sheet) respill(r Rect) {249 for a, sp := range s.spills {250 if _, ok := intersectRect(sp.area, r); ok {251 sp.stale = true252 s.wb.markDirty(loc{s, a})253 }254 }255}256257// LoadMerge adds a merge as a loader does, without recording undo; one258// cell or one overlapping another is left out.259func (s *Sheet) LoadMerge(r Rect) {260 if r.From == r.To || !r.To.Valid() || len(s.MergesIn(r)) > 0 {261 return262 }263 s.view.merges = append(slices.Clone(s.view.merges), r)264 s.version++265}266267// shiftMerges moves merges with inserted or deleted rows or columns, as268// a range reference moves; one left a single cell goes.269func shiftMerges(ms []Rect, rows bool, sp formula.Span) []Rect {270 if len(ms) == 0 {271 return ms272 }273 _, rng := formula.AxisMaps(rows, sp)274 var out []Rect275 for _, m := range ms {276 if r, ok := rng(m); ok && r.From != r.To {277 out = append(out, r)278 }279 }280 return out281}282283// mergesWithin is the merges wholly inside r, moved by its top-left corner,284// for a copy to carry.285func (s *Sheet) mergesWithin(r Rect) []Rect {286 var out []Rect287 for _, m := range s.view.merges {288 if r.Contains(m.From) && r.Contains(m.To) {289 out = append(out, Rect{From: subAddr(m.From, r.From), To: subAddr(m.To, r.From)})290 }291 }292 return out293}294295// pasteMerges merges what a copy carried at each tile of p, if anything,296// replacing the merges the paste lands over.297func (s *Sheet) pasteMerges(carried []Rect, p pasteLayout) {298 if len(carried) == 0 {299 return300 }301 // The merges it replaces may reach past it: arrays they blocked302 // there spill again too.303 next, span := s.mergesOutside(p.dst)304 for tr := range p.down {305 for tc := range p.across {306 at := Addr{Col: p.dst.From.Col + tc*p.tw, Row: p.dst.From.Row + tr*p.th}307 for _, m := range carried {308 if r := (Rect{From: addAddr(m.From, at), To: addAddr(m.To, at)}); r.To.Valid() && len(next) < maxMerges {309 next = append(next, r)310 }311 }312 }313 }314 s.setMerges(next, span)315}316317// moveMerges moves the merges wholly inside src on from to dst on s, as318// cutting and pasting them does, replacing those dst overlaps.319func (s *Sheet) moveMerges(from *Sheet, src, dst Rect) {320 carried := from.mergesWithin(src)321 if len(carried) > 0 {322 left := slices.DeleteFunc(slices.Clone(from.view.merges), func(m Rect) bool {323 return src.Contains(m.From) && src.Contains(m.To)324 })325 from.setMerges(left, src)326 }327 p := pasteLayout{dst: dst, across: 1, down: 1,328 tw: dst.To.Col - dst.From.Col + 1, th: dst.To.Row - dst.From.Row + 1}329 s.pasteMerges(carried, p)330}331332func subAddr(a, b Addr) Addr { return Addr{Col: a.Col - b.Col, Row: a.Row - b.Row} }333func addAddr(a, b Addr) Addr { return Addr{Col: a.Col + b.Col, Row: a.Row + b.Row} }