The one-line editor every text field shares depends on nothing in the UI, so components outside package ui can edit it.
- commit
- c0462c2be7ed95450a11a8e8354c19169fc6a393
- parent
- fe630b6b7696d9e4122b8b215f6ab4e5b0f2734a
- author
- Dami <dami.etoile@gmail.com>
@@ -10,6 +10,7 @@ import (
1010 1111 "github.com/FelineStateMachine/012/internal/sheet"1212 "github.com/FelineStateMachine/012/internal/ui/formula"13+ "github.com/FelineStateMachine/012/internal/ui/lineedit"1314 "github.com/FelineStateMachine/012/internal/ui/overlay"1415 "github.com/FelineStateMachine/012/internal/ui/theme"1516 )@@ -84,10 +85,10 @@ func suggestions(sh *sheet.Sheet, word string) []suggestion {
8485 // EDIT, in plain text, after the caret moved, or when the word already8586 // names a range exactly and nothing else matches.8687 func (a *assist) shown(m *Model) ([]suggestion, int) {87- if (m.mode != modeEnter && m.mode != modeEdit) || !m.line.isFormula() || !a.active || m.overlay != nil {88+ if (m.mode != modeEnter && m.mode != modeEdit) || !m.line.IsFormula() || !a.active || m.overlay != nil {8889 return nil, 08990 }90- c := formula.ScanCaret(m.line.buf, m.line.pos)91+ c := formula.ScanCaret(m.line.Buf, m.line.Pos)9192 if c.Word == "" {9293 return nil, 09394 }@@ -102,9 +103,9 @@ func (a *assist) shown(m *Model) ([]suggestion, int) {
102103 // typeKey applies a line-editing key to the entry. Typing or deleting103104 // shows suggestions; moving the caret hides them, as in Sheets.104105 func (m *Model) typeKey(k tea.KeyPressMsg) {105- before := m.line.text()106- m.line.key(k)107- m.entry.assist = assist{active: m.line.text() != before}106+ before := m.line.Text()107+ m.line.Key(k)108+ m.entry.assist = assist{active: m.line.Text() != before}108109 }109110 110111 // key handles the keys the suggestion list takes while it shows.@@ -132,7 +133,7 @@ func (a *assist) key(m *Model, key string) bool {
132133 // a function unless one is already there.133134 func (a *assist) accept(m *Model, s suggestion, start int) {134135 text := []rune(s.name)135- rest := m.line.buf[m.line.pos:]136+ rest := m.line.Buf[m.line.Pos:]136137 if s.fn && (len(rest) == 0 || rest[0] != '(') {137138 text = append(text, '(')138139 }@@ -143,8 +144,8 @@ func (a *assist) accept(m *Model, s suggestion, start int) {
143144 if s.fn && len(rest) > 0 && rest[0] == '(' {144145 pos++ // into the existing parentheses145146 }146- m.line.buf = slices.Concat(m.line.buf[:start], text, rest)147- m.line.pos = pos147+ m.line.Buf = slices.Concat(m.line.Buf[:start], text, rest)148+ m.line.Pos = pos148149 a.active = false149150 }150151 @@ -186,7 +187,7 @@ func (a *assist) box(m *Model) (overlay.Box, bool) {
186187 footer = strconv.Itoa(a.sel+1) + " of " + strconv.Itoa(len(list))187188 }188189 // The box's text starts one column in, under the word's first letter.189- x := formulaBarTextX() + ansi.StringWidth(string(m.line.buf[:start])) - 2190+ x := formulaBarTextX() + ansi.StringWidth(string(m.line.Buf[:start])) - 2190191 x = clamp(x, 0, max(m.width-inner-2, 0))191192 return overlay.Box{ID: assistID, X: x, Y: contextLine + 1, Lines: m.th.Frame(inner, "", footer, lines)}, true192193 }@@ -236,8 +237,8 @@ func (a *assist) status(m *Model) (desc, keys string, ok bool) {
236237 237238 // inFunction reports whether the caret is inside a known function's238239 // parentheses.239-func (l *lineEdit) inFunction() bool {240- _, ok := sheet.LookupFunc(formula.ScanCaret(l.buf, l.pos).Fn)240+func inFunction(l *lineedit.Line) bool {241+ _, ok := sheet.LookupFunc(formula.ScanCaret(l.Buf, l.Pos).Fn)241242 return ok242243 }243244 @@ -31,14 +31,14 @@ func TestAutocompleteFunctions(t *testing.T) {
3131 press(t, m, "<down>")3232 }3333 press(t, m, "<tab>")34- if m.mode != modeEnter || m.line.text() != "=SUM(" {35- t.Fatalf("mode %v buf %q", m.mode, m.line.text())34+ if m.mode != modeEnter || m.line.Text() != "=SUM(" {35+ t.Fatalf("mode %v buf %q", m.mode, m.line.Text())3636 }3737 if ctx := line(m, contextLine); !strings.HasPrefix(ctx, "SUM(value1, [value2, ...])") || !strings.Contains(ctx, "Sum of numbers") {3838 t.Errorf("signature %q", ctx)3939 }4040 press(t, m, "1,")41- sig, _ := signature(&m.th, m.line.buf, m.line.pos)41+ sig, _ := signature(&m.th, m.line.Buf, m.line.Pos)4242 if !strings.Contains(sig, m.th.Argument.Render("[value2, ...]")) {4343 t.Errorf("second argument not marked: %q", sig)4444 }@@ -52,8 +52,8 @@ func TestAutocompleteKeys(t *testing.T) {
5252 m := newModel()5353 // Enter inserts while the list shows, then commits when it doesn't.5454 press(t, m, "=ab", "<enter>")55- if m.line.text() != "=ABS(" {56- t.Fatalf("enter: %q", m.line.text())55+ if m.line.Text() != "=ABS(" {56+ t.Fatalf("enter: %q", m.line.Text())5757 }5858 press(t, m, "-4)", "<enter>")5959 if m.sheet.Value(addr("A1")).Num != 4 {@@ -102,15 +102,15 @@ func TestAutocompleteNamesAndMouse(t *testing.T) {
102102 // Clicking a suggestion inserts it.103103 b, _ := m.entry.assist.box(m)104104 send(m, tea.MouseClickMsg{X: b.X + 3, Y: b.Y + 1, Button: tea.MouseLeft})105- if m.line.text() != "=SUM(Sales" || m.line.pos != start+5 {106- t.Fatalf("click: %q", m.line.text())105+ if m.line.Text() != "=SUM(Sales" || m.line.Pos != start+5 {106+ t.Fatalf("click: %q", m.line.Text())107107 }108108 // Editing inside existing parentheses doesn't double them.109109 m = newModel()110110 press(t, m, "=AB(1)", "<f2>", "<left>", "<left>", "<left>", "<backspace>", "B")111111 press(t, m, "<tab>")112- if m.line.text() != "=ABS(1)" || m.line.pos != 5 {113- t.Errorf("into parens: %q at %d", m.line.text(), m.line.pos)112+ if m.line.Text() != "=ABS(1)" || m.line.Pos != 5 {113+ t.Errorf("into parens: %q at %d", m.line.Text(), m.line.Pos)114114 }115115 }116116 @@ -5,6 +5,7 @@ import (
55 "strings"66 77 "github.com/FelineStateMachine/012/internal/sheet"8+ "github.com/FelineStateMachine/012/internal/ui/lineedit"89 )910 1011 // Sheet names in formula suggestions: typing =Su offers Summary! with the@@ -61,8 +62,8 @@ func trimSheetEnd(rest []rune) []rune {
6162 6263 // sheetBeforeCaret returns the sheet named just before the caret, as in6364 // "=SUM(Summary!" or "='Q3 plan'!", and where its name starts.64-func (l *lineEdit) sheetBeforeCaret(book *sheet.Workbook) (*sheet.Sheet, int, bool) {65- head := l.buf[:l.pos]65+func sheetBeforeCaret(l *lineedit.Line, book *sheet.Workbook) (*sheet.Sheet, int, bool) {66+ head := l.Buf[:l.Pos]6667 end := len(head) - 1 // the "!"6768 if end < 2 || head[end] != '!' {6869 return nil, 0, false@@ -106,15 +107,15 @@ func (m *Model) pointAfterSheet(key string) bool {
106107 if _, extend := extendKey(key); !extend && (!isMoveKey(key) || key == "tab" || key == "shift+tab") {107108 return false108109 }109- s, start, ok := m.line.sheetBeforeCaret(m.book())110+ s, start, ok := sheetBeforeCaret(&m.line, m.book())110111 if !ok || s.Hidden() {111112 return false112113 }113114 if s == m.sheet && !m.away() {114115 return m.startPoint(key) // its own sheet: the name stays as typed115116 }116- m.line.buf = append(m.line.buf[:start:start], m.line.buf[m.line.pos:]...)117- m.line.pos = start117+ m.line.Buf = append(m.line.Buf[:start:start], m.line.Buf[m.line.Pos:]...)118+ m.line.Pos = start118119 if s == m.sheet {119120 return m.startPoint(key)120121 }@@ -42,8 +42,8 @@ func TestAutocompleteSheets(t *testing.T) {
4242 t.Errorf("status %q", st)4343 }4444 press(t, m, "<tab>")45- if m.line.text() != "=Summary!" || m.mode != modeEnter {46- t.Fatalf("accepted: %q, mode %v", m.line.text(), m.mode)45+ if m.line.Text() != "=Summary!" || m.mode != modeEnter {46+ t.Fatalf("accepted: %q, mode %v", m.line.Text(), m.mode)4747 }4848 // Typing the address works as ever.4949 press(t, m, "B3*2", "<enter>")@@ -61,8 +61,8 @@ func TestAutocompleteSheets(t *testing.T) {
6161 }6262 }6363 press(t, m, "<tab>")64- if m.line.text() != "='Q3 plan'!" {65- t.Fatalf("quoted: %q", m.line.text())64+ if m.line.Text() != "='Q3 plan'!" {65+ t.Fatalf("quoted: %q", m.line.Text())6666 }6767 // An arrow then points into the sheet, as clicking its tab would.6868 press(t, m, "<down>")@@ -98,12 +98,12 @@ func TestAutocompleteSheetsEdges(t *testing.T) {
9898 press(t, m, "<esc>")9999 100100 // Accepting inside an existing reference replaces the name only.101- m.line.set("='Q3 p'!A1")102- m.line.pos = len("='Q3 p")101+ m.line.Set("='Q3 p'!A1")102+ m.line.Pos = len("='Q3 p")103103 m.entry.assist.active = true104104 m.mode = modeEnter105105 press(t, m, "<tab>")106- if m.line.text() != "='Q3 plan'!A1" || m.line.pos != len("='Q3 plan'!") {107- t.Errorf("replaced: %q at %d", m.line.text(), m.line.pos)106+ if m.line.Text() != "='Q3 plan'!A1" || m.line.Pos != len("='Q3 plan'!") {107+ t.Errorf("replaced: %q at %d", m.line.Text(), m.line.Pos)108108 }109109 }@@ -89,7 +89,7 @@ func TestChartEditorPrompts(t *testing.T) {
8989 if m.mode != modePrompt {9090 t.Fatalf("title prompt not open, mode %v", m.mode)9191 }92- m.line.buf, m.line.pos = []rune("Spend"), 592+ m.line.Buf, m.line.Pos = []rune("Spend"), 59393 press(t, m, "<enter>")9494 if c := m.sheet.Charts()[0]; c.Title != "Spend" || m.indicator() != "CHART" {9595 t.Errorf("title %q, indicator %q", c.Title, m.indicator())@@ -55,7 +55,7 @@ func init() {
5555 desc: "Type a command: a cell to go to (B12), w, q, wq, e file, or any command by name",5656 run: func(m *Model) tea.Cmd {5757 c := &cmdLine{m: m}58- m.line.clear()58+ m.line.Clear()5959 m.openOverlay(c)6060 c.Changed()6161 return nil@@ -71,7 +71,7 @@ func (c *cmdLine) Changed() {
7171 m := c.m7272 c.Sel, c.Top, c.tabbed = 0, 0, false7373 c.shown = c.shown[:0]74- q := strings.ToLower(strings.TrimPrefix(strings.TrimLeft(m.line.text(), " "), ":"))74+ q := strings.ToLower(strings.TrimPrefix(strings.TrimLeft(m.line.Text(), " "), ":"))7575 if word, _, arg := strings.Cut(q, " "); arg && isFileWord(word) {7676 return7777 }@@ -117,7 +117,7 @@ func commandIDs() []string {
117117 func (c *cmdLine) Key(k tea.KeyPressMsg) tea.Cmd {118118 m := c.m119119 switch key := k.String(); {120- case key == "esc", key == "backspace" && len(m.line.buf) == 0:120+ case key == "esc", key == "backspace" && len(m.line.Buf) == 0:121121 m.closeOverlay()122122 case key == "enter":123123 return c.run(m)@@ -130,9 +130,9 @@ func (c *cmdLine) Key(k tea.KeyPressMsg) tea.Cmd {
130130 case key == "down", key == "ctrl+n":131131 c.Move(1, len(c.shown))132132 default:133- before := m.line.text()134- m.line.key(k)135- if m.line.text() != before {133+ before := m.line.Text()134+ m.line.Key(k)135+ if m.line.Text() != before {136136 c.Changed()137137 }138138 }@@ -148,7 +148,7 @@ func (c *cmdLine) complete(m *Model, d int) {
148148 if c.tabbed {149149 c.Move(d, len(c.shown))150150 }151- m.line.set(c.shown[c.Sel].word)151+ m.line.Set(c.shown[c.Sel].word)152152 c.tabbed = true153153 }154154 @@ -156,7 +156,7 @@ func (c *cmdLine) complete(m *Model, d int) {
156156 // cell or range runs the highlighted completion, so ":fill d" Enter157157 // fills down.158158 func (c *cmdLine) run(m *Model) tea.Cmd {159- text := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(m.line.text()), ":"))159+ text := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(m.line.Text()), ":"))160160 fallback := ""161161 if c.Sel < len(c.shown) {162162 fallback = c.shown[c.Sel].word@@ -270,12 +270,12 @@ func (m *Model) editFile(name string, force bool) tea.Cmd {
270270 271271 func (c *cmdLine) ContextLine() (string, string) {272272 m := c.m273- return m.th.Title.Render(":") + m.line.text(), ""273+ return m.th.Title.Render(":") + m.line.Text(), ""274274 }275275 276276 func (c *cmdLine) Cursor() (int, int) {277277 m := c.m278- return 1 + ansi.StringWidth(m.line.head()), contextLine278+ return 1 + ansi.StringWidth(m.line.Head()), contextLine279279 }280280 281281 func (c *cmdLine) Status() (string, string) {@@ -363,7 +363,7 @@ func (c *cmdLine) Mouse(e overlay.MouseEvent) tea.Cmd {
363363 case e.Kind == overlay.MouseMotion:364364 c.Sel = i365365 case e.Kind == overlay.MousePress && e.Button == tea.MouseLeft:366- m.line.set(c.shown[i].word)366+ m.line.Set(c.shown[i].word)367367 c.Sel = i368368 return c.run(m)369369 }@@ -151,7 +151,7 @@ func (m *Model) structural(err error) tea.Cmd {
151151 // fillEntry stores the entry being typed in every selected cell, adjusting152152 // references as if it were copied from the active cell (Ctrl+Enter).153153 func (m *Model) fillEntry() bool {154- input := m.line.text()154+ input := m.line.Text()155155 if err := m.entrySheet().FillEntry(m.selection(), m.cur, input); err != nil {156156 m.entryError(err, input)157157 return false@@ -168,15 +168,15 @@ func (m *Model) entryError(err error, input string) {
168168 m.mode = modeEdit169169 m.entry.hint = err.Error()170170 if errors.As(err, &pe) {171- m.line.pos = utf8.RuneCountInString(input[:min(pe.Pos, len(input))])171+ m.line.Pos = utf8.RuneCountInString(input[:min(pe.Pos, len(input))])172172 }173173 }174174 175175 // toggleAbsolute cycles the reference at the caret through A1, $A$1, A$1176176 // and $A1, as F4 does in Sheets. A range cycles both corners.177177 func (m *Model) toggleAbsolute() {178- if buf, pos, ok := formula.CycleRef(m.line.buf, m.line.pos); ok {179- m.line.buf, m.line.pos = buf, pos178+ if buf, pos, ok := formula.CycleRef(m.line.Buf, m.line.Pos); ok {179+ m.line.Buf, m.line.Pos = buf, pos180180 }181181 }182182 @@ -194,8 +194,8 @@ func TestPasteTSVFromTerminal(t *testing.T) {
194194 press(t, m, "<esc>")195195 // A single line still starts an entry.196196 send(m, tea.PasteMsg{Content: "hello\n"})197- if m.mode != modeEnter || m.line.text() != "hello" {198- t.Errorf("mode %v buf %q", m.mode, m.line.text())197+ if m.mode != modeEnter || m.line.Text() != "hello" {198+ t.Errorf("mode %v buf %q", m.mode, m.line.Text())199199 }200200 }201201 @@ -277,14 +277,14 @@ func TestF4CyclesReferences(t *testing.T) {
277277 press(t, m, "=A1+B2")278278 for _, want := range []string{"=A1+$B$2", "=A1+B$2", "=A1+$B2", "=A1+B2"} {279279 press(t, m, "<f4>")280- if m.line.text() != want {281- t.Errorf("F4 gave %q, want %q", m.line.text(), want)280+ if m.line.Text() != want {281+ t.Errorf("F4 gave %q, want %q", m.line.Text(), want)282282 }283283 }284284 // F4 while pointing puts the reference in first.285285 press(t, m, "*", "<up>", "<f4>")286- if m.line.text() != "=A1+B2*$A$1" || m.mode != modeEnter {287- t.Errorf("F4 in POINT: %q mode %v", m.line.text(), m.mode)286+ if m.line.Text() != "=A1+B2*$A$1" || m.mode != modeEnter {287+ t.Errorf("F4 in POINT: %q mode %v", m.line.Text(), m.mode)288288 }289289 }290290 @@ -44,16 +44,16 @@ func (m *Model) startEntry(md mode, text string) {
4444 }4545 m.mode = md4646 m.entry.home = nil47- m.line.clear()47+ m.line.Clear()4848 m.entry.hint, m.entry.assist = "", assist{}49- m.line.insert(text)49+ m.line.Insert(text)5050 }5151 5252 // startEdit edits the active cell's contents with the caret at the end.5353 func (m *Model) startEdit() tea.Cmd {5454 m.startEntry(modeEdit, "")5555 if c := m.sheet.Cell(m.cur); c != nil && m.mode == modeEdit {56- m.line.set(c.Input)56+ m.line.Set(c.Input)5757 }5858 return nil5959 }@@ -73,10 +73,10 @@ func (m *Model) enterKey(k tea.KeyPressMsg) tea.Cmd {
7373 m.toggleAbsolute()7474 return nil7575 }76- if m.line.isFormula() && (m.pointAfterSheet(key) || m.line.canPoint() && m.startPoint(key)) {76+ if m.line.IsFormula() && (m.pointAfterSheet(key) || m.line.CanPoint() && m.startPoint(key)) {7777 return nil7878 }79- if m.line.isFormula() && (key == "left" || key == "right") {79+ if m.line.IsFormula() && (key == "left" || key == "right") {8080 m.typeKey(k) // move the caret within a formula8181 return nil8282 }@@ -96,7 +96,7 @@ func (m *Model) editKey(k tea.KeyPressMsg) tea.Cmd {
9696 if m.entry.assist.key(m, key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {9797 return nil9898 }99- if m.line.isFormula() && strings.HasPrefix(key, "shift+") && (m.pointAfterSheet(key) || m.line.canPoint() && m.startPoint(key)) {99+ if m.line.IsFormula() && strings.HasPrefix(key, "shift+") && (m.pointAfterSheet(key) || m.line.CanPoint() && m.startPoint(key)) {100100 return nil101101 }102102 switch key {@@ -171,8 +171,8 @@ func (m *Model) startPoint(key string) bool {
171171 } else {172172 return false173173 }174- m.entry.prefix = m.line.head()175- m.entry.suffix = m.line.tail()174+ m.entry.prefix = m.line.Head()175+ m.entry.suffix = m.line.Tail()176176 m.mode = modePoint177177 m.navigate(key, &m.point.at)178178 return true@@ -206,7 +206,7 @@ func (m *Model) pointKey(k tea.KeyPressMsg) tea.Cmd {
206206 }207207 if text := typed(k); text != "" {208208 m.resumeEntry(m.pointRef())209- m.line.insert(text)209+ m.line.Insert(text)210210 m.entry.assist = assist{active: true}211211 }212212 return nil@@ -215,14 +215,14 @@ func (m *Model) pointKey(k tea.KeyPressMsg) tea.Cmd {
215215 // resumeEntry leaves POINT mode, inserting ref at the caret.216216 func (m *Model) resumeEntry(ref string) {217217 m.mode = modeEnter218- m.line.buf = []rune(m.entry.prefix + ref + m.entry.suffix)219- m.line.pos = utf8.RuneCountInString(m.entry.prefix + ref)218+ m.line.Buf = []rune(m.entry.prefix + ref + m.entry.suffix)219+ m.line.Pos = utf8.RuneCountInString(m.entry.prefix + ref)220220 }221221 222222 // commit stores the edit line in the active cell. An invalid formula stays223223 // in EDIT mode with the caret at the problem and the reason on line 3.224224 func (m *Model) commit() bool {225- input := m.line.text()225+ input := m.line.Text()226226 if err := m.set(m.cur, input); err != nil {227227 m.entryError(err, input)228228 return false@@ -246,7 +246,7 @@ func (m *Model) set(a sheet.Addr, input string) error {
246246 func (m *Model) cancelEntry() {247247 m.returnHome()248248 m.mode = modeReady249- m.line.clear()249+ m.line.Clear()250250 m.entry.hint, m.entry.assist = "", assist{}251251 }252252 @@ -262,14 +262,14 @@ func (m *Model) handlePaste(content string) {
262262 m.startEntry(modeEnter, content)263263 }264264 case modeEnter, modeEdit:265- m.line.insert(content)265+ m.line.Insert(content)266266 case modePrompt:267267 if m.prompt.kind != promptRange {268268 m.prompt.paste(m, content)269269 }270270 case modeMenu:271271 if o, ok := m.overlay.(overlay.Text); ok {272- m.line.insert(content)272+ m.line.Insert(content)273273 o.Changed()274274 }275275 }@@ -145,7 +145,7 @@ func (m *Model) openValuesPicker(title string, x int, values []sheet.FilterValue
145145 }146146 p.fields[1] = p.cond.Arg147147 m.openOverlay(p)148- m.line.clear()148+ m.line.Clear()149149 p.search()150150 return p151151 }@@ -162,14 +162,14 @@ func (p *filterPicker) Indicator() string { return "FILTER" }
162162 163163 // focus moves editing to field i, keeping the other field's text.164164 func (p *filterPicker) focus(m *Model, i int) {165- p.fields[p.field] = m.line.text()165+ p.fields[p.field] = m.line.Text()166166 p.field = i167- m.line.set(p.fields[i])167+ m.line.Set(p.fields[i])168168 }169169 170170 func (p *filterPicker) Changed() {171171 m := p.m172- p.fields[p.field] = m.line.text()172+ p.fields[p.field] = m.line.Text()173173 if p.field == 0 {174174 p.search()175175 }@@ -234,7 +234,7 @@ func (p *filterPicker) cycle(d int) {
234234 235235 // apply sets the column's criteria and closes the picker.236236 func (p *filterPicker) apply(m *Model) {237- p.fields[p.field] = m.line.text()237+ p.fields[p.field] = m.line.Text()238238 var cr sheet.Criteria239239 for _, v := range p.values {240240 if !p.checked[v.Text] {@@ -298,9 +298,9 @@ func (p *filterPicker) Key(k tea.KeyPressMsg) tea.Cmd {
298298 case p.field == 0 && key == "space":299299 p.toggle(p.Sel)300300 default:301- before := m.line.text()302- m.line.key(k)303- if m.line.text() != before {301+ before := m.line.Text()302+ m.line.Key(k)303+ if m.line.Text() != before {304304 p.Changed()305305 }306306 }@@ -388,7 +388,7 @@ func (p *filterPicker) box(m *Model) (x, y, inner int) {
388388 func (p *filterPicker) Cursor() (int, int) {389389 m := p.m390390 x, y, _ := p.box(m)391- caret := ansi.StringWidth(m.line.head())391+ caret := ansi.StringWidth(m.line.Head())392392 if p.field == 1 {393393 return x + 1 + len(" If ") + ansi.StringWidth(p.condChip()) + 2 + caret, y + 1394394 }@@ -414,7 +414,7 @@ func (p *filterPicker) Layout() []overlay.Box {
414414 if p.cond.Op.TakesArg() {415415 arg := p.fields[1]416416 if p.field == 1 {417- arg = m.line.text()417+ arg = m.line.Text()418418 }419419 if arg == "" && p.field != 1 {420420 arg = m.th.Muted.Render("value")@@ -423,7 +423,7 @@ func (p *filterPicker) Layout() []overlay.Box {
423423 }424424 search := p.fields[0]425425 if p.field == 0 {426- search = m.line.text()426+ search = m.line.Text()427427 }428428 input := m.th.Title.Render(searchPrompt) + search429429 if search == "" {@@ -128,7 +128,7 @@ func (m *Model) openFind(replace bool) {
128128 m.clearSelection()129129 m.openOverlay(f)130130 // Load the kept query so focus doesn't overwrite it.131- m.line.buf = []rune(f.fields[f.field])131+ m.line.Buf = []rune(f.fields[f.field])132132 }133133 f.replace = f.replace || replace134134 if replace && f.fields[0] != "" {@@ -145,15 +145,15 @@ func (f *findBar) Layout() []overlay.Box { return nil }
145145 146146 // focus moves editing to field i, keeping the other field's text.147147 func (f *findBar) focus(m *Model, i int) {148- f.fields[f.field] = m.line.text()148+ f.fields[f.field] = m.line.Text()149149 f.field = i150- m.line.set(f.fields[i])150+ m.line.Set(f.fields[i])151151 }152152 153153 // Changed re-runs the search as the query is typed.154154 func (f *findBar) Changed() {155155 m := f.m156- f.fields[f.field] = m.line.text()156+ f.fields[f.field] = m.line.Text()157157 if f.field == 0 {158158 f.search(m)159159 }@@ -303,9 +303,9 @@ func (f *findBar) Key(k tea.KeyPressMsg) tea.Cmd {
303303 f.search(m)304304 }305305 default:306- before := m.line.text()307- m.line.key(k)308- if m.line.text() != before {306+ before := m.line.Text()307+ m.line.Key(k)308+ if m.line.Text() != before {309309 f.Changed()310310 }311311 }@@ -70,8 +70,8 @@ func TestFindAsYouType(t *testing.T) {
7070 }7171 // Ctrl+F again reopens the last search.7272 press(t, m, "<ctrl+f>")73- if f := findBarOf(t, m); m.line.text() != "ren" || len(f.matches) != 3 {74- t.Errorf("reopened with %q", m.line.text())73+ if f := findBarOf(t, m); m.line.Text() != "ren" || len(f.matches) != 3 {74+ t.Errorf("reopened with %q", m.line.Text())7575 }7676 }7777 @@ -57,7 +57,7 @@ func (f *findBar) parts(m *Model) []findPart {
5757 // fieldText is a field's text: the live edit buffer when focused.5858 func (f *findBar) fieldText(m *Model, i int) string {5959 if f.field == i {60- return m.line.text()60+ return m.line.Text()6161 }6262 return f.fields[i]6363 }@@ -118,7 +118,7 @@ func (f *findBar) Cursor() (x, y int) {
118118 parts, xs := f.spans(m)119119 for i, p := range parts {120120 if p.field == f.field {121- return xs[i] + ansi.StringWidth(p.text) - ansi.StringWidth(m.line.tail()), contextLine121+ return xs[i] + ansi.StringWidth(p.text) - ansi.StringWidth(m.line.Tail()), contextLine122122 }123123 }124124 return 0, contextLine@@ -94,8 +94,8 @@ func TestFunctionListInsertsFunction(t *testing.T) {
9494 t.Errorf("status %q", line(m, m.height-1))9595 }9696 press(t, m, "<enter>")97- if m.mode != modeEnter || m.line.text() != "=IFERROR(" {98- t.Errorf("mode %v buf %q", m.mode, m.line.text())97+ if m.mode != modeEnter || m.line.Text() != "=IFERROR(" {98+ t.Errorf("mode %v buf %q", m.mode, m.line.Text())9999 }100100 }101101 @@ -13,7 +13,7 @@ func hiddenBook(t *testing.T) *Model {
1313 press(t, m, "=", "D", "a", "t", "a", "!", "A", "1", "*", "2", "<enter>")1414 press(t, m, "<shift+f11>")1515 run(m, m.runCommand("sheet.rename"))16- m.line.set("Data")16+ m.line.Set("Data")1717 press(t, m, "<enter>", "2", "1", "<enter>", "<shift+f11>", "<ctrl+pgup>")1818 if sheetNames(m) != "Sheet1,Data,Sheet3" || m.sheet.Name() != "Data" {1919 t.Fatalf("sheets %s, on %s", sheetNames(m), m.sheet.Name())@@ -59,7 +59,7 @@ func TestHideSheetCommand(t *testing.T) {
5959 6060 // Going to a cell on a hidden sheet says it's hidden.6161 run(m, m.runCommand("goto"))62- m.line.set("Data!A1")62+ m.line.Set("Data!A1")6363 press(t, m, "<enter>")6464 if m.sheet.Name() != "Sheet1" || m.mode != modeError || m.errMsg != "Data is hidden; View > Hidden sheets shows it again" {6565 t.Errorf("goto: on %s, %q", m.sheet.Name(), m.errMsg)@@ -114,9 +114,9 @@ func TestRecordHideAndUnhide(t *testing.T) {
114114 run(m, m.runCommand("sheet.unhide"))115115 press(t, m, "<enter>")116116 run(m, m.runCommand("macro.stop"))117- m.line.set("Toggle")117+ m.line.Set("Toggle")118118 press(t, m, "<enter>")119- m.line.set("")119+ m.line.Set("")120120 press(t, m, "<enter>")121121 mc, ok := m.book().Macro("Toggle")122122 if !ok {@@ -1,105 +0,0 @@
1-package ui2-3-import (4- "slices"5- "strings"6- "unicode"7-8- tea "charm.land/bubbletea/v2"9- "github.com/charmbracelet/x/ansi"10-)11-12-// lineEdit is the one-line text editor behind every text field: the13-// entry typed into a cell (ENTER, EDIT), prompts on the context line, and14-// the search fields of the find bar and the pickers. Only one of them is15-// edited at a time, so they share it; a component with several fields16-// keeps the others' text itself and loads a field here to edit it.17-type lineEdit struct {18- buf []rune19- pos int // the caret, an index into buf20-}21-22-// set replaces the text, with the caret at its end.23-func (l *lineEdit) set(text string) {24- l.buf = []rune(text)25- l.pos = len(l.buf)26-}27-28-// clear empties the line.29-func (l *lineEdit) clear() {30- l.buf, l.pos = nil, 031-}32-33-// text is the whole line.34-func (l *lineEdit) text() string { return string(l.buf) }35-36-// head and tail are the text before and after the caret.37-func (l *lineEdit) head() string { return string(l.buf[:l.pos]) }38-func (l *lineEdit) tail() string { return string(l.buf[l.pos:]) }39-40-// insert types text at the caret. Line breaks and tabs become spaces, and41-// other control characters are dropped.42-func (l *lineEdit) insert(text string) {43- for _, r := range text {44- if r == '\n' || r == '\r' || r == '\t' {45- r = ' '46- }47- if !unicode.IsPrint(r) {48- continue49- }50- l.buf = slices.Insert(l.buf, l.pos, r)51- l.pos++52- }53-}54-55-// key applies a line-editing key.56-func (l *lineEdit) key(k tea.KeyPressMsg) {57- switch k.String() {58- case "left":59- l.pos = max(l.pos-1, 0)60- case "right":61- l.pos = min(l.pos+1, len(l.buf))62- case "home", "ctrl+a":63- l.pos = 064- case "end", "ctrl+e":65- l.pos = len(l.buf)66- case "backspace":67- if l.pos > 0 {68- l.buf = slices.Delete(l.buf, l.pos-1, l.pos)69- l.pos--70- }71- case "delete":72- if l.pos < len(l.buf) {73- l.buf = slices.Delete(l.buf, l.pos, l.pos+1)74- }75- default:76- l.insert(typed(k))77- }78-}79-80-// setCaret puts the edit caret at display column x of the entry.81-func (l *lineEdit) setCaret(x int) {82- w := 083- for i, r := range l.buf {84- if w >= x {85- l.pos = i86- return87- }88- w += ansi.StringWidth(string(r))89- }90- l.pos = len(l.buf)91-}92-93-// isFormula reports whether the edit line holds a formula.94-func (l *lineEdit) isFormula() bool {95- return len(l.buf) > 0 && (l.buf[0] == '=' || l.buf[0] == '+' || l.buf[0] == '-')96-}97-98-// canPoint reports whether the text before the caret ends where a cell99-// reference may follow.100-func (l *lineEdit) canPoint() bool {101- if l.pos == 0 {102- return false103- }104- return strings.ContainsRune("=+-*/^(,;<>&:", l.buf[l.pos-1])105-}@@ -0,0 +1,116 @@
1+// Package lineedit is the one-line text editor behind every text field of2+// the UI. It depends on nothing in package ui, so components in their own3+// packages can edit the shared line.4+package lineedit5+6+import (7+ "slices"8+ "strings"9+ "unicode"10+11+ tea "charm.land/bubbletea/v2"12+ "github.com/charmbracelet/x/ansi"13+)14+15+// Line is the one-line text editor behind every text field: the16+// entry typed into a cell (ENTER, EDIT), prompts on the context line, and17+// the search fields of the find bar and the pickers. Only one of them is18+// edited at a time, so they share it; a component with several fields19+// keeps the others' text itself and loads a field here to edit it.20+type Line struct {21+ Buf []rune22+ Pos int // the caret, an index into Buf23+}24+25+// Set replaces the text, with the caret at its end.26+func (l *Line) Set(text string) {27+ l.Buf = []rune(text)28+ l.Pos = len(l.Buf)29+}30+31+// Clear empties the line.32+func (l *Line) Clear() {33+ l.Buf, l.Pos = nil, 034+}35+36+// Text is the whole line.37+func (l *Line) Text() string { return string(l.Buf) }38+39+// Head and tail are the text before and after the caret.40+func (l *Line) Head() string { return string(l.Buf[:l.Pos]) }41+func (l *Line) Tail() string { return string(l.Buf[l.Pos:]) }42+43+// Insert types text at the caret. Line breaks and tabs become spaces, and44+// other control characters are dropped.45+func (l *Line) Insert(text string) {46+ for _, r := range text {47+ if r == '\n' || r == '\r' || r == '\t' {48+ r = ' '49+ }50+ if !unicode.IsPrint(r) {51+ continue52+ }53+ l.Buf = slices.Insert(l.Buf, l.Pos, r)54+ l.Pos++55+ }56+}57+58+// Key applies a line-editing Key.59+func (l *Line) Key(k tea.KeyPressMsg) {60+ switch k.String() {61+ case "left":62+ l.Pos = max(l.Pos-1, 0)63+ case "right":64+ l.Pos = min(l.Pos+1, len(l.Buf))65+ case "home", "ctrl+a":66+ l.Pos = 067+ case "end", "ctrl+e":68+ l.Pos = len(l.Buf)69+ case "backspace":70+ if l.Pos > 0 {71+ l.Buf = slices.Delete(l.Buf, l.Pos-1, l.Pos)72+ l.Pos--73+ }74+ case "delete":75+ if l.Pos < len(l.Buf) {76+ l.Buf = slices.Delete(l.Buf, l.Pos, l.Pos+1)77+ }78+ default:79+ l.Insert(Typed(k))80+ }81+}82+83+// SetCaret puts the edit caret at display column x of the entry.84+func (l *Line) SetCaret(x int) {85+ w := 086+ for i, r := range l.Buf {87+ if w >= x {88+ l.Pos = i89+ return90+ }91+ w += ansi.StringWidth(string(r))92+ }93+ l.Pos = len(l.Buf)94+}95+96+// IsFormula reports whether the edit line holds a formula.97+func (l *Line) IsFormula() bool {98+ return len(l.Buf) > 0 && (l.Buf[0] == '=' || l.Buf[0] == '+' || l.Buf[0] == '-')99+}100+101+// CanPoint reports whether the text before the caret ends where a cell102+// reference may follow.103+func (l *Line) CanPoint() bool {104+ if l.Pos == 0 {105+ return false106+ }107+ return strings.ContainsRune("=+-*/^(,;<>&:", l.Buf[l.Pos-1])108+}109+110+// Typed returns the printable text of a key press, if any.111+func Typed(k tea.KeyPressMsg) string {112+ if k.Mod.Contains(tea.ModCtrl) || k.Mod.Contains(tea.ModAlt) {113+ return ""114+ }115+ return k.Text116+}@@ -0,0 +1,45 @@
1+package lineedit2+3+import (4+ "testing"5+6+ tea "charm.land/bubbletea/v2"7+)8+9+func TestEditing(t *testing.T) {10+ var l Line11+ l.Set("=SUM(A1")12+ l.Key(tea.KeyPressMsg{Code: tea.KeyLeft})13+ l.Key(tea.KeyPressMsg{Code: tea.KeyBackspace})14+ l.Insert("B\tC")15+ if got := l.Text(); got != "=SUM(B C1" {16+ t.Fatalf("text %q", got)17+ }18+ if l.Head() != "=SUM(B C" || l.Tail() != "1" {19+ t.Errorf("head %q tail %q", l.Head(), l.Tail())20+ }21+ if !l.IsFormula() {22+ t.Error("not a formula")23+ }24+ l.Key(tea.KeyPressMsg{Code: 'a', Mod: tea.ModCtrl})25+ if l.Pos != 0 || l.CanPoint() {26+ t.Errorf("home: pos %d", l.Pos)27+ }28+ l.SetCaret(5)29+ if !l.CanPoint() {30+ t.Errorf("after ( a reference may follow: pos %d", l.Pos)31+ }32+ l.Clear()33+ if l.Text() != "" || l.Pos != 0 {34+ t.Errorf("clear left %q", l.Text())35+ }36+}37+38+func TestTyped(t *testing.T) {39+ if Typed(tea.KeyPressMsg{Code: 'x', Text: "x", Mod: tea.ModCtrl}) != "" {40+ t.Error("ctrl+x typed text")41+ }42+ if Typed(tea.KeyPressMsg{Code: 'x', Text: "x"}) != "x" {43+ t.Error("x typed nothing")44+ }45+}@@ -33,9 +33,9 @@ func record(t *testing.T, m *Model, relative bool, name, key string, keys ...str
3333 if m.mode != modePrompt || !strings.HasPrefix(m.prompt.label, "Save macro as") {3434 t.Fatalf("no name prompt: mode %v", m.mode)3535 }36- m.line.set(name)36+ m.line.Set(name)3737 press(t, m, "<enter>")38- m.line.set(key)38+ m.line.Set(key)3939 press(t, m, "<enter>")4040 if m.rec != nil {4141 t.Fatalf("still recording: %q %q", m.note, m.errMsg)@@ -201,7 +201,7 @@ func TestRecordingAnswersAndSkips(t *testing.T) {
201201 m = newModel()202202 run(m, m.runCommand("macro.record"))203203 run(m, m.runCommand("column.width"))204- m.line.set("15")204+ m.line.Set("15")205205 press(t, m, "<enter>")206206 run(m, m.runCommand("macro.stop"))207207 press(t, m, "<enter>", "<enter>")@@ -320,10 +320,10 @@ func TestManageMacros(t *testing.T) {
320320 t.Fatalf("manager:\n%s", screen(m))321321 }322322 press(t, m, "<down>", "<f2>")323- m.line.set("Uno")323+ m.line.Set("Uno")324324 press(t, m, "<enter>")325325 press(t, m, "<f3>")326- m.line.set("7")326+ m.line.Set("7")327327 press(t, m, "<enter>")328328 mc := mustMacro(t, m, "Uno")329329 if mc.Key != "7" {@@ -270,8 +270,8 @@ func (h scriptHost) answer(c *command, answer *string) error {
270270 return h.failure()271271 case m.mode == modePrompt && answer != nil:272272 p := m.prompt273- m.line.clear()274- m.line.insert(*answer)273+ m.line.Clear()274+ m.line.Insert(*answer)275275 p.fresh, p.typing = false, p.kind == promptRange276276 m.macros.cmds = append(m.macros.cmds, p.accept(m))277277 case m.mode == modePrompt:@@ -12,6 +12,7 @@ import (
1212 "github.com/FelineStateMachine/012/internal/confine"1313 "github.com/FelineStateMachine/012/internal/sheet"1414 "github.com/FelineStateMachine/012/internal/telemetry"15+ "github.com/FelineStateMachine/012/internal/ui/lineedit"1516 "github.com/FelineStateMachine/012/internal/ui/overlay"1617 "github.com/FelineStateMachine/012/internal/ui/theme"1718 )@@ -84,7 +85,7 @@ type Model struct {
8485 8586 // Components. Each owns its state and the handling of the input it8687 // takes; Model routes messages to them and composes what they draw.87- line lineEdit // the edit line of entries, prompts and search fields: line.go88+ line lineedit.Line // the edit line of entries, prompts and search fields: line.go8889 entry entry // typing into a cell: entry.go8990 point pointer // the cell or range pointed at in POINT mode and range prompts9091 prompt *prompt // a question on the context line: prompt.go@@ -335,12 +336,7 @@ func (m *Model) focus() *sheet.Addr {
335336 }336337 337338 // typed returns the printable text of a key press, if any.338-func typed(k tea.KeyPressMsg) string {339- if k.Mod.Contains(tea.ModCtrl) || k.Mod.Contains(tea.ModAlt) {340- return ""341- }342- return k.Text343-}339+func typed(k tea.KeyPressMsg) string { return lineedit.Typed(k) }344340 345341 func clamp(v, lo, hi int) int {346342 return max(lo, min(v, hi))@@ -219,8 +219,8 @@ func TestEditExisting(t *testing.T) {
219219 t.Errorf("input %q, want =243", got)220220 }221221 press(t, m, "<up>", "<enter>")222- if m.mode != modeEdit || m.line.text() != "=243" {223- t.Errorf("Enter should edit: mode %v buf %q", m.mode, m.line.text())222+ if m.mode != modeEdit || m.line.Text() != "=243" {223+ t.Errorf("Enter should edit: mode %v buf %q", m.mode, m.line.Text())224224 }225225 }226226 @@ -321,8 +321,8 @@ func TestDoubleClickEdits(t *testing.T) {
321321 press(t, m, "hi", "<enter>")322322 click(m, cellX(0), gridTop, 0)323323 click(m, cellX(0), gridTop, 0)324- if m.mode != modeEdit || m.line.text() != "hi" {325- t.Errorf("mode %v buf %q", m.mode, m.line.text())324+ if m.mode != modeEdit || m.line.Text() != "hi" {325+ t.Errorf("mode %v buf %q", m.mode, m.line.Text())326326 }327327 press(t, m, "<esc>")328328 m.mouse.lastClick = time.Time{}@@ -171,17 +171,17 @@ func (m *Model) leftPress(h hit, mouse tea.Mouse, double bool) tea.Cmd {
171171 m.mouse.drag = dragPoint172172 case hitEditLine:173173 m.resumeEntry(m.pointRef())174- m.line.setCaret(h.x)174+ m.line.SetCaret(h.x)175175 }176176 return nil177177 case modeEnter, modeEdit:178178 switch {179179 case h.kind == hitEditLine:180- m.line.setCaret(h.x)180+ m.line.SetCaret(h.x)181181 return nil182- case h.kind == hitCell && m.line.isFormula() && m.line.canPoint():183- m.entry.prefix = m.line.head()184- m.entry.suffix = m.line.tail()182+ case h.kind == hitCell && m.line.IsFormula() && m.line.CanPoint():183+ m.entry.prefix = m.line.Head()184+ m.entry.suffix = m.line.Tail()185185 m.point = pointer{at: h.addr, anchor: h.addr}186186 m.mode, m.mouse.drag = modePoint, dragPoint187187 return nil@@ -205,7 +205,7 @@ func (m *Model) readyPress(h hit, mouse tea.Mouse, double bool) tea.Cmd {
205205 switch h.kind {206206 case hitFormulaBar:207207 m.runCommand("edit")208- m.line.setCaret(h.x)208+ m.line.SetCaret(h.x)209209 case hitCorner:210210 m.cur = sheet.Addr{Col: m.left, Row: m.top}211211 m.selecting, m.whole, m.ext = true, wholeAll, m.cur@@ -54,14 +54,14 @@ func TestClickFormulaBarEdits(t *testing.T) {
5454 press(t, m, "=1+2", "<enter>", "<up>")5555 line0, x0 := formulaBarAt()5656 click(m, x0+2, line0, 0)57- if m.mode != modeEdit || m.line.pos != 2 {58- t.Fatalf("mode %v caret %d", m.mode, m.line.pos)57+ if m.mode != modeEdit || m.line.Pos != 2 {58+ t.Fatalf("mode %v caret %d", m.mode, m.line.Pos)5959 }6060 // Clicking in the edit line moves the caret.6161 line1, x1 := editLineAt()6262 click(m, x1+4, line1, 0)63- if m.line.pos != 4 {64- t.Errorf("caret %d", m.line.pos)63+ if m.line.Pos != 4 {64+ t.Errorf("caret %d", m.line.Pos)6565 }6666 }6767 @@ -17,7 +17,7 @@ func (m *Model) openOverlay(o overlay.Overlay) {
1717 // closeOverlay closes the open overlay and returns to READY.1818 func (m *Model) closeOverlay() {1919 m.overlay = nil20- m.line.clear()20+ m.line.Clear()2121 if m.mode == modeMenu {2222 m.mode = modeReady2323 }@@ -123,7 +123,7 @@ const (
123123 124124 func newPicker(m *Model, title, placeholder string, maxW int, items []pickItem) *picker {125125 p := &picker{m: m, title: title, placeholder: placeholder, maxW: maxW, items: items}126- m.line.clear()126+ m.line.Clear()127127 p.Changed()128128 return p129129 }@@ -136,7 +136,7 @@ func (p *picker) Changed() {
136136 m := p.m137137 p.Sel, p.Top = 0, 0138138 p.shown = p.shown[:0]139- q := strings.TrimSpace(m.line.text())139+ q := strings.TrimSpace(m.line.Text())140140 if q == "" {141141 for i := range p.items {142142 p.shown = append(p.shown, pickMatch{item: &p.items[i]})@@ -228,9 +228,9 @@ func (p *picker) Key(k tea.KeyPressMsg) tea.Cmd {
228228 case "esc":229229 p.close(m)230230 default:231- before := m.line.text()232- m.line.key(k)233- if m.line.text() != before {231+ before := m.line.Text()232+ m.line.Key(k)233+ if m.line.Text() != before {234234 p.Changed()235235 }236236 }@@ -239,7 +239,7 @@ func (p *picker) Key(k tea.KeyPressMsg) tea.Cmd {
239239 240240 func (p *picker) pick(m *Model) tea.Cmd {241241 if p.enter != nil {242- if cmd, ok := p.enter(m, strings.TrimSpace(m.line.text())); ok {242+ if cmd, ok := p.enter(m, strings.TrimSpace(m.line.Text())); ok {243243 return cmd244244 }245245 }@@ -337,7 +337,7 @@ func (p *picker) box(m *Model) (x, y, inner int) {
337337 func (p *picker) Cursor() (int, int) {338338 m := p.m339339 x, y, _ := p.box(m)340- return x + 1 + ansi.StringWidth(searchPrompt) + ansi.StringWidth(m.line.head()), y + 1340+ return x + 1 + ansi.StringWidth(searchPrompt) + ansi.StringWidth(m.line.Head()), y + 1341341 }342342 343343 func (p *picker) Layout() []overlay.Box {@@ -347,8 +347,8 @@ func (p *picker) Layout() []overlay.Box {
347347 if len(p.shown) > 0 {348348 p.Show(rows)349349 }350- input := m.th.Title.Render(searchPrompt) + m.line.text()351- if len(m.line.buf) == 0 {350+ input := m.th.Title.Render(searchPrompt) + m.line.Text()351+ if len(m.line.Buf) == 0 {352352 input += m.th.Muted.Render(p.placeholder)353353 }354354 lines := []string{theme.Cells(m.th.MenuBar, input, inner), theme.SepRow}@@ -143,8 +143,8 @@ func TestPaletteNoMatchAndEsc(t *testing.T) {
143143 t.Errorf("paste searched for %q", selected(t, m))144144 }145145 press(t, m, "<esc>")146- if m.overlay != nil || m.mode != modeReady || len(m.line.buf) != 0 {147- t.Errorf("Esc: mode %v buf %q", m.mode, m.line.text())146+ if m.overlay != nil || m.mode != modeReady || len(m.line.Buf) != 0 {147+ t.Errorf("Esc: mode %v buf %q", m.mode, m.line.Text())148148 }149149 }150150 @@ -180,7 +180,7 @@ func (m *Model) formulaBar() string {
180180 box := m.th.Header.Render(theme.PadRight(" "+ansi.Truncate(name, nameBoxW-1, "…"), nameBoxW)) + " "181181 switch m.mode {182182 case modeEnter, modeEdit:183- return box + m.line.text()183+ return box + m.line.Text()184184 case modePoint:185185 return box + m.entry.prefix + m.th.Selection.Render(m.pointRef()) + m.entry.suffix186186 }@@ -232,9 +232,9 @@ func (m *Model) contextLineText() string {
232232 if left, right, ok = signatureLine(&m.th, m.width, prefix, len(prefix), m.th.KeyHints("Shift+arrows", "range", "Esc", "back")); !ok {233233 left = m.th.KeyHints("Arrows", "pick a cell", "Shift+arrows", "pick a range", "Enter", "accept", "Esc", "back")234234 }235- case (m.mode == modeEnter || m.mode == modeEdit) && m.line.isFormula() && m.line.inFunction():236- left, right, _ = signatureLine(&m.th, m.width, m.line.buf, m.line.pos, m.th.KeyHints("Enter", "accept", "Esc", "cancel"))237- case m.mode == modeEnter && m.line.isFormula():235+ case (m.mode == modeEnter || m.mode == modeEdit) && m.line.IsFormula() && inFunction(&m.line):236+ left, right, _ = signatureLine(&m.th, m.width, m.line.Buf, m.line.Pos, m.th.KeyHints("Enter", "accept", "Esc", "cancel"))237+ case m.mode == modeEnter && m.line.IsFormula():238238 left = m.th.KeyHints("Enter", "accept", "Tab", "accept and go right", "Arrows", "pick cells after an operator", "Esc", "cancel")239239 case m.mode == modeEnter:240240 left = m.th.KeyHints("Enter", "accept", "Tab", "accept and go right", "Arrows", "accept and move", "Esc", "cancel")@@ -264,7 +264,7 @@ func (m *Model) cursorPos() (x, y int, ok bool) {
264264 }265265 switch {266266 case m.mode == modeEnter, m.mode == modeEdit:267- return formulaBarTextX() + ansi.StringWidth(m.line.head()), formulaLine, true267+ return formulaBarTextX() + ansi.StringWidth(m.line.Head()), formulaLine, true268268 case m.mode == modePrompt && !m.pointing():269269 return ansi.StringWidth(m.prompt.prefix() + m.prompt.head(m)), contextLine, true270270 }@@ -176,7 +176,7 @@ func TestAPIKeyPrompt(t *testing.T) {
176176 if m.jev == nil || !strings.Contains(line(m, contextLine), "API key saved in the test store") {177177 t.Errorf("JEV not on: %q", line(m, contextLine))178178 }179- if strings.Contains(screen(m), key) || m.line.text() != "" {179+ if strings.Contains(screen(m), key) || m.line.Text() != "" {180180 t.Error("the key is still around")181181 }182182 @@ -47,9 +47,9 @@ func (m *Model) pointing() bool {
4747 func (m *Model) openPrompt(p *prompt, initial string) {4848 m.mode = modePrompt4949 m.prompt = p50- m.line.clear()50+ m.line.Clear()5151 m.entry.hint = ""52- m.line.insert(initial)52+ m.line.Insert(initial)5353 }5454 5555 // openText asks for text, starting from initial, which typing replaces.@@ -102,7 +102,7 @@ func (p *prompt) cancel(m *Model) {
102102 103103 // accept closes the prompt and acts on the answer.104104 func (p *prompt) accept(m *Model) tea.Cmd {105- text := strings.TrimSpace(m.line.text())105+ text := strings.TrimSpace(m.line.Text())106106 m.closePrompt()107107 if p.kind != promptRange {108108 cmd := p.onText(m, text)@@ -132,44 +132,44 @@ func (p *prompt) pointKey(m *Model, k tea.KeyPressMsg) {
132132 m.point.anchor, m.point.anchored = m.point.at, true133133 case typed(k) != "":134134 p.typing = true135- m.line.insert(typed(k))135+ m.line.Insert(typed(k))136136 }137137 }138138 139139 // stepWidth changes a width by one, previewing it live.140140 func (p *prompt) stepWidth(m *Model, key string) {141- w, _ := strconv.Atoi(m.line.text())141+ w, _ := strconv.Atoi(m.line.Text())142142 if key == "left" {143143 w--144144 } else {145145 w++146146 }147147 w = clamp(w, 1, 240)148- m.line.clear()148+ m.line.Clear()149149 p.fresh = false150- m.line.insert(strconv.Itoa(w))150+ m.line.Insert(strconv.Itoa(w))151151 m.setWidths(w) // live preview152152 }153153 154154 // typeKey edits the answer; the first key typed replaces the default.155155 func (p *prompt) typeKey(m *Model, k tea.KeyPressMsg) {156156 if p.fresh && typed(k) != "" {157- m.line.clear()157+ m.line.Clear()158158 }159159 p.fresh = false160160 if p.kind == promptWidth && !isDigits(typed(k)) {161161 return162162 }163- m.line.key(k)163+ m.line.Key(k)164164 }165165 166166 // paste types pasted text into the answer.167167 func (p *prompt) paste(m *Model, text string) {168168 if p.fresh {169- m.line.clear()169+ m.line.Clear()170170 p.fresh = false171171 }172- m.line.insert(text)172+ m.line.Insert(text)173173 }174174 175175 // line is the prompt as the context line shows it, e.g. "Save as:@@ -180,13 +180,13 @@ func (p *prompt) line(m *Model) (left, right string) {
180180 return p.prefix() + m.th.Selection.Render(m.point.text()),181181 m.th.KeyHints("Arrows", "move", "Shift+arrows", "extend", "Enter", "apply", "Esc", "cancel")182182 case len(p.files) > 0:183- return p.prefix() + m.line.text(), m.th.Muted.Render(strings.Join(p.files, " "))183+ return p.prefix() + m.line.Text(), m.th.Muted.Render(strings.Join(p.files, " "))184184 case p.kind == promptWidth:185- return p.prefix() + m.line.text(), m.th.KeyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")185+ return p.prefix() + m.line.Text(), m.th.KeyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")186186 case p.secret:187- return p.prefix() + mask(len(m.line.buf)), m.th.KeyHints("Enter", "save", "Esc", "cancel")187+ return p.prefix() + mask(len(m.line.Buf)), m.th.KeyHints("Enter", "save", "Esc", "cancel")188188 }189- return p.prefix() + m.line.text(), m.th.KeyHints("Enter", "apply", "Esc", "cancel")189+ return p.prefix() + m.line.Text(), m.th.KeyHints("Enter", "apply", "Esc", "cancel")190190 }191191 192192 func (p *prompt) prefix() string {@@ -199,9 +199,9 @@ func mask(n int) string { return strings.Repeat("•", n) }
199199 // head is the answer before the caret, as shown.200200 func (p *prompt) head(m *Model) string {201201 if p.secret {202- return mask(m.line.pos)202+ return mask(m.line.Pos)203203 }204- return m.line.head()204+ return m.line.Head()205205 }206206 207207 func isDigits(s string) bool {@@ -167,14 +167,14 @@ func (m *Model) pointInto(s *sheet.Sheet) {
167167 return168168 }169169 if m.mode != modePoint {170- if !m.line.isFormula() || !m.line.canPoint() {170+ if !m.line.IsFormula() || !m.line.CanPoint() {171171 if m.commit() {172172 m.showSheet(s)173173 }174174 return175175 }176- m.entry.prefix = m.line.head()177- m.entry.suffix = m.line.tail()176+ m.entry.prefix = m.line.Head()177+ m.entry.suffix = m.line.Tail()178178 m.mode = modePoint179179 }180180 if m.entry.home == nil {@@ -118,8 +118,8 @@ func TestOpenImportsOtherFormats(t *testing.T) {
118118 t.Errorf("save choices %q", l)119119 }120120 press(t, m, "<enter>")121- if m.mode != modePrompt || m.line.text() != "data.012" {122- t.Errorf("save as %v %q", m.mode, m.line.text())121+ if m.mode != modePrompt || m.line.Text() != "data.012" {122+ t.Errorf("save as %v %q", m.mode, m.line.Text())123123 }124124 press(t, m, "<enter>")125125 if m.filename != "data.012" {@@ -163,11 +163,11 @@ func TestSQLiteTablePicker(t *testing.T) {
163163 164164 m.runCommand("file.import")165165 press(t, m, "<enter>", "Replace spreadsheet", "<enter>", "query", "<enter>")166- if m.mode != modePrompt || m.line.text() != "SELECT * FROM orders" {167- t.Fatalf("query prompt %v %q", m.mode, m.line.text())166+ if m.mode != modePrompt || m.line.Text() != "SELECT * FROM orders" {167+ t.Fatalf("query prompt %v %q", m.mode, m.line.Text())168168 }169169 press(t, m, "<ctrl+a>")170- m.line.buf, m.line.pos = []rune("SELECT sum(total) AS s FROM orders"), len("SELECT sum(total) AS s FROM orders")170+ m.line.Buf, m.line.Pos = []rune("SELECT sum(total) AS s FROM orders"), len("SELECT sum(total) AS s FROM orders")171171 press(t, m, "<enter>")172172 if m.sheet.Value(addr("A2")).Num != 12.5 {173173 t.Errorf("query A2 %+v err %q", m.sheet.Value(addr("A2")), m.errMsg)@@ -221,7 +221,7 @@ func renderSpan(th *theme.Theme, sp rowtext.Span, base lipgloss.Style, colored b
221221 // inCellText shows the entry being typed inside the cell, keeping the end222222 // of long entries visible, as Sheets does.223223 func (m *Model) inCellText(w int) string {224- text := " " + m.line.text()224+ text := " " + m.line.Text()225225 if over := ansi.StringWidth(text) - w; over > 0 {226226 text = ansi.TruncateLeft(text, over+1, "…")227227 }@@ -292,12 +292,12 @@ func TestVimCommandLine(t *testing.T) {
292292 t.Errorf("status: %q", line(m, m.height-1))293293 }294294 press(t, m, "<tab>")295- if m.line.text() != "edit.fill_down" {296- t.Errorf("Tab: %q", m.line.text())295+ if m.line.Text() != "edit.fill_down" {296+ t.Errorf("Tab: %q", m.line.Text())297297 }298298 press(t, m, "<tab>")299- if m.line.text() != "edit.fill_right" {300- t.Errorf("Tab Tab: %q", m.line.text())299+ if m.line.Text() != "edit.fill_right" {300+ t.Errorf("Tab Tab: %q", m.line.Text())301301 }302302 press(t, m, "<esc>")303303 // A title runs its command; so does the first completion of a prefix.@@ -64,7 +64,7 @@ func (b vimBinding) run(m *Model, n int) tea.Cmd {
6464 var vimNormal = map[string]vimBinding{6565 "i": {id: "edit", label: "Edit the cell, caret at the start", do: func(m *Model, _ int) tea.Cmd {6666 cmd := m.runCommand("edit")67- m.line.pos = 067+ m.line.Pos = 06868 return cmd6969 }},7070 "a": {id: "edit", span: spanOnce},