1package ui23import (4 "slices"5 "strconv"6 "strings"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/sheet"11)1213// Vim keys (File > Settings > Vim keys), in the spirit of sc-im and14// VisiData: in READY mode, which the indicator calls NORMAL, letters move15// and act instead of starting an entry. Counts repeat a motion or say how16// many rows or cells an operator takes (5j, 3dd, 4x); v and V select17// cells or whole rows (VISUAL); : opens a command line (COMMAND). Keys18// vim doesn't claim keep their Sheets meaning, so arrows, Ctrl+S, Alt19// menus and F-keys work as always. Every action but moving and selecting20// runs a registered command; the bindings are tables in vimkeys.go.21// Registers are in vimregs.go, marks in vimmarks.go and . in22// vimrepeat.go.2324// vimState is where a vim key sequence has got to.25type vimState struct {26 count int // the count typed so far, 0 for none27 keys string // a sequence begun but not finished, e.g. "d" or "g"28 visual visualKind29 // rows is the clip dd or yy last copied: pasting it inserts rows.30 rows *sheet.Clip31 reg rune // the register named with " for the next operator, 0 for none32 // typed are the keys of the sequence so far, after any count and33 // register, kept for . to repeat.34 typed []tea.Msg35 regs map[rune]vimRegister // named, numbered and small-delete registers36 marks map[rune]mark37 back *mark // where the last jump left from, for '' and ``38 // last is the change . repeats; inserting is one still taking the39 // keys of the entry it started (i, o, cc).40 last, inserting *vimChange41 replaying bool // . is replaying last42 clipPaste bool // "+p asked the terminal for the clipboard43}4445type visualKind int4647const (48 visualNone visualKind = iota49 visualCells50 visualRows51)5253// maxCount caps a count, so a slip of the finger can't loop for long.54const maxCount = 99995556// n is the count, 1 when none was typed.57func (v *vimState) n() int { return max(v.count, 1) }5859func (v *vimState) reset() { v.count, v.keys, v.reg, v.typed = 0, "", 0, nil }6061// pending is what's been typed of an unfinished sequence, e.g. "3d" or62// "a2y.63func (v *vimState) pending() string {64 p := v.keys65 if v.count > 0 {66 p = strconv.Itoa(v.count) + p67 }68 if v.reg != 0 {69 p = `"` + string(v.reg) + p70 }71 return p72}7374// argPrefixes are the keys whose next key is a name rather than a75// command: a register after ", a mark after m, ' and `.76var argPrefixes = map[string]string{77 `"`: "register", "m": "set a mark", "'": "go to a mark's row", "`": "go to a mark",78}7980// vimKey names a key press as the vim tables do: the character typed,81// or the key's name with its modifiers, e.g. "G", "$", "ctrl+d", "up".82func vimKey(k tea.KeyPressMsg) string {83 if t := typed(k); t != "" && t != " " {84 return t85 }86 return k.String()87}8889// vimActive reports whether vim keys have READY mode.90func (m *Model) vimActive() bool {91 return m.prefs.vim && m.mode == modeReady92}9394// visual reports whether a visual selection is in progress; a selection95// made any other way (a click, Shift+arrows, a command) ends it.96func (m *Model) visual() visualKind {97 if !m.selecting {98 m.vim.visual = visualNone99 }100 return m.vim.visual101}102103// vimKeyPress handles a key in NORMAL or VISUAL mode and reports whether104// it took it. Keys it leaves go to the Sheets bindings.105func (m *Model) vimKeyPress(k tea.KeyPressMsg) (tea.Cmd, bool) {106 v := &m.vim107 key := vimKey(k)108 if key == "esc" {109 return m.vimEscape()110 }111 if _, ok := argPrefixes[v.keys]; ok {112 return m.vimArgKey(key), true113 }114 if _, ok := argPrefixes[key]; ok && v.keys == "" {115 v.keys = key116 return nil, true117 }118 if d := digit(key); v.keys == "" && (d > 0 || d == 0 && v.count > 0) {119 v.count = min(v.count*10+d, maxCount)120 return nil, true121 }122 v.typed = append(v.typed, k)123 seq := v.keys + key124 if mv, ok := vimMotions[seq]; ok {125 if jumpMotions[seq] {126 m.jumped()127 }128 m.vimMove(mv)129 v.reset()130 return nil, true131 }132 table := vimNormal133 if m.visual() != visualNone {134 table = vimVisual135 }136 if b, ok := table[seq]; ok {137 cmd := m.runBinding(b)138 v.reset()139 return cmd, true140 }141 if vimPrefix(table, seq) {142 v.keys = seq143 return nil, true144 }145 begun := v.pending() != ""146 v.reset()147 // A broken sequence and stray letters do nothing, as in vim; other148 // keys (arrows, Ctrl+S, F1) fall through to their Sheets meaning.149 return nil, begun || typed(k) != ""150}151152// vimEscape cancels a sequence being typed, then a visual selection, then153// deselects, as Esc does in Sheets.154func (m *Model) vimEscape() (tea.Cmd, bool) {155 switch {156 case m.vim.pending() != "":157 m.vim.reset()158 case m.visual() != visualNone:159 m.vim.visual = visualNone160 m.clearSelection()161 default:162 return m.runCommand("select.none"), true163 }164 return nil, true165}166167func digit(key string) int {168 if len(key) == 1 && key[0] >= '0' && key[0] <= '9' {169 return int(key[0] - '0')170 }171 return -1172}173174// vimPrefix reports whether seq begins a longer binding or motion.175func vimPrefix(table map[string]vimBinding, seq string) bool {176 for k := range table {177 if len(k) > len(seq) && strings.HasPrefix(k, seq) {178 return true179 }180 }181 for k := range vimMotions {182 if len(k) > len(seq) && strings.HasPrefix(k, seq) {183 return true184 }185 }186 return false187}188189// motion moves a, the active cell or a visual selection's moving corner;190// counted is whether a count was typed, which some motions read as a row.191type motion func(m *Model, a *sheet.Addr, n int, counted bool)192193// vimMove applies a motion: in NORMAL mode to the active cell, dropping194// any selection; in VISUAL mode to the moving corner.195func (m *Model) vimMove(mv motion) {196 a := &m.cur197 if m.visual() != visualNone {198 a = &m.ext199 }200 mv(m, a, m.vim.n(), m.vim.count > 0)201 *a = clampAddr(*a)202 a.Row = m.visibleRow(a.Row)203 if m.visual() == visualNone {204 m.clearSelection()205 }206 m.entry.tabbing = false207 m.clampView()208}209210// nav repeats a Sheets movement key n times.211func nav(key string) motion {212 return func(m *Model, a *sheet.Addr, n int, _ bool) {213 for range n {214 before := *a215 m.navigate(key, a)216 if *a == before {217 return // at an edge; the rest of the count does nothing218 }219 }220 }221}222223// toRow goes to row n when a count was typed, else to the row first()224// picks.225func toRow(first func(m *Model) int) motion {226 return func(m *Model, a *sheet.Addr, n int, counted bool) {227 if counted {228 a.Row = n - 1229 } else {230 a.Row = first(m)231 }232 }233}234235// screenLine goes to a row on screen: pick chooses from the rows shown.236func screenLine(pick func(rows []int, n int) int) motion {237 return func(m *Model, a *sheet.Addr, n int, _ bool) {238 var rows []int239 for _, r := range m.screenRows() {240 if fr, _ := m.frozen(); r != divider && r >= fr {241 rows = append(rows, r)242 }243 }244 if len(rows) > 0 {245 a.Row = rows[clamp(pick(rows, n), 0, len(rows)-1)]246 }247 }248}249250// halfPage moves the cell and the view half a screen, d = 1 down or -1 up.251func halfPage(d int) motion {252 return func(m *Model, a *sheet.Addr, n int, _ bool) {253 rows := d * max(m.scrollRows()/2, 1) * n254 a.Row = m.stepRow(a.Row, rows)255 m.top = m.stepRow(m.top, rows)256 }257}258259// vimMotions move in NORMAL and VISUAL modes. The arrows and Sheets'260// movement keys are here too, so they move a visual selection's corner261// rather than dropping it.262var vimMotions = map[string]motion{263 "h": nav("left"), "j": nav("down"), "k": nav("up"), "l": nav("right"),264 "left": nav("left"), "down": nav("down"), "up": nav("up"), "right": nav("right"),265 "w": nav("ctrl+right"), "b": nav("ctrl+left"),266 "ctrl+left": nav("ctrl+left"), "ctrl+right": nav("ctrl+right"), "ctrl+up": nav("ctrl+up"), "ctrl+down": nav("ctrl+down"),267 "pgup": nav("pgup"), "pgdown": nav("pgdown"), "home": nav("home"), "end": nav("end"),268 "ctrl+home": nav("ctrl+home"), "ctrl+end": nav("ctrl+end"),269 "0": nav("home"), "^": nav("home"),270 "$": func(m *Model, a *sheet.Addr, _ int, _ bool) {271 last := sheet.Addr{Col: sheet.MaxCols - 1, Row: a.Row}272 if !m.sheet.Filled(last) {273 last = m.sheet.Edge(last, -1, 0)274 }275 a.Col = last.Col276 },277 "gg": toRow(func(*Model) int { return 0 }),278 "G": toRow(func(m *Model) int {279 used, _ := m.sheet.UsedRange()280 return used.To.Row281 }),282 "H": screenLine(func(_ []int, n int) int { return n - 1 }),283 "M": screenLine(func(rows []int, _ int) int { return (len(rows) - 1) / 2 }),284 "L": screenLine(func(rows []int, n int) int { return len(rows) - n }),285 "ctrl+d": halfPage(1),286 "ctrl+u": halfPage(-1),287}288289// vimLine is the context line while a sequence is being typed (the keys290// so far, as vim's showcmd, and the keys that finish it) or in VISUAL291// mode (what applies).292func (m *Model) vimLine() string {293 if p := m.vim.pending(); p != "" {294 return m.th.Key.Render(p) + " " + m.th.KeyHints(append(m.vimNext(), "Esc", "cancel")...)295 }296 if m.vim.visual == visualRows {297 return m.th.KeyHints("d", "delete rows", "y", "copy rows", "o", "other end", ":", "command", "Esc", "back")298 }299 return m.th.KeyHints("d", "delete", "y", "copy", "p", "paste", "o", "other corner", "V", "rows", "Esc", "back")300}301302// vimNext lists the keys that finish the sequence begun, and what they303// do, as pairs for KeyHints: after d, "d" and "cut rows".304func (m *Model) vimNext() []string {305 switch m.vim.keys {306 case "":307 return nil // just a count or a register: anything may follow308 case `"`:309 return []string{"a-z", "named", "0-9", "copies, rows cut", "-", "cells cut", "+", "clipboard"}310 case "m":311 return []string{"a-z", "mark the cell"}312 case "'", "`":313 return []string{"a-z", argPrefixes[m.vim.keys], m.vim.keys, "back"}314 }315 table := vimNormal316 if m.visual() != visualNone {317 table = vimVisual318 }319 var seqs []string320 for seq := range table {321 if len(seq) > len(m.vim.keys) && strings.HasPrefix(seq, m.vim.keys) {322 seqs = append(seqs, seq)323 }324 }325 if m.vim.keys == "g" {326 seqs = append(seqs, "gg")327 }328 slices.Sort(seqs)329 var out []string330 for _, seq := range seqs {331 what := "first row"332 if b, ok := table[seq]; ok && b.id != "" {333 what = strings.ToLower(commands[b.id].title)334 } else if ok {335 what = strings.ToLower(b.label[:1]) + b.label[1:]336 }337 out = append(out, seq[len(m.vim.keys):], what)338 }339 return out340}341342// startVisual selects from the active cell: cells, or whole rows.343func (m *Model) startVisual(kind visualKind) {344 if m.visual() == visualNone {345 m.selecting, m.ext = true, m.cur346 }347 m.vim.visual = kind348 m.whole = wholeNone349 if kind == visualRows {350 m.whole = wholeRows351 }352}353354// spanRows selects n whole rows from the active cell's, for an operator355// with a count (3dd).356func (m *Model) spanRows(n int) {357 m.ext = sheet.Addr{Col: m.cur.Col, Row: min(m.cur.Row+n-1, sheet.MaxRows-1)}358 m.selecting, m.whole = true, wholeRows359}360361// spanCols selects n cells from the active cell rightwards (4x).362func (m *Model) spanCols(n int) {363 m.ext = sheet.Addr{Col: min(m.cur.Col+n-1, sheet.MaxCols-1), Row: m.cur.Row}364 m.selecting, m.whole = n > 1, wholeNone365}