cat / 012
clone

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

cat/module-path
012 / internal / ui / vimkeys.go
vimkeys.go 10.8K
1package ui23import (4 tea "charm.land/bubbletea/v2"5)67// The vim key tables. Each binding runs a registered command, so vim keys8// show next to commands in the palette and the shortcuts, and do what the9// menus do; the few that only change mode (v, V, =) say so in help.1011// vimSpan is what a count means to a binding.12type vimSpan int1314const (15 spanRepeat vimSpan = iota // run the command n times (3u)16 spanOnce // the count means nothing (:)17 spanRows // act on n rows from the active cell's (3dd)18 spanCols // act on n cells from the active cell rightwards (4x)19)2021// vimBinding is what a key sequence does in NORMAL or VISUAL mode.22type vimBinding struct {23 id string // the command run, or shown in help for do24 span vimSpan25 label string // for help, when not the command's title26 // do, when set, acts instead of running id: for keys that change27 // mode or choose between commands.28 do func(m *Model, n int) tea.Cmd29 // change marks a change that . repeats; yank a copy that goes to30 // the registers ("0), and cut a delete that does ("1 to "9 or "-).31 change, yank, cut bool32 jump bool // a jump that '' comes back from (n, N, /)33}3435// runBinding carries out b with the count typed, remembering a change36// for . and what it copied in the registers.37func (m *Model) runBinding(b vimBinding) tea.Cmd {38 v := &m.vim39 var c *vimChange40 if b.change && !v.replaying {41 c = &vimChange{keys: v.typed, count: v.count, reg: v.reg}42 }43 if b.jump {44 m.jumped()45 }46 copied := m.copied.clip47 cmd := b.run(m, v.n())48 if (b.yank || b.cut) && m.copied.clip != copied {49 m.storeRegister(b.cut)50 }51 if c != nil {52 m.vimChanged(c)53 }54 return cmd55}5657// run carries out the binding with count n.58func (b vimBinding) run(m *Model, n int) tea.Cmd {59 if b.do != nil {60 return b.do(m, n)61 }62 c := commands[b.id]63 if !c.available(m) {64 m.note = c.title + " isn't available now"65 return nil66 }67 switch b.span {68 case spanOnce:69 return m.runCommand(b.id)70 case spanRows:71 m.spanRows(n)72 case spanCols:73 m.spanCols(n)74 default:75 cmds := make([]tea.Cmd, 0, n)76 for range n {77 cmds = append(cmds, m.runCommand(b.id))78 }79 return tea.Batch(cmds...)80 }81 cmd := m.runCommand(b.id)82 if m.mode == modeReady {83 m.clearSelection()84 }85 return cmd86}8788// vimNormal binds key sequences in NORMAL mode, in the order help lists89// them.90var vimNormal = map[string]vimBinding{91 "i": {id: "edit", label: "Edit the cell, caret at the start", change: true, do: func(m *Model, _ int) tea.Cmd {92 cmd := m.runCommand("edit")93 m.line.Pos = 094 return cmd95 }},96 "a": {id: "edit", span: spanOnce, change: true},97 "=": {label: "Start a formula", change: true, do: func(m *Model, _ int) tea.Cmd { m.startEntry(modeEnter, "="); return nil }},98 "o": {id: "row.open_below", span: spanOnce, change: true},99 "O": {id: "row.open_above", span: spanOnce, change: true},100 "x": {id: "clear", label: "Clear the cell, keeping a copy", change: true, cut: true, do: deleteCells},101 "s": {label: "Clear the cell and start typing", change: true, cut: true, do: substitute},102 "dd": {id: "row.cut", span: spanRows, change: true, cut: true},103 "cc": {label: "Clear the row and start typing", change: true, cut: true, do: changeRows},104 "S": {label: "Clear the row and start typing", change: true, cut: true, do: changeRows},105 "yy": {id: "row.yank", span: spanRows, yank: true},106 "p": {id: "row.paste_below", label: "Paste rows below, or cells here", change: true, do: putAfter},107 "P": {id: "row.paste_above", label: "Paste rows above, or cells here", change: true, do: putBefore},108 ".": {label: "Repeat the last change"}, // do is repeatChange, set in vimrepeat.go109 "u": {id: "edit.undo"},110 "ctrl+r": {id: "edit.redo"},111 "v": {label: "Select cells (VISUAL)", do: func(m *Model, _ int) tea.Cmd { m.startVisual(visualCells); return nil }},112 "V": {label: "Select rows (VISUAL)", do: func(m *Model, _ int) tea.Cmd { m.startVisual(visualRows); return nil }},113 "/": {id: "edit.find", span: spanOnce, jump: true},114 "n": {id: "edit.find_next", jump: true},115 "N": {id: "edit.find_prev", jump: true},116 ":": {id: "vim.command", span: spanOnce},117 "gt": {id: "sheet.next"},118 "gT": {id: "sheet.prev"},119}120121// vimVisual binds keys in VISUAL mode; motions extend the selection.122var vimVisual = map[string]vimBinding{123 "d": {label: "Delete the selection, keeping a copy", cut: true, do: visualDelete},124 "x": {label: "Delete the selection, keeping a copy", cut: true, do: visualDelete},125 "y": {label: "Copy the selection", yank: true, do: func(m *Model, _ int) tea.Cmd {126 id := "edit.copy"127 if m.vim.visual == visualRows {128 id = "row.yank"129 }130 return m.endVisual(m.runCommand(id))131 }},132 "p": {id: "edit.paste", label: "Paste over the selection", do: func(m *Model, _ int) tea.Cmd {133 m.vim.visual = visualNone134 return m.fromRegister(func() tea.Cmd { return m.runCommand("edit.paste") })135 }},136 "o": {label: "Go to the other corner", do: func(m *Model, _ int) tea.Cmd {137 m.cur, m.ext = m.ext, m.cur138 return nil139 }},140 "v": {label: "Select cells, or back to NORMAL", do: func(m *Model, _ int) tea.Cmd { return m.switchVisual(visualCells) }},141 "V": {label: "Select rows, or back to NORMAL", do: func(m *Model, _ int) tea.Cmd { return m.switchVisual(visualRows) }},142 ":": {id: "vim.command", span: spanOnce},143}144145// vimHelpOrder lists the NORMAL and VISUAL bindings for help, grouped.146var vimHelpOrder = []string{"i", "a", "=", "o", "O", "x", "s", "dd", "cc", "S", "yy", "p", "P", ".", "u", "ctrl+r", "v", "V", "/", "n", "N", ":", "gt", "gT"}147148// visualDelete deletes the selection, keeping a copy to paste: whole149// rows with V, the cells' contents with v.150func visualDelete(m *Model, _ int) tea.Cmd {151 if m.vim.visual == visualRows {152 return m.endVisual(m.runCommand("row.cut"))153 }154 cmd := m.runCommand("edit.copy")155 m.runCommand("clear")156 return m.endVisual(cmd)157}158159// deleteCells is x: clear n cells from the active one rightwards,160// keeping a copy to paste, as vim's x keeps the characters it deletes.161func deleteCells(m *Model, n int) tea.Cmd {162 if c := commands["clear"]; !c.available(m) {163 m.note = c.title + " isn't available now"164 return nil165 }166 m.spanCols(n)167 cmd := m.runCommand("edit.copy")168 m.runCommand("clear")169 if m.mode == modeReady {170 m.clearSelection()171 }172 return cmd173}174175// substitute is s: x, then an entry in the cleared cell.176func substitute(m *Model, n int) tea.Cmd {177 cmd := deleteCells(m, n)178 if m.mode == modeReady {179 m.startEntry(modeEnter, "")180 }181 return cmd182}183184// changeRows is cc and S: clear the contents of n rows from the active185// cell's, keeping a copy of them to paste as rows, and start an entry in186// the active cell. The rows and their formats stay.187func changeRows(m *Model, n int) tea.Cmd {188 m.spanRows(n)189 cmd := m.runCommand("row.yank")190 m.runCommand("clear")191 if m.mode == modeReady {192 m.clearSelection()193 m.startEntry(modeEnter, "")194 }195 return cmd196}197198// endVisual goes back to NORMAL mode after an operator.199func (m *Model) endVisual(cmd tea.Cmd) tea.Cmd {200 m.vim.visual = visualNone201 if m.mode == modeReady {202 m.clearSelection()203 }204 return cmd205}206207// switchVisual changes a visual selection to kind, or ends it when it208// already is one.209func (m *Model) switchVisual(kind visualKind) tea.Cmd {210 if m.vim.visual == kind {211 return m.endVisual(nil)212 }213 m.startVisual(kind)214 return nil215}216217// putAfter is p: rows copied with yy or dd go in as new rows below the218// active one; cells copied any other way are pasted at the active cell.219// A register named with " is pasted rather than the clipboard.220func putAfter(m *Model, _ int) tea.Cmd {221 return m.fromRegister(func() tea.Cmd {222 if !m.rowsCopied() {223 return m.runCommand("edit.paste")224 }225 return m.runCommand("row.paste_below") // it reads the count, n226 })227}228229// putBefore is P: as p, but rows go in above the active one.230func putBefore(m *Model, _ int) tea.Cmd {231 return m.fromRegister(func() tea.Cmd {232 if !m.rowsCopied() {233 return m.runCommand("edit.paste")234 }235 return m.runCommand("row.paste_above")236 })237}238239// vimKeysFor returns the vim keys bound to a command, NORMAL mode first.240func vimKeysFor(id string) []string {241 var keys []string242 for _, k := range vimHelpOrder {243 if vimNormal[k].id == id {244 keys = append(keys, k)245 }246 }247 return keys248}249250// vimShadows reports whether vim keys take key from its Sheets binding:251// Ctrl+D and Ctrl+U scroll, Ctrl+R redoes.252func vimShadows(key string) bool {253 _, bound := vimNormal[key]254 _, moves := vimMotions[key]255 return bound || moves256}257258// isVimKey reports whether k is one of the vim tables' keys, which are259// shown as typed (dd, G, $) rather than as a named key.260func isVimKey(k string) bool {261 _, bound := vimNormal[k]262 _, moves := vimMotions[k]263 _, visual := vimVisual[k]264 return (bound || moves || visual) && !isNamedKey(k)265}266267// isNamedKey reports whether k names a key rather than a character.268func isNamedKey(k string) bool {269 return len(k) > 2 || k == "up"270}271272// vimVisualOrder lists the VISUAL bindings for help.273var vimVisualOrder = []string{"d", "x", "y", "p", "o", "v", "V"}274275// vimHelpRows are the vim keys for the shortcuts view: motions, then the276// NORMAL and VISUAL bindings, from the tables above.277func vimHelpRows() []helpRow {278 rows := []helpRow{279 {heading: "Vim keys: NORMAL"},280 {keys: []string{"h", "j", "k", "l"}, action: "Left, down, up, right"},281 {keys: []string{"w", "b"}, action: "Jump to the edge of the data"},282 {keys: []string{"gg", "G"}, action: "First row, last row of data (5G: row 5)"},283 {keys: []string{"0", "$"}, action: "Column A, last cell of the row"},284 {keys: []string{"H", "M", "L"}, action: "Top, middle, bottom of the screen"},285 {keys: []string{"Ctrl+D", "Ctrl+U"}, action: "Half a screen down, up"},286 {keys: []string{"5j", "3dd"}, action: "A count repeats a move or sizes an operator"},287 }288 rows = append(rows, bindingRows(vimNormal, vimHelpOrder)...)289 rows = append(rows,290 helpRow{keys: []string{`"a`, `"0`, `"+`}, action: "Name the register the next operator uses"},291 helpRow{keys: []string{"ma", "`a", "'a"}, action: "Mark the cell; go to the mark, or its row"},292 helpRow{keys: []string{"''"}, action: "Back to where the last jump left from"},293 helpRow{heading: "Vim keys: VISUAL"},294 helpRow{keys: []string{"Motions"}, action: "Extend the selection"})295 rows = append(rows, bindingRows(vimVisual, vimVisualOrder)...)296 return append(rows, helpRow{keys: []string{"Esc"}, action: "Back to NORMAL"})297}298299// bindingRows lists bindings in order, joining neighbors that do the300// same thing (d and x).301func bindingRows(table map[string]vimBinding, order []string) []helpRow {302 var rows []helpRow303 for _, k := range order {304 b := table[k]305 action := b.label306 if action == "" {307 action = commands[b.id].title308 }309 if n := len(rows); n > 0 && rows[n-1].action == action {310 rows[n-1].keys = append(rows[n-1].keys, keyLabel(k))311 continue312 }313 rows = append(rows, helpRow{keys: []string{keyLabel(k)}, action: action})314 }315 return rows316}