cat / 012
clone

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

Show spilled cells and refuse editing them

Dami committed 998955f
Spilled values draw in their own theme role (the progress bar's cyan or
blue), the formula bar shows a spilled cell's formula dimmed, as Sheets
does, and the context line says "Spilled from B2" there and "Spills
into B2:C9" on the formula's own cell. Commands writing spilled cells
are refused with the formula to edit, through the pivot guard; those
that move or format cells, and edits of a range holding the formula,
go ahead.

Co-Authored-By: Claude <noreply@anthropic.com>
commit
998955f3e73e10568ef00491239e6ffc370e419d
parent
6c720034b15ba0f4de87c96b9fe1e470961a9cc8
author
Dami <dami.etoile@gmail.com>
13 files changed, +133 -24
Minternal/ui/clipboard.go2+-
Minternal/ui/commands.go12++++-
Minternal/ui/edit.go12+++---
Minternal/ui/entry.go2+-
Minternal/ui/fill.go2+-
Minternal/ui/format.go12+++---
Minternal/ui/links.go14++++++
Minternal/ui/panel.go8++++
Minternal/ui/pivot.go21+++++++--
Minternal/ui/sort.go2+-
Ainternal/ui/spill_test.go66++++++++++++++++++++++++++++++
Minternal/ui/theme/theme.go2+
Minternal/ui/view.go2+
M internal/ui/clipboard.go +1 -1
M internal/ui/commands.go +9 -3
M internal/ui/edit.go +6 -6
@@ -26,34 +26,34 @@ func init() {
2626 &command{id: "edit.fill_right", title: "Fill right", desc: "Copy the left column of the selection into the columns right of it", edits: (*Model).selection, run: func(m *Model) tea.Cmd {2727 return m.fill(m.sheet.FillRight)2828 }},29- &command{id: "insert.row_above", title: "Insert rows above", desc: "Insert as many rows above the selection as it spans", edits: rowsTarget, run: func(m *Model) tea.Cmd {29+ &command{id: "insert.row_above", title: "Insert rows above", desc: "Insert as many rows above the selection as it spans", edits: rowsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {3030 r := m.selection()3131 return m.structural(m.sheet.InsertRows(r.From.Row, r.To.Row-r.From.Row+1))3232 }},33- &command{id: "insert.row_below", title: "Insert rows below", desc: "Insert as many rows below the selection as it spans", edits: rowsBelowTarget, run: func(m *Model) tea.Cmd {33+ &command{id: "insert.row_below", title: "Insert rows below", desc: "Insert as many rows below the selection as it spans", edits: rowsBelowTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {3434 r := m.selection()3535 if r.To.Row == sheet.MaxRows-1 {3636 return m.structural(sheet.ErrPushedOff)3737 }3838 return m.structural(m.sheet.InsertRows(r.To.Row+1, r.To.Row-r.From.Row+1))3939 }},40- &command{id: "insert.col_left", title: "Insert columns left", desc: "Insert as many columns left of the selection as it spans", edits: colsTarget, run: func(m *Model) tea.Cmd {40+ &command{id: "insert.col_left", title: "Insert columns left", desc: "Insert as many columns left of the selection as it spans", edits: colsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {4141 r := m.selection()4242 return m.structural(m.sheet.InsertCols(r.From.Col, r.To.Col-r.From.Col+1))4343 }},44- &command{id: "insert.col_right", title: "Insert columns right", desc: "Insert as many columns right of the selection as it spans", edits: colsRightTarget, run: func(m *Model) tea.Cmd {44+ &command{id: "insert.col_right", title: "Insert columns right", desc: "Insert as many columns right of the selection as it spans", edits: colsRightTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {4545 r := m.selection()4646 if r.To.Col == sheet.MaxCols-1 {4747 return m.structural(sheet.ErrPushedOff)4848 }4949 return m.structural(m.sheet.InsertCols(r.To.Col+1, r.To.Col-r.From.Col+1))5050 }},51- &command{id: "delete.row", title: "Delete rows", desc: "Delete the selected rows; references to them become #REF!", edits: rowsTarget, run: func(m *Model) tea.Cmd {51+ &command{id: "delete.row", title: "Delete rows", desc: "Delete the selected rows; references to them become #REF!", edits: rowsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {5252 r := m.selection()5353 m.sheet.DeleteRows(r.From.Row, r.To.Row-r.From.Row+1)5454 return nil5555 }},56- &command{id: "delete.col", title: "Delete columns", desc: "Delete the selected columns; references to them become #REF!", edits: colsTarget, run: func(m *Model) tea.Cmd {56+ &command{id: "delete.col", title: "Delete columns", desc: "Delete the selected columns; references to them become #REF!", edits: colsTarget, keepsSpills: true, run: func(m *Model) tea.Cmd {5757 r := m.selection()5858 m.sheet.DeleteCols(r.From.Col, r.To.Col-r.From.Col+1)5959 return nil
M internal/ui/entry.go +1 -1
M internal/ui/fill.go +1 -1
M internal/ui/format.go +6 -6
@@ -58,7 +58,7 @@ var alignments = []struct {
5858 func init() {5959 for _, nf := range numberFormats {6060 f, title := nf.f, nf.title61- register(&command{id: nf.id, title: title, desc: nf.desc, edits: (*Model).selection, run: func(m *Model) tea.Cmd {61+ register(&command{id: nf.id, title: title, desc: nf.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {6262 r := m.selection()6363 m.sheet.SetFormat(r, f)6464 m.formatted(title + " format")
@@ -67,7 +67,7 @@ func init() {
6767 }6868 for _, ts := range textStyles {6969 get, title := ts.get, ts.title70- register(&command{id: ts.id, title: title, desc: ts.desc, edits: (*Model).selection, run: func(m *Model) tea.Cmd {70+ register(&command{id: ts.id, title: title, desc: ts.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {7171 cur := m.sheet.CellStyle(m.cur)7272 on := !*get(&cur)7373 r := m.selection()
@@ -85,7 +85,7 @@ func init() {
8585 }8686 for _, al := range alignments {8787 a, title := al.a, al.title88- register(&command{id: al.id, title: title, desc: al.desc, edits: (*Model).selection, run: func(m *Model) tea.Cmd {88+ register(&command{id: al.id, title: title, desc: al.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {8989 r := m.selection()9090 m.sheet.Batch(sheet.Change{Label: "align " + r.String() + " " + strings.ToLower(title), Focus: r}, func() error {9191 m.sheet.SetStyle(r, func(s *sheet.Style) { s.Align = a })
@@ -96,17 +96,17 @@ func init() {
9696 }})9797 }9898 register(99- &command{id: "format.decimals_more", title: "Increase decimal places", desc: "Show one more decimal place", edits: (*Model).selection, run: func(m *Model) tea.Cmd {99+ &command{id: "format.decimals_more", title: "Increase decimal places", desc: "Show one more decimal place", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {100100 m.sheet.AdjustDecimals(m.selection(), 1)101101 m.formatted("One more decimal place")102102 return nil103103 }},104- &command{id: "format.decimals_less", title: "Decrease decimal places", desc: "Show one less decimal place", edits: (*Model).selection, run: func(m *Model) tea.Cmd {104+ &command{id: "format.decimals_less", title: "Decrease decimal places", desc: "Show one less decimal place", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {105105 m.sheet.AdjustDecimals(m.selection(), -1)106106 m.formatted("One less decimal place")107107 return nil108108 }},109- &command{id: "format.clear", title: "Clear formatting", desc: "Reset number formats and text styles, keeping contents", edits: (*Model).selection, run: func(m *Model) tea.Cmd {109+ &command{id: "format.clear", title: "Clear formatting", desc: "Reset number formats and text styles, keeping contents", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {110110 m.sheet.ClearFormatting(m.selection())111111 m.formatted("Formatting cleared")112112 return nil
M internal/ui/links.go +14 -0
M internal/ui/panel.go +8 -0
M internal/ui/pivot.go +16 -5
M internal/ui/sort.go +1 -1
A internal/ui/spill_test.go +66 -0
@@ -0,0 +1,66 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+)7+8+// An array formula spills: the spilled cells say where they come from,9+// show its formula dimmed, and refuse edits as Sheets' array results do,10+// while the formula's own cell and a range holding it stay editable.11+func TestSpillInGrid(t *testing.T) {12+ m := newModel()13+ press(t, m, "=SEQUENCE(3, 2)", "<enter>", "<up>")14+ if shows(m, "B3") != "6" {15+ t.Fatalf("B3 shows %q", shows(m, "B3"))16+ }17+ if !strings.Contains(line(m, contextLine), "Spills into A1:B3") {18+ t.Errorf("anchor's context line %q", line(m, contextLine))19+ }20+ press(t, m, "<down>", "<right>")21+ if got := line(m, contextLine); !strings.Contains(got, "Spilled from A1") {22+ t.Errorf("spilled cell's context line %q", got)23+ }24+ if got := bar(m); got != "=SEQUENCE(3, 2)" {25+ t.Errorf("formula bar %q, want the anchor's formula", got)26+ }27+ for _, keys := range [][]string{{"x"}, {"<delete>"}, {"<enter>"}, {"<ctrl+d>"}} {28+ press(t, m, keys...)29+ if m.mode != modeReady || !strings.Contains(line(m, contextLine), "B2 shows part of the array A1 spills") {30+ t.Errorf("%v: mode %v, %q", keys, m.mode, line(m, contextLine))31+ }32+ press(t, m, "<esc>")33+ }34+ // Formatting a spilled cell keeps its value.35+ press(t, m, "<ctrl+b>")36+ if c := m.sheet.Cell(addr("B2")); shows(m, "B2") != "4" || c == nil || !c.Style.Bold {37+ t.Errorf("B2 after bold: %q %+v", shows(m, "B2"), c)38+ }39+ // Inserting a row through the spill recomputes it.40+ press(t, m, "<ctrl+alt+=>")41+ if strings.Contains(m.note, "spills") || shows(m, "B3") != "6" || shows(m, "B4") != "" {42+ t.Errorf("after inserting a row: %q, B3 %q, B4 %q", m.note, shows(m, "B3"), shows(m, "B4"))43+ }44+ // Copying pastes values; clearing the whole spill clears the formula.45+ press(t, m, "<ctrl+g>", "A1:B4", "<enter>", "<delete>")46+ if shows(m, "A1") != "" || shows(m, "B3") != "" {47+ t.Errorf("after clearing the spill: A1 %q B3 %q", shows(m, "A1"), shows(m, "B3"))48+ }49+}50+51+// Cells in the way of a spill make the formula #REF!, with Sheets'52+// explanation, until they're cleared.53+func TestSpillBlockedInGrid(t *testing.T) {54+ m := newModel()55+ press(t, m, "<down>", "<down>", "x", "<enter>", "<ctrl+home>", "=SEQUENCE(3)", "<enter>", "<up>")56+ if shows(m, "A1") != "#REF!" {57+ t.Fatalf("A1 shows %q", shows(m, "A1"))58+ }59+ if got := line(m, contextLine); !strings.Contains(got, "would overwrite data in A3") {60+ t.Errorf("context line %q", got)61+ }62+ press(t, m, "<down>", "<down>", "<delete>")63+ if shows(m, "A1") != "1" || shows(m, "A3") != "3" {64+ t.Errorf("after clearing A3: A1 %q A3 %q", shows(m, "A1"), shows(m, "A3"))65+ }66+}
M internal/ui/theme/theme.go +2 -0
M internal/ui/view.go +2 -0