cat / 012
clone

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

main
012 / internal / sheet / region.go
region.go 9.9K
1package sheet23import (4 "errors"5 "fmt"6 "slices"78 "src.rest/cat/012/internal/notebook"9)1011// Regions are blocks of cells whose values come from outside the12// engine, of two kinds: a linked file (a file followed as it grows or is13// rewritten, linked.go) and a notebook cell's output sent to a sheet14// (Output). A region is an anchor (At, its table's first cell) and a15// definition saying where its table comes from. Like a spill, its cells16// are derived: written by the engine outside the undo history, refused17// by Set, values to formulas, copies and exports, formatting kept, never18// saved; the file keeps the definitions only.19//20// Both kinds are one indexed set, the sheet's regionState, and their21// cells have one write path: rows arrive as live operations (LiveOp,22// live.go), written by writeTable (regionwrite.go). Their rows are never23// part of the undo state: the source is where they come from, and undo24// can't bring back what it held. When undo puts a region back, or it25// moves, it is emptied and marked stale, and the UI sends its rows26// again: the file read again, or the cell's output as it stands.27//28// The definitions are undo steps. So are a notebook tab's cells, which29// the same state holds (notebook.go).30//31// In formulas a region is a name: nu. and its own (=SUM(nu.sales)),32// which stands for its table, header row included.3334// Region is a region's definition.35type Region struct {36 // Name identifies the region in the workbook, ignoring case: a37 // linked file's is made from the file's name, a sent output's is its38 // cell's. Notebook cells read linked files as $name.39 Name string40 // At is the table's first cell.41 At Addr42 // File is the file a linked file follows; zero for a sent output.43 File LinkSource44 // Output is set on a notebook cell's output sent to the sheet: the45 // output of the cell of the region's name.46 Output bool47}4849// Linked reports whether the region is a linked file, not a source.50func (r Region) Linked() bool { return r.File.Path != "" && !r.File.Paged }5152// regionState is what a sheet keeps of its regions in the undo history:53// the definitions in the order they were made and, on a notebook tab,54// its cells. It is replaced whole, never changed in place.55type regionState struct {56 notebook bool57 list []Region58 cells []notebook.Cell59 reactive bool60}6162// regionMeta is what isn't undone: the cells written last and what the63// rows written made of the region.64type regionMeta struct {65 written Rect66 has bool // written holds something67 why string // why the table isn't shown, or ""68 placed bool // contents were placed in a blank cell of its table69 need Rect // the cells the table needs while it isn't shown70 // rows and cols are the table's size as written, header included.71 rows, cols int72 liveMeta // how its rows arrive: see linked.go73 // src is what a source's host found of it: see source.go.74 src *sourceMeta75}7677// Errors of regions.78var (79 // ErrOutputEdit is what typing into a sent output's cell says.80 ErrOutputEdit = errors.New("That cell shows a notebook cell's output: change the cell, or freeze the region to edit its values")81 // ErrNoRegion is returned for a region the workbook doesn't hold:82 // deleted, unlinked, undone, or on a sheet deleted.83 ErrNoRegion = errors.New("There's no region by that name")84)8586// regionPrefix starts a region's name in formulas.87const regionPrefix = "nu."8889// FormulaName is how formulas name the region: nu.sales.90func (r Region) FormulaName() string { return regionPrefix + r.Name }9192// ValidRegionName checks that name can name a region: a nushell93// variable's name of letters, digits and _, not starting with a digit,94// as a notebook cell's name is.95func ValidRegionName(name string) error { return notebook.ValidName(name) }9697// HasRegions reports whether the sheet has regions, so what draws it can98// skip looking for them.99func (s *Sheet) HasRegions() bool { return len(s.regions.list) > 0 }100101// Regions returns the sheet's regions in the order they were made.102func (s *Sheet) Regions() []Region { return slices.Clone(s.regions.list) }103104// Region finds a region by name, ignoring case, on any sheet.105func (w *Workbook) Region(name string) (*Sheet, Region, bool) {106 k := nameKey(name)107 for _, s := range w.sheets {108 if i := s.regionIndex(k); i >= 0 {109 return s, s.regions.list[i], true110 }111 }112 return nil, Region{}, false113}114115func (s *Sheet) regionIndex(k string) int {116 return slices.IndexFunc(s.regions.list, func(r Region) bool { return nameKey(r.Name) == k })117}118119// allRegions returns every region of the workbook's sheets, sheet by120// sheet.121func (w *Workbook) allRegions() []Region {122 var out []Region123 for _, s := range w.sheets {124 out = append(out, s.regions.list...)125 }126 return out127}128129// checkRegionName reports why name can't name a new region. A sent130// output (output set) takes its cell's name, which a notebook's cell131// has; any other region's name is one no cell has.132func (w *Workbook) checkRegionName(name string, output bool) error {133 if err := ValidRegionName(name); err != nil {134 return err135 }136 if _, r, ok := w.Region(name); ok {137 return fmt.Errorf("There's already a region named %s", r.Name)138 }139 if n, ok := w.LookupName(regionPrefix + name); ok {140 return fmt.Errorf("%s already names %s", n.Name, n.Ref())141 }142 if _, t, ok := w.Table(name); ok {143 return fmt.Errorf("There's already a table named %s", t.Name)144 }145 if !output && w.cellNamed(name) {146 return fmt.Errorf("A notebook cell is named %s", name)147 }148 return nil149}150151// AddRegion adds a region to the sheet as one undo step. It starts152// empty and stale: the UI sends its rows.153func (s *Sheet) AddRegion(r Region) error {154 if err := s.wb.checkRegionName(r.Name, r.Output); err != nil {155 return err156 }157 if !r.At.Valid() || s.cells.filledAt(r.At) || s.InPivot(Rect{From: r.At, To: r.At}) {158 return errLinkOver159 }160 if _, ok := s.RegionAt(r.At); ok {161 return errLinkOver162 }163 s.change("add "+r.Name, Rect{From: r.At, To: r.At}, func() {164 st := s.regionsCopy()165 st.list = append(st.list, r)166 s.putRegions(st)167 })168 s.meta(nameKey(r.Name)).stale = true169 return nil170}171172// DeleteRegion removes a region and its cells, as one undo step.173func (s *Sheet) DeleteRegion(name string) error {174 k := nameKey(name)175 i := s.regionIndex(k)176 if i < 0 {177 return ErrNoRegion178 }179 r := s.regions.list[i]180 s.change("delete "+r.Name, s.covered(r), func() {181 st := s.regionsCopy()182 st.list = slices.Delete(st.list, i, i+1)183 s.putRegions(st)184 })185 return nil186}187188// FreezeRegion turns a region's table into plain values, each in the189// format it showed, and removes the region, as one undo step.190func (s *Sheet) FreezeRegion(name string) error {191 k := nameKey(name)192 i := s.regionIndex(k)193 if i < 0 {194 return ErrNoRegion195 }196 r := s.regions.list[i]197 table, ok := s.RegionTable(r.Name)198 var cells []*Cell199 var at []Addr200 if ok {201 for a := range s.cells.anyKeysIn(table) {202 if c := s.cells.get(a); c.Spilled() {203 cells, at = append(cells, frozen(c)), append(at, a)204 }205 }206 }207 s.change("freeze "+r.Name, s.covered(r), func() {208 // The region goes first, so its cells take the values placed.209 st := s.regionsCopy()210 st.list = slices.Delete(st.list, i, i+1)211 s.putRegions(st)212 for j, a := range at {213 s.place(a, cells[j])214 }215 })216 return nil217}218219// frozen is a region's cell as a plain one: its value typed in, in the220// format it shows.221func frozen(c *Cell) *Cell {222 p := &Cell{Value: c.Value, Format: c.Format, Style: c.Style, spilled: true}223 if p.Format.IsZero() {224 p.Format = c.auto225 }226 return p.plain().withNote(c.Note)227}228229// regionsCopy is a copy of the sheet's regions to change and put back.230func (s *Sheet) regionsCopy() regionState {231 st := s.regions232 st.list = slices.Clone(st.list)233 return st234}235236// putRegions replaces the sheet's regions, recording them for undo, and237// has their cells written again.238func (s *Sheet) putRegions(st regionState) {239 s.recordRegions()240 s.regions = st241 s.regionsStale = true242}243244// recordRegions saves the sheet's regions before their first change in245// the open step.246func (s *Sheet) recordRegions() {247 if st := s.wb.hist.open; st != nil {248 if _, seen := st.regions[s]; !seen {249 st.regions[s] = s.regions250 }251 }252}253254// sameRegions reports whether two states of a sheet's regions are the255// same.256func sameRegions(a, b regionState) bool {257 return a.notebook == b.notebook && a.reactive == b.reactive && slices.Equal(a.list, b.list) && slices.Equal(a.cells, b.cells)258}259260// putRegionsBack puts back a sheet's regions from an undo step. A261// region the step brings back, or moves, is emptied and fed again once262// the change ends: its rows aren't part of the undo state, so the cells263// undo put there may not be its source's.264func (s *Sheet) putRegionsBack(st regionState) {265 for _, r := range st.list {266 i := s.regionIndex(nameKey(r.Name))267 if i < 0 || s.regions.list[i] != r {268 s.meta(nameKey(r.Name)).reread = true269 }270 }271 s.regions, s.regionsStale = st, true272}273274// emptyUnder empties the regions holding cells that undo puts contents275// back in (what a region's table grew over once they left), so the276// contents come back and the region, sent again, finds them in its way,277// as before they left.278func (s *Sheet) emptyUnder(img *image) []loc {279 if len(s.regions.list) == 0 {280 return nil281 }282 var changed []loc283 img.each(func(a Addr, c *Cell) {284 if c == nil || c.Blank() || c.Spilled() {285 return286 }287 if r, me, ok := s.ownerOf(a); ok {288 changed = append(changed, s.emptyRegion(r, me)...)289 }290 })291 return changed292}293294// emptyMoved empties the regions that putting st back moves or takes295// away, outside the undo history, as moving them does, before undo puts296// cells back: a cell a region holds keeps its value when placed, so297// what undo brings back where a region moved to would be lost.298func (s *Sheet) emptyMoved(st regionState) []loc {299 var changed []loc300 for _, r := range s.regions.list {301 i := slices.IndexFunc(st.list, func(x Region) bool { return nameKey(x.Name) == nameKey(r.Name) })302 if me := s.regionMeta[nameKey(r.Name)]; me != nil && (i < 0 || st.list[i] != r) {303 changed = append(changed, s.emptyRegion(r, me)...)304 }305 }306 return changed307}