cat / 012
clone

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

Cut overflowing text into columns in one pass

Dami committed e1daf4c
rowText cut each column's piece of a cell's text with ansi.Cut, which
rescans the text from its start, so drawing long text cost O(columns x
length) per row. textCutter walks the text once, cutting exactly what
ansi.Cut would (tested against it with wide, combining and emoji
clusters).

  View/longtext-8192x500/80x24     1.24 ms -> 0.22 ms
  View/longtext-8192x500/200x60    9.08 ms -> 0.73 ms
  View/longtext-8192x500/400x120  40.5 ms  -> 2.0 ms
commit
e1daf4c58fb2e7ed65e21bad36c0053d21be80d0
parent
484093772d17b89d1dd7402805f1ecd886468054
author
Dami <dami.etoile@gmail.com>
2 files changed, +78 -1
Minternal/ui/view.go46+++++++++++++++++++++++++++++-
Ainternal/ui/view_test.go33++++++++++++++++++++++
M internal/ui/view.go +45 -1
@@ -325,6 +325,7 @@ func (m *Model) rowText(row, lo, ncols, minCol, maxCol int) []span {
325325 }326326 }327327 claimed = to328+ cut := textCutter{s: text}328329 for k := max(first, lo); k <= min(last, hi); k++ {329330 kx0, kx1 := col(k)330331 if kx1 <= from || kx0 >= to {
@@ -332,7 +333,7 @@ func (m *Model) rowText(row, lo, ncols, minCol, maxCol int) []span {
332333 }333334 sp := span{trail: kx1 - kx0, style: cell.Style, owner: c}334335 if seg0, seg1 := max(start, kx0, from), min(start+tw, kx1, to); seg1 > seg0 {335- sp.lead, sp.text, sp.trail = seg0-kx0, ansi.Cut(text, seg0-start, seg1-start), kx1-seg1336+ sp.lead, sp.text, sp.trail = seg0-kx0, cut.cut(seg0-start, seg1-start), kx1-seg1336337 }337338 out[k-lo] = sp338339 }
@@ -349,6 +350,49 @@ func (m *Model) rowText(row, lo, ncols, minCol, maxCol int) []span {
349350 return out350351 }351352 353+// textCutter cuts successive column ranges, left to right, out of one354+// line of text in a single pass, as ansi.Cut would one at a time: a355+// cluster is in [l, r) when its right edge is past l and not past r.356+// Cutting each column's piece with ansi.Cut rescanned the text from its357+// start, so a screen of 500-character text took 40 ms to draw.358+type textCutter struct {359+ s string360+ i int // byte offset of the next cluster361+ right int // right edge, in columns, of the text before s[i:]362+}363+364+func (c *textCutter) cut(l, r int) string {365+ for c.i < len(c.s) {366+ n, w := nextCluster(c.s[c.i:])367+ if c.right+w > l {368+ break369+ }370+ c.i, c.right = c.i+n, c.right+w371+ }372+ from := c.i373+ for c.i < len(c.s) {374+ n, w := nextCluster(c.s[c.i:])375+ if c.right+w > r {376+ break377+ }378+ c.i, c.right = c.i+n, c.right+w379+ }380+ return c.s[from:c.i]381+}382+383+// nextCluster returns the length in bytes and width in columns of the384+// grapheme cluster s starts with, measured as ansi.Cut measures it.385+func nextCluster(s string) (n, w int) {386+ switch b := s[0]; {387+ case b >= 0x20 && b < 0x7f:388+ return 1, 1389+ case b < 0x80:390+ return 1, 0391+ }392+ g, w := ansi.FirstGraphemeCluster(s, ansi.GraphemeWidth)393+ return len(g), w394+}395+352396 // keyLabel formats a key binding for display, e.g. "ctrl+s" -> "Ctrl+S".353397 func keyLabel(k string) string {354398 switch k {
A internal/ui/view_test.go +33 -0