cat / 012
clone

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

main
012 / internal / ui / entry.go
entry.go 7.7K
1package ui23import (4 "strings"5 "unicode/utf8"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/macro"10 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/overlay"12 "src.rest/cat/012/internal/ui/suggest"13)1415// Entry follows Google Sheets: typing replaces the cell (ENTER), Enter or16// F2 edits it in place (EDIT). Enter commits and moves down, Tab commits17// and moves right, Esc cancels. While typing a formula, arrow keys after an18// operator point at cells (POINT) and insert their references. The19// selection stays while typing, so Ctrl+Enter can fill it.2021// entry is the state of typing into a cell, beyond the text itself (in22// Model.line) and the cell or range pointed at (Model.point).23type entry struct {24 hint string // shown on the context line, e.g. a formula error2526 // While pointing, the entry's text before and after the reference27 // being pointed at.28 prefix, suffix string2930 // home is the entry's sheet while the pointer is on another one; see31 // tabs.go.32 home *sheet.Sheet3334 assist suggest.List // function and name suggestions: package suggest3536 // tabStart remembers where a run of Tab-committed entries began, so37 // Enter returns to that column on the next row, as in Sheets.38 tabStart int39 tabbing bool40}4142func (m *Model) startEntry(md mode, text string) {43 if m.refuseEdit(sheet.Rect{From: m.cur, To: m.cur}, false) {44 return45 }46 if m.askProtected(sheet.Rect{From: m.cur, To: m.cur}, func(m *Model) tea.Cmd {47 m.startEntry(md, text)48 return nil49 }) {50 return51 }52 m.mode = md53 m.entry.home = nil54 m.share.entryFrom = m.roomLast()55 m.line.Clear()56 m.entry.hint, m.entry.assist = "", suggest.List{}57 m.line.Insert(text)58}5960// startEdit edits the active cell's contents with the caret at the end.61func (m *Model) startEdit() tea.Cmd {62 m.startEntry(modeEdit, "")63 if c := m.sheet.Cell(m.cur); c != nil && m.mode == modeEdit {64 m.line.Set(m.shownEntry(c.Input))65 }66 return nil67}6869// enterKey handles ENTER mode. Arrow keys accept the entry and move, except70// in a formula where a reference may follow the caret: then they point.71func (m *Model) enterKey(k tea.KeyPressMsg) tea.Cmd {72 key := k.String()73 if m.entry.assist.Key(m.host(), key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {74 return nil75 }76 if key == "f2" {77 m.mode = modeEdit78 return nil79 }80 if key == "f4" {81 m.toggleAbsolute()82 return nil83 }84 if m.line.IsFormula() && (m.pointAfterSheet(key) || m.canPoint() && m.startPoint(key)) {85 return nil86 }87 if m.line.IsFormula() && (key == "left" || key == "right") {88 m.typeKey(k) // move the caret within a formula89 return nil90 }91 if isMoveKey(key) {92 if m.commit() {93 m.navigate(key, &m.cur)94 }95 return nil96 }97 m.typeKey(k)98 return nil99}100101// editKey handles EDIT mode, where left and right move the caret.102func (m *Model) editKey(k tea.KeyPressMsg) tea.Cmd {103 key := k.String()104 if m.entry.assist.Key(m.host(), key) || m.commitKey(key) || m.cancelKey(key) || m.sheetKey(key) {105 return nil106 }107 if m.line.IsFormula() && strings.HasPrefix(key, "shift+") && (m.pointAfterSheet(key) || m.canPoint() && m.startPoint(key)) {108 return nil109 }110 switch key {111 case "f4":112 m.toggleAbsolute()113 case "up", "down", "pgup", "pgdown":114 if m.commit() {115 m.navigate(key, &m.cur)116 }117 default:118 m.typeKey(k)119 }120 return nil121}122123// commitKey handles the keys that accept an entry, moving afterwards the124// way Sheets does. It reports whether key was one of them.125func (m *Model) commitKey(key string) bool {126 switch key {127 case "enter", "shift+enter", "tab", "shift+tab", "ctrl+enter":128 default:129 return false130 }131 if key == "ctrl+enter" && m.hasRange() {132 m.fillEntry()133 return true134 }135 if !m.commit() {136 return true137 }138 switch key {139 case "enter":140 if m.entry.tabbing {141 m.cur.Col = m.entry.tabStart142 m.entry.tabbing = false143 }144 m.cur.Row++145 case "shift+enter":146 m.cur.Row--147 case "tab":148 if !m.entry.tabbing {149 m.entry.tabStart, m.entry.tabbing = m.cur.Col, true150 }151 m.cur.Col++152 case "shift+tab":153 m.cur.Col--154 }155 m.cur = clampAddr(m.cur)156 return true157}158159func (m *Model) cancelKey(key string) bool {160 if key != "esc" {161 return false162 }163 m.cancelEntry()164 return true165}166167// startPoint enters POINT mode if key moves (Shift extends). The pointer168// starts at the active cell, or on another sheet where it last pointed.169func (m *Model) startPoint(key string) bool {170 from := m.cur171 if m.away() {172 from = m.point.at173 }174 if base, ok := extendKey(key); ok {175 key = base176 m.point = pointer{at: from, anchor: from, anchored: true}177 } else if isMoveKey(key) && key != "tab" && key != "shift+tab" {178 m.point = pointer{at: from}179 } else {180 return false181 }182 m.entry.prefix = m.line.Head()183 m.entry.suffix = m.line.Tail()184 m.mode = modePoint185 m.navigate(key, &m.point.at)186 return true187}188189// pointKey handles POINT mode. Arrows move the pointer, Shift+arrows190// extend it to a range, and anything else puts the reference into the191// formula and carries on typing.192func (m *Model) pointKey(k tea.KeyPressMsg) tea.Cmd {193 key := k.String()194 if m.pointMoveKey(key) || m.sheetKey(key) {195 return nil196 }197 switch key {198 case "esc", "backspace":199 m.resumeEntry("")200 return nil201 case "f4":202 m.resumeEntry(m.pointRef())203 m.toggleAbsolute()204 return nil205 case "enter", "shift+enter", "tab", "shift+tab", "ctrl+enter":206 m.resumeEntry(m.pointRef())207 m.commitKey(key)208 return nil209 case ":":210 if !m.point.anchored {211 m.point.anchor, m.point.anchored = m.point.at, true212 return nil213 }214 }215 if text := typed(k); text != "" {216 m.resumeEntry(m.pointRef())217 m.line.Insert(text)218 m.entry.assist = suggest.List{Active: true}219 }220 return nil221}222223// resumeEntry leaves POINT mode, inserting ref at the caret.224func (m *Model) resumeEntry(ref string) {225 m.mode = modeEnter226 m.line.Buf = []rune(m.entry.prefix + ref + m.entry.suffix)227 m.line.Pos = utf8.RuneCountInString(m.entry.prefix + ref)228}229230// commit stores the edit line in the active cell. An invalid formula stays231// in EDIT mode with the caret at the problem and the reason on line 3.232func (m *Model) commit() bool {233 input := m.storedEntry(m.line.Text())234 warn, ok := m.checkEntry(input) // data validation, rules.go235 if !ok {236 return false237 }238 if err := m.set(m.cur, input); err != nil {239 m.entryError(err, input)240 return false241 }242 m.cancelEntry()243 m.clearSelection()244 m.recordEntry(input, false)245 m.overwrote()246 if warn != "" {247 m.warn = warn248 }249 return true250}251252func (m *Model) set(a sheet.Addr, input string) error {253 if err := m.entrySheet().Set(a, input); err != nil {254 return err255 }256 m.changed = true257 return nil258}259260// cancelEntry ends the entry, showing its sheet again if the pointer261// went into another.262func (m *Model) cancelEntry() {263 m.returnHome()264 m.mode = modeReady265 m.line.Clear()266 m.entry.hint, m.entry.assist = "", suggest.List{}267}268269func (m *Model) handlePaste(content string) {270 switch m.mode {271 case modeReady:272 if !m.mayEdit() {273 return274 }275 if m.askProtected(pasteTextRange(m.cur, content), func(m *Model) tea.Cmd {276 m.handlePaste(content)277 return nil278 }) {279 return280 }281 m.recordFlush()282 if m.pasteText(content) {283 m.record(macro.Call("paste_text", content))284 return285 }286 if content = strings.TrimSpace(content); content != "" {287 m.startEntry(modeEnter, content)288 }289 case modeEnter, modeEdit:290 m.line.Insert(content)291 case modePrompt:292 if m.prompt.kind != promptRange {293 m.prompt.paste(m, content)294 }295 case modeMenu:296 if o, ok := m.overlay.(overlay.Text); ok {297 m.line.Insert(content)298 o.Changed()299 }300 }301}302303// pointer is a cell or range being selected in POINT mode or a range304// prompt.305type pointer struct {306 at, anchor sheet.Addr307 anchored bool308}309310func (p pointer) rect() sheet.Rect {311 if !p.anchored {312 return sheet.Rect{From: p.at, To: p.at}313 }314 return sheet.NewRect(p.anchor, p.at)315}316317// text is the reference as inserted into a formula, e.g. B3 or B3:B5.318func (p pointer) text() string {319 return p.rect().String()320}