cat / 012
clone

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

UI at a million rows: wider row numbers, filtered stepping by index

Dami committed 08ee498
Row numbers widen past row 9999 as Excel's do, to fit the widest number
on screen. Stepping over rows the filter hides, and laying out text that
runs in from other columns, go through the sheet's indexes instead of
testing every row or column between; text runs in from at most 256
columns away. Styles shown come from the cell, its row or its column.

The stress shapes keep their 8192 x 256 size (stress.Rows, stress.Cols)
so runs compare across the change.
commit
08ee498c8d16e719404833293a7b3023c0e084dd
parent
1c459ce04e8eeda23b0cb4e64fee22ebf4ca5219
author
Dami <dami.etoile@gmail.com>
19 files changed, +105 -78
Minternal/chart/stress_test.go2+-
Minternal/stress/gen.go29+++++++++++-------
Minternal/ui/charts.go8+++---
Minternal/ui/chartsel.go4+-
Minternal/ui/data_test.go2+-
Minternal/ui/format.go5+---
Minternal/ui/format_test.go4+-
Minternal/ui/macro_test.go2+-
Minternal/ui/model.go2+-
Minternal/ui/model_test.go4+-
Minternal/ui/mouse.go4+-
Minternal/ui/mouse_test.go6++--
Minternal/ui/panes.go48+++++++++++++++++++++---------
Minternal/ui/panes_test.go16+++++-----
Minternal/ui/prefs.go2+
Minternal/ui/rowtext/rowtext.go27+++++++++--------
Minternal/ui/stress_test.go10+++---
Minternal/ui/trace_test.go2+-
Minternal/ui/view.go6++--
M internal/chart/stress_test.go +1 -1
M internal/stress/gen.go +18 -11
@@ -23,6 +23,13 @@ type Shape struct {
2323 Input string2424 }2525 26+// Rows and Cols are the size of the shapes: the grid of Lotus 1-2-3 that27+// 012 had before it grew to Excel's, so runs before and after compare.28+const (29+ Rows = 819230+ Cols = 25631+)32+2633 func at(col, row int) sheet.Addr { return sheet.Addr{Col: col, Row: row} }2734 2835 func load(s *sheet.Sheet, a sheet.Addr, input string) {
@@ -75,7 +82,7 @@ func FanOut(k int) *sheet.Sheet {
7582 s := sheet.New()7683 load(s, at(0, 0), "1")7784 for i := range k {78- load(s, at(1+i/sheet.MaxRows, i%sheet.MaxRows), fmt.Sprintf("=$A$1*%d", i))85+ load(s, at(1+i/Rows, i%Rows), fmt.Sprintf("=$A$1*%d", i))7986 }8087 s.RecalcAll()8188 return s
@@ -102,7 +109,7 @@ func Volatile(k int) *sheet.Sheet {
102109 if i%2 == 1 {103110 f = "=TODAY()"104111 }105- load(s, at(i/sheet.MaxRows, i%sheet.MaxRows), f)112+ load(s, at(i/Rows, i%Rows), f)106113 }107114 s.RecalcAll()108115 return s
@@ -148,16 +155,16 @@ func LongText(rows, n int) *sheet.Sheet {
148155 // formula using each: every changed cell is checked against every name.149156 func Names(k int) *sheet.Sheet {150157 s := sheet.New()151- for row := range sheet.MaxRows {158+ for row := range Rows {152159 load(s, at(0, row), fmt.Sprint(row%100))153160 }154161 for i := range k {155- from := (i * 7) % (sheet.MaxRows - 100)162+ from := (i * 7) % (Rows - 100)156163 name := fmt.Sprintf("Region_%d", i)157164 if err := s.DefineName(name, sheet.NewRect(at(0, from), at(0, from+99))); err != nil {158165 panic(err)159166 }160- load(s, at(1+i/sheet.MaxRows, i%sheet.MaxRows), "=SUM("+name+")")167+ load(s, at(1+i/Rows, i%Rows), "=SUM("+name+")")161168 }162169 s.RecalcAll()163170 return s
@@ -206,12 +213,12 @@ func Charts(s *sheet.Sheet, k int) {
206213 // Shapes are the edit-latency topologies at their stress sizes.207214 func Shapes() []Shape {208215 return []Shape{209- {"dense-8192x26", func() *sheet.Sheet { return Dense(sheet.MaxRows, 26) }, at(0, 0), "5"},210- {"chain-8192", func() *sheet.Sheet { return Chain(sheet.MaxRows) }, at(0, 0), "2"},211- {"fanin-1000xSUM8192", func() *sheet.Sheet { return FanIn(sheet.MaxRows, 1000) }, at(0, 4000), "7"},212- {"fanout-8192", func() *sheet.Sheet { return FanOut(sheet.MaxRows) }, at(0, 0), "2"},213- {"running-8192", func() *sheet.Sheet { return RunningTotals(sheet.MaxRows) }, at(0, 0), "3"},214- {"volatile-8192", func() *sheet.Sheet { return Volatile(sheet.MaxRows) }, at(200, 0), "1"},216+ {"dense-8192x26", func() *sheet.Sheet { return Dense(Rows, 26) }, at(0, 0), "5"},217+ {"chain-8192", func() *sheet.Sheet { return Chain(Rows) }, at(0, 0), "2"},218+ {"fanin-1000xSUM8192", func() *sheet.Sheet { return FanIn(Rows, 1000) }, at(0, 4000), "7"},219+ {"fanout-8192", func() *sheet.Sheet { return FanOut(Rows) }, at(0, 0), "2"},220+ {"running-8192", func() *sheet.Sheet { return RunningTotals(Rows) }, at(0, 0), "3"},221+ {"volatile-8192", func() *sheet.Sheet { return Volatile(Rows) }, at(200, 0), "1"},215222 {"names-1000", func() *sheet.Sheet { return Names(1000) }, at(0, 50), "9"},216223 }217224 }
M internal/ui/charts.go +4 -4
@@ -94,7 +94,7 @@ func (m *Model) insertChart() tea.Cmd {
9494 }9595 start := m.sheet.StateID()9696 c := m.sheet.GuessChart(r)97- c.W = clamp(newChartW, sheet.MinChartW, max(m.width-rowHdrW-2, sheet.MinChartW))97+ c.W = clamp(newChartW, sheet.MinChartW, max(m.width-m.hdrW()-2, sheet.MinChartW))9898 c.H = clamp(newChartH, sheet.MinChartH, max(m.visibleRows()-1, sheet.MinChartH))9999 // Next to the data when it fits on screen, as Sheets does; otherwise100100 // below it.
@@ -164,7 +164,7 @@ func (g *grid) chartScreen(c sheet.Chart) (x, y int) {
164164 165165 // chartAt returns the topmost chart drawn under x, y, or -1.166166 func (m *Model) chartAt(x, y int) int {167- if y < gridTop || y >= gridTop+m.visibleRows() || x < rowHdrW {167+ if y < gridTop || y >= gridTop+m.visibleRows() || x < m.hdrW() {168168 return -1169169 }170170 charts := m.displayCharts()
@@ -196,12 +196,12 @@ func (m *Model) chartBoxes() []box {
196196 top, bottom := gridTop, gridTop+m.visibleRows()197197 for i, c := range m.displayCharts() {198198 x, y := m.chartScreen(c)199- if x >= m.width || y >= bottom || x+c.W <= rowHdrW || y+c.H <= top {199+ if x >= m.width || y >= bottom || x+c.W <= m.hdrW() || y+c.H <= top {200200 continue201201 }202202 lines := m.drawChart(i, c, i == sel)203203 // Clip to the grid: the headers and panel stay on top.204- from, to := max(rowHdrW-x, 0), min(c.W, m.width-x)204+ from, to := max(m.hdrW()-x, 0), min(c.W, m.width-x)205205 b := box{id: chartBoxID(i), x: x + from, y: max(y, top)}206206 for k, l := range lines {207207 if y+k >= top && y+k < bottom {
M internal/ui/chartsel.go +2 -2
M internal/ui/data_test.go +1 -1
M internal/ui/format.go +1 -4
M internal/ui/format_test.go +2 -2
M internal/ui/macro_test.go +1 -1
M internal/ui/model.go +1 -1
M internal/ui/model_test.go +2 -2
M internal/ui/mouse.go +2 -2
M internal/ui/mouse_test.go +3 -3
M internal/ui/panes.go +34 -14
@@ -1,6 +1,8 @@
11 package ui22 33 import (4+ "strconv"5+46 "github.com/FelineStateMachine/012/internal/sheet"57 )68
@@ -22,7 +24,7 @@ func (g *grid) frozen() (rows, cols int) {
2224 w := 02325 for c := range cols {2426 w += g.sheet.ColWidth(c)25- if rowHdrW+w+1+3 > g.width {27+ if g.hdrW()+w+1+3 > g.width {2628 cols = c2729 break2830 }
@@ -71,14 +73,35 @@ func (g *grid) screenRows() []int {
7173 if fr > 0 {7274 out = append(out, divider)7375 }74- for r := g.top; len(out) < n && r < sheet.MaxRows; r++ {75- if !g.sheet.RowHidden(r) {76- out = append(out, r)77- }76+ r, ok := g.top, true77+ if g.sheet.RowHidden(r) {78+ r, ok = g.sheet.NextShownRow(r, 1)79+ }80+ for ; ok && len(out) < n; r, ok = g.sheet.NextShownRow(r, 1) {81+ out = append(out, r)7882 }7983 return out8084 }8185 86+// hdrW is the width of the row numbers: the widest number on screen with87+// a space either side, and at least minRowHdrW, so it widens past row88+// 9999 as Excel's does.89+func (g *grid) hdrW() int {90+ last := g.top + g.visibleRows()91+ if g.sheet.HiddenRows() > 0 {92+ last = g.top93+ for range g.visibleRows() {94+ r, ok := g.sheet.NextShownRow(last, 1)95+ if !ok {96+ break97+ }98+ last = r99+ }100+ last++101+ }102+ return max(minRowHdrW, len(strconv.Itoa(min(last, sheet.MaxRows)))+2)103+}104+82105 // rowAt returns the row on screen line y, or false for the divider and83106 // lines outside the grid.84107 func (g *grid) rowAt(y int) (int, bool) {
@@ -107,11 +130,8 @@ func (g *grid) stepRow(r, n int) int {
107130 d, n = -1, -n108131 }109132 for ; n > 0; n-- {110- next := r + d111- for next >= 0 && next < sheet.MaxRows && g.sheet.RowHidden(next) {112- next += d113- }114- if next < 0 || next >= sheet.MaxRows {133+ next, ok := g.sheet.NextShownRow(r, d)134+ if !ok {115135 break116136 }117137 r = next
@@ -136,7 +156,7 @@ func (g *grid) visibleRow(r int) int {
136156 // row numbers, the frozen columns and their divider.137157 func (g *grid) scrollX() int {138158 _, fc := g.frozen()139- x := rowHdrW159+ x := g.hdrW()140160 for c := range fc {141161 x += g.sheet.ColWidth(c)142162 }
@@ -206,7 +226,7 @@ func (g *grid) visibleCols(left int) int {
206226 // a frozen column or a scrolling one, but not the divider between them.207227 func (g *grid) colSpan(x int) (col, start int, ok bool) {208228 _, fc := g.frozen()209- cx := rowHdrW229+ cx := g.hdrW()210230 for c := 0; c < fc; c++ {211231 w := g.sheet.ColWidth(c)212232 if x >= cx && x < cx+w {
@@ -231,7 +251,7 @@ func (g *grid) colSpan(x int) (col, start int, ok bool) {
231251 func (g *grid) colStart(c int) int {232252 _, fc := g.frozen()233253 if c < fc {234- x := rowHdrW254+ x := g.hdrW()235255 for k := range c {236256 x += g.sheet.ColWidth(k)237257 }
@@ -301,7 +321,7 @@ func (g *grid) dragTarget(x, y int, anchor sheet.Addr) (a sheet.Addr, dc, dr int
301321 a.Col, dc = g.left+cols-1, 1302322 case ok && (col >= fc || !scrolledRight || anchor.Col < fc):303323 a.Col = col304- case x >= rowHdrW && !scrolledRight:324+ case x >= g.hdrW() && !scrolledRight:305325 a.Col = g.left306326 case scrolledRight && anchor.Col >= fc:307327 a.Col, dc = g.left, -1
M internal/ui/panes_test.go +8 -8
@@ -90,21 +90,21 @@ func TestMouseWithFrozenPanes(t *testing.T) {
9090 table(t, m, 40)9191 m.sheet.SetFrozen(2, 1)9292 m.top, m.left = 20, 393- divX := rowHdrW + sheet.DefaultWidth // the column divider93+ divX := minRowHdrW + sheet.DefaultWidth // the column divider9494 tests := []struct {9595 x, y int9696 want hit9797 }{98- {rowHdrW + 1, gridTop, hit{kind: hitCell, addr: addr("A1")}},99- {rowHdrW + 1, gridTop + 1, hit{kind: hitCell, addr: addr("A2")}},100- {rowHdrW + 1, gridTop + 2, hit{}}, // the divider line101- {rowHdrW + 1, gridTop + 3, hit{kind: hitCell, addr: addr("A21")}},98+ {minRowHdrW + 1, gridTop, hit{kind: hitCell, addr: addr("A1")}},99+ {minRowHdrW + 1, gridTop + 1, hit{kind: hitCell, addr: addr("A2")}},100+ {minRowHdrW + 1, gridTop + 2, hit{}}, // the divider line101+ {minRowHdrW + 1, gridTop + 3, hit{kind: hitCell, addr: addr("A21")}},102102 {divX, gridTop + 3, hit{}},103103 {divX + 1, gridTop + 3, hit{kind: hitCell, addr: addr("D21")}},104104 {divX + 1, gridTop, hit{kind: hitCell, addr: addr("D1")}},105105 {2, gridTop + 3, hit{kind: hitRowHeader, addr: sheet.Addr{Col: 3, Row: 20}}},106106 {divX + sheet.DefaultWidth, headerLine, hit{kind: hitColBorder, addr: sheet.Addr{Col: 3, Row: 20}}},107- {rowHdrW + sheet.DefaultWidth - 1, headerLine, hit{kind: hitColBorder, addr: sheet.Addr{Col: 0, Row: 20}}},107+ {minRowHdrW + sheet.DefaultWidth - 1, headerLine, hit{kind: hitColBorder, addr: sheet.Addr{Col: 0, Row: 20}}},108108 }109109 for _, tt := range tests {110110 if got := m.hitTest(tt.x, tt.y); got != tt.want {
@@ -200,9 +200,9 @@ func TestNavigationSkipsFilteredRows(t *testing.T) {
200200 201201 func TestFilteredNavigationIsFast(t *testing.T) {202202 m := newModel()203- table(t, m, sheet.MaxRows-2)203+ table(t, m, 8190)204204 m.sheet.SetFrozen(1, 1)205- m.sheet.CreateFilter(sheet.Rect{To: sheet.Addr{Col: 1, Row: sheet.MaxRows - 1}})205+ m.sheet.CreateFilter(sheet.Rect{To: sheet.Addr{Col: 1, Row: sheet.MaxRows - 1}}) // whole columns206206 m.sheet.FilterColumn(1, sheet.Criteria{Hidden: []string{"1", "2", "3"}})207207 m.View()208208 start := time.Now()
M internal/ui/prefs.go +2 -0
M internal/ui/rowtext/rowtext.go +14 -13
@@ -79,21 +79,21 @@ func (l *rowLayout) content(c int) *sheet.Cell {
7979 // col returns where column c starts and ends, relative to column first.8080 func (l *rowLayout) col(c int) (int, int) { return l.x[c-l.first], l.x[c-l.first+1] }8181 82+// maxReach is how many columns away text may run in from: 2,56083+// characters in default columns.84+const maxReach = 25685+8286 // reach returns the nearest filled columns outside lo..hi, within minCol83-// and maxCol, whose text might run into view; lo and hi if there are none.87+// and maxCol and maxReach of the viewport, whose text might run into88+// view; lo and hi if there are none. The filled cells are found through89+// the sheet's index, not by looking at every column between.8490 func (l *rowLayout) reach(minCol, maxCol int) (first, last int) {8591 first, last = l.lo, l.hi86- for c := l.lo - 1; c >= minCol; c-- {87- if l.content(c) != nil {88- first = c89- break90- }92+ if c, ok := l.s.NextFilledCol(l.row, l.lo-1, -1, max(minCol, l.lo-maxReach)); ok {93+ first = c9194 }92- for c := l.hi + 1; c <= maxCol; c++ {93- if l.content(c) != nil {94- last = c95- break96- }95+ if c, ok := l.s.NextFilledCol(l.row, l.hi+1, 1, min(maxCol, l.hi+maxReach)); ok {96+ last = c9797 }9898 return first, last9999 }
@@ -116,12 +116,13 @@ func (l *rowLayout) place(c int, cell *sheet.Cell) {
116116 from, to := l.room(c, cell, start, tw)117117 l.claimed = to118118 cut := textCutter{s: text}119+ style := l.s.CellStyle(sheet.Addr{Col: c, Row: l.row})119120 for k := max(l.first, l.lo); k <= min(l.last, l.hi); k++ {120121 kx0, kx1 := l.col(k)121122 if kx1 <= from || kx0 >= to {122123 continue123124 }124- sp := Span{Trail: kx1 - kx0, Style: cell.Style, Owner: c}125+ sp := Span{Trail: kx1 - kx0, Style: style, Owner: c}125126 if seg0, seg1 := max(start, kx0, from), min(start+tw, kx1, to); seg1 > seg0 {126127 sp.Lead, sp.Text, sp.Trail = seg0-kx0, cut.cut(seg0-start, seg1-start), kx1-seg1127128 }
@@ -143,7 +144,7 @@ func (l *rowLayout) display(c int, cell *sheet.Cell, w int) (string, sheet.Align
143144 text, pad = wider, 0144145 }145146 }146- if a := cell.Style.Align; a != sheet.AlignAuto && align != sheet.AlignFill {147+ if a := l.s.CellStyle(sheet.Addr{Col: c, Row: l.row}).Align; a != sheet.AlignAuto && align != sheet.AlignFill {147148 align = a148149 }149150 return text, align, pad
M internal/ui/stress_test.go +5 -5
@@ -36,9 +36,9 @@ type uiShape struct {
3636 func uiShapes() []uiShape {3737 return []uiShape{3838 {"empty", sheet.New},39- {"dense-8192x26", func() *sheet.Sheet { return stress.Dense(sheet.MaxRows, 26) }},40- {"dense-8192x256", func() *sheet.Sheet { return stress.Dense(sheet.MaxRows, sheet.MaxCols) }},41- {"longtext-8192x500", func() *sheet.Sheet { return stress.LongText(sheet.MaxRows, 500) }},39+ {"dense-8192x26", func() *sheet.Sheet { return stress.Dense(stress.Rows, 26) }},40+ {"dense-8192x256", func() *sheet.Sheet { return stress.Dense(stress.Rows, stress.Cols) }},41+ {"longtext-8192x500", func() *sheet.Sheet { return stress.LongText(stress.Rows, 500) }},4242 {"names-1000", func() *sheet.Sheet { return stress.Names(1000) }},4343 {"charts-20", func() *sheet.Sheet {4444 s := stress.Table(200, 3)
@@ -156,7 +156,7 @@ func BenchmarkKeystroke(b *testing.B) {
156156 {"select-sheet/dense-8192x256", uiShapes()[2], []string{"ctrl+a", "ctrl+a"}, nil},157157 {"extend-data/dense-8192x256", uiShapes()[2], []string{"ctrl+a"}, []string{"shift+up", "shift+down"}},158158 {"extend/dense-8192x26", uiShapes()[1], []string{"shift+down"}, []string{"shift+down", "shift+up"}},159- {"type/fanin-1000", uiShape{"fanin", func() *sheet.Sheet { return stress.FanIn(sheet.MaxRows, 1000) }},159+ {"type/fanin-1000", uiShape{"fanin", func() *sheet.Sheet { return stress.FanIn(stress.Rows, 1000) }},160160 nil, []string{"7", "enter", "up"}},161161 {"type/dense-8192x26", uiShapes()[1], nil, []string{"7", "enter", "up"}},162162 }
@@ -186,7 +186,7 @@ func BenchmarkKeystroke(b *testing.B) {
186186 // that recalculates, with telemetry off and on (a JSON log in a temp187187 // file): what the instrumentation costs.188188 func BenchmarkTelemetry(b *testing.B) {189- s := lazy(func() *sheet.Sheet { return stress.Dense(sheet.MaxRows, 26) })189+ s := lazy(func() *sheet.Sheet { return stress.Dense(stress.Rows, 26) })190190 for _, on := range []bool{false, true} {191191 name := "off"192192 if on {
M internal/ui/trace_test.go +1 -1
M internal/ui/view.go +3 -3