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>
| M | internal/ | 91 | +++++++++++++++--------------- |
| M | internal/ | 16 | +++--- |
| M | internal/ | 4 | +- |
| M | internal/ | 4 | +- |
| M | internal/ | 8 | +- |
@@ -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 }@@ -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())@@ -57,7 +57,7 @@ func (m *Model) startEdit() tea.Cmd {
5757 // in a formula where a reference may follow the caret: then they point.5858 func (m *Model) enterKey(k tea.KeyPressMsg) tea.Cmd {5959 key := k.String()60- if m.assistKey(key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {60+ if m.entry.assist.key(m, key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {6161 return nil6262 }6363 if key == "f2" {@@ -88,7 +88,7 @@ func (m *Model) enterKey(k tea.KeyPressMsg) tea.Cmd {
8888 // editKey handles EDIT mode, where left and right move the caret.8989 func (m *Model) editKey(k tea.KeyPressMsg) tea.Cmd {9090 key := k.String()91- if m.assistKey(key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {91+ if m.entry.assist.key(m, key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {9292 return nil9393 }9494 if m.line.isFormula() && m.line.canPoint() && strings.HasPrefix(key, "shift+") && m.startPoint(key) {@@ -103,7 +103,7 @@ func (m *Model) floating() []box {
103103 if m.overlay != nil {104104 return m.overlay.layout(m)105105 }106- if b, ok := m.assistBox(); ok {106+ if b, ok := m.entry.assist.box(m); ok {107107 return []box{b}108108 }109109 return nil@@ -129,7 +129,7 @@ func (m *Model) shellMouse(msg tea.MouseMsg) (tea.Cmd, bool) {
129129 if m.overlay != nil {130130 return m.overlayMouse(msg), true131131 }132- if m.assistMouse(msg) {132+ if m.entry.assist.mouse(m, msg) {133133 return nil, true134134 }135135 click, ok := msg.(tea.MouseClickMsg)@@ -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