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 / vimmore_test.go
vimmore_test.go 10.8K
1package ui23import (4 "os"5 "path/filepath"6 "strings"7 "testing"8 "time"910 tea "charm.land/bubbletea/v2"11)1213// . repeats the last change with its count, or the count typed before14// it; an edit begun with i, a, o or = repeats with what was typed.15func TestVimRepeat(t *testing.T) {16 m := vimModel(t)17 press(t, m, ".")18 if !strings.Contains(line(m, contextLine), "No change to repeat") {19 t.Errorf(". first: %q", line(m, contextLine))20 }21 press(t, m, "x", "j", ".")22 if input(m, "A1") != "" || input(m, "A2") != "" || input(m, "A3") != "A3" {23 t.Fatalf("x j .: A1 %q A2 %q", input(m, "A1"), input(m, "A2"))24 }25 // A count before . replaces the change's own.26 press(t, m, "l", "2x", "j", ".")27 if input(m, "B3") != "" || input(m, "C3") != "" {28 t.Fatalf("2x .: B3 %q C3 %q", input(m, "B3"), input(m, "C3"))29 }30 press(t, m, "j", "h", "3.")31 if input(m, "A4") != "" || input(m, "B4") != "" || input(m, "C4") != "" {32 t.Fatalf("3.: %q %q %q", input(m, "A4"), input(m, "B4"), input(m, "C4"))33 }34 // An edit repeats with its text and the key that accepted it.35 press(t, m, "gg", "0", "a", "!", "<enter>")36 if input(m, "A1") != "!" || m.cur != addr("A2") {37 t.Fatalf("a!: A1 %q at %v", input(m, "A1"), m.cur)38 }39 press(t, m, "j", "j", "j", ".")40 if input(m, "A5") != "A5!" || m.cur != addr("A6") {41 t.Fatalf("a . : A5 %q at %v", input(m, "A5"), m.cur)42 }43 // o repeats the row it opened and what was typed in it.44 press(t, m, "G", "o", "new", "<enter>")45 press(t, m, ".")46 if input(m, "A7") != "new" || input(m, "A8") != "" || input(m, "A9") != "new" {47 t.Fatalf("o .: A7 %q A9 %q", input(m, "A7"), input(m, "A9"))48 }49 // dd repeats with its count; moves and copies don't replace the change.50 press(t, m, "gg", "2dd", "yy", "j", ".")51 if input(m, "A1") != "A3" || input(m, "A2") != "A6" {52 t.Fatalf("2dd yy .: A1 %q A2 %q", input(m, "A1"), input(m, "A2"))53 }54 // Each change undoes on its own.55 press(t, m, "u")56 if input(m, "A3") != "A5!" {57 t.Errorf("u after .: A3 %q", input(m, "A3"))58 }59}6061// Every sequence begun shows what finishes it.62func TestVimPendingLines(t *testing.T) {63 m := vimModel(t)64 for _, prefix := range []string{"d", "y", "c", "g", `"`, "m", "'", "`", `"a`, "3"} {65 press(t, m, prefix)66 if l := line(m, contextLine); !strings.HasPrefix(l, prefix) || !strings.Contains(l, "Esc") {67 t.Errorf("%s: %q", prefix, l)68 }69 press(t, m, "<esc>")70 }71 press(t, m, "V", "3")72 if l := line(m, contextLine); !strings.HasPrefix(l, "3") {73 t.Errorf("V 3: %q", l)74 }75}7677// Registers: "a keeps its own copy, "0 the last copy, "1 to "9 deleted78// rows and "- deleted cells, while the clipboard follows every copy.79func TestVimRegisters(t *testing.T) {80 m := vimModel(t)81 press(t, m, `"ayy`)82 if r := m.vim.regs['a']; r.clip == nil || !r.rows {83 t.Fatalf(`"ayy: %+v`, r)84 }85 press(t, m, "j", "yy", "j", "dd") // "0 row 2, "1 row 3; A3 is now A486 press(t, m, "l", "x") // "- is B3, which now holds B487 for name, want := range map[rune]string{'a': "A1", '0': "A2", '1': "A3", '-': "B4"} {88 r := m.vim.regs[name]89 if r.clip == nil {90 t.Fatalf(`"%c is empty`, name)91 }92 if got := formatTSV(r.clip.TextIn(m.locale()))[:2]; got != want {93 t.Errorf(`"%c holds %q, want %q`, name, got, want)94 }95 }96 // "ap pastes row 1 as a new row below; the clipboard stays "-.97 press(t, m, "G", `"ap`)98 if input(m, "A6") != "A1" || input(m, "C6") != "C1" {99 t.Fatalf(`"ap: A6 %q C6 %q`, input(m, "A6"), input(m, "C6"))100 }101 press(t, m, "0", "p")102 if input(m, "A6") != "B4" {103 t.Errorf("p after \"ap pastes the clipboard: A6 %q", input(m, "A6"))104 }105 // A second delete of rows moves "1 to "2.106 press(t, m, "gg", "dd")107 if formatTSV(m.vim.regs['2'].clip.TextIn(m.locale()))[:2] != "A3" {108 t.Errorf(`"2 after dd: %q`, formatTSV(m.vim.regs['2'].clip.TextIn(m.locale())))109 }110 press(t, m, `"bp`)111 if !strings.Contains(line(m, contextLine), `Register "b is empty`) {112 t.Errorf(`"bp: %q`, line(m, contextLine))113 }114 press(t, m, `"A`)115 if !strings.Contains(line(m, contextLine), `No register "A`) || m.vim.pending() != "" {116 t.Errorf(`"A: %q, pending %q`, line(m, contextLine), m.vim.pending())117 }118 // The register shows while the sequence is typed.119 press(t, m, `"a2`)120 if l := line(m, contextLine); !strings.HasPrefix(l, `"a2`) {121 t.Errorf("pending register: %q", l)122 }123 press(t, m, "<esc>")124 // VISUAL y into a register, and p from one over a selection.125 press(t, m, "gg", "0", "v", "l", `"cy`)126 if r := m.vim.regs['c']; r.clip == nil || r.rows || r.clip.Src.String() != "A1:B1" {127 t.Fatalf(`v "cy: %+v`, r)128 }129 press(t, m, "G", "v", `"cp`)130 if at := m.cur.String(); input(m, at) != input(m, "A1") {131 t.Errorf(`v "cp: %s %q`, at, input(m, at))132 }133}134135// "+p asks the terminal for the system clipboard and pastes its answer.136func TestVimSystemClipboard(t *testing.T) {137 m := vimModel(t)138 press(t, m, "G", "j", `"+p`)139 if !m.vim.clipPaste {140 t.Fatal(`"+p didn't ask for the clipboard`)141 }142 send(m, tea.ClipboardMsg{Content: "x\ty\n1\t2"})143 if input(m, "A7") != "x" || input(m, "B8") != "2" || m.vim.clipPaste {144 t.Fatalf(`"+p block: A7 %q B8 %q`, input(m, "A7"), input(m, "B8"))145 }146 press(t, m, "<esc>", "gg", `"+p`)147 send(m, tea.ClipboardMsg{Content: "one"})148 if input(m, "A1") != "one" || m.mode != modeReady {149 t.Fatalf(`"+p value: A1 %q mode %v`, input(m, "A1"), m.mode)150 }151 // An answer nobody asked for is ignored.152 send(m, tea.ClipboardMsg{Content: "two"})153 if input(m, "A1") != "one" {154 t.Errorf("unasked clipboard pasted: %q", input(m, "A1"))155 }156}157158// cc and S clear the row and start typing; s clears the cell. Both keep159// what they cleared to paste.160func TestVimChange(t *testing.T) {161 m := vimModel(t)162 press(t, m, "j", "l", "c")163 if l := line(m, contextLine); !strings.Contains(l, "clear the row and start typing") {164 t.Errorf("c: %q", l)165 }166 press(t, m, "c")167 if m.mode != modeEnter || m.cur != addr("B2") || input(m, "A2") != "" || input(m, "C2") != "" {168 t.Fatalf("cc: mode %v at %v, A2 %q", m.mode, m.cur, input(m, "A2"))169 }170 press(t, m, "new", "<enter>")171 if input(m, "B2") != "new" || input(m, "A3") != "A3" || m.sheet.Len() != 16 {172 t.Fatalf("cc new: B2 %q, %d cells", input(m, "B2"), m.sheet.Len())173 }174 if r := m.vim.regs['1']; r.clip == nil || !r.rows {175 t.Errorf(`cc didn't keep the row in "1`)176 }177 press(t, m, "2s")178 if m.mode != modeEnter || input(m, "B3") != "" || input(m, "C3") != "" || input(m, "A3") != "A3" {179 t.Fatalf("2s: mode %v B3 %q C3 %q", m.mode, input(m, "B3"), input(m, "C3"))180 }181 press(t, m, "<esc>", "p")182 if input(m, "B3") != "B3" || input(m, "C3") != "C3" {183 t.Errorf("p after s: B3 %q C3 %q", input(m, "B3"), input(m, "C3"))184 }185 press(t, m, "gg", "2S", "top", "<enter>")186 if input(m, "B1") != "top" || input(m, "A2") != "" || input(m, "A3") != "A3" {187 t.Errorf("2S: B1 %q A2 %q", input(m, "B1"), input(m, "A2"))188 }189 press(t, m, "j", ".")190 if input(m, "B3") != "top" || input(m, "A3") != "" {191 t.Errorf("S .: B3 %q A3 %q", input(m, "B3"), input(m, "A3"))192 }193}194195// Marks: m sets one, ` goes back to the cell and ' to its row, across196// sheets; a quote typed twice goes back from a jump.197func TestVimMarks(t *testing.T) {198 m := vimModel(t)199 press(t, m, "j", "l", "ma")200 if !strings.Contains(line(m, contextLine), "Mark a at B2") {201 t.Errorf("ma: %q", line(m, contextLine))202 }203 press(t, m, "G", "$", "`a")204 if m.cur != addr("B2") {205 t.Fatalf("`a: at %v", m.cur)206 }207 press(t, m, "l", "'a")208 if m.cur != addr("A2") {209 t.Fatalf("'a: at %v", m.cur)210 }211 press(t, m, "``")212 if m.cur != addr("C2") {213 t.Fatalf("``: at %v", m.cur)214 }215 press(t, m, "G", "``")216 if m.cur != addr("C2") {217 t.Fatalf("G ``: at %v", m.cur)218 }219 press(t, m, ":B12", "<enter>", "''")220 if m.cur != addr("A2") {221 t.Fatalf(":B12 '' goes to the row: at %v", m.cur)222 }223 press(t, m, "'b")224 if !strings.Contains(line(m, contextLine), "Mark b isn't set") {225 t.Errorf("'b: %q", line(m, contextLine))226 }227 // A mark on another sheet shows that sheet.228 first := m.sheet229 m.runCommand("sheet.new")230 press(t, m, "`a")231 if m.sheet != first || m.cur != addr("B2") {232 t.Errorf("`a from another sheet: %s at %v", m.sheet.Name(), m.cur)233 }234 // In VISUAL mode a mark moves the selection's corner.235 press(t, m, "gg", "0", "v", "`a")236 if indicator(m) != "VISUAL" || m.selection().String() != "A1:B2" {237 t.Errorf("v `a: %q %v", indicator(m), m.selection())238 }239}240241// Up and Down on the : line go through the lines run before, those242// starting with what's typed; the history outlasts the file.243func TestVimCommandHistory(t *testing.T) {244 m := vimModel(t)245 press(t, m, ":B5", "<enter>", ":fill down", "<enter>", ":B7", "<enter>")246 press(t, m, ":", "<up>")247 if m.line.Text() != "B7" {248 t.Fatalf("Up: %q", m.line.Text())249 }250 press(t, m, "<up>", "<up>")251 if m.line.Text() != "B5" {252 t.Fatalf("Up Up Up: %q", m.line.Text())253 }254 press(t, m, "<up>")255 if m.line.Text() != "B5" {256 t.Errorf("Up past the oldest: %q", m.line.Text())257 }258 press(t, m, "<down>", "<down>", "<down>")259 if m.line.Text() != "" {260 t.Errorf("Down back to what was typed: %q", m.line.Text())261 }262 press(t, m, "<esc>", ":B", "<up>")263 if m.line.Text() != "B7" {264 t.Errorf("B Up: %q", m.line.Text())265 }266 press(t, m, "<up>", "<enter>")267 if m.cur != addr("B5") {268 t.Errorf("recalled :B5 at %v", m.cur)269 }270 // Ctrl+N moves the completions.271 press(t, m, ":fill", "<ctrl+n>")272 if s := screen(m); !strings.Contains(line(m, m.height-1), "Copy the") || !strings.Contains(s, "Fill right") {273 t.Errorf("Ctrl+N: %q", line(m, m.height-1))274 }275 press(t, m, "<esc>")276 m.runCommand("file.new")277 press(t, m, ":", "<up>")278 if m.line.Text() != "B5" {279 t.Errorf("history after File > New: %q", m.line.Text())280 }281}282283// :w! writes over a file changed on disk since it was opened, and over a284// file of the name given, without asking.285func TestVimWriteForced(t *testing.T) {286 dir := t.TempDir()287 m := vimModel(t)288 name := filepath.Join(dir, "book.012")289 press(t, m, ":w "+name, "<enter>")290 if m.filename != name {291 t.Fatalf(":w: %q", m.filename)292 }293 // Another program writes the file.294 later := time.Now().Add(time.Minute)295 if err := os.WriteFile(name, []byte("{}"), 0o644); err != nil {296 t.Fatal(err)297 }298 os.Chtimes(name, later, later)299 press(t, m, "x", ":w", "<enter>")300 if !strings.Contains(line(m, contextLine), "changed on disk") {301 t.Fatalf(":w after a change on disk: %q", line(m, contextLine))302 }303 press(t, m, "<esc>", ":w!", "<enter>")304 if data, _ := os.ReadFile(name); string(data) == "{}" || m.changed {305 t.Fatalf(":w! didn't write: changed %v", m.changed)306 }307 other := filepath.Join(dir, "other.012")308 os.WriteFile(other, []byte("{}"), 0o644)309 press(t, m, ":w "+other, "<enter>")310 if !strings.Contains(line(m, contextLine), "exists") {311 t.Fatalf(":w other: %q", line(m, contextLine))312 }313 press(t, m, "<esc>", ":w! "+other, "<enter>")314 if data, _ := os.ReadFile(other); string(data) == "{}" || m.filename != other {315 t.Errorf(":w! other: %q, filename %q", data, m.filename)316 }317 csv := filepath.Join(dir, "out.csv")318 os.WriteFile(csv, []byte("old"), 0o644)319 press(t, m, ":w! "+csv, "<enter>")320 if data, _ := os.ReadFile(csv); !strings.Contains(string(data), "B1") {321 t.Errorf(":w! out.csv: %q", data)322 }323}