src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/builtins_test.go (about) 1 package edit 2 3 import ( 4 "io" 5 "strings" 6 "testing" 7 8 "src.elv.sh/pkg/cli/modes" 9 "src.elv.sh/pkg/cli/term" 10 "src.elv.sh/pkg/cli/tk" 11 "src.elv.sh/pkg/tt" 12 "src.elv.sh/pkg/ui" 13 ) 14 15 func TestBindingTable(t *testing.T) { 16 f := setup(t) 17 18 evals(f.Evaler, `var called = $false`) 19 evals(f.Evaler, `var m = (edit:binding-table [&a={ set called = $true }])`) 20 _, ok := getGlobal(f.Evaler, "m").(bindingsMap) 21 if !ok { 22 t.Errorf("edit:binding-table did not create BindingMap variable") 23 } 24 } 25 26 func TestCloseMode(t *testing.T) { 27 f := setup(t) 28 29 f.Editor.app.PushAddon(tk.Empty{}) 30 evals(f.Evaler, `edit:close-mode`) 31 32 if addons := f.Editor.app.CopyState().Addons; len(addons) > 0 { 33 t.Errorf("got addons %v, want nil or empty slice", addons) 34 } 35 } 36 37 func TestInsertRaw(t *testing.T) { 38 f := setup(t) 39 40 f.TTYCtrl.Inject(term.K('V', ui.Ctrl)) 41 wantBuf := f.MakeBuffer( 42 "~> ", term.DotHere, "\n", 43 " RAW ", Styles, 44 "*****", 45 ) 46 f.TTYCtrl.TestBuffer(t, wantBuf) 47 // Since we do not use real terminals in the test, we cannot have a 48 // realistic test case against actual raw inputs. However, we can still 49 // check that the builtin command does call the SetRawInput method with 1. 50 if raw := f.TTYCtrl.RawInput(); raw != 1 { 51 t.Errorf("RawInput() -> %d, want 1", raw) 52 } 53 54 // Raw mode does not respond to non-key events. 55 f.TTYCtrl.Inject(term.MouseEvent{}) 56 f.TTYCtrl.TestBuffer(t, wantBuf) 57 58 // Raw mode is dismissed after a single key event. 59 f.TTYCtrl.Inject(term.K('+')) 60 f.TestTTY(t, 61 "~> +", Styles, 62 " v", term.DotHere, 63 ) 64 } 65 66 func TestEndOfHistory(t *testing.T) { 67 f := setup(t) 68 69 evals(f.Evaler, `edit:end-of-history`) 70 f.TestTTYNotes(t, "End of history") 71 } 72 73 func TestKey(t *testing.T) { 74 f := setup(t) 75 76 evals(f.Evaler, `var k = (edit:key a)`) 77 wantK := ui.K('a') 78 if k, _ := f.Evaler.Global().Index("k"); k != wantK { 79 t.Errorf("$k is %v, want %v", k, wantK) 80 } 81 } 82 83 func TestRedraw(t *testing.T) { 84 f := setup(t) 85 86 evals(f.Evaler, 87 `set edit:current-command = echo`, 88 `edit:redraw`) 89 f.TestTTY(t, 90 "~> echo", Styles, 91 " vvvv", term.DotHere) 92 evals(f.Evaler, `edit:redraw &full=$true`) 93 // TODO(xiaq): Test that this is actually a full redraw. 94 f.TestTTY(t, 95 "~> echo", Styles, 96 " vvvv", term.DotHere) 97 } 98 99 func TestClear(t *testing.T) { 100 f := setup(t) 101 102 evals(f.Evaler, `set edit:current-command = echo`, `edit:clear`) 103 f.TestTTY(t, 104 "~> echo", Styles, 105 " vvvv", term.DotHere) 106 if cleared := f.TTYCtrl.ScreenCleared(); cleared != 1 { 107 t.Errorf("screen cleared %v times, want 1", cleared) 108 } 109 } 110 111 func TestNotify(t *testing.T) { 112 f := setup(t) 113 evals(f.Evaler, "edit:notify string") 114 f.TestTTYNotes(t, "string") 115 116 evals(f.Evaler, "edit:notify (styled styled red)") 117 f.TestTTYNotes(t, 118 "styled", Styles, 119 "!!!!!!") 120 121 evals(f.Evaler, "var err = ?(edit:notify [])") 122 if _, hasErr := getGlobal(f.Evaler, "err").(error); !hasErr { 123 t.Errorf("calling edit:notify with [] did not result in error") 124 // TODO: Test the exact error 125 } 126 } 127 128 func TestReturnCode(t *testing.T) { 129 f := setup(t) 130 131 codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) { 132 s.Buffer.Content = "test code" 133 }) 134 evals(f.Evaler, `edit:return-line`) 135 code, err := f.Wait() 136 if code != "test code" { 137 t.Errorf("got code %q, want %q", code, "test code") 138 } 139 if err != nil { 140 t.Errorf("got err %v, want nil", err) 141 } 142 } 143 144 func TestReturnEOF(t *testing.T) { 145 f := setup(t) 146 147 evals(f.Evaler, `edit:return-eof`) 148 if _, err := f.Wait(); err != io.EOF { 149 t.Errorf("got err %v, want %v", err, io.EOF) 150 } 151 } 152 153 func TestSmartEnter_InsertsNewlineWhenIncomplete(t *testing.T) { 154 f := setup(t) 155 156 f.SetCodeBuffer(tk.CodeBuffer{Content: "put [", Dot: 5}) 157 evals(f.Evaler, `edit:smart-enter`) 158 wantBuf := tk.CodeBuffer{Content: "put [\n", Dot: 6} 159 if buf := codeArea(f.Editor.app).CopyState().Buffer; buf != wantBuf { 160 t.Errorf("got code buffer %v, want %v", buf, wantBuf) 161 } 162 } 163 164 func TestSmartEnter_AcceptsCodeWhenWholeBufferIsComplete(t *testing.T) { 165 f := setup(t) 166 167 f.SetCodeBuffer(tk.CodeBuffer{Content: "put []", Dot: 5}) 168 evals(f.Evaler, `edit:smart-enter`) 169 wantCode := "put []" 170 if code, _ := f.Wait(); code != wantCode { 171 t.Errorf("got return code %q, want %q", code, wantCode) 172 } 173 } 174 175 // TODO: Test that smart-enter applies autofix. 176 177 var bufferBuiltinsTests = []struct { 178 name string 179 bufBefore tk.CodeBuffer 180 bufAfter tk.CodeBuffer 181 }{ 182 { 183 "move-dot-left", 184 tk.CodeBuffer{Content: "ab", Dot: 1}, 185 tk.CodeBuffer{Content: "ab", Dot: 0}, 186 }, 187 { 188 "move-dot-right", 189 tk.CodeBuffer{Content: "ab", Dot: 1}, 190 tk.CodeBuffer{Content: "ab", Dot: 2}, 191 }, 192 { 193 "kill-rune-left", 194 tk.CodeBuffer{Content: "ab", Dot: 1}, 195 tk.CodeBuffer{Content: "b", Dot: 0}, 196 }, 197 { 198 "kill-rune-right", 199 tk.CodeBuffer{Content: "ab", Dot: 1}, 200 tk.CodeBuffer{Content: "a", Dot: 1}, 201 }, 202 { 203 "transpose-rune with empty buffer", 204 tk.CodeBuffer{Content: "", Dot: 0}, 205 tk.CodeBuffer{Content: "", Dot: 0}, 206 }, 207 { 208 "transpose-rune with dot at beginning", 209 tk.CodeBuffer{Content: "abc", Dot: 0}, 210 tk.CodeBuffer{Content: "bac", Dot: 2}, 211 }, 212 { 213 "transpose-rune with dot in middle", 214 tk.CodeBuffer{Content: "abc", Dot: 1}, 215 tk.CodeBuffer{Content: "bac", Dot: 2}, 216 }, 217 { 218 "transpose-rune with dot at end", 219 tk.CodeBuffer{Content: "abc", Dot: 3}, 220 tk.CodeBuffer{Content: "acb", Dot: 3}, 221 }, 222 { 223 "transpose-rune with one character and dot at end", 224 tk.CodeBuffer{Content: "a", Dot: 1}, 225 tk.CodeBuffer{Content: "a", Dot: 1}, 226 }, 227 { 228 "transpose-rune with one character and dot at beginning", 229 tk.CodeBuffer{Content: "a", Dot: 0}, 230 tk.CodeBuffer{Content: "a", Dot: 0}, 231 }, 232 { 233 "transpose-word with dot at beginning", 234 tk.CodeBuffer{Content: "ab bc cd", Dot: 0}, 235 tk.CodeBuffer{Content: "bc ab cd", Dot: 6}, 236 }, 237 { 238 "transpose-word with dot in between words", 239 tk.CodeBuffer{Content: "ab bc cd", Dot: 6}, 240 tk.CodeBuffer{Content: "ab cd bc", Dot: 9}, 241 }, 242 { 243 "transpose-word with dot at end", 244 tk.CodeBuffer{Content: "ab bc cd", Dot: 9}, 245 tk.CodeBuffer{Content: "ab cd bc", Dot: 9}, 246 }, 247 { 248 "transpose-word with dot in the middle of a word", 249 tk.CodeBuffer{Content: "ab bc cd", Dot: 5}, 250 tk.CodeBuffer{Content: "bc ab cd", Dot: 6}, 251 }, 252 { 253 "transpose-word with one word", 254 tk.CodeBuffer{Content: " ab ", Dot: 4}, 255 tk.CodeBuffer{Content: " ab ", Dot: 4}, 256 }, 257 { 258 "transpose-word with no words", 259 tk.CodeBuffer{Content: " \t\n ", Dot: 4}, 260 tk.CodeBuffer{Content: " \t\n ", Dot: 4}, 261 }, 262 { 263 "transpose-word with complex input", 264 tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, 265 tk.CodeBuffer{Content: "~/downloads; cd", Dot: 15}, 266 }, 267 { 268 "transpose-small-word", 269 tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, 270 tk.CodeBuffer{Content: "~/ cddownloads;", Dot: 5}, 271 }, 272 { 273 "transpose-alnum-word", 274 tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4}, 275 tk.CodeBuffer{Content: "downloads ~/cd;", Dot: 14}, 276 }, 277 } 278 279 func TestBufferBuiltins(t *testing.T) { 280 f := setup(t) 281 app := f.Editor.app 282 283 for _, test := range bufferBuiltinsTests { 284 t.Run(test.name, func(t *testing.T) { 285 codeArea(app).MutateState(func(s *tk.CodeAreaState) { 286 s.Buffer = test.bufBefore 287 }) 288 cmd := strings.Split(test.name, " ")[0] 289 evals(f.Evaler, "edit:"+cmd) 290 if buf := codeArea(app).CopyState().Buffer; buf != test.bufAfter { 291 t.Errorf("got buf %v, want %v", buf, test.bufAfter) 292 } 293 }) 294 } 295 } 296 297 // Builtins that expect the focused widget to be code areas. This 298 // includes some builtins defined in files other than builtins.go. 299 var focusedWidgetNotCodeAreaTests = []string{ 300 "edit:insert-raw", 301 "edit:smart-enter", 302 "edit:move-dot-right", // other buffer builtins not tested 303 "edit:completion:start", 304 "edit:history:start", 305 } 306 307 func TestBuiltins_FocusedWidgetNotCodeArea(t *testing.T) { 308 for _, code := range focusedWidgetNotCodeAreaTests { 309 t.Run(code, func(t *testing.T) { 310 f := setup(t) 311 f.Editor.app.PushAddon(tk.Label{}) 312 313 evals(f.Evaler, code) 314 f.TestTTYNotes(t, 315 "error: "+modes.ErrFocusedWidgetNotCodeArea.Error(), Styles, 316 "!!!!!!") 317 }) 318 } 319 } 320 321 // Tests for pure movers. 322 323 func TestMoveDotLeftRight(t *testing.T) { 324 tt.Test(t, moveDotLeft, 325 Args("foo", 0).Rets(0), 326 Args("bar", 3).Rets(2), 327 Args("精灵", 0).Rets(0), 328 Args("精灵", 3).Rets(0), 329 Args("精灵", 6).Rets(3), 330 ) 331 332 tt.Test(t, moveDotRight, 333 Args("foo", 0).Rets(1), 334 Args("bar", 3).Rets(3), 335 Args("精灵", 0).Rets(3), 336 Args("精灵", 3).Rets(6), 337 Args("精灵", 6).Rets(6), 338 ) 339 } 340 341 func TestMoveDotSOLEOL(t *testing.T) { 342 buffer := "abc\ndef" 343 // Index: 344 // 012 34567 345 tt.Test(t, moveDotSOL, 346 Args(buffer, 0).Rets(0), 347 Args(buffer, 1).Rets(0), 348 Args(buffer, 2).Rets(0), 349 Args(buffer, 3).Rets(0), 350 Args(buffer, 4).Rets(4), 351 Args(buffer, 5).Rets(4), 352 Args(buffer, 6).Rets(4), 353 Args(buffer, 7).Rets(4), 354 ) 355 tt.Test(t, moveDotEOL, 356 Args(buffer, 0).Rets(3), 357 Args(buffer, 1).Rets(3), 358 Args(buffer, 2).Rets(3), 359 Args(buffer, 3).Rets(3), 360 Args(buffer, 4).Rets(7), 361 Args(buffer, 5).Rets(7), 362 Args(buffer, 6).Rets(7), 363 Args(buffer, 7).Rets(7), 364 ) 365 } 366 367 func TestMoveDotUpDown(t *testing.T) { 368 buffer := "abc\n精灵语\ndef" 369 // Index: 370 // 012 34 7 0 34567 371 // + 10 * 0 1 372 373 tt.Test(t, moveDotUp, 374 Args(buffer, 0).Rets(0), // a -> a 375 Args(buffer, 1).Rets(1), // b -> b 376 Args(buffer, 2).Rets(2), // c -> c 377 Args(buffer, 3).Rets(3), // EOL1 -> EOL1 378 Args(buffer, 4).Rets(0), // 精 -> a 379 Args(buffer, 7).Rets(2), // 灵 -> c 380 Args(buffer, 10).Rets(3), // 语 -> EOL1 381 Args(buffer, 13).Rets(3), // EOL2 -> EOL1 382 Args(buffer, 14).Rets(4), // d -> 精 383 Args(buffer, 15).Rets(4), // e -> 精 (jump left half width) 384 Args(buffer, 16).Rets(7), // f -> 灵 385 Args(buffer, 17).Rets(7), // EOL3 -> 灵 (jump left half width) 386 ) 387 388 tt.Test(t, moveDotDown, 389 Args(buffer, 0).Rets(4), // a -> 精 390 Args(buffer, 1).Rets(4), // b -> 精 (jump left half width) 391 Args(buffer, 2).Rets(7), // c -> 灵 392 Args(buffer, 3).Rets(7), // EOL1 -> 灵 (jump left half width) 393 Args(buffer, 4).Rets(14), // 精 -> d 394 Args(buffer, 7).Rets(16), // 灵 -> f 395 Args(buffer, 10).Rets(17), // 语 -> EOL3 396 Args(buffer, 13).Rets(17), // EOL2 -> EOL3 397 Args(buffer, 14).Rets(14), // d -> d 398 Args(buffer, 15).Rets(15), // e -> e 399 Args(buffer, 16).Rets(16), // f -> f 400 Args(buffer, 17).Rets(17), // EOL3 -> EOL3 401 ) 402 } 403 404 // Word movement tests. 405 406 // The string below is carefully chosen to test all word, small-word, and 407 // alnum-word move/kill functions, because it contains features to set the 408 // different movement behaviors apart. 409 // 410 // The string is annotated with carets (^) to indicate the beginning of words, 411 // and periods (.) to indicate trailing runes of words. Indices are also 412 // annotated. 413 // 414 // cd ~/downloads; rm -rf 2018aug07-pics/*; 415 // ^. ^........... ^. ^.. ^................ (word) 416 // ^. ^.^........^ ^. ^^. ^........^^...^.. (small-word) 417 // ^. ^........ ^. ^. ^........ ^... (alnum-word) 418 // 01234567890123456789012345678901234567890 419 // 0 1 2 3 4 420 // 421 // word boundaries: 0 3 16 19 23 422 // small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37 423 // alnum-word boundaries: 0 5 16 20 23 33 424 var wordMoveTestBuffer = "cd ~/downloads; rm -rf 2018aug07-pics/*;" 425 426 var ( 427 // word boundaries: 0 3 16 19 23 428 moveDotLeftWordTests = []*tt.Case{ 429 Args(wordMoveTestBuffer, 0).Rets(0), 430 Args(wordMoveTestBuffer, 1).Rets(0), 431 Args(wordMoveTestBuffer, 2).Rets(0), 432 Args(wordMoveTestBuffer, 3).Rets(0), 433 Args(wordMoveTestBuffer, 4).Rets(3), 434 Args(wordMoveTestBuffer, 16).Rets(3), 435 Args(wordMoveTestBuffer, 19).Rets(16), 436 Args(wordMoveTestBuffer, 23).Rets(19), 437 Args(wordMoveTestBuffer, 40).Rets(23), 438 } 439 moveDotRightWordTests = []*tt.Case{ 440 Args(wordMoveTestBuffer, 0).Rets(3), 441 Args(wordMoveTestBuffer, 1).Rets(3), 442 Args(wordMoveTestBuffer, 2).Rets(3), 443 Args(wordMoveTestBuffer, 3).Rets(16), 444 Args(wordMoveTestBuffer, 16).Rets(19), 445 Args(wordMoveTestBuffer, 19).Rets(23), 446 Args(wordMoveTestBuffer, 23).Rets(40), 447 } 448 449 // small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37 450 moveDotLeftSmallWordTests = []*tt.Case{ 451 Args(wordMoveTestBuffer, 0).Rets(0), 452 Args(wordMoveTestBuffer, 1).Rets(0), 453 Args(wordMoveTestBuffer, 2).Rets(0), 454 Args(wordMoveTestBuffer, 3).Rets(0), 455 Args(wordMoveTestBuffer, 4).Rets(3), 456 Args(wordMoveTestBuffer, 5).Rets(3), 457 Args(wordMoveTestBuffer, 14).Rets(5), 458 Args(wordMoveTestBuffer, 16).Rets(14), 459 Args(wordMoveTestBuffer, 19).Rets(16), 460 Args(wordMoveTestBuffer, 20).Rets(19), 461 Args(wordMoveTestBuffer, 23).Rets(20), 462 Args(wordMoveTestBuffer, 32).Rets(23), 463 Args(wordMoveTestBuffer, 33).Rets(32), 464 Args(wordMoveTestBuffer, 37).Rets(33), 465 Args(wordMoveTestBuffer, 40).Rets(37), 466 } 467 moveDotRightSmallWordTests = []*tt.Case{ 468 Args(wordMoveTestBuffer, 0).Rets(3), 469 Args(wordMoveTestBuffer, 1).Rets(3), 470 Args(wordMoveTestBuffer, 2).Rets(3), 471 Args(wordMoveTestBuffer, 3).Rets(5), 472 Args(wordMoveTestBuffer, 5).Rets(14), 473 Args(wordMoveTestBuffer, 14).Rets(16), 474 Args(wordMoveTestBuffer, 16).Rets(19), 475 Args(wordMoveTestBuffer, 19).Rets(20), 476 Args(wordMoveTestBuffer, 20).Rets(23), 477 Args(wordMoveTestBuffer, 23).Rets(32), 478 Args(wordMoveTestBuffer, 32).Rets(33), 479 Args(wordMoveTestBuffer, 33).Rets(37), 480 Args(wordMoveTestBuffer, 37).Rets(40), 481 } 482 483 // alnum-word boundaries: 0 5 16 20 23 33 484 moveDotLeftAlnumWordTests = []*tt.Case{ 485 Args(wordMoveTestBuffer, 0).Rets(0), 486 Args(wordMoveTestBuffer, 1).Rets(0), 487 Args(wordMoveTestBuffer, 2).Rets(0), 488 Args(wordMoveTestBuffer, 3).Rets(0), 489 Args(wordMoveTestBuffer, 4).Rets(0), 490 Args(wordMoveTestBuffer, 5).Rets(0), 491 Args(wordMoveTestBuffer, 6).Rets(5), 492 Args(wordMoveTestBuffer, 16).Rets(5), 493 Args(wordMoveTestBuffer, 20).Rets(16), 494 Args(wordMoveTestBuffer, 23).Rets(20), 495 Args(wordMoveTestBuffer, 33).Rets(23), 496 Args(wordMoveTestBuffer, 40).Rets(33), 497 } 498 moveDotRightAlnumWordTests = []*tt.Case{ 499 Args(wordMoveTestBuffer, 0).Rets(5), 500 Args(wordMoveTestBuffer, 1).Rets(5), 501 Args(wordMoveTestBuffer, 2).Rets(5), 502 Args(wordMoveTestBuffer, 3).Rets(5), 503 Args(wordMoveTestBuffer, 4).Rets(5), 504 Args(wordMoveTestBuffer, 5).Rets(16), 505 Args(wordMoveTestBuffer, 16).Rets(20), 506 Args(wordMoveTestBuffer, 20).Rets(23), 507 Args(wordMoveTestBuffer, 23).Rets(33), 508 Args(wordMoveTestBuffer, 33).Rets(40), 509 } 510 ) 511 512 func TestMoveDotWord(t *testing.T) { 513 tt.Test(t, moveDotLeftWord, moveDotLeftWordTests...) 514 tt.Test(t, moveDotRightWord, moveDotRightWordTests...) 515 } 516 517 func TestMoveDotSmallWord(t *testing.T) { 518 tt.Test(t, moveDotLeftSmallWord, moveDotLeftSmallWordTests...) 519 tt.Test(t, moveDotRightSmallWord, moveDotRightSmallWordTests...) 520 } 521 522 func TestMoveDotAlnumWord(t *testing.T) { 523 tt.Test(t, moveDotLeftAlnumWord, moveDotLeftAlnumWordTests...) 524 tt.Test(t, moveDotRightAlnumWord, moveDotRightAlnumWordTests...) 525 }