cat / 012
clone

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

cat/module-path
012 / internal / ui / traceview.go
traceview.go 7.7K
1package ui23import (4 "slices"5 "strconv"6 "strings"78 tea "charm.land/bubbletea/v2"9 "charm.land/lipgloss/v2"10 "github.com/charmbracelet/x/ansi"1112 "src.rest/cat/012/internal/sheet"13 "src.rest/cat/012/internal/ui/picker"14)1516// Tracing that stays on (Data > Formula tracing > Show precedents and17// dependents, Alt+;): as the pointer moves, the cells the active18// cell's formula reads are marked in the Precedent role and the19// formulas reading it in the Dependent role, and the context line lists20// both, those on other sheets or off screen included, with an arrow21// toward the ones off screen.22// Alt+' lists them in a picker to go to one, which then shows its own.23//24// The links are found once per cell and change (the engine's25// DependentLinks and PrecedentLinks), and each cell drawn asks the26// engine whether its formula reads the active cell (Reads), so drawing27// costs the cells on screen however many formulas read the cell. Off,28// it costs nothing: the model holds no traceView.2930// traceView is what tracing shows of the active cell.31type traceView struct {32 key traceViewKey33 home *sheet.Sheet34 subject sheet.Rect // the cell, or the cells its formula spills into35 prec []sheet.Link36 deps []sheet.Link37 more bool // dependents past maxListed left out38}3940// traceViewKey is what the links were found for: the cell and the41// workbook's state and values.42type traceViewKey struct {43 s *sheet.Sheet44 a sheet.Addr45 state int46 version uint6447}4849func init() {50 register(51 &command{id: "view.trace", macro: macroView, title: "Show precedents and dependents",52 desc: "Mark the cells the active cell's formula reads and the formulas that read it, as the pointer moves",53 checked: func(m *Model) bool { return m.tview != nil },54 run: func(m *Model) tea.Cmd {55 if m.tview != nil {56 m.tview = nil57 return nil58 }59 m.tview = &traceView{}60 m.syncTrace()61 return nil62 }},63 &command{id: "data.trace_list", macro: macroView, title: "Go to a precedent or dependent",64 desc: "List what the active cell reads and the formulas that read it, on any sheet, and go to one",65 run: func(m *Model) tea.Cmd {66 m.openTraceList()67 return nil68 }},69 )70 keymap["alt+;"] = "view.trace"71 keymap["alt+'"] = "data.trace_list"72}7374// syncTrace finds the active cell's links again when it or the workbook75// changed.76func (m *Model) syncTrace() {77 v := m.tview78 if v == nil {79 return80 }81 k := traceViewKey{m.sheet, m.cur, m.sheet.StateID(), m.sheet.Version()}82 if k == v.key {83 return84 }85 *v = traceView{key: k, home: m.sheet, subject: sheet.Rect{From: m.cur, To: m.cur}}86 v.prec = m.sheet.PrecedentLinks(m.cur)87 v.deps, v.more = m.sheet.DependentLinks(m.cur, maxListed)88 if area, ok := m.sheet.SpillArea(m.cur); ok {89 v.subject = area90 }91}9293// traceRole is the role a, on the sheet shown, is drawn in for a trace,94// or nil.95func (m *Model) traceRole(a sheet.Addr) *lipgloss.Style {96 if m.trace != nil {97 return m.trace.role(&m.th, m.sheet, a)98 }99 v := m.tview100 if v == nil {101 return nil102 }103 for _, l := range v.prec {104 if l.Sheet == m.sheet && l.Range.Contains(a) {105 return &m.th.Precedent106 }107 }108 if m.sheet == v.home && v.subject.Contains(a) || m.sheet.Reads(a, v.home, v.subject) {109 return &m.th.Dependent110 }111 return nil112}113114// traceLine is the context line while tracing is on: "C1 reads A1,115// Data!B2:B9; read by D1 +3 more", with an arrow after cells off screen116// toward them, and the keys.117func (m *Model) traceLine() (left, right string) {118 v, th := m.tview, &m.th119 right = th.KeyHints(m.shortcut("data.trace_list"), "list", m.shortcut("view.trace"), "stop")120 name := m.cur.String()121 if len(v.prec) == 0 && len(v.deps) == 0 {122 return th.Muted.Render(name + " reads no cells and no formula reads it"), right123 }124 room := m.width - ansi.StringWidth(right) - 3125 if room < 40 {126 right, room = "", m.width127 }128 // Labels past what the line can show aren't made: a cell read by a129 // thousand formulas shows a few and counts the rest.130 shown := screenArea(m)131 labels := func(links []sheet.Link) []string {132 out := make([]string, min(len(links), room/3+1))133 for i := range out {134 out[i] = linkLabel(m.sheet, links[i], m.sheet) + shown.arrow(m.sheet, links[i])135 }136 return out137 }138 var b strings.Builder139 if len(v.prec) > 0 {140 lead := name + " reads "141 fit := room - len(lead)142 if len(v.deps) > 0 {143 fit = room/2 - len(lead)144 }145 b.WriteString(th.Muted.Render(lead) + listFit(th, labels(v.prec), len(v.prec), fit, false))146 }147 if len(v.deps) > 0 {148 lead := name + " is read by "149 if b.Len() > 0 {150 lead = "; read by "151 }152 fit := room - ansi.StringWidth(b.String()) - len(lead)153 b.WriteString(th.Muted.Render(lead) + listFit(th, labels(v.deps), len(v.deps), fit, v.more))154 }155 return b.String(), right156}157158// area is the rows and columns on screen, frozen ones included.159type area struct {160 top, bottom, left, right int161 ok bool162}163164func screenArea(m *Model) area {165 rows := slices.DeleteFunc(m.screenRows(), func(r int) bool { return r == divider })166 cols := slices.DeleteFunc(m.screenCols(), func(c int) bool { return c == divider })167 if len(rows) == 0 || len(cols) == 0 {168 return area{}169 }170 return area{slices.Min(rows), slices.Max(rows), slices.Min(cols), slices.Max(cols), true}171}172173// arrow points toward a link on the sheet shown that is off screen, or174// is "".175func (sa area) arrow(shown *sheet.Sheet, l sheet.Link) string {176 if l.Sheet != shown || !sa.ok {177 return ""178 }179 r := l.Range180 switch {181 case r.To.Row < sa.top:182 return "↑"183 case r.From.Row > sa.bottom:184 return "↓"185 case r.To.Col < sa.left:186 return "←"187 case r.From.Col > sa.right:188 return "→"189 }190 return ""191}192193// openTraceList opens a picker of the active cell's precedents, then its194// dependents; picking one goes to it.195func (m *Model) openTraceList() {196 prec := m.sheet.PrecedentLinks(m.cur)197 deps, more := m.sheet.DependentLinks(m.cur, maxListed)198 if len(prec) == 0 && len(deps) == 0 {199 m.note = m.cur.String() + " reads no cells and no formula reads it"200 return201 }202 var items []picker.Item203 for i, links := range [][]sheet.Link{prec, deps} {204 for _, l := range links {205 items = append(items, m.traceItem(l, i == 1))206 }207 }208 title := "Precedents and dependents of " + m.cur.String()209 if more {210 title += ", the first " + strconv.Itoa(maxListed) + " dependents"211 }212 p := m.newPicker(title, "Type a cell, range or name", 72, items)213 p.Action = "go to"214 m.openOverlay(p)215}216217// traceItem is a picker row for a link: its label, and what it is to218// the active cell.219func (m *Model) traceItem(l sheet.Link, dependent bool) picker.Item {220 label := linkLabel(m.sheet, l, m.sheet)221 detail := "dependent"222 switch {223 case dependent && l.Kind == sheet.LinkSpill:224 detail = "dependent, the cells it spills into"225 case !dependent:226 detail = "precedent" + map[sheet.LinkKind]string{sheet.LinkName: ", named range " + l.Range.String(),227 sheet.LinkRegion: ", a region's table", sheet.LinkTable: ", a table's " + l.Range.String(), sheet.LinkSpill: ", the formula that spilled it",228 sheet.LinkFile: ", the linked file its rows come from", sheet.LinkOutput: ", the notebook cell its rows come from"}[l.Kind]229 }230 if l.Sheet != m.sheet && l.OnGrid() {231 detail += ", on " + l.Sheet.Name()232 }233 return picker.Item{Title: label, Name: len(label), Detail: detail, Desc: "Go to " + label,234 Pick: func() tea.Cmd {235 m.closeOverlay()236 m.goToLink(l)237 return nil238 }}239}240241// goToLink shows a link: its cells selected, or a notebook's cell.242func (m *Model) goToLink(l sheet.Link) {243 if m.refuseHidden(l.Sheet) {244 return245 }246 m.showSheet(l.Sheet)247 if !l.OnGrid() {248 if v := m.viewOf(l.Sheet); v != nil {249 v.Select(l.Range.From.Row, false)250 }251 return252 }253 if l.Range.From == l.Range.To {254 m.clearSelection()255 m.cur = l.Range.From256 return257 }258 m.selectRect(l.Range)259}