cat / 012
clone

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

Make formula suggestions a component

Dami committed 2260941
The suggestion list's key, mouse, box and status handling become
methods of assist, handed the model as overlays are. Listing
suggestions needs only the sheet, and drawing a signature only the
theme, so they become plain functions.
commit
2260941271530e8306e69627d5239a0c4f35a85d
parent
ea9feab6ba4bcdb0a1a7422fae4054a952b5a1eb
author
Dami <dami.etoile@gmail.com>
5 files changed, +62 -61
Minternal/ui/assist.go91+++++++++++++++---------------
Minternal/ui/assist_test.go16+++---
Minternal/ui/entry.go4+-
Minternal/ui/overlay.go4+-
Minternal/ui/panel.go8+-
M internal/ui/assist.go +46 -45
@@ -20,7 +20,9 @@ import (
2020 // (Up/Down, Tab, Enter, Esc) and only while it shows, so arrows after an2121 // operator still point at cells.2222 23-// assist is the state of the suggestion list.23+// assist is the suggestion list: its state, and the keys, mouse, box24+// and status line it takes over while it shows. Like an overlay, it is25+// handed the model, for the entry and the sheet's names.2426 type assist struct {2527 active bool // the last key typed or deleted text; moving the caret hides the list2628 sel, top int // highlighted suggestion and first one shown; reset as the word changes
@@ -45,7 +47,7 @@ type suggestion struct {
4547 // letters on, those containing it, such as COUNTIFS for "ifs". A word4648 // that is already a cell reference only gets names and functions that4749 // start with it.48-func (m *Model) suggestions(word string) []suggestion {50+func suggestions(sh *sheet.Sheet, word string) []suggestion {4951 w := strings.ToUpper(word)5052 var prefix, inner []suggestion5153 add := func(s suggestion, name string) {
@@ -56,7 +58,7 @@ func (m *Model) suggestions(word string) []suggestion {
5658 inner = append(inner, s)5759 }5860 }59- for _, n := range m.sheet.Names() {61+ for _, n := range sh.Names() {6062 add(suggestion{name: n.Name, detail: n.Ref(), desc: "Named range " + n.Name + ": " + n.Ref()}, strings.ToUpper(n.Name))6163 }6264 for _, f := range sheet.Funcs() {
@@ -68,23 +70,23 @@ func (m *Model) suggestions(word string) []suggestion {
6870 return append(prefix, inner...)6971 }7072 71-// shownSuggestions returns the list for the word at the caret, and where73+// shown returns the list for the word at the caret, and where7274 // the word starts, or nothing when the list is hidden: outside ENTER and7375 // EDIT, in plain text, after the caret moved, or when the word already7476 // names a range exactly and nothing else matches.75-func (m *Model) shownSuggestions() ([]suggestion, int) {76- if (m.mode != modeEnter && m.mode != modeEdit) || !m.line.isFormula() || !m.entry.assist.active || m.overlay != nil {77+func (a *assist) shown(m *Model) ([]suggestion, int) {78+ if (m.mode != modeEnter && m.mode != modeEdit) || !m.line.isFormula() || !a.active || m.overlay != nil {7779 return nil, 07880 }7981 c := scanCaret(m.line.buf, m.line.pos)8082 if c.word == "" {8183 return nil, 08284 }83- list := m.suggestions(c.word)85+ list := suggestions(m.sheet, c.word)8486 if len(list) == 0 || len(list) == 1 && !list[0].fn && strings.EqualFold(list[0].name, c.word) {8587 return nil, 08688 }87- m.entry.assist.sel = min(m.entry.assist.sel, len(list)-1)89+ a.sel = min(a.sel, len(list)-1)8890 return list, c.wordStart8991 }9092
@@ -96,30 +98,30 @@ func (m *Model) typeKey(k tea.KeyPressMsg) {
9698 m.entry.assist = assist{active: m.line.text() != before}9799 }98100 99-// assistKey handles the keys the suggestion list takes while it shows.100-func (m *Model) assistKey(key string) bool {101- list, start := m.shownSuggestions()101+// key handles the keys the suggestion list takes while it shows.102+func (a *assist) key(m *Model, key string) bool {103+ list, start := a.shown(m)102104 if list == nil {103105 return false104106 }105107 switch key {106108 case "up", "ctrl+p":107- m.entry.assist.sel = (m.entry.assist.sel - 1 + len(list)) % len(list)109+ a.sel = (a.sel - 1 + len(list)) % len(list)108110 case "down", "ctrl+n":109- m.entry.assist.sel = (m.entry.assist.sel + 1) % len(list)111+ a.sel = (a.sel + 1) % len(list)110112 case "tab", "enter":111- m.acceptSuggestion(list[m.entry.assist.sel], start)113+ a.accept(m, list[a.sel], start)112114 case "esc":113- m.entry.assist.active = false115+ a.active = false114116 default:115117 return false116118 }117119 return true118120 }119121 120-// acceptSuggestion replaces the word at the caret with s, adding "(" after122+// accept replaces the word at the caret with s, adding "(" after121123 // a function unless one is already there.122-func (m *Model) acceptSuggestion(s suggestion, start int) {124+func (a *assist) accept(m *Model, s suggestion, start int) {123125 text := []rune(s.name)124126 rest := m.line.buf[m.line.pos:]125127 if s.fn && (len(rest) == 0 || rest[0] != '(') {
@@ -131,13 +133,13 @@ func (m *Model) acceptSuggestion(s suggestion, start int) {
131133 }132134 m.line.buf = slices.Concat(m.line.buf[:start], text, rest)133135 m.line.pos = pos134- m.entry.assist.active = false136+ a.active = false135137 }136138 137-// assistBox draws the list under the formula bar, its text aligned with139+// box draws the list under the formula bar, its text aligned with138140 // the word being typed: the name, then the signature or range dimmed.139-func (m *Model) assistBox() (box, bool) {140- list, start := m.shownSuggestions()141+func (a *assist) box(m *Model) (box, bool) {142+ list, start := a.shown(m)141143 if list == nil {142144 return box{}, false143145 }
@@ -145,7 +147,6 @@ func (m *Model) assistBox() (box, bool) {
145147 if rows < 1 {146148 return box{}, false147149 }148- a := &m.entry.assist149150 a.top = clamp(a.top, a.sel-rows+1, a.sel)150151 a.top = clamp(a.top, 0, len(list)-rows)151152 nw, dw := 0, 0
@@ -178,10 +179,10 @@ func (m *Model) assistBox() (box, bool) {
178179 return box{id: assistID, x: x, y: contextLine + 1, lines: m.th.Frame(inner, "", footer, lines)}, true179180 }180181 181-// assistMouse lets the mouse hover and click suggestions and scroll the182+// mouse lets the mouse hover and click suggestions and scroll the182183 // list. It reports false for events outside the list.183-func (m *Model) assistMouse(msg tea.MouseMsg) bool {184- b, ok := m.assistBox()184+func (a *assist) mouse(m *Model, msg tea.MouseMsg) bool {185+ b, ok := a.box(m)185186 if !ok {186187 return false187188 }
@@ -190,55 +191,55 @@ func (m *Model) assistMouse(msg tea.MouseMsg) bool {
190191 if h.Empty() {191192 return false192193 }193- list, start := m.shownSuggestions()194- i := m.entry.assist.top + mouse.Y - b.y - 1194+ list, start := a.shown(m)195+ i := a.top + mouse.Y - b.y - 1195196 switch msg.(type) {196197 case tea.MouseWheelMsg:197198 d := 1198199 if mouse.Button == tea.MouseWheelUp {199200 d = -1200201 }201- m.entry.assist.sel = clamp(m.entry.assist.sel+d, 0, len(list)-1)202+ a.sel = clamp(a.sel+d, 0, len(list)-1)202203 case tea.MouseMotionMsg:203- if i >= m.entry.assist.top && i < min(len(list), m.entry.assist.top+b.height()-2) {204- m.entry.assist.sel = i204+ if i >= a.top && i < min(len(list), a.top+b.height()-2) {205+ a.sel = i205206 }206207 case tea.MouseClickMsg:207- if mouse.Button == tea.MouseLeft && i >= m.entry.assist.top && i < min(len(list), m.entry.assist.top+b.height()-2) {208- m.acceptSuggestion(list[i], start)208+ if mouse.Button == tea.MouseLeft && i >= a.top && i < min(len(list), a.top+b.height()-2) {209+ a.accept(m, list[i], start)209210 }210211 }211212 return true212213 }213214 214-// assistStatus is the status line while the list shows: what the215+// status is the status line while the list shows: what the215216 // highlighted suggestion is, and the keys that apply.216-func (m *Model) assistStatus() (desc, keys string, ok bool) {217- list, _ := m.shownSuggestions()217+func (a *assist) status(m *Model) (desc, keys string, ok bool) {218+ list, _ := a.shown(m)218219 if list == nil {219220 return "", "", false220221 }221- return list[m.entry.assist.sel].desc, m.th.KeyHints("Up/Down", "move", "Tab", "insert", "Esc", "hide"), true222+ return list[a.sel].desc, m.th.KeyHints("Up/Down", "move", "Tab", "insert", "Esc", "hide"), true222223 }223224 224225 // inFunction reports whether the caret is inside a known function's225226 // parentheses.226-func (m *Model) inFunction() bool {227- _, ok := sheet.LookupFunc(scanCaret(m.line.buf, m.line.pos).fn)227+func (l *lineEdit) inFunction() bool {228+ _, ok := sheet.LookupFunc(scanCaret(l.buf, l.pos).fn)228229 return ok229230 }230231 231232 // signatureLine puts the signature of the function around the caret on232233 // the context line, with what the function does and the keys that apply233234 // (hints) as room allows. It reports false outside a function.234-func (m *Model) signatureLine(buf []rune, pos int, hints string) (left, right string, ok bool) {235- sig, desc := m.signature(buf, pos)235+func signatureLine(th *theme.Theme, width int, buf []rune, pos int, hints string) (left, right string, ok bool) {236+ sig, desc := signature(th, buf, pos)236237 if sig == "" {237238 return "", "", false238239 }239- full := sig + " " + m.th.Muted.Render(desc)240+ full := sig + " " + th.Muted.Render(desc)240241 for _, try := range [][2]string{{full, hints}, {sig, hints}, {full, ""}} {241- if ansi.StringWidth(try[0])+3+ansi.StringWidth(try[1]) <= m.width {242+ if ansi.StringWidth(try[0])+3+ansi.StringWidth(try[1]) <= width {242243 return try[0], try[1], true243244 }244245 }
@@ -248,7 +249,7 @@ func (m *Model) signatureLine(buf []rune, pos int, hints string) (left, right st
248249 // signature renders the signature of the function around the caret, e.g.249250 // SUM(value1, [value2, ...]), with the current argument marked, and what250251 // the function does. It is "" outside a function.251-func (m *Model) signature(buf []rune, pos int) (sig, desc string) {252+func signature(th *theme.Theme, buf []rune, pos int) (sig, desc string) {252253 c := scanCaret(buf, pos)253254 f, ok := sheet.LookupFunc(c.fn)254255 if c.fn == "" || !ok {
@@ -257,13 +258,13 @@ func (m *Model) signature(buf []rune, pos int) (sig, desc string) {
257258 parts := splitArgs(f.Args)258259 cur := argPart(parts, c.arg, f.Max < 0)259260 var b strings.Builder260- b.WriteString(m.th.Key.Render(f.Name) + "(")261+ b.WriteString(th.Key.Render(f.Name) + "(")261262 for i, p := range parts {262263 if i > 0 {263264 b.WriteString(", ")264265 }265266 if i == cur {266- b.WriteString(m.th.Argument.Render(p))267+ b.WriteString(th.Argument.Render(p))267268 } else {268269 b.WriteString(p)269270 }
M internal/ui/assist_test.go +8 -8
@@ -64,7 +64,7 @@ func TestArgPart(t *testing.T) {
6464 func TestAutocompleteFunctions(t *testing.T) {6565 m := newModel()6666 press(t, m, "=su")67- list, _ := m.shownSuggestions()67+ list, _ := m.entry.assist.shown(m)6868 if len(list) == 0 || list[0].name != "SUBSTITUTE" && list[0].name != "SUM" {6969 t.Fatalf("suggestions %v", list)7070 }
@@ -92,7 +92,7 @@ func TestAutocompleteFunctions(t *testing.T) {
9292 t.Errorf("signature %q", ctx)9393 }9494 press(t, m, "1,")95- sig, _ := m.signature(m.line.buf, m.line.pos)95+ sig, _ := signature(&m.th, m.line.buf, m.line.pos)9696 if !strings.Contains(sig, m.th.Argument.Render("[value2, ...]")) {9797 t.Errorf("second argument not marked: %q", sig)9898 }
@@ -116,7 +116,7 @@ func TestAutocompleteKeys(t *testing.T) {
116116 // Esc hides the list, a second Esc cancels the entry.117117 press(t, m, "=ro")118118 press(t, m, "<esc>")119- if list, _ := m.shownSuggestions(); list != nil || m.mode != modeEnter {119+ if list, _ := m.entry.assist.shown(m); list != nil || m.mode != modeEnter {120120 t.Fatalf("esc: %v %v", list, m.mode)121121 }122122 press(t, m, "<esc>")
@@ -131,16 +131,16 @@ func TestAutocompleteKeys(t *testing.T) {
131131 press(t, m, "<esc>", "<esc>")132132 // Moving the caret hides the list; a cell reference gets no list.133133 press(t, m, "=co", "<left>")134- if list, _ := m.shownSuggestions(); list != nil {134+ if list, _ := m.entry.assist.shown(m); list != nil {135135 t.Error("list after moving the caret")136136 }137137 press(t, m, "<esc>", "=B2")138- if list, _ := m.shownSuggestions(); list != nil {138+ if list, _ := m.entry.assist.shown(m); list != nil {139139 t.Errorf("list for a reference: %v", list)140140 }141141 // Plain text gets no list.142142 press(t, m, "<esc>", "su")143- if list, _ := m.shownSuggestions(); list != nil {143+ if list, _ := m.entry.assist.shown(m); list != nil {144144 t.Error("list for text")145145 }146146 }
@@ -149,12 +149,12 @@ func TestAutocompleteNamesAndMouse(t *testing.T) {
149149 m := tallModel()150150 m.sheet.DefineName("Sales", rectOf("B1:B3"))151151 press(t, m, "=SUM(sa")152- list, start := m.shownSuggestions()152+ list, start := m.entry.assist.shown(m)153153 if list[0].name != "Sales" || list[0].fn {154154 t.Fatalf("names first: %v", list)155155 }156156 // Clicking a suggestion inserts it.157- b, _ := m.assistBox()157+ b, _ := m.entry.assist.box(m)158158 send(m, tea.MouseClickMsg{X: b.x + 3, Y: b.y + 1, Button: tea.MouseLeft})159159 if m.line.text() != "=SUM(Sales" || m.line.pos != start+5 {160160 t.Fatalf("click: %q", m.line.text())
M internal/ui/entry.go +2 -2
M internal/ui/overlay.go +2 -2
M internal/ui/panel.go +4 -4
@@ -176,11 +176,11 @@ func (m *Model) contextLineText() string {
176176 case m.mode == modePoint:177177 prefix := []rune(m.entry.prefix)178178 var ok bool179- if left, right, ok = m.signatureLine(prefix, len(prefix), m.th.KeyHints("Shift+arrows", "range", "Esc", "back")); !ok {179+ if left, right, ok = signatureLine(&m.th, m.width, prefix, len(prefix), m.th.KeyHints("Shift+arrows", "range", "Esc", "back")); !ok {180180 left = m.th.KeyHints("Arrows", "pick a cell", "Shift+arrows", "pick a range", "Enter", "accept", "Esc", "back")181181 }182- case (m.mode == modeEnter || m.mode == modeEdit) && m.line.isFormula() && m.inFunction():183- left, right, _ = m.signatureLine(m.line.buf, m.line.pos, m.th.KeyHints("Enter", "accept", "Esc", "cancel"))182+ case (m.mode == modeEnter || m.mode == modeEdit) && m.line.isFormula() && m.line.inFunction():183+ left, right, _ = signatureLine(&m.th, m.width, m.line.buf, m.line.pos, m.th.KeyHints("Enter", "accept", "Esc", "cancel"))184184 case m.mode == modeEnter && m.line.isFormula():185185 left = m.th.KeyHints("Enter", "accept", "Tab", "accept and go right", "Arrows", "pick cells after an operator", "Esc", "cancel")186186 case m.mode == modeEnter:
@@ -244,7 +244,7 @@ func (m *Model) statusLayout() (string, []tabSpan) {
244244 // floatingStatus is what the highlighted item of the open overlay or the245245 // formula suggestions does, and the keys that apply.246246 func (m *Model) floatingStatus() (string, bool) {247- desc, keys, floating := m.assistStatus()247+ desc, keys, floating := m.entry.assist.status(m)248248 if m.overlay != nil {249249 desc, keys = m.overlay.status(m)250250 floating = true